Skip to content

Commit

Permalink
Merge pull request #2981 from Hyperkid123/townhall-feo-demo
Browse files Browse the repository at this point in the history
Enable consumption of FEO generated routing, search, and navigation files
  • Loading branch information
Hyperkid123 authored Jan 14, 2025
2 parents 6e3a870 + 22477b2 commit 9b4be4b
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 49 deletions.
82 changes: 59 additions & 23 deletions src/state/atoms/localSearchAtom.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { atom } from 'jotai';
import { Orama, create, insert } from '@orama/orama';

import { getChromeStaticPathname } from '../../utils/common';
import { GENERATED_SEARCH_FLAG, getChromeStaticPathname } from '../../utils/common';
import axios from 'axios';
import { NavItemPermission } from '../../@types/types';

Expand Down Expand Up @@ -29,37 +29,73 @@ type SearchEntry = {
altTitle?: string[];
};

type GeneratedSearchIndexResponse = {
alt_title?: string[];
id: string;
href: string;
title: string;
description?: string;
};

export const SearchPermissions = new Map<string, NavItemPermission[]>();
export const SearchPermissionsCache = new Map<string, boolean>();

const asyncSearchIndexAtom = atom(async () => {
const staticPath = getChromeStaticPathname('search');
const { data: rawIndex } = await axios.get<IndexEntry[]>(`${staticPath}/search-index.json`);
const searchIndex: SearchEntry[] = [];
const idSet = new Set<string>();
rawIndex.forEach((entry) => {
if (idSet.has(entry.id)) {
console.warn('Duplicate id found in index', entry.id);
return;
}
if (localStorage.getItem(GENERATED_SEARCH_FLAG) === 'true') {
// parse data from generated search index
const { data: rawIndex } = await axios.get<GeneratedSearchIndexResponse[]>(`/api/chrome-service/v1/static/search-index-generated.json`);
rawIndex.forEach((entry) => {
if (idSet.has(entry.id)) {
console.warn('Duplicate id found in index', entry.id);
return;
}

if (!entry.relative_uri.startsWith('/')) {
console.warn('External ink found in the index. Ignoring: ', entry.relative_uri);
return;
}
idSet.add(entry.id);
SearchPermissions.set(entry.id, entry.permissions ?? []);
searchIndex.push({
title: entry.title[0],
uri: entry.uri,
pathname: entry.relative_uri,
description: entry.poc_description_t || entry.relative_uri,
icon: entry.icon,
id: entry.id,
bundleTitle: entry.bundleTitle[0],
altTitle: entry.alt_title,
if (!entry.href.startsWith('/')) {
console.warn('External ink found in the index. Ignoring: ', entry.href);
return;
}
idSet.add(entry.id);
SearchPermissions.set(entry.id, []);
searchIndex.push({
title: entry.title,
uri: entry.href,
pathname: entry.href,
description: entry.description ?? entry.href,
icon: undefined,
id: entry.id,
bundleTitle: entry.title,
altTitle: entry.alt_title,
});
});
});
} else {
const { data: rawIndex } = await axios.get<IndexEntry[]>(`${staticPath}/search-index.json`);
rawIndex.forEach((entry) => {
if (idSet.has(entry.id)) {
console.warn('Duplicate id found in index', entry.id);
return;
}

if (!entry.relative_uri.startsWith('/')) {
console.warn('External ink found in the index. Ignoring: ', entry.relative_uri);
return;
}
idSet.add(entry.id);
SearchPermissions.set(entry.id, entry.permissions ?? []);
searchIndex.push({
title: entry.title[0],
uri: entry.uri,
pathname: entry.relative_uri,
description: entry.poc_description_t || entry.relative_uri,
icon: entry.icon,
id: entry.id,
bundleTitle: entry.bundleTitle[0],
altTitle: entry.alt_title,
});
});
}

return searchIndex;
});
Expand Down
13 changes: 10 additions & 3 deletions src/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,8 @@ const fedModulesheaders = {
Expires: '0',
};

export const GENERATED_SEARCH_FLAG = '@chrome:generated-search-index';

// FIXME: Remove once qaprodauth is dealt with
// can't use /beta because it will ge redirected by Akamai to /preview and we don't have any assets there\\
// Always use stable
Expand All @@ -356,10 +358,14 @@ const loadCSCFedModules = () =>
headers: fedModulesheaders,
});

