diff --git a/extensions/packageManager/src/components/FeedModal.tsx b/extensions/packageManager/src/components/FeedModal.tsx index d8eb8c2509..fde2d4e71a 100644 --- a/extensions/packageManager/src/components/FeedModal.tsx +++ b/extensions/packageManager/src/components/FeedModal.tsx @@ -15,6 +15,8 @@ import { IconButton, ActionButton, TextField, + Toggle, + Dropdown, } from 'office-ui-fabric-react'; import { useState, useEffect, Fragment } from 'react'; import { useApplicationApi, useTelemetryClient, TelemetryClient } from '@bfc/extension-client'; @@ -42,6 +44,18 @@ export interface WorkingModalProps { closeDialog: any; onUpdateFeed: any; } + +const FEED_TYPES = [ + { + key: 'npm', + text: 'npm', + }, + { + key: 'nuget', + text: 'NuGet', + }, +]; + export const FeedModal: React.FC = (props) => { const [selectedItem, setSelectedItem] = useState(undefined); const [items, setItems] = useState(props.feeds); @@ -98,11 +112,33 @@ export const FeedModal: React.FC = (props) => { ); }, }, + { + key: 'column1', + name: 'Type', + fieldName: 'text', + minWidth: 75, + maxWidth: 75, + height: 32, + isResizable: false, + onRender: (item: PackageSourceFeed) => { + if (!selectedItem || item.key !== selectedItem.key || !editRow) return ; + return ( + { + updateSelected('type')(event, item.key); + }} + /> + ); + }, + }, { key: 'column2', name: 'URL', fieldName: 'url', - minWidth: 300, + minWidth: 200, isResizable: false, height: 32, onRender: (item: PackageSourceFeed) => { @@ -120,6 +156,46 @@ export const FeedModal: React.FC = (props) => { }, { key: 'column3', + name: 'Filter', + fieldName: 'defaultQuery.query', + minWidth: 200, + isResizable: false, + height: 32, + onRender: (item: PackageSourceFeed) => { + if (!selectedItem || item.key !== selectedItem.key || !editRow) + return ; + return ( + + ); + }, + }, + { + key: 'column4', + name: 'Prerelease', + fieldName: 'defaultQuery.prerelease', + minWidth: 40, + maxWidth: 40, + isResizable: false, + height: 32, + onRender: (item: PackageSourceFeed) => { + return ( + + ); + }, + }, + { + key: 'column5', minWidth: 40, maxWidth: 40, isResizable: false, @@ -150,6 +226,20 @@ export const FeedModal: React.FC = (props) => { }; }; + const updateSelectedDefaultQuery = (field: string) => { + return (evt, val) => { + const newSelection = { + ...selectedItem, + defaultQuery: { + ...selectedItem.defaultQuery, + [field]: val, + }, + }; + setSelectedItem(newSelection); + setItems(items.map((i) => (i.key === newSelection.key ? newSelection : i))); + }; + }; + const savePendingEdits = () => { props.onUpdateFeed(items); }; @@ -159,6 +249,11 @@ export const FeedModal: React.FC = (props) => { key: uuid(), text: '', url: '', + defaultQuery: { + semVerLevel: '2.0.0', + prerelease: false, + query: '', + }, } as PackageSourceFeed; const newItems = items.concat([newItem]); diff --git a/extensions/packageManager/src/node/feeds/feedInterfaces.ts b/extensions/packageManager/src/node/feeds/feedInterfaces.ts index d0d6c07497..a0ad5c4d55 100644 --- a/extensions/packageManager/src/node/feeds/feedInterfaces.ts +++ b/extensions/packageManager/src/node/feeds/feedInterfaces.ts @@ -32,10 +32,9 @@ export interface IPackageSource { key: string; text: string; url: string; - searchUrl?: string; readonly?: boolean; - defaultQuery?: IPackageQuery; - type?: PackageSourceType.NuGet | PackageSourceType.NPM; + defaultQuery: IPackageQuery; + type: PackageSourceType.NuGet | PackageSourceType.NPM; } /** diff --git a/extensions/packageManager/src/node/feeds/npm/npmFeed.ts b/extensions/packageManager/src/node/feeds/npm/npmFeed.ts index 95931b81aa..fd88d9d0bd 100644 --- a/extensions/packageManager/src/node/feeds/npm/npmFeed.ts +++ b/extensions/packageManager/src/node/feeds/npm/npmFeed.ts @@ -42,7 +42,8 @@ export class NpmFeed implements IFeed { throw new Error('Package source or data should be provided'); } - const httpResponse = await axios.get(this.packageSource.url); + const npmGetSearchUrl = this.buildNPMGetSearchUrl(this.packageSource.url, query); + const httpResponse = await axios.get(npmGetSearchUrl); const feed = httpResponse?.data; if (!feed) { @@ -72,4 +73,33 @@ export class NpmFeed implements IFeed { }; }); } + + /** + * Build npm search url based on the NuGet search spec. + * Spec: https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#get-v1search + * @param baseUrl The NPM search service url. + * @param query The desired query parameters. Note that if a package source provides a query, that query will be prioritized. + * @todo Eventually que parameter query should augment the package query to support paging and further filtering. So the effective query will be a combination of the + * package query plus the specified query parameters. + */ + private buildNPMGetSearchUrl(baseUrl: string, query?: IPackageQuery): string { + let url = `${baseUrl}?`; + + if (query?.query) { + url = `${url}&text=${query.query}`; + if (!query?.prerelease) { + url = `${url}+not:unstable`; + } + } + + if (query?.take) { + url = `${url}&size=${query.take}`; + } + + if (query?.skip) { + url = `${url}&from=${query.skip}`; + } + + return url; + } } diff --git a/extensions/packageManager/src/node/index.ts b/extensions/packageManager/src/node/index.ts index 38a8bcf60e..81967b4305 100644 --- a/extensions/packageManager/src/node/index.ts +++ b/extensions/packageManager/src/node/index.ts @@ -9,7 +9,7 @@ import formatMessage from 'format-message'; import { IExtensionRegistration } from '@botframework-composer/types'; import { SchemaMerger } from '@microsoft/bf-dialog/lib/library/schemaMerger'; -import { IFeed, IPackageDefinition, IPackageQuery, IPackageSource, PackageSourceType } from './feeds/feedInterfaces'; +import { IFeed, IPackageDefinition, IPackageSource, PackageSourceType } from './feeds/feedInterfaces'; import { FeedFactory } from './feeds/feedFactory'; const API_ROOT = '/api'; @@ -132,16 +132,24 @@ export default async (composer: IExtensionRegistration): Promise => { { key: 'npm', text: formatMessage('npm'), - url: `https://registry.npmjs.org/-/v1/search?text=keywords:${botComponentTag}+scope:microsoft&size=100&from=0`, - searchUrl: `https://registry.npmjs.org/-/v1/search?text={{keyword}}+keywords:${botComponentTag}&size=100&from=0`, + url: `https://registry.npmjs.org/-/v1/search`, readonly: true, + defaultQuery: { + prerelease: true, + query: `keywords:${botComponentTag}+scope:microsoft`, + }, + type: PackageSourceType.NPM, }, { key: 'npm-community', text: formatMessage('JS community packages'), - url: `https://registry.npmjs.org/-/v1/search?text=keywords:${botComponentTag}&size=100&from=0`, - searchUrl: `https://registry.npmjs.org/-/v1/search?text={{keyword}}+keywords:${botComponentTag}&size=100&from=0`, + url: `https://registry.npmjs.org/-/v1/search`, readonly: true, + defaultQuery: { + prerelease: true, + query: `keywords:${botComponentTag}`, + }, + type: PackageSourceType.NPM, }, ]; @@ -209,15 +217,17 @@ export default async (composer: IExtensionRegistration): Promise => { if (packageSource) { try { const feed: IFeed = await new FeedFactory(composer).build(packageSource); - const packageQuery: IPackageQuery = { - prerelease: true, - semVerLevel: '2.0.0', - query: 'tags:msbot-component', - }; - composer.log('GETTING FEED', packageSource, packageSource.defaultQuery ?? packageQuery); + // append user specified query to defaultQuery + if (req.query.term) { + packageSource.defaultQuery.query = `${packageSource.defaultQuery.query}+${encodeURIComponent( + req.query.term + )}`; + } + + composer.log('GETTING FEED', packageSource, packageSource.defaultQuery); - const packages = await feed.getPackages(packageSource.defaultQuery ?? packageQuery); + const packages = await feed.getPackages(packageSource.defaultQuery); if (Array.isArray(packages)) { combined.push(...packages); diff --git a/extensions/packageManager/src/pages/Library.tsx b/extensions/packageManager/src/pages/Library.tsx index 5af9ff841f..f7db2e1b94 100644 --- a/extensions/packageManager/src/pages/Library.tsx +++ b/extensions/packageManager/src/pages/Library.tsx @@ -48,7 +48,12 @@ export interface PackageSourceFeed extends IDropdownOption { name: string; key: string; url: string; - searchUrl?: string; + type: string; + defaultQuery?: { + prerelease: boolean; + semVerLevel: string; + query: string; + }; readonly?: boolean; } @@ -444,10 +449,8 @@ const Library: React.FC = () => { telemetryClient.track('PackageSearch', { term: 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); - } + // if we are searching, apply a local filter + response.data.available = response.data.available.filter(applySearchTerm); updateAvailableLibraries(response.data.available); setRecentlyUsed(response.data.recentlyUsed); } else {