diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 00000000..e1d9e108 --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,4 @@ +const { TextEncoder, TextDecoder } = require('util'); + +global.TextEncoder = TextEncoder; +global.TextDecoder = TextDecoder; diff --git a/package.json b/package.json index c0e49a8c..b70f59b5 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,15 @@ "prettier --write" ] }, + "jest": { + "setupFilesAfterEnv": [ + "/../../../jest.setup.js", + "@testing-library/jest-dom" + ], + "moduleNameMapper": { + "monaco-editor": "/../../../node_modules/monaco-editor/monaco.d.ts" + } + }, "dependencies": { "@types/react": "^17", "@types/react-dom": "^17" diff --git a/plugins/cad/src/components/Controls/Select.tsx b/plugins/cad/src/components/Controls/Select.tsx index eebd0fac..4a6d69c7 100644 --- a/plugins/cad/src/components/Controls/Select.tsx +++ b/plugins/cad/src/components/Controls/Select.tsx @@ -27,13 +27,13 @@ type SelectProps = { helperText?: string; }; -export const Select = ({ label, selected, items, onChange, className, helperText }: SelectProps) => { +export const Select = ({ label, selected, items, onChange, className, helperText, ...otherProps }: SelectProps) => { const handleChange = (event: ChangeEvent<{ name?: string | undefined; value: unknown }>): void => { onChange(event.target.value as string); }; return ( - + {label} {items.map(item => ( diff --git a/plugins/cad/src/components/ResourceEditorDialog/ResourceEditorDialog.tsx b/plugins/cad/src/components/ResourceEditorDialog/ResourceEditorDialog.tsx index fea788a9..a7cb7d8d 100644 --- a/plugins/cad/src/components/ResourceEditorDialog/ResourceEditorDialog.tsx +++ b/plugins/cad/src/components/ResourceEditorDialog/ResourceEditorDialog.tsx @@ -15,7 +15,7 @@ */ import { Button, Dialog, DialogActions, DialogContent, DialogTitle, makeStyles } from '@material-ui/core'; -import React, { Fragment, useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { KubernetesResource } from '../../types/KubernetesResource'; import { getGroupVersionKind } from '../../utils/kubernetesResource'; import { PackageResource } from '../../utils/packageRevisionResources'; @@ -91,7 +91,7 @@ export const ResourceEditorDialog = ({ open, onClose, yaml, onSaveYaml, packageR {title} - + <>
{!showYamlView && isNamedEditor ? ( )} - + - - ); -}; - -const itemChunksFromValue = (value: PxeValue): readonly RosterItemResourceChunk[] => - Object.entries(value ?? {}).map(([itemKey, itemValue]) => ({ - key: itemKey, - value: itemValue as PxeValue, - })); - -const valueFromItemChunks = (itemChunks: readonly RosterItemResourceChunk[], rosterType: RosterValueType): PxeValue => - rosterType === PxeValueType.Object - ? Object.fromEntries(itemChunks.map(itemChunk => [itemChunk.key, itemChunk.value])) - : itemChunks.map(itemChunk => itemChunk.value); - -const useStyles = makeStyles(() => ({ - item: { - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - }, - itemContent: { - flex: '1 1 auto', - }, - itemActions: { - flex: '0 0 auto', - paddingLeft: '16px', - }, -})); diff --git a/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/nodes/widget/PxeSelectValueWidgetNode.tsx b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/nodes/widget/PxeSelectValueWidgetNode.tsx index 5af98385..068c40f8 100644 --- a/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/nodes/widget/PxeSelectValueWidgetNode.tsx +++ b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/nodes/widget/PxeSelectValueWidgetNode.tsx @@ -14,39 +14,41 @@ * limitations under the License. */ -import { get } from 'lodash'; import React from 'react'; import { Select } from '../../../../../Controls'; import { PxeSelectValueWidgetEntry } from '../../types/PxeConfiguration.types'; +import { withCurrentValues } from '../../utils/rendering/withCurrentValues'; import { generateValueLabel } from '../../utils/generateLabelsForWidgets'; import { PxeParametricEditorNodeProps } from '../../PxeParametricEditorNode'; +import { useDiagnostics } from '../../PxeDiagnosticsContext'; const DEFAULT_VALUE = '__DEFAULT_VALUE__'; -export const PxeSelectValueWidgetNode: React.FC = ({ - configurationEntry, - onResourceChangeRequest, - resourceChunk, -}) => { - const widgetEntry = configurationEntry as PxeSelectValueWidgetEntry; - const valueDescriptor = widgetEntry.values[0]; +export const PxeSelectValueWidgetNode: React.FC = withCurrentValues( + ({ configurationEntry, onResourceChangeRequest, currentValues: [currentValue] }) => { + useDiagnostics(configurationEntry); - const selectItems = widgetEntry.options.map(({ value, label }) => ({ - value: value !== undefined ? String(value) : DEFAULT_VALUE, - label, - })); + const widgetEntry = configurationEntry as PxeSelectValueWidgetEntry; + const [valueDescriptor] = widgetEntry.valueDescriptors; - return ( - { + onResourceChangeRequest({ + valueDescriptor, + newValue: value !== DEFAULT_VALUE ? value : undefined, + }); + }} + /> + ); + }, +); diff --git a/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/nodes/widget/PxeSingleLineTextWidgetNode.tsx b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/nodes/widget/PxeSingleLineTextWidgetNode.tsx index 9a815266..de70a668 100644 --- a/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/nodes/widget/PxeSingleLineTextWidgetNode.tsx +++ b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/nodes/widget/PxeSingleLineTextWidgetNode.tsx @@ -15,34 +15,36 @@ */ import { TextField } from '@material-ui/core'; -import { get } from 'lodash'; import React from 'react'; import { PxeSingleLineTextWidgetEntry } from '../../types/PxeConfiguration.types'; import { PxeParametricEditorNodeProps } from '../../PxeParametricEditorNode'; +import { withCurrentValues } from '../../utils/rendering/withCurrentValues'; import { generateValueLabel } from '../../utils/generateLabelsForWidgets'; +import { useDiagnostics } from '../../PxeDiagnosticsContext'; -export const PxeSingleLineTextWidgetNode: React.FC = ({ - configurationEntry, - onResourceChangeRequest, - resourceChunk, -}) => { - const { - textFilter, - values: [valueDescriptor], - } = configurationEntry as PxeSingleLineTextWidgetEntry; +export const PxeSingleLineTextWidgetNode: React.FC = withCurrentValues( + ({ configurationEntry, onResourceChangeRequest, currentValues: [currentValue] }) => { + useDiagnostics(configurationEntry); - return ( - { - onResourceChangeRequest({ - valueDescriptor, - newValue: textFilter(e.target.value), - }); - }} - fullWidth - /> - ); -}; + const { + textFilter, + valueDescriptors: [valueDescriptor], + } = configurationEntry as PxeSingleLineTextWidgetEntry; + + return ( + { + onResourceChangeRequest({ + valueDescriptor, + newValue: textFilter(e.target.value), + }); + }} + fullWidth + /> + ); + }, +); diff --git a/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/nodes/widget/roster/PxeRosterItem.tsx b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/nodes/widget/roster/PxeRosterItem.tsx new file mode 100644 index 00000000..2630fc94 --- /dev/null +++ b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/nodes/widget/roster/PxeRosterItem.tsx @@ -0,0 +1,92 @@ +/** + * Copyright 2024 The Nephio Authors. + * + * Licensed 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 { Button, makeStyles } from '@material-ui/core'; +import DeleteIcon from '@material-ui/icons/Delete'; +import { isEqual } from 'lodash'; +import React from 'react'; +import { IconButton } from '../../../../../../Controls'; +import { PxeParametricEditorNode } from '../../../PxeParametricEditorNode'; +import { PxeConfigurationEntry, PxeNodeType, PxeValueDescriptor } from '../../../types/PxeConfiguration.types'; +import { generateValueLabel } from '../../../utils/generateLabelsForWidgets'; +import { isSectionNode } from '../../../utils/nodePredicates'; +import { PxeResourceChangeRequest } from '../../../types/PxeParametricEditor.types'; +import { useEditorStyles } from '../../../../FirstClassEditors/styles'; + +type PxeRosterItemProps = { + readonly rosterValueDescriptor: PxeValueDescriptor; + readonly itemIndex: number; + readonly entries: readonly PxeConfigurationEntry[]; + readonly onResourceChangeRequestForItem: (itemIndex: number, changeRequest: PxeResourceChangeRequest) => void; + readonly onItemDeletion: (itemIndex: number) => void; +}; + +export const PxeRosterItem: React.FC = React.memo( + ({ + rosterValueDescriptor, + itemIndex, + entries, + onResourceChangeRequestForItem: handleResourceChangeRequestForItem, + onItemDeletion: handleItemDeletion, + }) => { + const editorClasses = useEditorStyles(); + const rosterClasses = useStyles(); + + return ( +
+
+ handleResourceChangeRequestForItem(itemIndex, changeRequest)} + > + {isSectionNode(entries[0]) && ( + + )} + +
+ {entries[0].type !== PxeNodeType.Section && ( +
+ handleItemDeletion(itemIndex)} + > + + +
+ )} +
+ ); + }, + isEqual, +); + +const useStyles = makeStyles(() => ({ + item: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + }, + itemContent: { + flex: '1 1 auto', + }, + itemActions: { + flex: '0 0 auto', + paddingLeft: '16px', + }, +})); diff --git a/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/nodes/widget/roster/PxeRosterWidgetNode.tsx b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/nodes/widget/roster/PxeRosterWidgetNode.tsx new file mode 100644 index 00000000..97ff162e --- /dev/null +++ b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/nodes/widget/roster/PxeRosterWidgetNode.tsx @@ -0,0 +1,138 @@ +/** + * Copyright 2024 The Nephio Authors. + * + * Licensed 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 { Button } from '@material-ui/core'; +import AddIcon from '@material-ui/icons/Add'; +import React, { useState } from 'react'; +import { PxeParametricEditorNodeProps } from '../../../PxeParametricEditorNode'; +import { PxeRosterWidgetEntry, PxeValueType } from '../../../types/PxeConfiguration.types'; +import { PxeResourceChangeRequest, PxeValue } from '../../../types/PxeParametricEditor.types'; +import { createResourceChunkAfterChangeRequest } from '../../../utils/createResourceChunkAfterChangeRequest'; +import { generateValueLabel } from '../../../utils/generateLabelsForWidgets'; +import { arrayWithItemRemoved, arrayWithItemReplaced } from '../../../utils/general/immutableArrays'; +import { defaultValueForType } from '../../../utils/defaultValueForType'; +import { PxeResourceContext } from '../../../PxeResourceContext'; +import { useDiagnostics } from '../../../PxeDiagnosticsContext'; +import { withCurrentValues } from '../../../utils/rendering/withCurrentValues'; +import { PxeRosterItem } from './PxeRosterItem'; + +type RosterItemResourceChunk = { + readonly $key: string; + readonly $value: PxeValue; +}; + +type RosterValueType = PxeValueType.Object | PxeValueType.Array; + +// TODO Rework roster - instead of being index-based use temp ids for items. +export const PxeRosterWidgetNode: React.FC = withCurrentValues( + ({ configurationEntry, onResourceChangeRequest, currentValues: [currentValue] }) => { + useDiagnostics(configurationEntry); + + const { + valueDescriptors: [valueDescriptor], + itemValueDescriptor: itemValueDescriptor, + itemEntries, + } = configurationEntry as PxeRosterWidgetEntry; + const rosterValueType = valueDescriptor.type as RosterValueType; + + const [itemChunks, setItemChunks] = useState(itemChunksFromValue(currentValue)); + + const [previousResourceChunk, setPreviousResourceChunk] = useState(currentValue); + if (previousResourceChunk !== currentValue) { + setItemChunks(itemChunksFromValue(currentValue)); + setPreviousResourceChunk(currentValue); + } + + const handleItemAddition = () => { + const newItemChunk: RosterItemResourceChunk = { + $key: rosterValueType === PxeValueType.Array ? String(itemChunks.length) : '', + $value: itemValueDescriptor.isRequired ? defaultValueForType(itemValueDescriptor.type) : null, + }; + + const newValue = valueFromItemChunks([...itemChunks, newItemChunk], rosterValueType); + onResourceChangeRequest({ valueDescriptor, newValue }); + }; + + const handleItemDeletion = (itemIndex: number) => { + const newValue = valueFromItemChunks(arrayWithItemRemoved(itemChunks, itemIndex), rosterValueType); + onResourceChangeRequest({ valueDescriptor, newValue }); + }; + + const handleResourceChangeRequestForItem = (itemIndex: number, changeRequest: PxeResourceChangeRequest) => { + const newItemChunk = createResourceChunkAfterChangeRequest( + itemChunks[itemIndex], + changeRequest, + ) as RosterItemResourceChunk; + + const newItemChunks = arrayWithItemReplaced(itemChunks, itemIndex, newItemChunk); + const newValue = valueFromItemChunks(newItemChunks, rosterValueType); + if (Object.values(newValue).length === itemChunks.length) { + onResourceChangeRequest({ valueDescriptor, newValue }); + } else { + setItemChunks(newItemChunks); + } + }; + + const isAddButtonEnabled = rosterValueType === PxeValueType.Array || itemChunks.every(({ $key }) => $key !== ''); + + if (itemEntries.length === 0) { + return null; + } else if (itemEntries.length > 1) { + // TODO With proper styling this should be actually possible. Reconsider handling this. + throw new Error('Roster Widget does not support multiple configuration entries per item.'); + } else { + return ( + <> + {itemChunks.map((itemChunk, itemIndex) => ( + + + + ))} + + + ); + } + }, +); + +const itemChunksFromValue = (value: PxeValue): readonly RosterItemResourceChunk[] => + Object.entries(value ?? {}).map(([itemKey, itemValue]) => ({ + $key: itemKey, + $value: itemValue as PxeValue, + })); + +const valueFromItemChunks = ( + itemChunks: readonly RosterItemResourceChunk[], + rosterType: RosterValueType, +): object | readonly any[] => + rosterType === PxeValueType.Object + ? Object.fromEntries(itemChunks.map(itemChunk => [itemChunk.$key, itemChunk.$value])) + : itemChunks.map(itemChunk => itemChunk.$value); diff --git a/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/types/PxeConfiguration.types.ts b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/types/PxeConfiguration.types.ts index 1c50f769..c0e8ba07 100644 --- a/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/types/PxeConfiguration.types.ts +++ b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/types/PxeConfiguration.types.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { PxeValue } from './PxeParametricEditor.types'; + import { TextFilter } from '../validation/textFilters'; export type PxeConfiguration = { @@ -25,7 +25,7 @@ export type PxeConfiguration = { export type PxeConfigurationEntry = PxeSectionEntry | PxeRowLayoutEntry | PxeWidgetEntry; -export enum PxeConfigurationEntryType { +export enum PxeNodeType { Section = 'Section', Roster = 'Roster', @@ -38,7 +38,6 @@ export enum PxeConfigurationEntryType { // Values export enum PxeValueType { - Any = 'Any', String = 'String', Number = 'Number', Object = 'Object', @@ -57,7 +56,7 @@ export type PxeValueDescriptor = { // Section export type PxeSectionEntry = { - readonly type: PxeConfigurationEntryType.Section; + readonly type: PxeNodeType.Section; readonly name: string; readonly entries: readonly PxeConfigurationEntry[]; }; @@ -67,7 +66,7 @@ export type PxeSectionEntry = { export type PxeLayoutEntry = PxeRowLayoutEntry; export interface PxeRowLayoutEntry { - readonly type: PxeConfigurationEntryType.RowLayout; + readonly type: PxeNodeType.RowLayout; readonly entries: readonly PxeConfigurationEntry[]; } @@ -76,26 +75,27 @@ export interface PxeRowLayoutEntry { export type PxeWidgetEntry = PxeRosterWidgetEntry | PxeSingleLineTextWidgetEntry | PxeSelectValueWidgetEntry; type PxeWidgetEntryBase = { - readonly type: PxeConfigurationEntryType; - readonly values: readonly PxeValueDescriptor[]; + readonly type: PxeNodeType; + readonly valueDescriptors: readonly PxeValueDescriptor[]; }; export interface PxeRosterWidgetEntry extends PxeWidgetEntryBase { - readonly type: PxeConfigurationEntryType.Roster; + readonly type: PxeNodeType.Roster; + readonly itemValueDescriptor: PxeValueDescriptor; readonly itemEntries: readonly PxeConfigurationEntry[]; } export interface PxeSingleLineTextWidgetEntry extends PxeWidgetEntryBase { - readonly type: PxeConfigurationEntryType.SingleLineText; + readonly type: PxeNodeType.SingleLineText; readonly textFilter: TextFilter; } export interface PxeSelectValueWidgetEntry extends PxeWidgetEntryBase { - readonly type: PxeConfigurationEntryType.SelectValue; + readonly type: PxeNodeType.SelectValue; readonly options: readonly PxeValueOption[]; } export type PxeValueOption = { - readonly value: PxeValue; + readonly value: string | number | undefined; readonly label: string; }; diff --git a/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/types/PxeDiagnostics.types.ts b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/types/PxeDiagnostics.types.ts new file mode 100644 index 00000000..a2ad02ac --- /dev/null +++ b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/types/PxeDiagnostics.types.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2024 The Nephio Authors. + * + * Licensed 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 { PxeConfigurationEntry } from './PxeConfiguration.types'; + +export type PxeDiagnosticsReporter = { + readonly reportRender: (entry: PxeConfigurationEntry) => void; +}; diff --git a/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/types/PxeParametricEditor.types.ts b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/types/PxeParametricEditor.types.ts index 7b5e6f17..35c4b77b 100644 --- a/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/types/PxeParametricEditor.types.ts +++ b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/types/PxeParametricEditor.types.ts @@ -18,12 +18,13 @@ import { Dispatch, SetStateAction } from 'react'; import { PxeValueDescriptor } from './PxeConfiguration.types'; export type PxeResourceChunk = object; -export type PxeValue = undefined | string | object | readonly any[]; -// TODO Semantically "null" should be used instead of "undefined". -// Consider project-wide refactoring alongside the update of prettier settings. -export type PxeExpandedSectionState = string | undefined; -export type PxeExpandedSectionStateTuple = [PxeExpandedSectionState, Dispatch>]; +// `undefined` = absence of value in Yaml, `null` = null value in Yaml +export type PxeValue = undefined | null | string | number | object | readonly any[]; + +// TODO Semantically "null" should be used instead of "undefined". Consider project-wide refactoring. +export type PxeExpandedSection = string | undefined; +export type PxeExpandedSectionStateTuple = [PxeExpandedSection, Dispatch>]; export type PxeResourceChangeRequest = { readonly valueDescriptor: PxeValueDescriptor; diff --git a/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/createResourceChunkAfterChangeRequest.ts b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/createResourceChunkAfterChangeRequest.ts index 91febf37..51651ed3 100644 --- a/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/createResourceChunkAfterChangeRequest.ts +++ b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/createResourceChunkAfterChangeRequest.ts @@ -17,6 +17,7 @@ import { cloneDeep, set, unset } from 'lodash'; import { PxeResourceChangeRequest, PxeResourceChunk } from '../types/PxeParametricEditor.types'; import { isEmptyPxeValue } from './isEmptyPxeValue'; +import { defaultValueForType } from './defaultValueForType'; export const createResourceChunkAfterChangeRequest = ( resourceChunk: PxeResourceChunk, @@ -27,10 +28,10 @@ export const createResourceChunkAfterChangeRequest = ( // TODO Using cloneDeep may lead to performance issues. Consider different implementation. const newResourceChunk = cloneDeep(resourceChunk); - if (!isEmptyPxeValue(newValue) || isRequired) { - set(newResourceChunk, path, newValue); - } else { + if (isEmptyPxeValue(newValue) && !isRequired) { unset(newResourceChunk, path); + } else { + set(newResourceChunk, path, newValue ?? defaultValueForType(valueDescriptor.type)); } return newResourceChunk; diff --git a/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/defaultValueForType.ts b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/defaultValueForType.ts new file mode 100644 index 00000000..c4c98bbd --- /dev/null +++ b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/defaultValueForType.ts @@ -0,0 +1,33 @@ +/** + * Copyright 2024 The Nephio Authors. + * + * Licensed 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 { PxeValueType } from '../types/PxeConfiguration.types'; +import { PxeValue } from '../types/PxeParametricEditor.types'; + +export const defaultValueForType = (valueType: PxeValueType): PxeValue => { + switch (valueType) { + case PxeValueType.String: + return ''; + case PxeValueType.Number: + return 0; + case PxeValueType.Object: + return {}; + case PxeValueType.Array: + return []; + default: + throw new Error(`Unsupported value type ${valueType}`); + } +}; diff --git a/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/findInConfigurationEntries.ts b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/findInConfigurationEntries.ts new file mode 100644 index 00000000..cf1845d3 --- /dev/null +++ b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/findInConfigurationEntries.ts @@ -0,0 +1,65 @@ +/** + * Copyright 2024 The Nephio Authors. + * + * Licensed 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 { isArray } from 'lodash'; +import { PxeConfigurationEntry } from '../types/PxeConfiguration.types'; +import { isLayoutNode, isSectionNode } from './nodePredicates'; + +type SearchOptions = { + readonly searchInLayouts?: boolean; + readonly searchInSections?: boolean; +}; + +export const findInConfigurationEntries = ( + searchIn: PxeConfigurationEntry | readonly PxeConfigurationEntry[], + isSuccess: (entry: PxeConfigurationEntry) => boolean, + { searchInLayouts = true, searchInSections = true }: SearchOptions = {}, +): PxeConfigurationEntry | null => { + const options = { searchInLayouts, searchInSections }; + const entries: PxeConfigurationEntry[] = isArray(searchIn) ? searchIn : [searchIn]; + + for (const entry of entries) { + if (isSuccess(entry)) { + return entry; + } else if ((isLayoutNode(entry) && searchInLayouts) || (isSectionNode(entry) && searchInSections)) { + const foundEntry = findInConfigurationEntries(entry.entries, isSuccess, options); + if (foundEntry) { + return foundEntry; + } + } + } + return null; +}; + +export const findAllInConfigurationEntries = ( + searchIn: PxeConfigurationEntry | readonly PxeConfigurationEntry[], + isSuccess: (entry: PxeConfigurationEntry) => boolean, + { searchInLayouts = true, searchInSections = true }: SearchOptions = {}, +): PxeConfigurationEntry[] => { + const options = { searchInLayouts, searchInSections }; + const entries: PxeConfigurationEntry[] = isArray(searchIn) ? searchIn : [searchIn]; + + const foundEntries: PxeConfigurationEntry[] = []; + for (const entry of entries) { + if (isSuccess(entry)) { + foundEntries.push(entry); + } + if ((isLayoutNode(entry) && searchInLayouts) || (isSectionNode(entry) && searchInSections)) { + foundEntries.push(...findAllInConfigurationEntries(entry.entries, isSuccess, options)); + } + } + return foundEntries; +}; diff --git a/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/generateLabelsForWidgets.ts b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/generateLabelsForWidgets.ts index 05e5fc75..7d00f3b9 100644 --- a/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/generateLabelsForWidgets.ts +++ b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/generateLabelsForWidgets.ts @@ -16,24 +16,15 @@ import * as changeCase from 'change-case'; import { get, size } from 'lodash'; -import { - PxeConfigurationEntryType, - PxeSectionEntry, - PxeValueDescriptor, - PxeValueType, - PxeWidgetEntry, -} from '../types/PxeConfiguration.types'; +import { PxeValueDescriptor, PxeValueType, PxeWidgetEntry } from '../types/PxeConfiguration.types'; import { PxeResourceChunk } from '../types/PxeParametricEditor.types'; -import { isEmptyPxeValue } from './isEmptyPxeValue'; import { upperCaseFirstLetter } from './general/stringCasing'; +import { isEmptyPxeValue } from './isEmptyPxeValue'; const FALLBACK_DEFAULT_VALUE_NAME = 'Value'; -export const generateSectionDescription = (sectionEntry: PxeSectionEntry, resourceChunk: PxeResourceChunk): string => - sectionEntry.entries - .filter(childEntry => childEntry.type !== PxeConfigurationEntryType.Section) - .flatMap(childEntry => generateValueDescriptionsForWidget(childEntry as PxeWidgetEntry, resourceChunk)) - .join(', '); +export const generateDescriptionForEntries = (entries: PxeWidgetEntry[], resourceChunk: PxeResourceChunk): string => + entries.flatMap(widgetEntry => generateValueDescriptionsForWidget(widgetEntry, resourceChunk)).join(', '); export const generateValueLabel = (valueDescriptor: PxeValueDescriptor, uppercase: boolean = true): string => { if (valueDescriptor.display?.name) { @@ -47,7 +38,7 @@ export const generateValueLabel = (valueDescriptor: PxeValueDescriptor, uppercas }; const generateValueDescriptionsForWidget = (widgetEntry: PxeWidgetEntry, resourceChunk: PxeResourceChunk): string[] => - widgetEntry.values + widgetEntry.valueDescriptors .map(valueDescriptor => generateValueDescription(valueDescriptor, resourceChunk)) .filter(segment => segment !== null) as string[]; diff --git a/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/nodePredicates.ts b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/nodePredicates.ts new file mode 100644 index 00000000..d55655ae --- /dev/null +++ b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/nodePredicates.ts @@ -0,0 +1,31 @@ +/** + * Copyright 2024 The Nephio Authors. + * + * Licensed 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 { + PxeConfigurationEntry, + PxeLayoutEntry, + PxeNodeType, + PxeSectionEntry, + PxeWidgetEntry, +} from '../types/PxeConfiguration.types'; + +export const isSectionNode = (entry: PxeConfigurationEntry): entry is PxeSectionEntry => + entry.type === PxeNodeType.Section; + +export const isLayoutNode = (entry: PxeConfigurationEntry): entry is PxeLayoutEntry => + [PxeNodeType.RowLayout].includes(entry.type); + +export const isWidgetNode = (entry: PxeConfigurationEntry): entry is PxeWidgetEntry => 'valueDescriptors' in entry; diff --git a/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/rendering/withCurrentValues.tsx b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/rendering/withCurrentValues.tsx new file mode 100644 index 00000000..222d3930 --- /dev/null +++ b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/rendering/withCurrentValues.tsx @@ -0,0 +1,35 @@ +/** + * Copyright 2024 The Nephio Authors. + * + * Licensed 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 { get, isEqual } from 'lodash'; +import React, { useContext } from 'react'; +import { PxeValue } from '../../types/PxeParametricEditor.types'; +import { PxeParametricEditorNodeProps } from '../../PxeParametricEditorNode'; +import { PxeResourceContext } from '../../PxeResourceContext'; +import { PxeWidgetEntry } from '../../types/PxeConfiguration.types'; + +export const withCurrentValues = ( + Component: React.FC, +) => { + const MemoizedComponent = React.memo(Component, isEqual); + return (props: PxeParametricEditorNodeProps) => { + const { valueDescriptors } = props.configurationEntry as PxeWidgetEntry; + const resource = useContext(PxeResourceContext); + + const currentValues = valueDescriptors.map(({ path }) => get(resource, path)); + return ; + }; +}; diff --git a/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/rendering/withSectionDescription.tsx b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/rendering/withSectionDescription.tsx new file mode 100644 index 00000000..0ccbbbf2 --- /dev/null +++ b/plugins/cad/src/components/ResourceEditorDialog/components/PxeParametricEditor/utils/rendering/withSectionDescription.tsx @@ -0,0 +1,42 @@ +/** + * Copyright 2024 The Nephio Authors. + * + * Licensed 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 { isEqual } from 'lodash'; +import React, { useContext, useMemo } from 'react'; +import { PxeParametricEditorNodeProps } from '../../PxeParametricEditorNode'; +import { PxeResourceContext } from '../../PxeResourceContext'; +import { PxeSectionEntry, PxeWidgetEntry } from '../../types/PxeConfiguration.types'; +import { findAllInConfigurationEntries } from '../findInConfigurationEntries'; +import { isWidgetNode } from '../nodePredicates'; +import { generateDescriptionForEntries } from '../generateLabelsForWidgets'; + +export const withSectionDescription = ( + Component: React.FC, +) => { + const MemoizedComponent = React.memo(Component, isEqual); + return (props: PxeParametricEditorNodeProps) => { + const { entries } = props.configurationEntry as PxeSectionEntry; + const resource = useContext(PxeResourceContext); + + const entriesForDescription = useMemo( + () => findAllInConfigurationEntries(entries, entry => isWidgetNode(entry), { searchInSections: false }), + [entries], + ) as PxeWidgetEntry[]; + const sectionDescription = generateDescriptionForEntries(entriesForDescription, resource); + + return ; + }; +};