From 17cd50bf15af276a38bcebbabfe4708ed30e5835 Mon Sep 17 00:00:00 2001 From: Martin Marosi Date: Thu, 12 Dec 2024 13:22:48 +0100 Subject: [PATCH] feat(config-utils): add navigation bundle interceptor --- navnotes.md | 30 + packages/config-utils/project.json | 9 +- .../src/navigation-interceptor.test.ts | 654 ++++++++++++++++++ .../src/navigation-interceptor.ts | 218 ++++++ packages/config-utils/src/proxy.ts | 14 + 5 files changed, 924 insertions(+), 1 deletion(-) create mode 100644 navnotes.md create mode 100644 packages/config-utils/src/navigation-interceptor.test.ts create mode 100644 packages/config-utils/src/navigation-interceptor.ts diff --git a/navnotes.md b/navnotes.md new file mode 100644 index 000000000..407a89a71 --- /dev/null +++ b/navnotes.md @@ -0,0 +1,30 @@ +# 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 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/navigation-interceptor.test.ts b/packages/config-utils/src/navigation-interceptor.test.ts new file mode 100644 index 000000000..f24b60a63 --- /dev/null +++ b/packages/config-utils/src/navigation-interceptor.test.ts @@ -0,0 +1,654 @@ +import navigationInterceptor, { FrontendCRD, Nav, NavItem, SegmentRef } from './navigation-interceptor'; + +describe('NavigationInterceptor', () => { + describe('bundle segments', () => { + const bundleName = 'testing-bundle'; + const defaultFrontendName = 'testing-frontend'; + const bundleSegmentName = 'testing-bundle-segment'; + const baseNavItem: NavItem = { + id: 'link-one', + href: '/link-one', + title: 'Link one', + }; + function createLocalCRD({ bundleSegmentRef, frontendRef, ...navItem }: NavItem, frontendName: string): FrontendCRD { + return { + objects: [ + { + metadata: { + name: frontendName, + }, + spec: { + bundleSegments: [ + { + bundleId: bundleName, + position: 100, + segmentId: bundleSegmentName, + navItems: [navItem], + }, + ], + }, + }, + ], + }; + } + function createRemoteNav(navItem: NavItem): Nav { + return { + id: bundleName, + title: bundleName, + navItems: [navItem], + }; + } + function createExpectedNavItems(navItem: NavItem): NavItem[] { + return [navItem]; + } + function crateTestData( + navItem: NavItem, + { + shouldChange, + isNestedRoute, + isNestedNav, + frontendName, + }: { shouldChange?: boolean; isNestedRoute?: boolean; isNestedNav?: boolean; frontendName?: string } = {} + ) { + const internalFrontendName = frontendName ?? defaultFrontendName; + let internalNavItem: NavItem = { ...navItem }; + internalNavItem.bundleSegmentRef = bundleSegmentName; + internalNavItem.frontendRef = internalFrontendName; + if (isNestedRoute) { + internalNavItem = { + ...internalNavItem, + href: undefined, + expandable: true, + bundleSegmentRef: bundleSegmentName, + frontendRef: internalFrontendName, + routes: [ + { + id: 'nested-one', + href: '/nested/one', + title: 'Nested one', + bundleSegmentRef: bundleSegmentName, + frontendRef: internalFrontendName, + }, + ], + }; + } else if (isNestedNav) { + internalNavItem = { + ...internalNavItem, + href: undefined, + bundleSegmentRef: bundleSegmentName, + frontendRef: internalFrontendName, + navItems: [ + { + id: 'nested-one', + href: '/nested/one', + title: 'Nested one', + bundleSegmentRef: bundleSegmentName, + frontendRef: internalFrontendName, + }, + ], + }; + } + let changedNavItem: NavItem; + if (shouldChange) { + if (isNestedRoute) { + changedNavItem = { + ...internalNavItem, + routes: [ + { + id: 'nested-one', + href: '/nested/one', + title: internalNavItem?.routes?.[0]?.title + ' changed', + 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, + navItems: [ + { + id: 'nested-one', + href: '/nested/one', + title: internalNavItem?.navItems?.[0]?.title + ' changed', + 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, + title: internalNavItem.title + ' changed', + }; + internalNavItem.title = internalNavItem.title + ' classic'; + } + } else { + changedNavItem = internalNavItem; + } + 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: NavItem = { + 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: { + 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: NavItem[] = [ + { + ...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: NavItem[] = [ + { + 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: { + 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: NavItem[] = [ + { + 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: NavItem[] = [ + { + 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: { + 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: NavItem[] = 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: NavItem[] = [ + { + 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: { + 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: NavItem[] = [{ ...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: { + 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: NavItem[] = [ + { + 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', + }, + { + 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/navigation-interceptor.ts b/packages/config-utils/src/navigation-interceptor.ts new file mode 100644 index 000000000..916bc739d --- /dev/null +++ b/packages/config-utils/src/navigation-interceptor.ts @@ -0,0 +1,218 @@ +export type SegmentRef = { + segmentId: string; + frontendName: string; +}; + +type DirectNavItem = { + id?: string; + frontendRef?: string; + href?: string; + title?: string; + expandable?: boolean; + // should be removed + appId?: string; + routes?: NavItem[]; + navItems?: NavItem[]; + bundleSegmentRef?: string; + segmentRef?: SegmentRef; + segmentId?: string; +}; + +export type NavItem = DirectNavItem; + +export type Nav = { + title?: string; + id: string; + navItems: NavItem[]; +}; + +type BundleSegment = { + segmentId: string; + bundleId: string; + position: number; + navItems: NavItem[]; +}; + +type CRDObject = { + metadata: { + name: string; + }; + spec: { + bundleSegments?: BundleSegment[]; + navigationSegments?: DirectNavItem[]; + }; +}; + +export type FrontendCRD = { + objects: CRDObject[]; +}; + +function hasSegmentRef(item: NavItem): item is Omit & { 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((acc, curr) => { + acc[curr.segmentId] = curr; + return acc; + }, {}); +}; + +function findMatchingSegmentItem(navItems: NavItem[], matchId: string): NavItem | 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 +): NavItem { + const { routes, navItems, ...segmentItem } = segmentMatch; + let parsedRoutes = originalNavItem.routes; + let parsedNavItems = 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, + routes: parsedRoutes, + navItems: parsedNavItems, + }; +} + +function findNavItemsFirstSegmentIndex(navItems: NavItem[], frontendName: string) { + return navItems.findIndex((item) => { + return hasSegmentRef(item) && item.segmentRef.frontendName === frontendName; + }); +} + +function findSegmentSequenceLength(navItems: NavItem[], 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: NavItem[], + bSegmentCache: typeof bundleSegmentsCache, + nSegmentCache: typeof navSegmentCache, + bundleId: string, + currentFrontendName: string +): NavItem[] { + 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 segmentItemMatch = findMatchingSegmentItem(relevantSegments[bundleSegmentRef].navItems, id); + if (segmentItemMatch && !hasSegmentRef(segmentItemMatch)) { + return handleNestedNav(segmentItemMatch, navItem, bSegmentCache, nSegmentCache, bundleId, currentFrontendName); + } + } + } + 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: NavItem[] = []; + 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; + } + }); + + res = parseNavItems(nav.navItems, bundleSegmentsCache, navSegmentCache, bundleName, obj.metadata.name); + }); + return res; +}; + +export default substituteLocalNav; diff --git a/packages/config-utils/src/proxy.ts b/packages/config-utils/src/proxy.ts index dfc4fe104..268492c76 100644 --- a/packages/config-utils/src/proxy.ts +++ b/packages/config-utils/src/proxy.ts @@ -108,6 +108,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,6 +128,9 @@ const proxy = ({ bounceProd = false, useAgent = true, localApps = process.env.LOCAL_APPS, + // should be just a mock, if not passed, the interceptor will not start + // will be used once the interceptor is ready + frontendCRDPath = path.resolve(process.cwd(), 'deploy/frontend.yaml'), }: ProxyOptions) => { const proxy: ProxyConfigItem[] = []; const majorEnv = env.split('-')[0]; @@ -192,6 +197,15 @@ const proxy = ({ secure: false, changeOrigin: true, autoRewrite: true, + onProxyReq: (proxyReq, req) => { + if (req.url.match(/\/api\/chrome-service\/v1\/static\/.*\/iam-navigation\.json/)) { + // 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) => { + // will be used to match and replace the configuration files with the crd values + }, context: (url: string) => { const shouldProxy = !appUrl.find((u) => (typeof u === 'string' ? url.startsWith(u) : u.test(url))); if (shouldProxy) {