diff --git a/extensions/packageManager/src/client/Library.tsx b/extensions/packageManager/src/client/Library.tsx index 663e688e46..0acc030674 100644 --- a/extensions/packageManager/src/client/Library.tsx +++ b/extensions/packageManager/src/client/Library.tsx @@ -18,6 +18,7 @@ import { ScrollablePane, ScrollbarVisibility, Stack, + SearchBox, } from 'office-ui-fabric-react'; import { render, useHttpClient, useProjectApi, useApplicationApi } from '@bfc/extension-client'; import { Toolbar, IToolbarItem, LoadingSpinner } from '@bfc/ui-shared'; @@ -45,6 +46,7 @@ const Library: React.FC = () => { const [runtimeLanguage, setRuntimeLanguage] = useState('c#'); const [feeds, updateFeeds] = useState([]); const [feed, setFeed] = useState(undefined); + const [searchTerm, setSearchTerm] = useState(''); const [loading, setLoading] = useState(false); const [selectedItem, setSelectedItem] = useState(); const [currentProjectId, setCurrentProjectId] = useState(projectId); @@ -79,7 +81,7 @@ const Library: React.FC = () => { ), ejectRuntime: formatMessage('Eject Runtime'), noComponentsInstalled: formatMessage('No packages installed'), - noComponentsFound: formatMessage('No packages found. Check extension configuration.'), + noComponentsFound: formatMessage('No packages found'), browseHeader: formatMessage('Browse'), installHeader: formatMessage('Installed'), libraryError: formatMessage('Package Manager Error'), @@ -104,6 +106,14 @@ const Library: React.FC = () => { return httpClient.get(feedUrl); }; + const getSearchResults = () => { + const feedUrl = feeds.find((f) => f.key == feed).searchUrl + ? `${API_ROOT}/feed?url=` + + encodeURIComponent(feeds.find((f) => f.key == feed).searchUrl.replace(/\{\{keyword\}\}/g, searchTerm)) + : `${API_ROOT}/feed?url=` + encodeURIComponent(feeds.find((f) => f.key == feed).url); + return httpClient.get(feedUrl); + }; + const getFeeds = () => { return httpClient.get(`${API_ROOT}/feeds`); }; @@ -141,7 +151,7 @@ const Library: React.FC = () => { if (feed && feeds.length) { getLibraries(); } - }, [feed, feeds]); + }, [feed, feeds, searchTerm]); useEffect(() => { const settings = projectCollection.find((b) => b.projectId === currentProjectId).setting; @@ -276,13 +286,33 @@ const Library: React.FC = () => { } }; + // return true if the name, description or any of the keywords match the search term + const applySearchTerm = (i): boolean => { + const term = searchTerm.trim().toLocaleLowerCase(); + return ( + i.name.toLowerCase().match(term) || + i.description.toLowerCase().match(term) || + i.keywords.filter((tag) => tag.toLowerCase().match(term)).length + ); + }; + const getLibraries = async () => { try { updateAvailableLibraries(undefined); setLoading(true); - const response = await getLibraryAPI(); - updateAvailableLibraries(response.data.available); - setRecentlyUsed(response.data.recentlyUsed); + if (searchTerm) { + const response = await getSearchResults(); + // if we are searching, but there is not a searchUrl, apply a local filter + if (!feeds.find((f) => f.key === feed).searchUrl) { + response.data.available = response.data.available.filter(applySearchTerm); + } + updateAvailableLibraries(response.data.available); + setRecentlyUsed(response.data.recentlyUsed); + } else { + const response = await getLibraryAPI(); + updateAvailableLibraries(response.data.available); + setRecentlyUsed(response.data.recentlyUsed); + } setLoading(false); } catch (err) { setApplicationLevelError({ @@ -422,6 +452,14 @@ const Library: React.FC = () => { }} > +
+ setSearchTerm('')} + onSearch={setSearchTerm} + disabled={!feeds || !feed} + /> +
{loading && } {items?.length ? ( { +const normalizeFeed = async (feed) => { if (feed.objects) { // this is an NPM feed return feed.objects.map((i) => { @@ -38,6 +38,15 @@ const normalizeFeed = (feed) => { source: 'nuget', }; }); + } else if (feed.resources) { + // this is actually a myget feed that points to the feed we want... + const queryEndpoint = feed.resources.find((resource) => resource['@type'] === 'SearchQueryService'); + if (queryEndpoint) { + const raw = await axios.get(queryEndpoint['@id']); + return normalizeFeed(raw.data); + } else { + return []; + } } else { return null; } @@ -57,7 +66,12 @@ export default async (composer: IExtensionRegistration): Promise => { const LibraryController = { getFeeds: async function (req, res) { // read the list of sources from the config file. - let packageSources = composer.store.read('feeds') as { key: string; text: string; url: string }[]; + let packageSources = composer.store.read('feeds') as { + key: string; + text: string; + url: string; + searchUrl?: string; + }[]; // if no sources are in the config file, set the default list to our 1st party feed. if (!packageSources) { @@ -66,11 +80,13 @@ export default async (composer: IExtensionRegistration): Promise => { key: 'npm', text: 'npm', url: 'https://registry.npmjs.org/-/v1/search?text=keywords:bf-component&size=100&from=0', + searchUrl: 'https://registry.npmjs.org/-/v1/search?text={{keyword}}+keywords:bf-component&size=100&from=0', }, { key: 'nuget', text: 'nuget', url: 'https://azuresearch-usnc.nuget.org/query?q=Tags:%22bf-component%22&prerelease=true', + searchUrl: 'https://azuresearch-usnc.nuget.org/query?q={{keyword}}+Tags:%22bf-component%22&prerelease=true', // only ours // https://azuresearch-usnc.nuget.org/query?q={search keyword}+preview.bot.component+Tags:%22bf-component%22&prerelease=true }, @@ -125,7 +141,7 @@ export default async (composer: IExtensionRegistration): Promise => { for (const url of packageSources) { try { const raw = await axios.get(url); - const feed = normalizeFeed(raw.data); + const feed = await normalizeFeed(raw.data); if (Array.isArray(feed)) { combined = combined.concat(feed); } else {