diff --git a/docs/migrations/frontend-components-config.md b/docs/migrations/frontend-components-config.md new file mode 100644 index 000000000..11dbc2430 --- /dev/null +++ b/docs/migrations/frontend-components-config.md @@ -0,0 +1,7 @@ +# Frontend components config migrations + +## 6.3.x -> 6.4.x + +The configuration is now using data from the Frontend CRD. The Frontend CRD is now mandatory to use the shared build configuration. + +The default location of the CRD is **deploy/frontend.yaml**. If the CRD is not located at this path in your frontend repository, please use the `frontendCRDPath` configuration option in the `fec.config.js` or as an attribute of the create config function if you are using custom webpack configuration. diff --git a/docs/migrations/frontend-coomponents-config-utilities.md b/docs/migrations/frontend-coomponents-config-utilities.md new file mode 100644 index 000000000..af9180707 --- /dev/null +++ b/docs/migrations/frontend-coomponents-config-utilities.md @@ -0,0 +1,7 @@ +# Frontend components config utilities + +# 4.0.x -> 4.1.x + +The proxy configuration is now using data from the Frontend CRD. The Frontend CRD is now mandatory to use the development proxy. + +The default location of the CRD is **deploy/frontend.yaml**. If the CRD is not located at this path in your frontend repository, please use the `frontendCRDPath` as an attribute of the proxy function if you are using custom webpack configuration. diff --git a/navnotes.md b/navnotes.md new file mode 100644 index 000000000..289e27af0 --- /dev/null +++ b/navnotes.md @@ -0,0 +1,48 @@ +# Nav notes updates + +## Current attributes + +### appId + +For some reason nav items in expandable item require `appId` to show. This should not be required a it needs to b fixed in chrome: https://github.com/RedHatInsights/insights-chrome/blob/master/src/components/Navigation/ChromeNavExpandable.tsx#L7 + +### id + +Id should be mandatory attribute of any non segment nav item + + +## Missing FEO nav attributes + +### bundleSegmentRef + +Required to match nav item to bundle segment from frontend crd. + +Nav items should inherit this from the bundle segment they come from. + +Should be needed only by the first level. + +### segmentRef + +Same as `bundleSegmentRef`, but for global segments. + +### frontendRef + +Required to match nav item in bundle to current app + +# Search interceptor notes + +## frontendRef + +search entries need a `frontendRef` attribute. Without the attribute, we can modify/add frontend entries, but we can't remove them + +# Service tiles interceptor + +## frontendRef + +Service tile entries need a `frontendRef` attribute. Without the attribute, we can modify/add frontend entries, but we can't remove them + +# Widget registry interceptor + +## frontendRef + +Widget registry entries need a `frontendRef` attribute. Without the attribute, we can modify/add frontend entries, but we can't remove them diff --git a/nx.json b/nx.json index 492ea24d2..88dad6699 100644 --- a/nx.json +++ b/nx.json @@ -20,7 +20,11 @@ "inputs": ["default", "{workspaceRoot}/.eslintrc.js", "{workspaceRoot}/.eslintignore", "{workspaceRoot}/eslint.config.js"] }, "test:unit": { - "dependsOn": ["^test"], + "dependsOn": ["^test:unit"], + "cache": true + }, + "test:component": { + "dependsOn": ["^test:component"], "cache": true }, "version": { diff --git a/package-lock.json b/package-lock.json index 961ea6dc8..d769b0723 100644 --- a/package-lock.json +++ b/package-lock.json @@ -104,7 +104,7 @@ "ts-jest": "^29.2.5", "ts-patch": "^3.2.1", "typescript": "^5.6.3", - "vite": "^5.4.10", + "vite": "^5.4.14", "vitest": "^1.6.0", "whatwg-fetch": "^3.6.20" }, @@ -34833,9 +34833,9 @@ } }, "node_modules/vite": { - "version": "5.4.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz", - "integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==", + "version": "5.4.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", + "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", "dev": true, "license": "MIT", "dependencies": { @@ -36617,7 +36617,7 @@ }, "packages/advisor-components": { "name": "@redhat-cloud-services/frontend-components-advisor-components", - "version": "2.0.8", + "version": "2.0.9", "license": "Apache-2.0", "dependencies": { "@redhat-cloud-services/frontend-components": "^5.0.1", @@ -37106,7 +37106,7 @@ }, "packages/components": { "name": "@redhat-cloud-services/frontend-components", - "version": "5.1.2", + "version": "5.1.3", "license": "Apache-2.0", "dependencies": { "@patternfly/react-component-groups": "^5.5.5", @@ -37708,7 +37708,7 @@ }, "packages/config": { "name": "@redhat-cloud-services/frontend-components-config", - "version": "6.3.6", + "version": "6.3.8", "license": "Apache-2.0", "dependencies": { "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", @@ -37764,11 +37764,13 @@ }, "packages/config-utils": { "name": "@redhat-cloud-services/frontend-components-config-utilities", - "version": "4.0.4", + "version": "4.0.6", "license": "Apache-2.0", "dependencies": { "@openshift/dynamic-plugin-sdk-webpack": "^4.0.1", + "ajv": "^8.17.1", "chalk": "^4.1.2", + "js-yaml": "^4.1.0", "node-fetch": "2.6.7" }, "devDependencies": { @@ -37873,6 +37875,18 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "packages/config-utils/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "packages/config-utils/node_modules/node-fetch": { "version": "2.6.7", "license": "MIT", @@ -38878,7 +38892,7 @@ }, "packages/notifications": { "name": "@redhat-cloud-services/frontend-components-notifications", - "version": "4.1.9", + "version": "4.1.10", "license": "Apache-2.0", "dependencies": { "@redhat-cloud-services/frontend-components": "^5.0.5", @@ -38971,7 +38985,7 @@ }, "packages/remediations": { "name": "@redhat-cloud-services/frontend-components-remediations", - "version": "3.2.20", + "version": "3.2.21", "license": "Apache-2.0", "dependencies": { "@data-driven-forms/pf4-component-mapper": "^3.21.0", @@ -38993,7 +39007,7 @@ }, "packages/rule-components": { "name": "@redhat-cloud-services/rule-components", - "version": "3.2.17", + "version": "3.2.18", "license": "Apache-2.0", "dependencies": { "@patternfly/react-core": "^5.0.0", diff --git a/package.json b/package.json index 309223c90..4387f9b5a 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "ts-jest": "^29.2.5", "ts-patch": "^3.2.1", "typescript": "^5.6.3", - "vite": "^5.4.10", + "vite": "^5.4.14", "vitest": "^1.6.0", "whatwg-fetch": "^3.6.20" }, diff --git a/packages/advisor-components/CHANGELOG.md b/packages/advisor-components/CHANGELOG.md index 961cbce3a..43c2c260f 100644 --- a/packages/advisor-components/CHANGELOG.md +++ b/packages/advisor-components/CHANGELOG.md @@ -2,6 +2,21 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [2.0.11](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-advisor-components-2.0.10...@redhat-cloud-services/frontend-components-advisor-components-2.0.11) (2025-01-27) + +### Dependency Updates + +* `@redhat-cloud-services/frontend-components` updated to version `5.2.1` + +### Bug Fixes + +* **build:** fix release postTarget nested dependencies ([4895cd2](https://github.com/RedHatInsights/frontend-components/commit/4895cd2eba32336a220ddec442916858400ebb3e)) + +## [2.0.10](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-advisor-components-2.0.9...@redhat-cloud-services/frontend-components-advisor-components-2.0.10) (2025-01-23) + +### Dependency Updates + +* `@redhat-cloud-services/frontend-components` updated to version `5.2.0` ## [2.0.9](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-advisor-components-2.0.8...@redhat-cloud-services/frontend-components-advisor-components-2.0.9) (2025-01-16) ### Dependency Updates diff --git a/packages/advisor-components/package.json b/packages/advisor-components/package.json index d66e68d2f..6371abe11 100644 --- a/packages/advisor-components/package.json +++ b/packages/advisor-components/package.json @@ -1,6 +1,6 @@ { "name": "@redhat-cloud-services/frontend-components-advisor-components", - "version": "2.0.9", + "version": "2.0.11", "description": "Components to be used in Advisor applications and integrations.", "main": "index.js", "module": "esm/index.js", diff --git a/packages/advisor-components/project.json b/packages/advisor-components/project.json index a7f93a344..74c4e1d32 100644 --- a/packages/advisor-components/project.json +++ b/packages/advisor-components/project.json @@ -18,14 +18,14 @@ }, "build:styles": { "executor": "@redhat-cloud-services/frontend-components-executors:build-styles", - "dependsOn": ["^build:styles", "build:bundles"], + "dependsOn": ["^build:styles"], "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components-advisor-components", "sourceDir": "packages/advisor-components" } }, "build:packages": { - "dependsOn": ["^build:packages", "build:bundles"], + "dependsOn": ["^build:packages"], "executor": "@redhat-cloud-services/frontend-components-executors:build-packages", "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components-advisor-components", @@ -33,7 +33,7 @@ } }, "transform:scss": { - "dependsOn": ["^transform:scss", "build:bundles"], + "dependsOn": ["^transform:scss"], "executor": "@redhat-cloud-services/frontend-components-executors:transform-scss", "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components-advisor-components" @@ -46,8 +46,12 @@ } }, "build": { - "executor": "nx:noop", - "dependsOn": ["^build", "build:styles", "build:packages", "transform:scss"] + "executor": "nx:run-commands", + "options": { + "parallel": false, + "commands": ["nx run @redhat-cloud-services/frontend-components-advisor-components:build:bundles", "nx run @redhat-cloud-services/frontend-components-advisor-components:build:styles", "nx run @redhat-cloud-services/frontend-components-advisor-components:build:packages", "nx run @redhat-cloud-services/frontend-components-advisor-components:transform:scss"] + }, + "dependsOn": ["^build"] }, "lint": { "executor": "@nx/eslint:lint", diff --git a/packages/chrome/CHANGELOG.md b/packages/chrome/CHANGELOG.md index b4a811153..81b94b217 100644 --- a/packages/chrome/CHANGELOG.md +++ b/packages/chrome/CHANGELOG.md @@ -2,6 +2,13 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [1.0.16](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/chrome-1.0.15...@redhat-cloud-services/chrome-1.0.16) (2025-01-27) + + +### Bug Fixes + +* **build:** fix release postTarget nested dependencies ([4895cd2](https://github.com/RedHatInsights/frontend-components/commit/4895cd2eba32336a220ddec442916858400ebb3e)) + ## [1.0.15](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/chrome-1.0.14...@redhat-cloud-services/chrome-1.0.15) (2025-01-15) diff --git a/packages/chrome/package.json b/packages/chrome/package.json index bcfe97e15..515490ea7 100644 --- a/packages/chrome/package.json +++ b/packages/chrome/package.json @@ -1,6 +1,6 @@ { "name": "@redhat-cloud-services/chrome", - "version": "1.0.15", + "version": "1.0.16", "description": "Chrome functions for RedHat Hybrid cloud console.", "main": "index.js", "typings": "index.d.ts", diff --git a/packages/chrome/project.json b/packages/chrome/project.json index 854e6695b..68e221db6 100644 --- a/packages/chrome/project.json +++ b/packages/chrome/project.json @@ -17,7 +17,7 @@ } }, "build:packages": { - "dependsOn": ["^build:packages", "build:bundles"], + "dependsOn": ["^build:packages"], "executor": "@redhat-cloud-services/frontend-components-executors:build-packages", "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components", @@ -31,8 +31,12 @@ } }, "build": { - "executor": "nx:noop", - "dependsOn": ["^build", "build:packages"] + "executor": "nx:run-commands", + "options": { + "parallel": false, + "commands": ["nx run @redhat-cloud-services/chrome:build:bundles", "nx run @redhat-cloud-services/chrome:build:packages"] + }, + "dependsOn": ["^build"] }, "lint": { "executor": "@nx/eslint:lint", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 397fe68db..a49ad3dc6 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,23 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [5.2.1](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-5.2.0...@redhat-cloud-services/frontend-components-5.2.1) (2025-01-27) + +### Dependency Updates + +* `@redhat-cloud-services/frontend-components-utilities` updated to version `5.0.8` + +### Bug Fixes + +* **build:** fix release postTarget nested dependencies ([4895cd2](https://github.com/RedHatInsights/frontend-components/commit/4895cd2eba32336a220ddec442916858400ebb3e)) + +## [5.2.0](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-5.1.3...@redhat-cloud-services/frontend-components-5.2.0) (2025-01-23) + + +### Features + +* add single select filter component ([00bcfd8](https://github.com/RedHatInsights/frontend-components/commit/00bcfd816dfe0413bf1e16315c6401b054900fb1)) + ## [5.1.3](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-5.1.2...@redhat-cloud-services/frontend-components-5.1.3) (2025-01-16) diff --git a/packages/components/doc/conditionalFilter.md b/packages/components/doc/conditionalFilter.md index ff6e60e4b..0f5db302f 100644 --- a/packages/components/doc/conditionalFilter.md +++ b/packages/components/doc/conditionalFilter.md @@ -294,7 +294,7 @@ class SomeCmp extends Component { } ``` -### *) Custom component +### 5) Custom component If you want to display some custom component, for instance color picker, date picker or something more complicated you can use this type. ```JSX @@ -321,3 +321,51 @@ class SomeCmp extends Component { { children: Proptypes.node } +``` + +### 6) Single select component +This component is similiar to `Radio` with a slight variation that you can select only one value. Props passed to this component are same as with `Radio`. +```JSX +import React, { Component, useState } from 'react'; +import { ConditionalFilter, conditionalFilterType } from '@redhat-cloud-services/frontend-components'; + +class SomeCmp extends Component { + render() { + const [ value, onChange ] = useState(); + return ( + <ConditionalFilter items={[{ + type: conditionalFilterType.singleSelect, + label: 'Single Select', + value: 'singleSelect', + filterValues: { + onChange: (event, value) => onChange(value), + value, + items: [ + { label: 'First value', value: 'first' }, + { label: 'Second value', value: 'second' }, + { label: 'Third value', value: 'third' } + ], + placeholder: 'placeholder' + } + }]} + /> + ); + } +} +``` +* `onChange` - callback has parameters `event`, `selection` where `selection` is curently selected value. +* Props - passed from `filterValues` +```JS +{ + onChange: PropTypes.func, + value: PropTypes.oneOfType([ PropTypes.string, PropTypes.shape({ + label: PropTypes.node, + value: PropTypes.string + }) ]), + placeholder: PropTypes.string, + items: PropTypes.arrayOf(PropTypes.shape({ + value: PropTypes.string, + label: PropTypes.node + })) +} +``` \ No newline at end of file diff --git a/packages/components/doc/filters.md b/packages/components/doc/filters.md index 453a42ed1..2491bea27 100644 --- a/packages/components/doc/filters.md +++ b/packages/components/doc/filters.md @@ -9,7 +9,7 @@ Import FilterInput from this package. The `type` of input can be `radio` or `che ```JSX import React from 'react'; -import { FilterInput } from '@redhat-cloud-services/frontend-components'; +import { FilterInput } from '@redhat-cloud-services/frontend-components/Filters'; class YourCmp extends React.Component { render() { @@ -38,7 +38,7 @@ Import FilterDropdown from this package. ```JSX import React from 'react'; -import { FilterDropdown } from '@redhat-cloud-services/frontend-components'; +import { FilterDropdown } from '@redhat-cloud-services/frontend-components/Filters'; class YourCmp extends React.Component { render() { @@ -53,4 +53,3 @@ class YourCmp extends React.Component { } } ``` - diff --git a/packages/components/package.json b/packages/components/package.json index 231c0f0ed..86761fdef 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@redhat-cloud-services/frontend-components", - "version": "5.1.3", + "version": "5.2.1", "description": "Common components for RedHat Cloud Services project.", "main": "index.js", "module": "esm/index.js", diff --git a/packages/components/project.json b/packages/components/project.json index 89e2e4a66..a2b5bcaa7 100644 --- a/packages/components/project.json +++ b/packages/components/project.json @@ -18,14 +18,14 @@ }, "build:styles": { "executor": "@redhat-cloud-services/frontend-components-executors:build-styles", - "dependsOn": ["^build:styles", "build:bundles"], + "dependsOn": ["^build:styles"], "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components", "sourceDir": "packages/components" } }, "build:packages": { - "dependsOn": ["^build:packages", "build:bundles"], + "dependsOn": ["^build:packages"], "executor": "@redhat-cloud-services/frontend-components-executors:build-packages", "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components", @@ -33,7 +33,7 @@ } }, "transform:scss": { - "dependsOn": ["^transform:scss", "build:bundles"], + "dependsOn": ["^transform:scss"], "executor": "@redhat-cloud-services/frontend-components-executors:transform-scss", "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components" @@ -46,8 +46,12 @@ } }, "build": { - "executor": "nx:noop", - "dependsOn": ["^build", "build:styles", "build:packages", "transform:scss"] + "executor": "nx:run-commands", + "options": { + "parallel": false, + "commands": ["nx run @redhat-cloud-services/frontend-components:build:bundles", "nx run @redhat-cloud-services/frontend-components:build:styles", "nx run @redhat-cloud-services/frontend-components:build:packages", "nx run @redhat-cloud-services/frontend-components:transform:scss"] + }, + "dependsOn": ["^build"] }, "lint": { "executor": "@nx/eslint:lint", diff --git a/packages/components/src/ConditionalFilter/ConditionalFilter.tsx b/packages/components/src/ConditionalFilter/ConditionalFilter.tsx index 535da6dff..d2388ee16 100644 --- a/packages/components/src/ConditionalFilter/ConditionalFilter.tsx +++ b/packages/components/src/ConditionalFilter/ConditionalFilter.tsx @@ -19,6 +19,7 @@ import RadioFilter, { RadioFilterProps } from './RadioFilter'; import CheckboxFilter, { CheckboxFilterProps } from './CheckboxFilter'; import GroupFilter, { GroupFilterProps } from './GroupFilter'; import './conditional-filter.scss'; +import SingleSelectFilter, { SingleSelectFilterProps } from './SingleSelectFilter'; export type FilterValues = TextInputProps & RadioFilterProps & @@ -65,6 +66,10 @@ export type ConditionalFilterItem = { type: 'group'; filterValues: GroupFilterProps; } + | { + type: 'singleSelect'; + filterValues: RadioFilterProps; + } | { type: 'custom'; filterValues: Record<string, any>; @@ -142,6 +147,10 @@ const ConditionalFilter: React.FunctionComponent<ConditionalFilterProps> = ({ {...activeItem.filterValues} /> ); + } else if (activeItem.type === 'singleSelect' && identifyComponent<SingleSelectFilterProps>(activeItem.type, activeItem.filterValues)) { + return ( + <SingleSelectFilter placeholder={placeholder || activeItem.placeholder || `Filter by ${activeItem.label}`} {...activeItem.filterValues} /> + ); } else if (activeItem.type === 'custom' && identifyComponent<Record<string, any>>(activeItem.type, activeItem.filterValues)) { const C = typeMapper.custom; return <C {...activeItem.filterValues} />; diff --git a/packages/components/src/ConditionalFilter/SingleSelectFilter.tests.js b/packages/components/src/ConditionalFilter/SingleSelectFilter.tests.js new file mode 100644 index 000000000..ed4106398 --- /dev/null +++ b/packages/components/src/ConditionalFilter/SingleSelectFilter.tests.js @@ -0,0 +1,56 @@ +import React from 'react'; +import userEvent from '@testing-library/user-event'; +import SingleSelectFilter from './SingleSelectFilter'; +import { render, screen, waitFor, within } from '@testing-library/react'; + +const items = [ + { value: 'op1', label: 'option 1' }, + { value: 'op2', label: 'option 2' }, + { value: 'op3', label: 'option 3' }, +]; +const filterId = 'my-filter'; +const setFilterData = jest.fn(); + +describe('SingleSelectFilter component', () => { + it('Should handle select values', async () => { + render(<SingleSelectFilter onChange={setFilterData} items={items} placeholder="placeholder" value={items[0].value} />); + + await waitFor(() => + userEvent.click( + screen.getByRole('button', { + name: /option 1/i, + }) + ) + ); + + const option1 = screen.getByRole('option', { + name: /option 1/i, + }); + const option2 = screen.getByRole('option', { + name: /option 2/i, + }); + const option3 = screen.getByRole('option', { + name: /option 3/i, + }); + + expect( + within(option1).getByRole('img', { + hidden: true, + }) + ).toBeTruthy(); + expect( + within(option2).queryByRole('img', { + hidden: true, + }) + ).toBeFalsy(); + expect( + within(option3).queryByRole('img', { + hidden: true, + }) + ).toBeFalsy(); + + await waitFor(() => userEvent.click(option2)); + + expect(setFilterData).toHaveBeenCalledWith(items[1].value); + }); +}); diff --git a/packages/components/src/ConditionalFilter/SingleSelectFilter.tsx b/packages/components/src/ConditionalFilter/SingleSelectFilter.tsx new file mode 100644 index 000000000..092fba1d1 --- /dev/null +++ b/packages/components/src/ConditionalFilter/SingleSelectFilter.tsx @@ -0,0 +1,88 @@ +import React, { useState } from 'react'; +import { MenuToggle } from '@patternfly/react-core/dist/dynamic/components/MenuToggle'; +import { Select } from '@patternfly/react-core/dist/dynamic/components/Select'; +import { SelectList } from '@patternfly/react-core/dist/dynamic/components/Select'; +import { SelectOption } from '@patternfly/react-core/dist/dynamic/components/Select'; + +import { FilterItem, FilterValue, isFilterValue } from './TextFilter'; + +export interface SingleSelectFilterProps { + /** Optional className. */ + className?: string; + /** Optional disabled flag. */ + isDisabled?: boolean; + /** Optional list of available options. */ + items?: FilterItem[]; + /** Optional onChange event callback. */ + onChange: ( + e: React.MouseEvent | React.ChangeEvent | React.FormEvent<HTMLInputElement> | undefined, + newSelection: string | FilterValue | (string | FilterValue)[], + selection?: string | FilterValue + ) => void; + /** Optional select value placeholder. */ + placeholder?: string; + /** Optional list of selected values. */ + value?: string | FilterValue | Record<string, any>; + /** Input element react ref for TextFilter */ + innerRef?: React.Ref<HTMLInputElement>; +} + +/** + * Component that works as a single select filter for ConditionalFilter component. + * + * It was not designed to be used as a standalone component, but rather within conditionalFilter. + */ +const SingleSelectFilter: React.FunctionComponent<SingleSelectFilterProps> = ({ + items = [], + onChange = () => undefined, + isDisabled = false, + ...props +}) => { + const { placeholder, className, value } = props; + const [isExpanded, setExpanded] = useState(false); + + const calculateSelected = () => { + if (value) { + return isFilterValue(value) ? value.value : value; + } + }; + + const onSelect = (event: React.MouseEvent<Element, MouseEvent> | React.ChangeEvent<Element> | undefined, selection: string | FilterValue) => { + onChange(event, selection); + }; + + const checkedValue = calculateSelected(); + return ( + <Select + className={className} + aria-label="Select Input" + toggle={(menuRef) => ( + <MenuToggle + aria-label="Options menu" + isExpanded={isExpanded} + onClick={() => setExpanded((prev) => !prev)} + isDisabled={isDisabled} + ref={menuRef} + isFullWidth + > + {placeholder} + </MenuToggle> + )} + onOpenChange={(value) => setExpanded(value)} + onSelect={(event, value) => onSelect(event, value as string | FilterValue)} + isOpen={isExpanded} + ouiaId={placeholder} + selected={checkedValue} + > + <SelectList aria-label="Options menu"> + {items.map(({ value, isChecked, onChange, label, id, ...item }, key) => ( + <SelectOption {...item} key={id || key} value={value || '' + key}> + {label} + </SelectOption> + ))} + </SelectList> + </Select> + ); +}; + +export default SingleSelectFilter; diff --git a/packages/components/src/ConditionalFilter/conditionalFilterConstants.test.js b/packages/components/src/ConditionalFilter/conditionalFilterConstants.test.js index 4c70c810e..2b5ef1296 100644 --- a/packages/components/src/ConditionalFilter/conditionalFilterConstants.test.js +++ b/packages/components/src/ConditionalFilter/conditionalFilterConstants.test.js @@ -1,7 +1,7 @@ import { conditionalFilterType, typeMapper } from './conditionalFilterConstants'; it('should have correct types', () => { - expect(Object.values(conditionalFilterType).length).toBe(5); + expect(Object.values(conditionalFilterType).length).toBe(6); }); it('should return correct type', () => { diff --git a/packages/components/src/ConditionalFilter/conditionalFilterConstants.ts b/packages/components/src/ConditionalFilter/conditionalFilterConstants.ts index 2d3f29201..0f25774e2 100644 --- a/packages/components/src/ConditionalFilter/conditionalFilterConstants.ts +++ b/packages/components/src/ConditionalFilter/conditionalFilterConstants.ts @@ -3,6 +3,7 @@ import Text, { TextFilterProps } from './TextFilter'; import Checkbox, { CheckboxFilterProps } from './CheckboxFilter'; import Radio, { RadioFilterProps } from './RadioFilter'; import Group, { GroupFilterProps } from './GroupFilter'; +import SingleSelectFilter, { SingleSelectFilterProps } from './SingleSelectFilter'; export const conditionalFilterType = { text: 'text', @@ -10,6 +11,7 @@ export const conditionalFilterType = { radio: 'radio', custom: 'custom', group: 'group', + singleSelect: 'singleSelect', }; export const typeMapper = { @@ -18,11 +20,11 @@ export const typeMapper = { radio: Radio, custom: Fragment, group: Group, + singleSelect: SingleSelectFilter, }; -export function identifyComponent<T extends TextFilterProps | CheckboxFilterProps | RadioFilterProps | GroupFilterProps | Record<string, any>>( - type: keyof typeof conditionalFilterType, - props: T -): props is T { +export function identifyComponent< + T extends TextFilterProps | CheckboxFilterProps | RadioFilterProps | GroupFilterProps | SingleSelectFilterProps | Record<string, any> +>(type: keyof typeof conditionalFilterType, props: T): props is T { return true; } diff --git a/packages/components/src/ConditionalFilter/index.ts b/packages/components/src/ConditionalFilter/index.ts index 8832d6a44..abb99793e 100644 --- a/packages/components/src/ConditionalFilter/index.ts +++ b/packages/components/src/ConditionalFilter/index.ts @@ -9,6 +9,8 @@ export { default as CheckboxFilter } from './CheckboxFilter'; export type { CheckboxFilterProps } from './CheckboxFilter'; export { default as RadioFilter } from './RadioFilter'; export type { RadioFilterProps } from './RadioFilter'; +export { default as SingleSelectFilter } from './SingleSelectFilter'; +export type { SingleSelectFilterProps } from './SingleSelectFilter'; export { default as TextFilter } from './TextFilter'; export type { TextFilterProps, FilterItem, FilterValue } from './TextFilter'; export { default as GroupType } from './groupType'; diff --git a/packages/config-utils/CHANGELOG.md b/packages/config-utils/CHANGELOG.md index 1a3552d84..9982baa18 100644 --- a/packages/config-utils/CHANGELOG.md +++ b/packages/config-utils/CHANGELOG.md @@ -2,6 +2,20 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [4.1.0](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-config-utilities-4.0.6...@redhat-cloud-services/frontend-components-config-utilities-4.1.0) (2025-01-28) + + +### Features + +* **config-utils:** add navigation bundle interceptor ([b31a445](https://github.com/RedHatInsights/frontend-components/commit/b31a445249b10ae5b77720b484e6a900579f8886)) +* **config-utils:** plugin feo nav interceptors ([1f9fe0f](https://github.com/RedHatInsights/frontend-components/commit/1f9fe0ff5191042c2020ee7bea9488c5b27b3876)) +* **config:** enable frontend CRD validation ([f8f477b](https://github.com/RedHatInsights/frontend-components/commit/f8f477b4798cb12ea7750106845d8813408965fe)) +* **feo:** add module registry interceptor ([8c7e221](https://github.com/RedHatInsights/frontend-components/commit/8c7e22132015e726d314620545a2f5fd724fa39b)) +* **feo:** add search index interceptor ([f9cb5a8](https://github.com/RedHatInsights/frontend-components/commit/f9cb5a831fd63c40f8ddd8111972c946d26e503b)) +* **feo:** add service tiles interceptor ([dbf5f3f](https://github.com/RedHatInsights/frontend-components/commit/dbf5f3f11c59b7eb3b77de7a6ffa0e59141d8ed2)) +* **feo:** add widget rehistry interceptor ([c21466f](https://github.com/RedHatInsights/frontend-components/commit/c21466f3f497d244c50d6cb4784a1bf3af88701a)) +* **feo:** enable crd path configuration option ([3183360](https://github.com/RedHatInsights/frontend-components/commit/3183360c83bcf9226493bd73109eb899de92e92b)) + ## [4.0.6](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-config-utilities-4.0.5...@redhat-cloud-services/frontend-components-config-utilities-4.0.6) (2025-01-16) diff --git a/packages/config-utils/package.json b/packages/config-utils/package.json index b5494c870..369707244 100644 --- a/packages/config-utils/package.json +++ b/packages/config-utils/package.json @@ -1,6 +1,6 @@ { "name": "@redhat-cloud-services/frontend-components-config-utilities", - "version": "4.0.6", + "version": "4.1.0", "description": "Utilities for shared config used in Red Hat Cloud Services project.", "main": "index.js", "types": "index.d.ts", @@ -25,7 +25,9 @@ }, "dependencies": { "@openshift/dynamic-plugin-sdk-webpack": "^4.0.1", + "ajv": "^8.17.1", "chalk": "^4.1.2", + "js-yaml": "^4.1.0", "node-fetch": "2.6.7" }, "devDependencies": { diff --git a/packages/config-utils/project.json b/packages/config-utils/project.json index 562978fa6..65e5516e3 100644 --- a/packages/config-utils/project.json +++ b/packages/config-utils/project.json @@ -55,6 +55,13 @@ "options": { "command": "git push --tags" } - } + }, + "test:unit": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "packages/config-utils/jest.config.ts" + } + }, } } diff --git a/packages/config-utils/src/feo/check-outgoing-requests.ts b/packages/config-utils/src/feo/check-outgoing-requests.ts new file mode 100644 index 000000000..dce10f54a --- /dev/null +++ b/packages/config-utils/src/feo/check-outgoing-requests.ts @@ -0,0 +1,3 @@ +export function matchNavigationRequest(url: string): boolean { + return !!url.match(/\/api\/chrome-service\/v1\/static\/bundles-generated\.json/); +} diff --git a/packages/config-utils/src/feo/crd-check.ts b/packages/config-utils/src/feo/crd-check.ts new file mode 100644 index 000000000..cd3eb5178 --- /dev/null +++ b/packages/config-utils/src/feo/crd-check.ts @@ -0,0 +1,25 @@ +import { readFileSync } from 'fs'; +import { load } from 'js-yaml'; +import { FrontendCRD } from './feo-types'; +import validateFrontendCrd from './validate-frontend-crd'; +import fecLogger, { LogType } from '../fec-logger'; + +export function readFrontendCRD(crdPath: string): FrontendCRD { + try { + const file = readFileSync(crdPath, 'utf8'); + const crd = load(file) as FrontendCRD; + try { + validateFrontendCrd(crd); + } catch (error) { + // log only warning for dev server + fecLogger(LogType.warn, error); + } + return crd; + } catch (error) { + throw new Error(`Error reading frontend CRD at ${crdPath}: ${error}`); + } +} + +export function hasFEOFeaturesEnabled(crd: FrontendCRD): boolean { + return crd.objects?.[0].spec.feoConfigEnabled || false; +} diff --git a/packages/config-utils/src/feo/feo-types.ts b/packages/config-utils/src/feo/feo-types.ts new file mode 100644 index 000000000..c140ba0c8 --- /dev/null +++ b/packages/config-utils/src/feo/feo-types.ts @@ -0,0 +1,143 @@ +export type SupportCaseData = { + version: string; + product: string; +}; + +export type ChromeGlobalModuleConfig = { + supportCaseData?: SupportCaseData; + ssoScopes?: string[]; +}; + +export type ChromePermissions = { + method: string; + apps?: string[]; + args?: unknown[]; +}; + +export type ChromeEntryModuleRoute = { + pathname: string; + exact?: boolean; + props?: object; + supportCaseData?: SupportCaseData; + permissions?: ChromePermissions; +}; + +export type ChromeEntryModule = { + id: string; + module: string; + routes: ChromeEntryModuleRoute[]; +}; + +type ChromeModuleAnalytics = { + APIKey: string; +}; + +export type ChromeModule = { + manifestLocation: string; + defaultDocumentTitle?: string; + /** + * @deprecated + * use `moduleConfig` instead + */ + config?: object; + moduleConfig?: ChromeGlobalModuleConfig; + modules?: ChromeEntryModule[]; + /** + * @deprecated + * Use feo generated resources to get permitted modules + */ + isFedramp?: boolean; + analytics?: ChromeModuleAnalytics; +}; + +export type ChromeModuleRegistry = { + [moduleName: string]: ChromeModule; +}; + +export type ChromeStaticSearchEntry = { + frontendRef: string; + id: string; + href: string; + title: string; + description: string; + alt_title?: string[]; + isExternal?: boolean; +}; + +export type SegmentRef = { + segmentId: string; + frontendName: string; +}; + +export type DirectNavItem = { + id?: string; + frontendRef?: string; + href?: string; + title?: string; + expandable?: boolean; + // should be removed + appId?: string; + routes?: DirectNavItem[]; + navItems?: DirectNavItem[]; + bundleSegmentRef?: string; + segmentRef?: SegmentRef; + segmentId?: string; + position?: number; +}; + +export type Nav = { + title?: string; + id: string; + navItems: DirectNavItem[]; +}; + +export type GeneratedBundles = Nav[]; + +export type BundleSegment = { + segmentId: string; + bundleId: string; + position: number; + navItems: DirectNavItem[]; +}; + +export type ServiceTile = { + section: string; + group: string; + id: string; + frontendRef: string; +}; + +export type ServiceGroup = { + id: string; + tiles: ServiceTile[]; +}; + +export type ServiceCategory = { + id: string; + groups: ServiceGroup[]; +}; + +export type ChromeWidgetEntry = { + scope: string; + module: string; + frontendRef: string; +}; + +export type CRDObject = { + metadata: { + name: string; + }; + spec: { + bundleSegments?: BundleSegment[]; + navigationSegments?: DirectNavItem[]; + module: ChromeModule; + searchEntries?: ChromeStaticSearchEntry[]; + serviceTiles?: ServiceTile[]; + widgetRegistry?: ChromeWidgetEntry[]; + feoConfigEnabled?: boolean; + }; +}; + +export type FrontendCRD = { + objects: CRDObject[]; +}; diff --git a/packages/config-utils/src/feo/module-interceptor.test.ts b/packages/config-utils/src/feo/module-interceptor.test.ts new file mode 100644 index 000000000..9b5cc131e --- /dev/null +++ b/packages/config-utils/src/feo/module-interceptor.test.ts @@ -0,0 +1,61 @@ +import { ChromeModule, ChromeModuleRegistry, FrontendCRD } from './feo-types'; +import moduleInterceptor from './module-interceptor'; + +describe('module-interceptor', () => { + it('should replace existing entry in moduleRegistry with new entry', () => { + const moduleName = 'module-name'; + const newEntry: ChromeModule = { + manifestLocation: 'new-location', + }; + const frontendCRD: FrontendCRD = { + objects: [ + { + metadata: { + name: moduleName, + }, + spec: { + module: newEntry, + }, + }, + ], + }; + const remoteModuleRegistry: ChromeModuleRegistry = { + [moduleName]: { + manifestLocation: 'old-location', + }, + }; + const expectedResult: ChromeModuleRegistry = { + [moduleName]: newEntry, + }; + + const result = moduleInterceptor(remoteModuleRegistry, frontendCRD); + expect(result).toEqual(expectedResult); + }); + + it('should add new entry to moduleRegistry', () => { + const moduleName = 'module-name'; + const newEntry: ChromeModule = { + manifestLocation: 'new-location', + }; + const frontendCRD: FrontendCRD = { + objects: [ + { + metadata: { + name: moduleName, + }, + spec: { + module: newEntry, + }, + }, + ], + }; + const remoteModuleRegistry: ChromeModuleRegistry = {}; + + const expectedResult: ChromeModuleRegistry = { + [moduleName]: newEntry, + }; + + const result = moduleInterceptor(remoteModuleRegistry, frontendCRD); + expect(result).toEqual(expectedResult); + }); +}); diff --git a/packages/config-utils/src/feo/module-interceptor.ts b/packages/config-utils/src/feo/module-interceptor.ts new file mode 100644 index 000000000..49b801a04 --- /dev/null +++ b/packages/config-utils/src/feo/module-interceptor.ts @@ -0,0 +1,11 @@ +import { ChromeModuleRegistry, FrontendCRD } from './feo-types'; + +function moduleInterceptor(moduleRegistry: ChromeModuleRegistry, frontendCRD: FrontendCRD): ChromeModuleRegistry { + const moduleName = frontendCRD.objects[0].metadata.name; + return { + ...moduleRegistry, + [moduleName]: frontendCRD.objects[0].spec.module, + }; +} + +export default moduleInterceptor; diff --git a/packages/config-utils/src/feo/navigation-interceptor.test.ts b/packages/config-utils/src/feo/navigation-interceptor.test.ts new file mode 100644 index 000000000..161d61d2f --- /dev/null +++ b/packages/config-utils/src/feo/navigation-interceptor.test.ts @@ -0,0 +1,684 @@ +import { DirectNavItem, FrontendCRD, Nav, SegmentRef } from './feo-types'; +import navigationInterceptor from './navigation-interceptor'; + +describe('NavigationInterceptor', () => { + describe('bundle segments', () => { + const bundleName = 'testing-bundle'; + const defaultFrontendName = 'testing-frontend'; + const bundleSegmentName = 'testing-bundle-segment'; + const baseNavItem: DirectNavItem = { + id: 'link-one', + href: '/link-one', + title: 'Link one', + }; + function createLocalCRD({ bundleSegmentRef, frontendRef, ...navItem }: DirectNavItem, frontendName: string): FrontendCRD { + return { + objects: [ + { + metadata: { + name: frontendName, + }, + spec: { + module: { + manifestLocation: 'http://localhost:3000/manifest.json', + }, + bundleSegments: [ + { + bundleId: bundleName, + position: 100, + segmentId: bundleSegmentName, + navItems: [navItem], + }, + ], + }, + }, + ], + }; + } + function createRemoteNav(navItem: DirectNavItem): Nav { + return { + id: bundleName, + title: bundleName, + navItems: [navItem], + }; + } + function createExpectedNavItems(navItem: DirectNavItem): DirectNavItem[] { + return [navItem]; + } + function crateTestData( + navItem: DirectNavItem, + { + shouldChange, + isNestedRoute, + isNestedNav, + frontendName, + }: { shouldChange?: boolean; isNestedRoute?: boolean; isNestedNav?: boolean; frontendName?: string } = {} + ) { + const internalFrontendName = frontendName ?? defaultFrontendName; + let internalNavItem: DirectNavItem = { ...navItem }; + internalNavItem.bundleSegmentRef = bundleSegmentName; + internalNavItem.frontendRef = internalFrontendName; + if (isNestedRoute) { + internalNavItem = { + ...internalNavItem, + href: undefined, + expandable: true, + bundleSegmentRef: bundleSegmentName, + frontendRef: internalFrontendName, + position: 100, + routes: [ + { + id: 'nested-one', + href: '/nested/one', + title: 'Nested one', + bundleSegmentRef: bundleSegmentName, + position: 100, + frontendRef: internalFrontendName, + }, + ], + }; + } else if (isNestedNav) { + internalNavItem = { + ...internalNavItem, + href: undefined, + bundleSegmentRef: bundleSegmentName, + frontendRef: internalFrontendName, + position: 100, + navItems: [ + { + id: 'nested-one', + href: '/nested/one', + title: 'Nested one', + position: 100, + bundleSegmentRef: bundleSegmentName, + frontendRef: internalFrontendName, + }, + ], + }; + } + let changedNavItem: DirectNavItem; + if (shouldChange) { + if (isNestedRoute) { + changedNavItem = { + ...internalNavItem, + position: 100, + routes: [ + { + id: 'nested-one', + href: '/nested/one', + title: internalNavItem?.routes?.[0]?.title + ' changed', + position: 100, + bundleSegmentRef: bundleSegmentName, + frontendRef: internalFrontendName, + }, + ], + }; + // @ts-ignore + internalNavItem?.routes?.[0]?.title = internalNavItem?.routes?.[0]?.title + ' classic'; + // @ts-ignore + internalNavItem?.routes?.[0]?.bundleSegmentRef = bundleSegmentName; + // @ts-ignore + internalNavItem?.routes?.[0]?.frontendRef = internalFrontendName; + } else if (isNestedNav) { + changedNavItem = { + ...internalNavItem, + position: 100, + navItems: [ + { + id: 'nested-one', + href: '/nested/one', + title: internalNavItem?.navItems?.[0]?.title + ' changed', + position: 100, + bundleSegmentRef: bundleSegmentName, + frontendRef: internalFrontendName, + }, + ], + }; + // @ts-ignore + internalNavItem?.navItems?.[0]?.title = internalNavItem?.navItems?.[0]?.title + ' classic'; + // @ts-ignore + internalNavItem?.navItems?.[0]?.bundleSegmentRef = bundleSegmentName; + // @ts-ignore + internalNavItem?.navItems?.[0]?.frontendRef = internalFrontendName; + } else { + changedNavItem = { + ...internalNavItem, + position: 100, + title: internalNavItem.title + ' changed', + }; + internalNavItem.title = internalNavItem.title + ' classic'; + } + } else { + changedNavItem = { ...internalNavItem, position: 100 }; + } + return { + frontendCRD: createLocalCRD(changedNavItem, internalFrontendName), + remoteNav: createRemoteNav(internalNavItem), + expectedResult: createExpectedNavItems(changedNavItem), + }; + } + it('should substitute top level flat nav item', () => { + const { frontendCRD, remoteNav, expectedResult } = crateTestData(baseNavItem, { shouldChange: true }); + + const result = navigationInterceptor(frontendCRD, remoteNav, bundleName); + expect(result).toEqual(expectedResult); + }); + + it('should substitute nested routes item', () => { + const { frontendCRD, remoteNav, expectedResult } = crateTestData(baseNavItem, { shouldChange: true, isNestedRoute: true }); + const result = navigationInterceptor(frontendCRD, remoteNav, bundleName); + expect(result).toEqual(expectedResult); + }); + + it('should substitute nested navItems item', () => { + const { frontendCRD, remoteNav, expectedResult } = crateTestData(baseNavItem, { shouldChange: true, isNestedNav: true }); + const result = navigationInterceptor(frontendCRD, remoteNav, bundleName); + expect(result).toEqual(expectedResult); + }); + + it('should ignore navItems with matching id but different frontend ref', () => { + const frontendName = 'flat-not-matching'; + const { frontendCRD, remoteNav, expectedResult } = crateTestData(baseNavItem, { shouldChange: false, frontendName }); + const result = navigationInterceptor(frontendCRD, remoteNav, bundleName); + expect(result).toEqual(expectedResult); + }); + }); + + describe('navigation segments', () => { + const bundleName = 'testing-bundle'; + const defaultFrontendName = 'testing-frontend'; + const bundleSegmentName = 'testing-bundle-segment'; + const navSegmentId = 'testing-nav-segment-id'; + const baseSegmentRef: SegmentRef = { + frontendName: defaultFrontendName, + segmentId: navSegmentId, + }; + const baseNavItem: DirectNavItem = { + id: 'link-one', + href: '/link-one', + title: 'Link one', + }; + + it('should replace top level nav segment data', () => { + const frontendCRD: FrontendCRD = { + objects: [ + { + metadata: { + name: defaultFrontendName, + }, + spec: { + module: { + manifestLocation: 'http://localhost:3000/manifest.json', + }, + bundleSegments: [ + { + bundleId: bundleName, + position: 100, + segmentId: bundleSegmentName, + navItems: [baseSegmentRef], + }, + ], + navigationSegments: [ + { + segmentId: navSegmentId, + navItems: [{ ...baseNavItem, title: 'Link one changed' }], + }, + ], + }, + }, + ], + }; + + const remoteNav: Nav = { + id: bundleName, + title: bundleName, + navItems: [{ ...baseNavItem, segmentRef: baseSegmentRef, frontendRef: defaultFrontendName, bundleSegmentRef: bundleSegmentName }], + }; + + const expectedResult: DirectNavItem[] = [ + { + ...baseNavItem, + title: 'Link one changed', + }, + ]; + + const result = navigationInterceptor(frontendCRD, remoteNav, bundleName); + expect(result).toEqual(expectedResult); + }); + + it('should replace one segment ref with multiple navItems', () => { + const bundleName = 'testing-bundle'; + const defaultFrontendName = 'testing-frontend'; + const bundleSegmentName = 'testing-bundle-segment'; + const navSegmentId = 'testing-nav-segment-id'; + const baseSegmentRef: SegmentRef = { + frontendName: defaultFrontendName, + segmentId: navSegmentId, + }; + const baseNavItems: DirectNavItem[] = [ + { + id: 'link-one', + href: '/link-one', + title: 'Link one', + }, + { + id: 'link-two', + href: '/link-two', + title: 'Link two', + }, + ]; + + const frontendCRD: FrontendCRD = { + objects: [ + { + metadata: { + name: defaultFrontendName, + }, + spec: { + module: { + manifestLocation: 'http://localhost:3000/manifest.json', + }, + bundleSegments: [ + { + bundleId: bundleName, + position: 100, + segmentId: bundleSegmentName, + navItems: [ + { + title: 'persistent item', + href: '/persistent', + id: 'persistent', + }, + baseSegmentRef, + ], + }, + ], + navigationSegments: [ + { + segmentId: navSegmentId, + navItems: baseNavItems.map(({ title, ...rest }) => ({ ...rest, title: `${title} changed` })), + }, + ], + }, + }, + ], + }; + + const remoteNav: Nav = { + id: bundleName, + title: bundleName, + navItems: [ + { + title: 'persistent item', + href: '/persistent', + id: 'persistent', + }, + ...baseNavItems.map((navItem) => ({ + ...navItem, + bundleSegmentRef: bundleSegmentName, + segmentRef: baseSegmentRef, + frontendRef: defaultFrontendName, + })), + ], + }; + + const expectedResult: DirectNavItem[] = [ + { + title: 'persistent item', + href: '/persistent', + id: 'persistent', + }, + ...baseNavItems.map(({ title, ...navItem }) => ({ + ...navItem, + title: `${title} changed`, + })), + ]; + + const result = navigationInterceptor(frontendCRD, remoteNav, bundleName); + expect(result).toEqual(expectedResult); + }); + + it('should replace remote segment with one item with multiple items', () => { + const bundleName = 'testing-bundle'; + const defaultFrontendName = 'testing-frontend'; + const bundleSegmentName = 'testing-bundle-segment'; + const navSegmentId = 'testing-nav-segment-id'; + const baseSegmentRef: SegmentRef = { + frontendName: defaultFrontendName, + segmentId: navSegmentId, + }; + const baseNavItems: DirectNavItem[] = [ + { + id: 'link-one', + href: '/link-one', + title: 'Link one', + }, + { + id: 'link-two', + href: '/link-two', + title: 'Link two', + }, + ]; + + const frontendCRD: FrontendCRD = { + objects: [ + { + metadata: { + name: defaultFrontendName, + }, + spec: { + module: { + manifestLocation: 'http://localhost:3000/manifest.json', + }, + bundleSegments: [ + { + bundleId: bundleName, + position: 100, + segmentId: bundleSegmentName, + navItems: [baseSegmentRef], + }, + ], + navigationSegments: [ + { + segmentId: navSegmentId, + navItems: baseNavItems.map(({ title, ...rest }) => ({ ...rest, title: `${title} changed` })), + }, + ], + }, + }, + ], + }; + + const remoteNav: Nav = { + id: bundleName, + title: bundleName, + navItems: [{ ...baseNavItems[0], segmentRef: baseSegmentRef, frontendRef: defaultFrontendName, bundleSegmentRef: bundleSegmentName }], + }; + + const expectedResult: DirectNavItem[] = baseNavItems.map(({ title, ...navItem }) => ({ + ...navItem, + title: `${title} changed`, + })); + + const result = navigationInterceptor(frontendCRD, remoteNav, bundleName); + expect(result).toEqual(expectedResult); + }); + + it('should replace remote segment with multiple items with one item', () => { + const bundleName = 'testing-bundle'; + const defaultFrontendName = 'testing-frontend'; + const bundleSegmentName = 'testing-bundle-segment'; + const navSegmentId = 'testing-nav-segment-id'; + const baseSegmentRef: SegmentRef = { + frontendName: defaultFrontendName, + segmentId: navSegmentId, + }; + const baseNavItems: DirectNavItem[] = [ + { + id: 'link-one', + href: '/link-one', + title: 'Link one', + }, + { + id: 'link-two', + href: '/link-two', + title: 'Link two', + }, + ]; + + const frontendCRD: FrontendCRD = { + objects: [ + { + metadata: { + name: defaultFrontendName, + }, + spec: { + module: { + manifestLocation: 'http://localhost:3000/manifest.json', + }, + bundleSegments: [ + { + bundleId: bundleName, + position: 100, + segmentId: bundleSegmentName, + navItems: [baseSegmentRef], + }, + ], + navigationSegments: [ + { + segmentId: navSegmentId, + navItems: [{ ...baseNavItems[0], title: `${baseNavItems[0].title} changed` }], + }, + ], + }, + }, + ], + }; + + const remoteNav: Nav = { + id: bundleName, + title: bundleName, + navItems: baseNavItems.map((navItem) => ({ + ...navItem, + bundleSegmentRef: bundleSegmentName, + segmentRef: baseSegmentRef, + frontendRef: defaultFrontendName, + })), + }; + + const expectedResult: DirectNavItem[] = [{ ...baseNavItems[0], title: `${baseNavItems[0].title} changed` }]; + + const result = navigationInterceptor(frontendCRD, remoteNav, bundleName); + expect(result).toEqual(expectedResult); + }); + }); + + describe('replacement of both navigation and bundle segments', () => { + it('should handle complex and deeply nested replacements', () => { + const frontendName = 'test-frontend'; + const bundleId = 'test-bundle-id'; + const bundleSegmentOneId = 'bundle-segment-one-id'; + const segmentOneId = 'segment-one-id'; + const segmentRefOne: SegmentRef = { + frontendName: frontendName, + segmentId: segmentOneId, + }; + const segmentTwoId = 'segment-two-id'; + const segmentRefTwo: SegmentRef = { + frontendName: frontendName, + segmentId: segmentTwoId, + }; + const segmentTreeId = 'segment-tree-id'; + const segmentRefThree: SegmentRef = { + frontendName: frontendName, + segmentId: segmentTreeId, + }; + const frontendCRD: FrontendCRD = { + objects: [ + { + metadata: { + name: frontendName, + }, + spec: { + module: { + manifestLocation: 'http://localhost:3000/manifest.json', + }, + navigationSegments: [ + { + segmentId: segmentOneId, + navItems: [ + { + id: 'segment-one-link-one', + href: '/segment-one-link-one', + title: 'Segment one link one', + }, + { + segmentRef: segmentRefTwo, + }, + ], + }, + { + segmentId: segmentTwoId, + navItems: [ + { + id: 'segment-two-link-one', + href: '/segment-two-link-one', + title: 'Segment two link one', + }, + { + id: 'segment-two-link-two', + href: '/segment-two-link-two', + title: 'Segment two link two changed', + }, + { + id: 'segment-two-expandable-one', + title: 'Segment two expandable one', + expandable: true, + routes: [ + { + segmentRef: segmentRefThree, + }, + ], + }, + ], + }, + { + segmentId: segmentTreeId, + navItems: [ + { + id: 'segment-tree-link-one', + href: '/segment-tree-link-one', + title: 'Segment tree link one changed', + }, + ], + }, + ], + bundleSegments: [ + { + bundleId: bundleId, + segmentId: bundleSegmentOneId, + position: 100, + navItems: [ + { + title: 'Link one', + href: '/link-one', + id: 'link-one', + }, + { + title: 'expandable', + expandable: true, + id: 'expandable', + routes: [ + { + segmentRef: segmentRefOne, + }, + ], + }, + ], + }, + ], + }, + }, + ], + }; + + const remoteNav: Nav = { + id: bundleId, + title: bundleId, + navItems: [ + { + title: 'Link one', + href: '/link-one', + id: 'link-one', + bundleSegmentRef: bundleSegmentOneId, + frontendRef: frontendName, + }, + { + title: 'expandable', + expandable: true, + id: 'expandable', + bundleSegmentRef: bundleSegmentOneId, + frontendRef: frontendName, + routes: [ + { + id: 'segment-one-link-one', + href: '/segment-one-link-one', + title: 'Segment one link one', + segmentRef: segmentRefOne, + bundleSegmentRef: bundleSegmentOneId, + frontendRef: frontendName, + }, + { + id: 'segment-two-link-one', + href: '/segment-two-link-one', + title: 'Segment two link one', + segmentRef: segmentRefTwo, + bundleSegmentRef: bundleSegmentOneId, + frontendRef: frontendName, + }, + { + id: 'segment-two-link-two', + href: '/segment-two-link-two', + title: 'Segment two link two', + segmentRef: segmentRefTwo, + bundleSegmentRef: bundleSegmentOneId, + frontendRef: frontendName, + }, + ], + }, + ], + }; + + const expectedResult: DirectNavItem[] = [ + { + title: 'Link one', + href: '/link-one', + position: 100, + id: 'link-one', + bundleSegmentRef: bundleSegmentOneId, + frontendRef: frontendName, + }, + { + title: 'expandable', + expandable: true, + id: 'expandable', + bundleSegmentRef: bundleSegmentOneId, + frontendRef: frontendName, + position: 100, + routes: [ + { + id: 'segment-one-link-one', + href: '/segment-one-link-one', + title: 'Segment one link one', + }, + { + id: 'segment-two-link-one', + href: '/segment-two-link-one', + title: 'Segment two link one', + }, + { + id: 'segment-two-link-two', + href: '/segment-two-link-two', + title: 'Segment two link two changed', + }, + { + id: 'segment-two-expandable-one', + title: 'Segment two expandable one', + expandable: true, + routes: [ + { + id: 'segment-tree-link-one', + href: '/segment-tree-link-one', + title: 'Segment tree link one changed', + }, + ], + }, + ], + }, + ]; + + const result = navigationInterceptor(frontendCRD, remoteNav, bundleId); + expect(result).toEqual(expectedResult); + }); + }); +}); diff --git a/packages/config-utils/src/feo/navigation-interceptor.ts b/packages/config-utils/src/feo/navigation-interceptor.ts new file mode 100644 index 000000000..9da676a0a --- /dev/null +++ b/packages/config-utils/src/feo/navigation-interceptor.ts @@ -0,0 +1,196 @@ +import { BundleSegment, DirectNavItem, FrontendCRD, Nav, SegmentRef } from './feo-types'; + +function hasSegmentRef(item: DirectNavItem): item is Omit<DirectNavItem, 'segmentRef'> & { segmentRef: SegmentRef } { + return typeof item?.segmentRef?.segmentId === 'string' && typeof item?.segmentRef?.frontendName === 'string'; +} + +const bundleSegmentsCache: { [bundleSegmentId: string]: BundleSegment } = {}; +const navSegmentCache: { [navSegmentId: string]: DirectNavItem } = {}; + +const getBundleSegments = (segmentCache: typeof bundleSegmentsCache, bundleId: string) => { + return Object.values(segmentCache) + .filter((segment) => segment.bundleId === bundleId) + .reduce<typeof bundleSegmentsCache>((acc, curr) => { + acc[curr.segmentId] = curr; + return acc; + }, {}); +}; + +function findMatchingSegmentItem(navItems: DirectNavItem[], matchId: string): DirectNavItem | undefined { + let match = navItems.find((item) => { + if (!hasSegmentRef(item)) { + return item.id === matchId; + } + return false; + }); + + if (!match) { + for (let i = 0; navItems[i] && !match; i += 1) { + const curr = navItems[i]; + if (!hasSegmentRef(curr) && curr.routes) { + match = findMatchingSegmentItem(curr.routes, matchId); + } else if (!hasSegmentRef(curr) && curr.navItems) { + match = findMatchingSegmentItem(curr.navItems, matchId); + } + } + } + + return match; +} + +function handleNestedNav( + segmentMatch: DirectNavItem, + originalNavItem: DirectNavItem, + bSegmentCache: typeof bundleSegmentsCache, + nSegmentCache: typeof navSegmentCache, + bundleId: string, + currentFrontendName: string, + parentSegment: BundleSegment +): DirectNavItem { + const { routes, navItems, ...segmentItem } = segmentMatch; + let parsedRoutes: DirectNavItem[] | undefined = originalNavItem.routes; + let parsedNavItems: DirectNavItem[] | undefined = originalNavItem.navItems; + if (parsedRoutes) { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + parsedRoutes = parseNavItems(parsedRoutes, bSegmentCache, nSegmentCache, bundleId, currentFrontendName); + } + if (parsedNavItems) { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + parsedNavItems = parseNavItems(parsedNavItems, bSegmentCache, nSegmentCache, bundleId, currentFrontendName); + } + return { + ...originalNavItem, + ...segmentItem, + position: parentSegment.position, + routes: parsedRoutes, + navItems: parsedNavItems, + }; +} + +function findNavItemsFirstSegmentIndex(navItems: DirectNavItem[], frontendName: string) { + return navItems.findIndex((item) => { + return hasSegmentRef(item) && item.segmentRef.frontendName === frontendName; + }); +} + +function findSegmentSequenceLength(navItems: DirectNavItem[], sequenceStartIndex: number, sementId: string, frontendName: string) { + let finalIndex = sequenceStartIndex; + for (let i = sequenceStartIndex; i < navItems.length; i += 1) { + const item = navItems[i]; + const prev = navItems[i - 1]; + if (!prev) { + finalIndex = i; + continue; + } + + if (item.segmentRef?.segmentId === sementId && item.segmentRef.frontendName === frontendName) { + finalIndex = i; + } else { + i = navItems.length; + } + } + return finalIndex - sequenceStartIndex + 1; +} + +function parseNavItems( + navItems: DirectNavItem[], + bSegmentCache: typeof bundleSegmentsCache, + nSegmentCache: typeof navSegmentCache, + bundleId: string, + currentFrontendName: string +): DirectNavItem[] { + const relevantSegments = getBundleSegments(bSegmentCache, bundleId); + const res = navItems.map((navItem) => { + if (!hasSegmentRef(navItem) && navItem.id) { + // replaces the attributes on matched items + const { id, bundleSegmentRef } = navItem; + if (navItem.frontendRef === currentFrontendName && bundleSegmentRef && relevantSegments[bundleSegmentRef]) { + const parentSegment = relevantSegments[bundleSegmentRef]; + const segmentItemMatch = findMatchingSegmentItem(relevantSegments[bundleSegmentRef].navItems, id); + if (segmentItemMatch && !hasSegmentRef(segmentItemMatch)) { + return handleNestedNav(segmentItemMatch, navItem, bSegmentCache, nSegmentCache, bundleId, currentFrontendName, parentSegment); + } + } + } + return navItem; + }); + // replace segment sequence with the segment data + let segmentIndex = findNavItemsFirstSegmentIndex(res, currentFrontendName); + let iterations = 0; + while (segmentIndex > -1 && iterations < 100) { + const segment = res[segmentIndex]; + if (hasSegmentRef(segment)) { + const replacement = nSegmentCache[segment.segmentRef.segmentId]; + if (replacement && replacement.navItems) { + // find how many items are in the original segment sequence + const replaceLength = findSegmentSequenceLength(res, segmentIndex, segment.segmentRef.segmentId, currentFrontendName); + const nestedNavItems = replacement.navItems.map((navItem) => { + if (navItem.routes) { + return { + ...navItem, + routes: parseNavItems(navItem.routes, bSegmentCache, nSegmentCache, bundleId, currentFrontendName), + }; + } else if (navItem.navItems) { + return { + ...navItem, + navItems: parseNavItems(navItem.navItems, bSegmentCache, nSegmentCache, bundleId, currentFrontendName), + }; + } + return navItem; + }); + res.splice(segmentIndex, replaceLength, ...nestedNavItems); + } + } + // make sure to try to find another + segmentIndex = findNavItemsFirstSegmentIndex(res, currentFrontendName); + iterations += 1; + } + + return res; +} + +// replaces changed nav items, local data overrides the remote data +const substituteLocalNav = (frontendCRD: FrontendCRD, nav: Nav, bundleName: string) => { + let res: DirectNavItem[] = []; + const bundleSegmentsCache: { [bundleSegmentId: string]: BundleSegment } = {}; + const navSegmentCache: { [navSegmentId: string]: DirectNavItem } = {}; + frontendCRD.objects.forEach((obj) => { + const bundleSegments = obj.spec.bundleSegments || []; + bundleSegments.forEach((bundleSegment) => { + bundleSegmentsCache[bundleSegment.segmentId] = bundleSegment; + }); + const navSegments = obj.spec.navigationSegments || []; + navSegments.forEach((navSegment) => { + if (navSegment.segmentId) { + navSegmentCache[navSegment.segmentId] = navSegment; + } + }); + + const missingSegments: BundleSegment[] = [...(obj.spec.bundleSegments || [])].filter((segment) => { + if (segment.bundleId !== bundleName) { + return false; + } + return !nav.navItems.find((navItem) => { + return navItem.bundleSegmentRef === segment.segmentId; + }); + }); + const missingNavItems: DirectNavItem[] = missingSegments + .map((segment) => segment.navItems.map((navItem) => ({ ...navItem, position: segment.position }))) + .flat(); + const parseInput = [...nav.navItems, ...missingNavItems]; + // handle top level missing bundle segments and sorting of them + res = parseNavItems(parseInput, bundleSegmentsCache, navSegmentCache, bundleName, obj.metadata.name); + }); + + // order top level segments based on position + res.sort((a, b) => { + if (typeof a.position !== 'number' || typeof b.position !== 'number') { + return 0; + } + + return a.position - b.position; + }); + return res; +}; + +export default substituteLocalNav; diff --git a/packages/config-utils/src/feo/search-interceptor.test.ts b/packages/config-utils/src/feo/search-interceptor.test.ts new file mode 100644 index 000000000..fb69a5d46 --- /dev/null +++ b/packages/config-utils/src/feo/search-interceptor.test.ts @@ -0,0 +1,58 @@ +import { ChromeStaticSearchEntry, FrontendCRD } from './feo-types'; +import searchInterceptor from './search-interceptor'; + +describe('SearchInterceptor', () => { + it('should replace search entries with the ones from the frontendCRD', () => { + const frontendName = 'frontendName'; + const frontendCRD: FrontendCRD = { + objects: [ + { + metadata: { + name: frontendName, + }, + spec: { + module: { + manifestLocation: 'location', + }, + searchEntries: [ + { + frontendRef: frontendName, + id: 'id-1', + href: 'href-1', + title: 'title-1', + description: 'description-1', + }, + { + frontendRef: frontendName, + id: 'id-1', + href: 'href-1', + title: 'title-1', + description: 'description-1', + }, + ], + }, + }, + ], + }; + const remoteSearchEntries: ChromeStaticSearchEntry[] = [ + { + frontendRef: 'otherFrontend', + id: 'otherFrontend', + href: 'otherFrontend', + title: 'otherFrontend', + description: 'otherFrontend', + }, + { + frontendRef: frontendName, + id: frontendName, + href: frontendName, + title: frontendName, + description: frontendName, + }, + ]; + + const expectedSearchEntries: ChromeStaticSearchEntry[] = [remoteSearchEntries[0], ...(frontendCRD.objects[0].spec.searchEntries ?? [])]; + const result = searchInterceptor(remoteSearchEntries, frontendCRD); + expect(result).toEqual(expectedSearchEntries); + }); +}); diff --git a/packages/config-utils/src/feo/search-interceptor.ts b/packages/config-utils/src/feo/search-interceptor.ts new file mode 100644 index 000000000..fd7b4a774 --- /dev/null +++ b/packages/config-utils/src/feo/search-interceptor.ts @@ -0,0 +1,9 @@ +import { ChromeStaticSearchEntry, FrontendCRD } from './feo-types'; + +function searchInterceptor(staticSearchIndex: ChromeStaticSearchEntry[], frontendCRD: FrontendCRD): ChromeStaticSearchEntry[] { + const frontendRef = frontendCRD.objects[0].metadata.name; + const result = staticSearchIndex.filter((entry) => entry.frontendRef !== frontendRef); + return [...result, ...(frontendCRD.objects[0].spec.searchEntries ?? [])]; +} + +export default searchInterceptor; diff --git a/packages/config-utils/src/feo/service-tiles-interceptor.test.ts b/packages/config-utils/src/feo/service-tiles-interceptor.test.ts new file mode 100644 index 000000000..95baf053a --- /dev/null +++ b/packages/config-utils/src/feo/service-tiles-interceptor.test.ts @@ -0,0 +1,127 @@ +import { FrontendCRD, ServiceCategory } from './feo-types'; +import serviceTilesInterceptor from './service-tiles-interceptor'; + +describe('Service tiles interceptor', () => { + it('should replace service tiles with the ones from the frontendCRD', () => { + const frontendName = 'frontendName'; + const frontendCrd: FrontendCRD = { + objects: [ + { + metadata: { + name: frontendName, + }, + spec: { + module: { + manifestLocation: 'location', + }, + serviceTiles: [ + { + section: 'section-1', + group: 'group-1', + id: 'id-1', + frontendRef: frontendName, + }, + { + section: 'section-1', + group: 'group-1', + id: 'id-2', + frontendRef: frontendName, + }, + { + section: 'section-2', + group: 'group-1', + id: 'id-3', + frontendRef: frontendName, + }, + ], + }, + }, + ], + }; + const remoteServiceTiles: ServiceCategory[] = [ + { + id: 'section-1', + groups: [ + { + id: 'group-1', + tiles: [ + { + section: 'section-1', + group: 'group-1', + id: 'otherFrontend', + frontendRef: 'otherFrontend', + }, + { + section: 'section-1', + group: 'group-1', + id: 'id-2', + frontendRef: frontendName, + }, + ], + }, + ], + }, + { + id: 'section-2', + groups: [ + { + id: 'group-1', + tiles: [ + { + section: 'section-2', + group: 'group-1', + id: 'otherFrontend', + frontendRef: 'otherFrontend', + }, + ], + }, + ], + }, + ]; + const expectedServiceTiles: ServiceCategory[] = [ + { + id: 'section-1', + groups: [ + { + id: 'group-1', + tiles: [ + remoteServiceTiles[0].groups[0].tiles[0], + { + section: 'section-1', + group: 'group-1', + id: 'id-1', + frontendRef: frontendName, + }, + { + section: 'section-1', + group: 'group-1', + id: 'id-2', + frontendRef: frontendName, + }, + ], + }, + ], + }, + { + id: 'section-2', + groups: [ + { + id: 'group-1', + tiles: [ + remoteServiceTiles[1].groups[0].tiles[0], + { + section: 'section-2', + group: 'group-1', + id: 'id-3', + frontendRef: frontendName, + }, + ], + }, + ], + }, + ]; + + const result = serviceTilesInterceptor(remoteServiceTiles, frontendCrd); + expect(result).toEqual(expectedServiceTiles); + }); +}); diff --git a/packages/config-utils/src/feo/service-tiles-interceptor.ts b/packages/config-utils/src/feo/service-tiles-interceptor.ts new file mode 100644 index 000000000..c2793cee3 --- /dev/null +++ b/packages/config-utils/src/feo/service-tiles-interceptor.ts @@ -0,0 +1,42 @@ +import { FrontendCRD, ServiceCategory, ServiceTile } from './feo-types'; + +function serviceTilesInterceptor(serviceCategories: ServiceCategory[], frontendCrd: FrontendCRD): ServiceCategory[] { + const frontendRef = frontendCrd.objects[0].metadata.name; + let result = [...serviceCategories]; + + const frontendCategories = + frontendCrd.objects[0].spec.serviceTiles?.reduce<{ + [section: string]: { [group: string]: ServiceTile[] }; + }>((acc, tile) => { + const section = tile.section; + const group = tile.group; + if (!acc[section]) { + acc[section] = {}; + } + + if (!acc[section][group]) { + acc[section][group] = []; + } + + acc[section][group].push({ ...tile }); + return acc; + }, {}) ?? {}; + + result = result.map((category) => { + const newGroups = category.groups.map((group) => { + const newTiles = group.tiles.filter((tile) => tile.frontendRef !== frontendRef); + return { + ...group, + tiles: [...newTiles, ...(frontendCategories[category.id]?.[group.id] ?? [])], + }; + }); + return { + ...category, + groups: newGroups, + }; + }); + + return result; +} + +export default serviceTilesInterceptor; diff --git a/packages/config-utils/src/feo/spec/frontend-crd.schema.json b/packages/config-utils/src/feo/spec/frontend-crd.schema.json new file mode 100644 index 000000000..751ed02ce --- /dev/null +++ b/packages/config-utils/src/feo/spec/frontend-crd.schema.json @@ -0,0 +1,675 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Frontend operator CRD CI validation schema", + "$defs": { + "parameter": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "required": { + "type": "boolean" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "metadata": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "apiCatalogEntry": { + "type": "object", + "properties": { + "versions": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "frontend": { + "type": "object", + "properties": { + "paths": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": [ + "paths" + ], + "additionalProperties": false + }, + "visibilityPermissions": { + "type": "object", + "properties": { + "method": { + "type": "string" + }, + "apps": { + "type": "array", + "items": { + "type": "string" + } + }, + "args": { + "type": "array" + } + }, + "required": [ + "method" + ], + "additionalProperties": false + }, + "supportCaseConfig": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "product": { + "type": "string" + } + }, + "required": [ + "version", + "product" + ], + "additionalProperties": false + }, + "scalprumModuleRoute": { + "type": "object", + "properties": { + "pathname": { + "type": "string" + }, + "exact": { + "type": "boolean" + }, + "props": { + "type": "object" + }, + "supportCaseData": { + "$ref": "#/$defs/supportCaseConfig" + }, + "permissions": { + "type": "array", + "items": { + "$ref": "#/$defs/visibilityPermissions" + } + } + }, + "required": [ + "pathname" + ], + "additionalProperties": false + }, + "moduleEntryConfig": { + "type": "object", + "properties": { + "supportCaseData": { + "$ref": "#/$defs/supportCaseConfig" + }, + "ssoScopes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [], + "additionalProperties": false + }, + "scalprumModuleEntry": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "module": { + "type": "string" + }, + "routes": { + "type": "array", + "items": { + "$ref": "#/$defs/scalprumModuleRoute" + } + }, + "moduleConfig": { + "$ref": "#/$defs/moduleEntryConfig" + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "analytics": { + "type": "object", + "properties": { + "APIKey": { + "type": "string" + } + }, + "required": [ + "APIKey" + ], + "additionalProperties": false + }, + "directNavItem": { + "type": "object", + "properties": { + "isHidden": { + "type": "boolean" + }, + "expandable": { + "type": "boolean" + }, + "href": { + "type": "string" + }, + "title": { + "type": "string" + }, + "group": { + "type": "string" + }, + "id": { + "type": "string" + }, + "isExternal": { + "type": "boolean" + }, + "product": { + "type": "string" + }, + "notifier": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "isBeta": { + "type": "boolean" + }, + "navItems": { + "type": "array", + "items": { + "$ref": "#/$defs/navItem" + } + }, + "routes": { + "type": "array", + "items": { + "$ref": "#/$defs/navItem" + } + }, + "permissions": { + "type": "array", + "items": { + "$ref": "#/$defs/visibilityPermissions" + } + } + }, + "required": [ + "title" + ], + "additionalProperties": false + }, + "navItemSegmentRef": { + "type": "object", + "properties": { + "segmentRef": { + "type": "object", + "properties": { + "frontendName": { + "type": "string" + }, + "segmentId": { + "type": "string" + } + }, + "required": [ + "frontendName", + "segmentId" + ], + "additionalProperties": false + } + }, + "required": [ + "segmentRef" + ], + "additionalProperties": false + }, + "navItem": { + "oneOf": [{ + "$ref": "#/$defs/directNavItem" + }, { + "$ref": "#/$defs/navItemSegmentRef" + }] + }, + "bundleSegment": { + "type": "object", + "properties": { + "segmentId": { + "type": "string" + }, + "bundleId": { + "type": "string" + }, + "navItems": { + "type": "array", + "items": { + "$ref": "#/$defs/navItem" + } + }, + "position": { + "type": "number" + } + }, + "required": [ + "segmentId", + "bundleId", + "navItems", + "position" + ], + "additionalProperties": false + }, + "navigationSegment": { + "type": "object", + "properties": { + "segmentId": { + "type": "string" + }, + "navItems": { + "type": "array", + "items": { + "$ref": "#/$defs/navItem" + } + } + }, + "required": [ + "segmentId" + ], + "additionalProperties": false + }, + "searchEntry": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "href": { + "type": "string" + }, + "description": { + "type": "string" + }, + "alt_title": { + "type": "array", + "items": { + "type": "string" + } + }, + "isExternal": { + "type": "boolean" + }, + "permissions": { + "type": "array", + "items": { + "$ref": "#/$defs/visibilityPermissions" + } + } + }, + "required": [ + "id", + "title", + "href", + "description" + ], + "additionalProperties": false + }, + "serviceTile": { + "type": "object", + "properties": { + "section": { + "type": "string" + }, + "group": { + "type": "string" + }, + "id": { + "type": "string" + }, + "href": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "isExternal": { + "type": "boolean" + }, + "permissions": { + "type": "array", + "items": { + "$ref": "#/$defs/visibilityPermissions" + } + } + }, + "required": [ + "section", + "group", + "id", + "href", + "title", + "description", + "icon" + ], + "additionalProperties": false + }, + "widgetHeaderLink": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "href": { + "type": "string" + } + }, + "required": [ + "title", + "href" + ], + "additionalProperties": false + }, + "widgetConfig": { + "type": "object", + "properties": { + "icon": { + "type": "string" + }, + "title": { + "type": "string" + }, + "permissions": { + "type": "array", + "items": { + "$ref": "#/$defs/visibilityPermissions" + } + }, + "headerLink": { + "$ref": "#/$defs/widgetHeaderLink" + } + }, + "required": [ + "icon", + "title" + ], + "additionalProperties": true + }, + "widgetVariant": { + "type": "object", + "properties": { + "w": { + "type": "number" + }, + "h": { + "type": "number" + }, + "maxH": { + "type": "number" + }, + "minH": { + "type": "number" + } + }, + "required": [], + "additionalProperties": false + }, + "widgetDefaults": { + "type": "object", + "properties": { + "sm": { + "$ref": "#/$defs/widgetVariant" + }, + "md": { + "$ref": "#/$defs/widgetVariant" + }, + "lg": { + "$ref": "#/$defs/widgetVariant" + }, + "xl": { + "$ref": "#/$defs/widgetVariant" + } + }, + "required": [ + "sm", + "md", + "lg", + "xl" + ], + "additionalProperties": false + }, + "widgetEntry": { + "type": "object", + "properties": { + "scope": { + "type": "string" + }, + "module": { + "type": "string" + }, + "config": { + "$ref": "#/$defs/widgetConfig" + }, + "defaults": { + "$ref": "#/$defs/widgetDefaults" + } + }, + "required": [ + "scope", + "module", + "config" + ], + "additionalProperties": false + }, + "frontendSpec": { + "type": "object", + "properties": { + "envName": { + "type": "string", + "const": "${ENV_NAME}" + }, + "deploymentRepo": { + "type": "string" + }, + "title": { + "type": "string" + }, + "image": { + "type": "string", + "const": "${IMAGE}:${IMAGE_TAG}" + }, + "API": { + "$ref": "#/$defs/apiCatalogEntry" + }, + "frontend": { + "$ref": "#/$defs/frontend" + }, + "feoConfigEnabled": { + "type": "boolean" + }, + "akamaiCacheBustDisable": { + "type": "boolean" + }, + "akamaiCacheBustPaths": { + "type": "array", + "items": { + "type": "string" + } + }, + "module": { + "type": "object", + "properties": { + "manifestLocation": { + "type": "string" + }, + "defaultDocumentTitle": { + "type": "string" + }, + "modules": { + "type": "array", + "items": { + "$ref": "#/$defs/scalprumModuleEntry" + } + }, + "moduleConfig": { + "$ref": "#/$defs/moduleEntryConfig" + }, + "config": { + "type": "object", + "deprecated": true, + "description": "Deprecated. Use moduleConfig instead." + }, + "analytics": { + "$ref": "#/$defs/analytics" + } + }, + "required": [ + "manifestLocation" + ], + "additionalProperties": false + }, + "bundleSegments": { + "type": "array", + "items": { + "$ref": "#/$defs/bundleSegment" + } + }, + "navigationSegments": { + "type": "array", + "items": { + "$ref": "#/$defs/navigationSegment" + } + }, + "searchEntries": { + "type": "array", + "items": { + "$ref": "#/$defs/searchEntry" + } + }, + "serviceTiles": { + "type": "array", + "items": { + "$ref": "#/$defs/serviceTile" + } + }, + "widgetRegistry": { + "type": "array", + "items": { + "$ref": "#/$defs/widgetEntry" + } + } + }, + "required": [ + "envName", + "deploymentRepo", + "title", + "image", + "frontend", + "module" + ], + "additionalProperties": false + }, + "frObject": { + "type": "object", + "properties": { + "apiVersion": { + "type": "string", + "const": "cloud.redhat.com/v1alpha1" + }, + "kind": { + "type": "string", + "const": "Frontend" + }, + "metadata": { + "$ref": "#/$defs/metadata" + }, + "spec": { + "$ref": "#/$defs/frontendSpec" + } + }, + "required": [ + "apiVersion", + "kind", + "metadata", + "spec" + ] + } + }, + "type": "object", + "properties": { + "apiVersion": { + "type": "string" + }, + "kind": { + "type": "string", + "const": "Template" + }, + "metadata": { + "$ref": "#/$defs/metadata" + }, + "objects": { + "type": "array", + "items": { + "$ref": "#/$defs/frObject" + }, + "minItems": 1, + "maxItems": 1 + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/$defs/parameter" + } + } + }, + "required": [ + "apiVersion", + "parameters", + "kind", + "metadata", + "objects" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/packages/config-utils/src/feo/validate-frontend-crd.test.ts b/packages/config-utils/src/feo/validate-frontend-crd.test.ts new file mode 100644 index 000000000..807aba1c1 --- /dev/null +++ b/packages/config-utils/src/feo/validate-frontend-crd.test.ts @@ -0,0 +1,94 @@ +import { FrontendCRD } from './feo-types'; +import validateFrontEndCrd from './validate-frontend-crd'; +import cloneDeep from 'lodash/cloneDeep'; + +describe('Validate FrontEnd CRD', () => { + const crdBase: FrontendCRD = { + apiVersion: 'v1', + kind: 'Template', + metadata: { + name: 'test', + }, + parameters: [], + objects: [ + { + apiVersion: 'cloud.redhat.com/v1alpha1', + kind: 'Frontend', + metadata: { + name: 'test', + }, + spec: { + envName: '${ENV_NAME}', + deploymentRepo: 'test', + title: 'Test CRD', + image: '${IMAGE}:${IMAGE_TAG}', + frontend: { + paths: ['/foo/bar'], + }, + module: { + manifestLocation: 'test', + }, + bundleSegments: [] as any[], + }, + }, + ], + } as any; + test('verify bundle segment position', () => { + const validBundleSegment = { + segmentId: 'test-segment', + bundleId: 'test-bundle', + position: 100, + navItems: [], + }; + const invalidBundleSegment = { + segmentId: 'invalid-segment', + bundleId: 'test-bundle', + navItems: [], + }; + const crd = cloneDeep(crdBase) as FrontendCRD; + // @ts-expect-error + crd.objects[0].spec.bundleSegments = [validBundleSegment, invalidBundleSegment]; + expect(() => validateFrontEndCrd(crd)).toThrowError(`must have required property 'position'`); + }); + + test('Should prevent mixing direct nav items and segment references', () => { + const mixedNavItem = { + title: 'A mixed nav item', + href: '/foo/bar', + segmentRef: { + segmentId: 'test-segment', + frontendName: 'test-frontend', + }, + }; + const validDirectNavItem = { + title: 'A valid nav item', + href: '/foo/bar', + }; + const validNavSegment = { + segmentRef: { + segmentId: 'test-segment', + frontendName: 'test-frontend', + }, + }; + const invalidBundleSegment = { + segmentId: 'bundle-segment', + bundleId: 'test-bundle', + position: 100, + navItems: [mixedNavItem], + }; + const validBundleSegment = { + segmentId: 'bundle-segment', + bundleId: 'test-bundle', + position: 100, + navItems: [validNavSegment, validDirectNavItem], + }; + const validCrd = cloneDeep(crdBase) as FrontendCRD; + validCrd.objects[0].spec.bundleSegments = [validBundleSegment]; + expect(() => validateFrontEndCrd(validCrd)).not.toThrow(); + const invalidCrd = cloneDeep(crdBase) as FrontendCRD; + invalidCrd.objects[0].spec.bundleSegments = [invalidBundleSegment]; + expect(() => validateFrontEndCrd(invalidCrd)).toThrowError( + `Frontend CRD validation failed! must NOT have additional properties, must NOT have additional properties, must match exactly one schema in oneOf` + ); + }); +}); diff --git a/packages/config-utils/src/feo/validate-frontend-crd.ts b/packages/config-utils/src/feo/validate-frontend-crd.ts new file mode 100644 index 000000000..2c655118e --- /dev/null +++ b/packages/config-utils/src/feo/validate-frontend-crd.ts @@ -0,0 +1,44 @@ +import Ajv from 'ajv/dist/2020'; +import { load } from 'js-yaml'; +import fs from 'fs'; +import { FrontendCRD } from './feo-types'; +import chalk from 'chalk'; +// CRD does not have a type and does not need one +// @ts-ignore +import frontendCrdSchema from './spec/frontend-crd.schema.json'; +import fecLogger, { LogType } from '../fec-logger'; + +function readCrdYaml(pathToCrd: string): FrontendCRD { + const data = fs.readFileSync(pathToCrd, 'utf8'); + return load(data) as FrontendCRD; +} + +function validateFrontendCrd(pathToCrd: string): void; +function validateFrontendCrd(crd: FrontendCRD): void; +function validateFrontendCrd(crd: FrontendCRD | string) { + const validator = new Ajv({ + strict: true, + }); + const crdInternal = typeof crd === 'string' ? readCrdYaml(crd) : crd; + // Remove $schema from the json as this is unknown to ajv + // @ts-ignore + delete frontendCrdSchema.$schema; + const validate = validator.compile(frontendCrdSchema); + const valid = validate(crdInternal); + if (!valid) { + validate.errors?.forEach((error) => { + console.group(); + console.log(chalk.red` +Frontend CRD validation error: + - ${error.message} + ${error.instancePath} + ${error.keyword} + ${JSON.stringify(error.params)}`); + console.groupEnd(); + }); + const errorMessages = validate.errors?.map((error) => error.message).join(', '); + throw new Error(`Frontend CRD validation failed! ${errorMessages?.length ?? 0 > 0 ? errorMessages : 'Unable to validate frontend CRD'}`); + } +} + +export default validateFrontendCrd; diff --git a/packages/config-utils/src/feo/widget-registry-interceptor.test.ts b/packages/config-utils/src/feo/widget-registry-interceptor.test.ts new file mode 100644 index 000000000..97120ee8b --- /dev/null +++ b/packages/config-utils/src/feo/widget-registry-interceptor.test.ts @@ -0,0 +1,35 @@ +import { FrontendCRD } from './feo-types'; +import widgetRegistryInterceptor from './widget-registry-interceptor'; + +describe('Widget registry interceptor', () => { + it('should replace the widget registry with the one from the server', () => { + const frontendName = 'name'; + const widgetEntries = [ + { module: 'module1', scope: 'scope1', frontendRef: frontendName }, + { module: 'module1', scope: 'scope2', frontendRef: frontendName }, + { module: 'module2', scope: 'scope1', frontendRef: 'foo' }, + ]; + const frontendCrd: FrontendCRD = { + objects: [ + { + metadata: { + name: 'name', + }, + spec: { + module: { + manifestLocation: 'location', + }, + widgetRegistry: [{ module: 'module1', scope: 'scope1', frontendRef: frontendName }], + }, + }, + ], + }; + + const result = widgetRegistryInterceptor(widgetEntries, frontendCrd); + + expect(result).toEqual([ + { module: 'module2', scope: 'scope1', frontendRef: 'foo' }, + { module: 'module1', scope: 'scope1', frontendRef: frontendName }, + ]); + }); +}); diff --git a/packages/config-utils/src/feo/widget-registry-interceptor.ts b/packages/config-utils/src/feo/widget-registry-interceptor.ts new file mode 100644 index 000000000..fc62c2917 --- /dev/null +++ b/packages/config-utils/src/feo/widget-registry-interceptor.ts @@ -0,0 +1,10 @@ +import { ChromeWidgetEntry, FrontendCRD } from './feo-types'; + +function widgetRegistryInterceptor(widgetEntries: ChromeWidgetEntry[], frontendCrd: FrontendCRD): ChromeWidgetEntry[] { + const frontendName = frontendCrd.objects[0].metadata.name; + const result = widgetEntries.filter((entry) => entry.frontendRef !== frontendName); + + return [...result, ...(frontendCrd.objects[0].spec.widgetRegistry ?? [])]; +} + +export default widgetRegistryInterceptor; diff --git a/packages/config-utils/src/index.ts b/packages/config-utils/src/index.ts index 5c794d4b9..f60b80bff 100644 --- a/packages/config-utils/src/index.ts +++ b/packages/config-utils/src/index.ts @@ -16,3 +16,4 @@ export { default as serveFederated } from './serve-federated'; export { default as generatePFSharedAssetsList } from './generate-pf-shared-assets-list'; export { default as babelTransformImports } from './babel-transform-imports'; export { default as fecLogger } from './fec-logger'; +export { default as validateFrontendCrd } from './feo/validate-frontend-crd'; diff --git a/packages/config-utils/src/proxy.ts b/packages/config-utils/src/proxy.ts index 9c5d81f0e..629b35539 100644 --- a/packages/config-utils/src/proxy.ts +++ b/packages/config-utils/src/proxy.ts @@ -7,6 +7,10 @@ import path from 'path'; import type { Configuration } from 'webpack-dev-server'; import { HttpsProxyAgent } from 'https-proxy-agent'; import cookieTransform from './cookieTransform'; +import { matchNavigationRequest } from './feo/check-outgoing-requests'; +import { hasFEOFeaturesEnabled, readFrontendCRD } from './feo/crd-check'; +import navigationInterceptor from './feo/navigation-interceptor'; +import { GeneratedBundles } from './feo/feo-types'; const defaultReposDir = path.join(__dirname, 'repos'); @@ -108,6 +112,8 @@ export type ProxyOptions = { * Chrome should be running from container from now on. */ blockLegacyChrome?: boolean; + // needs to be passed from the config directly to proxy + frontendCRDPath?: string; }; const proxy = ({ @@ -126,7 +132,10 @@ const proxy = ({ bounceProd = false, useAgent = true, localApps = process.env.LOCAL_APPS, + frontendCRDPath = path.resolve(process.cwd(), 'deploy/frontend.yaml'), }: ProxyOptions) => { + const frontendCrd = readFrontendCRD(frontendCRDPath); + const FEOFeaturesEnabled = hasFEOFeaturesEnabled(frontendCrd); const proxy: ProxyConfigItem[] = []; const majorEnv = env.split('-')[0]; const defaultLocalAppHost = process.env.LOCAL_APP_HOST || majorEnv + '.foo.redhat.com'; @@ -192,6 +201,45 @@ const proxy = ({ secure: false, changeOrigin: true, autoRewrite: true, + onProxyReq: (proxyReq, req) => { + if (matchNavigationRequest(req.url)) { + // necessary to avoid gzip encoding and issues with parsing the json body + proxyReq.setHeader('accept-encoding', 'gzip;q=0,deflate,sdch'); + } + }, + onProxyRes: (proxyRes, req, res) => { + // this should reading the aggregated bundles filed generated from chrome service + // The functionality is disabled until the interceptor is ready + // eslint-disable-next-line no-constant-condition + if (matchNavigationRequest(req.url)) { + // stub the original write function + const _write = res.write; + let body = ''; + proxyRes.on('data', (chunk) => { + body += chunk; + }); + + res.write = function () { + try { + const objectToModify = JSON.parse(body) as GeneratedBundles; + const resultBundles: GeneratedBundles = []; + if (FEOFeaturesEnabled) { + // these will be filled in chrome service once migration is ready to start + objectToModify.forEach((bundle) => { + const navItems = navigationInterceptor(frontendCrd, bundle, bundle.id); + resultBundles.push({ ...bundle, navItems }); + }); + } + const payload = JSON.stringify(resultBundles); + _write.call(res, payload, 'utf8'); + return true; + } catch { + // wait for all the chunks to arrive + return true; + } + }; + } + }, context: (url: string) => { const shouldProxy = !appUrl.find((u) => (typeof u === 'string' ? url.startsWith(u) : u.test(url))); if (shouldProxy) { diff --git a/packages/config-utils/tsconfig.json b/packages/config-utils/tsconfig.json index 28c66c274..41390a149 100644 --- a/packages/config-utils/tsconfig.json +++ b/packages/config-utils/tsconfig.json @@ -4,7 +4,7 @@ "module": "commonjs", "target": "ES5", "allowJs": true, - "resolveJsonModule": false, + "resolveJsonModule": true, "isolatedModules": true, "plugins": [], }, diff --git a/packages/config/.eslintrc b/packages/config/.eslintrc index 78ee62445..37fdd0772 100644 --- a/packages/config/.eslintrc +++ b/packages/config/.eslintrc @@ -1,6 +1,6 @@ { "extends": ["../../.eslintrc.js"], - "ignorePatterns": ["!**/*"], + "ignorePatterns": ["!**/*", "**/*.yaml"], "rules": { "@typescript-eslint/no-var-requires": "off" } diff --git a/packages/config/CHANGELOG.md b/packages/config/CHANGELOG.md index 7d2f22d64..4756107c3 100644 --- a/packages/config/CHANGELOG.md +++ b/packages/config/CHANGELOG.md @@ -2,6 +2,11 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [6.3.8](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-config-6.3.7...@redhat-cloud-services/frontend-components-config-6.3.8) (2025-01-16) + +### Dependency Updates + +* `@redhat-cloud-services/frontend-components-config-utilities` updated to version `4.0.6` ## [6.3.7](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-config-6.3.6...@redhat-cloud-services/frontend-components-config-6.3.7) (2025-01-16) ### Dependency Updates diff --git a/packages/config/package.json b/packages/config/package.json index 6af64c7b7..0fdc48ddb 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,6 +1,6 @@ { "name": "@redhat-cloud-services/frontend-components-config", - "version": "6.3.7", + "version": "6.3.8", "description": "Config plugins and settings for RedHat Cloud Services project.", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/packages/config/src/bin/prod.webpack.config.ts b/packages/config/src/bin/prod.webpack.config.ts index 1060dc6c9..75e7496ab 100644 --- a/packages/config/src/bin/prod.webpack.config.ts +++ b/packages/config/src/bin/prod.webpack.config.ts @@ -1,4 +1,7 @@ const { fecLogger, LogType } = require('@redhat-cloud-services/frontend-components-config-utilities'); +import path from 'path'; +import { hasFEOFeaturesEnabled, readFrontendCRD } from '@redhat-cloud-services/frontend-components-config-utilities/feo/crd-check'; +import validateFrontendCrd from '@redhat-cloud-services/frontend-components-config-utilities/feo/validate-frontend-crd'; import FECConfiguration from '../lib/fec.config'; import config from '../lib/index'; import commonPlugins from './webpack.plugins'; @@ -6,14 +9,31 @@ const fecConfig: FECConfiguration = require(process.env.FEC_CONFIG_PATH!); type Configuration = import('webpack').Configuration; -const { plugins: externalPlugins = [], interceptChromeConfig, routes, hotReload, appUrl, ...externalConfig } = fecConfig; +const rootFolder = process.env.FEC_ROOT_DIR || process.cwd(); +const { + plugins: externalPlugins = [], + interceptChromeConfig, + routes, + hotReload, + appUrl, + frontendCRDPath = path.resolve(rootFolder, 'deploy/frontend.yaml'), + ...externalConfig +} = fecConfig; const { config: webpackConfig, plugins } = config({ - rootFolder: process.env.FEC_ROOT_DIR || process.cwd(), + rootFolder, ...externalConfig, /** Do not use HMR for production builds */ hotReload: false, + /** Do configure/inti webpack dev server */ + deploymentBuild: true, }); +const frontendCrd = readFrontendCRD(frontendCRDPath); +const feoEnabled = hasFEOFeaturesEnabled(frontendCrd); +if (feoEnabled) { + validateFrontendCrd(frontendCrd); +} + plugins.push(...commonPlugins, ...externalPlugins); const start = (env: { analyze?: string }): Configuration => { diff --git a/packages/config/src/lib/config.test.js b/packages/config/src/lib/config.test.js index a1dac7b76..e391324bf 100644 --- a/packages/config/src/lib/config.test.js +++ b/packages/config/src/lib/config.test.js @@ -1,6 +1,8 @@ import config from './createConfig'; +import path from 'path'; +const crdMockPath = path.resolve(__dirname, './crd-mock.yaml'); -const configBuilder = (c) => config({ rootFolder: '', ...c }); +const configBuilder = (c) => config({ rootFolder: '', frontendCRDPath: crdMockPath, ...c }); describe('should create dummy config with no options', () => { const { mode, optimization, entry, output, devServer } = config({ @@ -9,6 +11,7 @@ describe('should create dummy config with no options', () => { appName: 'Fooapp', env: 'stage-stable', publicPath: 'foo/bar', + frontendCRDPath: crdMockPath, }); const { mode: prodMode } = configBuilder({ mode: 'production' }); diff --git a/packages/config/src/lib/crd-mock.yaml b/packages/config/src/lib/crd-mock.yaml new file mode 100644 index 000000000..a86c4f4ca --- /dev/null +++ b/packages/config/src/lib/crd-mock.yaml @@ -0,0 +1,32 @@ + +apiVersion: v1 +kind: Template +metadata: + name: mock-frontend +objects: + - apiVersion: cloud.redhat.com/v1alpha1 + kind: Frontend + metadata: + name: mock-frontend + spec: + API: + versions: + - v1 + envName: ${ENV_NAME} + title: Mock app + deploymentRepo: https://github.com/RedHatInsights/mock + frontend: + paths: + - /apps/mock-app + image: ${IMAGE}:${IMAGE_TAG} + module: + manifestLocation: '/apps/mock/fed-mods.json' + modules: [] + +parameters: + - name: ENV_NAME + required: true + - name: IMAGE_TAG + required: true + - name: IMAGE + value: quay.io/cloudservices/foo diff --git a/packages/config/src/lib/createConfig.ts b/packages/config/src/lib/createConfig.ts index b8f4ad359..401903fdd 100644 --- a/packages/config/src/lib/createConfig.ts +++ b/packages/config/src/lib/createConfig.ts @@ -55,6 +55,8 @@ export interface CreateConfigOptions extends CommonConfigOptions { blockLegacyChrome?: boolean; devtool?: Configuration['devtool']; _unstableSpdy?: boolean; + frontendCRDPath?: string; + deploymentBuild?: boolean; } export const createConfig = ({ @@ -101,6 +103,8 @@ export const createConfig = ({ devtool = false, // enables SPDY as a dev server _unstableSpdy = false, + frontendCRDPath = path.resolve(rootFolder, 'deploy/frontend.yaml'), + deploymentBuild = false, }: CreateConfigOptions): Configuration => { if (typeof _unstableHotReload !== 'undefined') { fecLogger(LogType.warn, `The _unstableHotReload option in shared webpack config is deprecated. Use hotReload config instead.`); @@ -269,56 +273,59 @@ export const createConfig = ({ ...resolve.fallback, }, }, - devServer: { - static: { - directory: `${rootFolder || ''}/dist`, - }, - port: devServerPort, - server: _unstableSpdy ? 'spdy' : https || Boolean(useProxy) ? 'https' : 'http', - host: '0.0.0.0', // This shares on local network. Needed for docker.host.internal - hot: internalHotReload, // Use livereload instead of HMR which is spotty with federated modules - liveReload: !internalHotReload, - allowedHosts: 'all', - // https://github.com/bripkens/connect-history-api-fallback - historyApiFallback: { - // We should really implement the same logic as cloud-services-config - // - // Until then let known api calls fall through instead of returning /index.html - // for easier `fetch` debugging - rewrites: [ - { from: /^\/api/, to: '/404.html' }, - { from: /^\/config/, to: '/404.html' }, - ], - verbose: Boolean(proxyVerbose), - disableDotRule: true, - }, - devMiddleware: { - writeToDisk: true, - }, - client, - ...proxy({ - env, - localChrome, - keycloakUri, - customProxy, - routes, - routesPath, - useProxy, - proxyURL, - standalone, + ...(!deploymentBuild && { + devServer: { + static: { + directory: `${rootFolder || ''}/dist`, + }, port: devServerPort, - reposDir, - appUrl, - publicPath, - proxyVerbose, - target, - registry, - bounceProd, - useAgent, - useDevBuild, - blockLegacyChrome, - }), - }, + server: _unstableSpdy ? 'spdy' : https || Boolean(useProxy) ? 'https' : 'http', + host: '0.0.0.0', // This shares on local network. Needed for docker.host.internal + hot: internalHotReload, // Use livereload instead of HMR which is spotty with federated modules + liveReload: !internalHotReload, + allowedHosts: 'all', + // https://github.com/bripkens/connect-history-api-fallback + historyApiFallback: { + // We should really implement the same logic as cloud-services-config + // + // Until then let known api calls fall through instead of returning /index.html + // for easier `fetch` debugging + rewrites: [ + { from: /^\/api/, to: '/404.html' }, + { from: /^\/config/, to: '/404.html' }, + ], + verbose: Boolean(proxyVerbose), + disableDotRule: true, + }, + devMiddleware: { + writeToDisk: true, + }, + client, + ...proxy({ + env, + localChrome, + keycloakUri, + customProxy, + routes, + routesPath, + useProxy, + proxyURL, + standalone, + port: devServerPort, + reposDir, + appUrl, + publicPath, + proxyVerbose, + target, + registry, + bounceProd, + useAgent, + useDevBuild, + blockLegacyChrome, + frontendCRDPath, + }), + }, + }), }; }; diff --git a/packages/config/src/lib/fec.config.ts b/packages/config/src/lib/fec.config.ts index 6db5cb357..2825b4ad2 100644 --- a/packages/config/src/lib/fec.config.ts +++ b/packages/config/src/lib/fec.config.ts @@ -13,6 +13,7 @@ export interface FECConfiguration debug?: boolean; chromeHost?: string; chromePort?: number; + frontendCRDPath?: string; } export default FECConfiguration; diff --git a/packages/executors/src/executors/builder/executor.ts b/packages/executors/src/executors/builder/executor.ts index 7fd49a728..cb407c598 100644 --- a/packages/executors/src/executors/builder/executor.ts +++ b/packages/executors/src/executors/builder/executor.ts @@ -53,8 +53,8 @@ export default async function runExecutor(options: BuilderExecutorSchemaType, co const { cjsTsConfig, esmTsConfig, ...tscOptions } = options; const esmOutputDir = options.outputPath + '/esm'; - const cjsTscOptions = { ...tscOptions, tsConfig: cjsTsConfig }; - const esmTscOptions = { ...tscOptions, outputPath: esmOutputDir, tsConfig: esmTsConfig }; + const cjsTscOptions: TscExecutorOptions = { clean: false, ...tscOptions, tsConfig: cjsTsConfig }; + const esmTscOptions: TscExecutorOptions = { clean: false, ...tscOptions, outputPath: esmOutputDir, tsConfig: esmTsConfig }; let executionResult = { success: false }; const results = await Promise.all([tscExecutor(cjsTscOptions, context as any), tscExecutor(esmTscOptions, context as any)]); executionResult = await resolveExecutors(...results); diff --git a/packages/notifications/CHANGELOG.md b/packages/notifications/CHANGELOG.md index b35cae68a..417a7586d 100644 --- a/packages/notifications/CHANGELOG.md +++ b/packages/notifications/CHANGELOG.md @@ -2,6 +2,22 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [4.1.12](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-notifications-4.1.11...@redhat-cloud-services/frontend-components-notifications-4.1.12) (2025-01-27) + +### Dependency Updates + +* `@redhat-cloud-services/frontend-components-utilities` updated to version `5.0.8` +* `@redhat-cloud-services/frontend-components` updated to version `5.2.1` + +### Bug Fixes + +* **build:** fix release postTarget nested dependencies ([4895cd2](https://github.com/RedHatInsights/frontend-components/commit/4895cd2eba32336a220ddec442916858400ebb3e)) + +## [4.1.11](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-notifications-4.1.10...@redhat-cloud-services/frontend-components-notifications-4.1.11) (2025-01-23) + +### Dependency Updates + +* `@redhat-cloud-services/frontend-components` updated to version `5.2.0` ## [4.1.10](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-notifications-4.1.9...@redhat-cloud-services/frontend-components-notifications-4.1.10) (2025-01-16) ### Dependency Updates diff --git a/packages/notifications/package.json b/packages/notifications/package.json index 00b21ea50..ce61f4421 100644 --- a/packages/notifications/package.json +++ b/packages/notifications/package.json @@ -1,6 +1,6 @@ { "name": "@redhat-cloud-services/frontend-components-notifications", - "version": "4.1.10", + "version": "4.1.12", "description": "Notifications portal to show toast notifications for RedHat Cloud Services project.", "browser": "index.js", "module": "esm/index.js", diff --git a/packages/notifications/project.json b/packages/notifications/project.json index e381b2d62..fd38ca317 100644 --- a/packages/notifications/project.json +++ b/packages/notifications/project.json @@ -18,14 +18,14 @@ }, "build:styles": { "executor": "@redhat-cloud-services/frontend-components-executors:build-styles", - "dependsOn": ["^build:styles", "build:bundles"], + "dependsOn": ["^build:styles"], "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components-notifications", "sourceDir": "packages/notifications" } }, "build:packages": { - "dependsOn": ["^build:packages", "build:bundles"], + "dependsOn": ["^build:packages"], "executor": "@redhat-cloud-services/frontend-components-executors:build-packages", "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components-notifications", @@ -33,7 +33,7 @@ } }, "transform:scss": { - "dependsOn": ["^transform:scss", "build:bundles"], + "dependsOn": ["^transform:scss"], "executor": "@redhat-cloud-services/frontend-components-executors:transform-scss", "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components-notifications" @@ -46,8 +46,12 @@ } }, "build": { - "executor": "nx:noop", - "dependsOn": ["^build", "build:styles", "build:packages", "transform:scss"] + "executor": "nx:run-commands", + "options": { + "parallel": false, + "commands": ["nx run @redhat-cloud-services/frontend-components-notifications:build:bundles", "nx run @redhat-cloud-services/frontend-components-notifications:build:styles", "nx run @redhat-cloud-services/frontend-components-notifications:build:packages", "nx run @redhat-cloud-services/frontend-components-notifications:transform:scss"] + }, + "dependsOn": ["^build"] }, "lint": { "executor": "@nx/eslint:lint", diff --git a/packages/remediations/CHANGELOG.md b/packages/remediations/CHANGELOG.md index 46411edbd..28e9fe001 100644 --- a/packages/remediations/CHANGELOG.md +++ b/packages/remediations/CHANGELOG.md @@ -2,6 +2,22 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [3.2.23](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-remediations-3.2.22...@redhat-cloud-services/frontend-components-remediations-3.2.23) (2025-01-27) + +### Dependency Updates + +* `@redhat-cloud-services/frontend-components` updated to version `5.2.1` +* `@redhat-cloud-services/frontend-components-utilities` updated to version `5.0.8` + +### Bug Fixes + +* **build:** fix release postTarget nested dependencies ([4895cd2](https://github.com/RedHatInsights/frontend-components/commit/4895cd2eba32336a220ddec442916858400ebb3e)) + +## [3.2.22](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-remediations-3.2.21...@redhat-cloud-services/frontend-components-remediations-3.2.22) (2025-01-23) + +### Dependency Updates + +* `@redhat-cloud-services/frontend-components` updated to version `5.2.0` ## [3.2.21](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-remediations-3.2.20...@redhat-cloud-services/frontend-components-remediations-3.2.21) (2025-01-16) ### Dependency Updates diff --git a/packages/remediations/package.json b/packages/remediations/package.json index 07f592f77..8ceeadaf9 100644 --- a/packages/remediations/package.json +++ b/packages/remediations/package.json @@ -1,6 +1,6 @@ { "name": "@redhat-cloud-services/frontend-components-remediations", - "version": "3.2.21", + "version": "3.2.23", "description": "Remediations components for RedHat Cloud Services project.", "main": "index.js", "module": "esm/index.js", diff --git a/packages/remediations/project.json b/packages/remediations/project.json index 9dfc55286..8895cdb64 100644 --- a/packages/remediations/project.json +++ b/packages/remediations/project.json @@ -18,14 +18,14 @@ }, "build:styles": { "executor": "@redhat-cloud-services/frontend-components-executors:build-styles", - "dependsOn": ["^build:styles", "build:bundles"], + "dependsOn": ["^build:styles"], "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components-remediations", "sourceDir": "packages/remediations" } }, "build:packages": { - "dependsOn": ["^build:packages", "build:bundles"], + "dependsOn": ["^build:packages"], "executor": "@redhat-cloud-services/frontend-components-executors:build-packages", "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components-remediations", @@ -39,15 +39,19 @@ } }, "transform:scss": { - "dependsOn": ["^transform:scss", "build:bundles"], + "dependsOn": ["^transform:scss"], "executor": "@redhat-cloud-services/frontend-components-executors:transform-scss", "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components-remediations" } }, "build": { - "executor": "nx:noop", - "dependsOn": ["^build", "build:styles", "build:packages", "transform:scss"] + "executor": "nx:run-commands", + "options": { + "parallel": false, + "commands": ["nx run @redhat-cloud-services/frontend-components-remediations:build:bundles", "nx run @redhat-cloud-services/frontend-components-remediations:build:styles", "nx run @redhat-cloud-services/frontend-components-remediations:build:packages", "nx run @redhat-cloud-services/frontend-components-remediations:transform:scss"] + }, + "dependsOn": ["^build"] }, "lint": { "executor": "@nx/eslint:lint", diff --git a/packages/rule-components/CHANGELOG.md b/packages/rule-components/CHANGELOG.md index 2bbb30d65..a0663644a 100644 --- a/packages/rule-components/CHANGELOG.md +++ b/packages/rule-components/CHANGELOG.md @@ -2,6 +2,22 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [3.2.20](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/rule-components-3.2.19...@redhat-cloud-services/rule-components-3.2.20) (2025-01-27) + +### Dependency Updates + +* `@redhat-cloud-services/frontend-components` updated to version `5.2.1` +* `@redhat-cloud-services/frontend-components-utilities` updated to version `5.0.8` + +### Bug Fixes + +* **build:** fix release postTarget nested dependencies ([4895cd2](https://github.com/RedHatInsights/frontend-components/commit/4895cd2eba32336a220ddec442916858400ebb3e)) + +## [3.2.19](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/rule-components-3.2.18...@redhat-cloud-services/rule-components-3.2.19) (2025-01-23) + +### Dependency Updates + +* `@redhat-cloud-services/frontend-components` updated to version `5.2.0` ## [3.2.18](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/rule-components-3.2.17...@redhat-cloud-services/rule-components-3.2.18) (2025-01-16) ### Dependency Updates diff --git a/packages/rule-components/package.json b/packages/rule-components/package.json index e1cde9382..f30819b57 100644 --- a/packages/rule-components/package.json +++ b/packages/rule-components/package.json @@ -1,6 +1,6 @@ { "name": "@redhat-cloud-services/rule-components", - "version": "3.2.18", + "version": "3.2.20", "description": "Components to be used when showing rule information", "main": "index.js", "module": "esm/index.js", diff --git a/packages/rule-components/project.json b/packages/rule-components/project.json index 1501a4eba..5762b0b81 100644 --- a/packages/rule-components/project.json +++ b/packages/rule-components/project.json @@ -18,14 +18,14 @@ }, "build:styles": { "executor": "@redhat-cloud-services/frontend-components-executors:build-styles", - "dependsOn": ["^build:styles", "build:bundles"], + "dependsOn": ["^build:styles"], "options": { "outputPath": "dist/@redhat-cloud-services/rule-components", "sourceDir": "packages/rule-components" } }, "build:packages": { - "dependsOn": ["^build:packages", "build:bundles"], + "dependsOn": ["^build:packages"], "executor": "@redhat-cloud-services/frontend-components-executors:build-packages", "options": { "outputPath": "dist/@redhat-cloud-services/rule-components", @@ -33,7 +33,7 @@ } }, "transform:scss": { - "dependsOn": ["^transform:scss", "build:bundles"], + "dependsOn": ["^transform:scss"], "executor": "@redhat-cloud-services/frontend-components-executors:transform-scss", "options": { "outputPath": "dist/@redhat-cloud-services/rule-components" @@ -46,8 +46,12 @@ } }, "build": { - "executor": "nx:noop", - "dependsOn": ["^build", "build:styles", "build:packages", "transform:scss"] + "executor": "nx:run-commands", + "options": { + "parallel": false, + "commands": ["nx run @redhat-cloud-services/rule-components:build:bundles", "nx run @redhat-cloud-services/rule-components:build:styles", "nx run @redhat-cloud-services/rule-components:build:packages", "nx run @redhat-cloud-services/rule-components:transform:scss"] + }, + "dependsOn": ["^build"] }, "lint": { "executor": "@nx/eslint:lint", diff --git a/packages/testing/CHANGELOG.md b/packages/testing/CHANGELOG.md index bbb832679..f31b533e3 100644 --- a/packages/testing/CHANGELOG.md +++ b/packages/testing/CHANGELOG.md @@ -2,6 +2,13 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [0.1.4](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-testing-0.1.3...@redhat-cloud-services/frontend-components-testing-0.1.4) (2025-01-27) + + +### Bug Fixes + +* **build:** fix release postTarget nested dependencies ([4895cd2](https://github.com/RedHatInsights/frontend-components/commit/4895cd2eba32336a220ddec442916858400ebb3e)) + ## [0.1.3](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-testing-0.1.2...@redhat-cloud-services/frontend-components-testing-0.1.3) (2024-12-02) ## [0.1.3](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-testing-0.1.2...@redhat-cloud-services/frontend-components-testing-0.1.3) (2024-12-02) diff --git a/packages/testing/package.json b/packages/testing/package.json index 8b515ea36..ca33d7e9e 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -1,6 +1,6 @@ { "name": "@redhat-cloud-services/frontend-components-testing", - "version": "0.1.3", + "version": "0.1.4", "description": "Testing utilities for RedHat Cloud Services project.", "main": "index.js", "module": "esm/index.js", diff --git a/packages/testing/project.json b/packages/testing/project.json index 1265d410a..7483820b5 100644 --- a/packages/testing/project.json +++ b/packages/testing/project.json @@ -18,14 +18,14 @@ }, "build:styles": { "executor": "@redhat-cloud-services/frontend-components-executors:build-styles", - "dependsOn": ["^build:styles", "build:bundles"], + "dependsOn": ["^build:styles"], "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components-testing", "sourceDir": "packages/testing" } }, "build:packages": { - "dependsOn": ["^build:packages", "build:bundles"], + "dependsOn": ["^build:packages"], "executor": "@redhat-cloud-services/frontend-components-executors:build-packages", "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components-testing", @@ -39,15 +39,19 @@ } }, "transform:scss": { - "dependsOn": ["^transform:scss", "build:bundles"], + "dependsOn": ["^transform:scss"], "executor": "@redhat-cloud-services/frontend-components-executors:transform-scss", "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components-testing" } }, "build": { - "executor": "nx:noop", - "dependsOn": ["^build", "build:styles", "build:packages", "transform:scss"] + "executor": "nx:run-commands", + "options": { + "parallel": false, + "commands": ["nx run @redhat-cloud-services/frontend-components-testing:build:bundles", "nx run @redhat-cloud-services/frontend-components-testing:build:styles", "nx run @redhat-cloud-services/frontend-components-testing:build:packages", "nx run @redhat-cloud-services/frontend-components-testing:transform:scss"] + }, + "dependsOn": ["^build"] }, "lint": { "executor": "@nx/eslint:lint", diff --git a/packages/translations/CHANGELOG.md b/packages/translations/CHANGELOG.md index a3ceea4c2..0d6d90099 100644 --- a/packages/translations/CHANGELOG.md +++ b/packages/translations/CHANGELOG.md @@ -2,6 +2,16 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [3.2.16](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-translations-3.2.15...@redhat-cloud-services/frontend-components-translations-3.2.16) (2025-01-27) + +### Dependency Updates + +* `@redhat-cloud-services/frontend-components-utilities` updated to version `5.0.8` + +### Bug Fixes + +* **build:** fix release postTarget nested dependencies ([4895cd2](https://github.com/RedHatInsights/frontend-components/commit/4895cd2eba32336a220ddec442916858400ebb3e)) + ## [3.2.15](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-translations-3.2.14...@redhat-cloud-services/frontend-components-translations-3.2.15) (2025-01-15) ### Dependency Updates diff --git a/packages/translations/package.json b/packages/translations/package.json index feff2c3b1..b6bc6965d 100644 --- a/packages/translations/package.json +++ b/packages/translations/package.json @@ -1,6 +1,6 @@ { "name": "@redhat-cloud-services/frontend-components-translations", - "version": "3.2.15", + "version": "3.2.16", "description": "Translations package for RedHat Cloud Services project.", "main": "index.js", "module": "esm/index.js", diff --git a/packages/translations/project.json b/packages/translations/project.json index afd1c638b..e0b3a8842 100644 --- a/packages/translations/project.json +++ b/packages/translations/project.json @@ -30,14 +30,14 @@ }, "build:styles": { "executor": "@redhat-cloud-services/frontend-components-executors:build-styles", - "dependsOn": ["^build:styles", "build:bundles"], + "dependsOn": ["^build:styles"], "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components-translations", "sourceDir": "packages/translations" } }, "build:packages": { - "dependsOn": ["^build:packages", "build:bundles"], + "dependsOn": ["^build:packages"], "executor": "@redhat-cloud-services/frontend-components-executors:build-packages", "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components-translations", @@ -45,7 +45,7 @@ } }, "transform:scss": { - "dependsOn": ["^transform:scss", "build:bundles"], + "dependsOn": ["^transform:scss"], "executor": "@redhat-cloud-services/frontend-components-executors:transform-scss", "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components-translations" @@ -58,8 +58,12 @@ } }, "build": { - "executor": "nx:noop", - "dependsOn": ["^build", "build:styles", "build:packages", "transform:scss"] + "executor": "nx:run-commands", + "options": { + "parallel": false, + "commands": ["nx run @redhat-cloud-services/frontend-components-translations:build:bundles", "nx run @redhat-cloud-services/frontend-components-translations:build:styles", "nx run @redhat-cloud-services/frontend-components-translations:build:packages", "nx run @redhat-cloud-services/frontend-components-translations:transform:scss"] + }, + "dependsOn": ["^build"] }, "lint": { "executor": "@nx/eslint:lint", diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md index cce700ecb..693025a7d 100644 --- a/packages/utils/CHANGELOG.md +++ b/packages/utils/CHANGELOG.md @@ -2,6 +2,13 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [5.0.8](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-utilities-5.0.7...@redhat-cloud-services/frontend-components-utilities-5.0.8) (2025-01-27) + + +### Bug Fixes + +* **build:** fix release postTarget nested dependencies ([4895cd2](https://github.com/RedHatInsights/frontend-components/commit/4895cd2eba32336a220ddec442916858400ebb3e)) + ## [5.0.7](https://github.com/RedHatInsights/frontend-components/compare/@redhat-cloud-services/frontend-components-utilities-5.0.6...@redhat-cloud-services/frontend-components-utilities-5.0.7) (2025-01-15) diff --git a/packages/utils/package.json b/packages/utils/package.json index de10c2415..5a0c0107b 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@redhat-cloud-services/frontend-components-utilities", - "version": "5.0.7", + "version": "5.0.8", "description": "Util functions for RedHat Cloud Services project.", "main": "index.js", "module": "esm/index.js", diff --git a/packages/utils/project.json b/packages/utils/project.json index 8e8c0187e..325c100b1 100644 --- a/packages/utils/project.json +++ b/packages/utils/project.json @@ -18,14 +18,14 @@ }, "build:styles": { "executor": "@redhat-cloud-services/frontend-components-executors:build-styles", - "dependsOn": ["^build:styles", "build:bundles"], + "dependsOn": ["^build:styles"], "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components-utilities", "sourceDir": "packages/utils" } }, "build:packages": { - "dependsOn": ["^build:packages", "build:bundles"], + "dependsOn": ["^build:packages"], "executor": "@redhat-cloud-services/frontend-components-executors:build-packages", "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components-utilities", @@ -33,7 +33,7 @@ } }, "transform:scss": { - "dependsOn": ["^transform:scss", "build:bundles"], + "dependsOn": ["^transform:scss"], "executor": "@redhat-cloud-services/frontend-components-executors:transform-scss", "options": { "outputPath": "dist/@redhat-cloud-services/frontend-components-utilities" @@ -46,8 +46,12 @@ } }, "build": { - "executor": "nx:noop", - "dependsOn": ["^build", "build:styles", "build:packages", "transform:scss"] + "executor": "nx:run-commands", + "options": { + "parallel": false, + "commands": ["nx run @redhat-cloud-services/frontend-components-utilities:build:bundles", "nx run @redhat-cloud-services/frontend-components-utilities:build:styles", "nx run @redhat-cloud-services/frontend-components-utilities:build:packages", "nx run @redhat-cloud-services/frontend-components-utilities:transform:scss"] + }, + "dependsOn": ["^build"] }, "lint": { "executor": "@nx/eslint:lint",