export const loadFedModules = async () =>
Promise.all([
export const loadFedModules = async () => {
const fedModulesPath =
localStorage.getItem(GENERATED_SEARCH_FLAG) === 'true'
? '/api/chrome-service/v1/static/fed-modules-generated.json'
: `${getChromeStaticPathname('modules')}/fed-modules.json`;
return Promise.all([
axios
.get(`${getChromeStaticPathname('modules')}/fed-modules.json`, {
.get(fedModulesPath, {
headers: fedModulesheaders,
})
.catch(loadCSCFedModules),
Expand All @@ -370,6 +376,7 @@ export const loadFedModules = async () =>
}
return staticConfig;
});
};

export const generateRoutesList = (modules: { [key: string]: ChromeModule }) =>
Object.entries(modules)
Expand Down
8 changes: 7 additions & 1 deletion src/utils/fetchNavigationFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import axios from 'axios';
import { BundleNavigation, NavItem, Navigation } from '../@types/types';
import { Required } from 'utility-types';
import { itLessBundles, requiredBundles } from '../components/AppFilter/useAppFilter';
import { ITLess, getChromeStaticPathname } from './common';
import { GENERATED_SEARCH_FLAG, ITLess, getChromeStaticPathname } from './common';

export function isBundleNavigation(item: unknown): item is BundleNavigation {
return typeof item !== 'undefined';
Expand Down Expand Up @@ -38,6 +38,12 @@ const filesCache: {
};

const fetchNavigationFiles = async () => {
if (localStorage.getItem(GENERATED_SEARCH_FLAG) === 'true') {
// aggregate data call
const { data: aggregateData } = await axios.get<BundleNavigation[]>('/api/chrome-service/v1/static/bundles-generated.json');
const bundleNavigation = aggregateData.filter(isBundleNavigation);
return bundleNavigation;
}
const bundles = ITLess() ? itLessBundles : requiredBundles;
if (filesCache.ready && filesCache.expires > Date.now()) {
return filesCache.data;
Expand Down
63 changes: 41 additions & 22 deletions src/utils/useNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import axios from 'axios';
import { useAtomValue, useSetAtom } from 'jotai';
import { useContext, useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { BLOCK_CLEAR_GATEWAY_ERROR, getChromeStaticPathname } from './common';
import { BLOCK_CLEAR_GATEWAY_ERROR, GENERATED_SEARCH_FLAG, getChromeStaticPathname } from './common';
import { evaluateVisibility } from './isNavItemVisible';
import { QuickStartContext } from '@patternfly/quickstarts';
import { useFlagsStatus } from '@unleash/proxy-client-react';
import { BundleNavigation, NavItem, Navigation } from '../@types/types';
import { clearGatewayErrorAtom } from '../state/atoms/gatewayErrorAtom';
import { navigationAtom, setNavigationSegmentAtom } from '../state/atoms/navigationAtom';
import fetchNavigationFiles from './fetchNavigationFiles';

function cleanNavItemsHref(navItem: NavItem) {
const result = { ...navItem };
Expand Down Expand Up @@ -100,38 +101,56 @@ const useNavigation = () => {
});
};

async function handleNavigationResponse(data: BundleNavigation) {
let observer: MutationObserver | undefined;
if (observer && typeof observer.disconnect === 'function') {
observer.disconnect();
}

try {
const navItems = await Promise.all(data.navItems.map(cleanNavItemsHref).map(evaluateVisibility));
const schema: any = {
...data,
navItems,
};
observer = registerLocationObserver(pathname, schema);
observer.observe(document.querySelector('body')!, {
childList: true,
subtree: true,
});
} catch (error) {
// Hide nav if an error was encountered. Can happen for non-existing navigation files.
setNoNav(true);
}
}

useEffect(() => {
let observer: MutationObserver | undefined;
// reset no nav flag
setNoNav(false);
if (currentNamespace && (flagsReady || flagsError)) {
if (localStorage.getItem(GENERATED_SEARCH_FLAG) === 'true' && currentNamespace && (flagsReady || flagsError)) {
fetchNavigationFiles()
.then((bundles) => {
const bundle = bundles.find((b) => b.id === currentNamespace);
if (!bundle) {
setNoNav(true);
return;
}

return handleNavigationResponse(bundle);
})
.catch(() => {
setNoNav(true);
});
} else if (currentNamespace && (flagsReady || flagsError)) {
axios
.get(`${getChromeStaticPathname('navigation')}/${currentNamespace}-navigation.json`)
// fallback static CSC for EE env
.catch(() => {
return axios.get<BundleNavigation>(`/config/chrome/${currentNamespace}-navigation.json?ts=${Date.now()}`);
})
.then(async (response) => {
if (observer && typeof observer.disconnect === 'function') {
observer.disconnect();
}

const data = response.data;
try {
const navItems = await Promise.all(data.navItems.map(cleanNavItemsHref).map(evaluateVisibility));
const schema = {
...data,
navItems,
};
observer = registerLocationObserver(pathname, schema);
observer.observe(document.querySelector('body')!, {
childList: true,
subtree: true,
});
} catch (error) {
// Hide nav if an error was encountered. Can happen for non-existing navigation files.
setNoNav(true);
}
return handleNavigationResponse(response.data);
})
.catch(() => {
setNoNav(true);
Expand Down

0 comments on commit 9b4be4b

Please sign in to comment.