Merge pull request #5960 from backstage/iameap/search-alpha-release-pt2

Search Alpha Release (Create-App Part)
This commit is contained in:
Eric Peterson
2021-06-10 14:13:14 +02:00
committed by GitHub
7 changed files with 311 additions and 0 deletions
+176
View File
@@ -0,0 +1,176 @@
---
'@backstage/create-app': patch
---
Added "out-of-the-box" alpha-milestone search features to scaffolded Backstage apps.
To apply this change to an existing app, do the following...
First, navigate to your backend package and install the two new search backend
packages:
```sh
cd packages/backend
yarn add @backstage/plugin-search-backend @backstage/plugin-search-backend-node
```
Wire up these new packages into your app backend by first creating a new
`search.ts` file at `src/plugins/search.ts` with contents like the following:
```typescript
import { useHotCleanup } from '@backstage/backend-common';
import { createRouter } from '@backstage/plugin-search-backend';
import {
IndexBuilder,
LunrSearchEngine,
} from '@backstage/plugin-search-backend-node';
import { PluginEnvironment } from '../types';
import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend';
export default async function createPlugin({
logger,
discovery,
}: PluginEnvironment) {
// Initialize a connection to a search engine.
const searchEngine = new LunrSearchEngine({ logger });
const indexBuilder = new IndexBuilder({ logger, searchEngine });
// Collators are responsible for gathering documents known to plugins. This
// particular collator gathers entities from the software catalog.
indexBuilder.addCollator({
defaultRefreshIntervalSeconds: 600,
collator: new DefaultCatalogCollator({ discovery }),
});
// The scheduler controls when documents are gathered from collators and sent
// to the search engine for indexing.
const { scheduler } = await indexBuilder.build();
// A 3 second delay gives the backend server a chance to initialize before
// any collators are executed, which may attempt requests against the API.
setTimeout(() => scheduler.start(), 3000);
useHotCleanup(module, () => scheduler.stop());
return await createRouter({
engine: indexBuilder.getSearchEngine(),
logger,
});
}
```
Then, ensure the search plugin you configured above is initialized by modifying
your backend's `index.ts` file in the following ways:
```diff
+import search from './plugins/search';
// ...
+const searchEnv = useHotMemoize(module, () => createEnv('search'));
// ...
+apiRouter.use('/search', await search(searchEnv));
// ...
```
In your frontend app package, create a new `searchPage` component at, for
example, `packages/app/src/components/search/SearchPage.tsx` with contents like
the following:
```tsx
import React from 'react';
import { makeStyles, Theme, Grid, List, Paper } from '@material-ui/core';
import { Content, Header, Lifecycle, Page } from '@backstage/core';
import { CatalogResultListItem } from '@backstage/plugin-catalog';
import {
SearchBar,
SearchFilter,
SearchResult,
DefaultResultListItem,
} from '@backstage/plugin-search';
const useStyles = makeStyles((theme: Theme) => ({
bar: {
padding: theme.spacing(1, 0),
},
filters: {
padding: theme.spacing(2),
},
filter: {
'& + &': {
marginTop: theme.spacing(2.5),
},
},
}));
const SearchPage = () => {
const classes = useStyles();
return (
<Page themeId="home">
<Header title="Search" subtitle={<Lifecycle alpha />} />
<Content>
<Grid container direction="row">
<Grid item xs={12}>
<Paper className={classes.bar}>
<SearchBar debounceTime={100} />
</Paper>
</Grid>
<Grid item xs={3}>
<Paper className={classes.filters}>
<SearchFilter.Select
className={classes.filter}
name="kind"
values={['Component', 'Template']}
/>
<SearchFilter.Checkbox
className={classes.filter}
name="lifecycle"
values={['experimental', 'production']}
/>
</Paper>
</Grid>
<Grid item xs={9}>
<SearchResult>
{({ results }) => (
<List>
{results.map(({ type, document }) => {
switch (type) {
case 'software-catalog':
return (
<CatalogResultListItem
key={document.location}
result={document}
/>
);
default:
return (
<DefaultResultListItem
key={document.location}
result={document}
/>
);
}
})}
</List>
)}
</SearchResult>
</Grid>
</Grid>
</Content>
</Page>
);
};
export const searchPage = <SearchPage />;
```
Then in `App.tsx`, import this new `searchPage` component, and set it as a
child of the existing `<SearchPage />` route so that it looks like this:
```diff
+import { searchPage } from './components/search/SearchPage';
// ...
-<Route path="/search" element={<SearchPage />} />
+<Route path="/search" element={<SearchPage />}>
+ {searchPage}
+</Route>;
```
+2
View File
@@ -64,6 +64,8 @@
"@backstage/plugin-rollbar-backend": "*",
"@backstage/plugin-scaffolder": "*",
"@backstage/plugin-search": "*",
"@backstage/plugin-search-backend": "*",
"@backstage/plugin-search-backend-node": "*",
"@backstage/plugin-scaffolder-backend": "*",
"@backstage/plugin-tech-radar": "*",
"@backstage/plugin-techdocs": "*",
+4
View File
@@ -58,6 +58,8 @@ import { version as pluginRollbarBackend } from '../../../../plugins/rollbar-bac
import { version as pluginScaffolder } from '../../../../plugins/scaffolder/package.json';
import { version as pluginScaffolderBackend } from '../../../../plugins/scaffolder-backend/package.json';
import { version as pluginSearch } from '../../../../plugins/search/package.json';
import { version as pluginSearchBackend } from '../../../../plugins/search-backend/package.json';
import { version as pluginSearchBackendNode } from '../../../../plugins/search-backend-node/package.json';
import { version as pluginTechRadar } from '../../../../plugins/tech-radar/package.json';
import { version as pluginTechdocs } from '../../../../plugins/techdocs/package.json';
import { version as pluginTechdocsBackend } from '../../../../plugins/techdocs-backend/package.json';
@@ -89,6 +91,8 @@ export const packageVersions = {
'@backstage/plugin-scaffolder': pluginScaffolder,
'@backstage/plugin-scaffolder-backend': pluginScaffolderBackend,
'@backstage/plugin-search': pluginSearch,
'@backstage/plugin-search-backend': pluginSearchBackend,
'@backstage/plugin-search-backend-node': pluginSearchBackendNode,
'@backstage/plugin-tech-radar': pluginTechRadar,
'@backstage/plugin-techdocs': pluginTechdocs,
'@backstage/plugin-techdocs-backend': pluginTechdocsBackend,
@@ -0,0 +1,86 @@
import React from 'react';
import { makeStyles, Theme, Grid, List, Paper } from '@material-ui/core';
import { Content, Header, Page } from '@backstage/core';
import { CatalogResultListItem } from '@backstage/plugin-catalog';
import {
SearchBar,
SearchFilter,
SearchResult,
DefaultResultListItem,
} from '@backstage/plugin-search';
const useStyles = makeStyles((theme: Theme) => ({
bar: {
padding: theme.spacing(1, 0),
},
filters: {
padding: theme.spacing(2),
},
filter: {
'& + &': {
marginTop: theme.spacing(2.5),
},
},
}));
const SearchPage = () => {
const classes = useStyles();
return (
<Page themeId="home">
<Header title="Search" />
<Content>
<Grid container direction="row">
<Grid item xs={12}>
<Paper className={classes.bar}>
<SearchBar debounceTime={100} />
</Paper>
</Grid>
<Grid item xs={3}>
<Paper className={classes.filters}>
<SearchFilter.Select
className={classes.filter}
name="kind"
values={['Component', 'Template']}
/>
<SearchFilter.Checkbox
className={classes.filter}
name="lifecycle"
values={['experimental', 'production']}
/>
</Paper>
</Grid>
<Grid item xs={9}>
<SearchResult>
{({ results }) => (
<List>
{results.map(({ type, document }) => {
switch (type) {
case 'software-catalog':
return (
<CatalogResultListItem
key={document.location}
result={document}
/>
);
default:
return (
<DefaultResultListItem
key={document.location}
result={document}
/>
);
}
})}
</List>
)}
</SearchResult>
</Grid>
</Grid>
</Content>
</Page>
);
};
export const searchPage = <SearchPage />;
@@ -27,6 +27,8 @@
"@backstage/plugin-catalog-backend": "^{{version '@backstage/plugin-catalog-backend'}}",
"@backstage/plugin-proxy-backend": "^{{version '@backstage/plugin-proxy-backend'}}",
"@backstage/plugin-scaffolder-backend": "^{{version '@backstage/plugin-scaffolder-backend'}}",
"@backstage/plugin-search-backend": "^{{version '@backstage/plugin-search-backend'}}",
"@backstage/plugin-search-backend-node": "^{{version '@backstage/plugin-search-backend-node'}}",
"@backstage/plugin-techdocs-backend": "^{{version '@backstage/plugin-techdocs-backend'}}",
"@gitbeaker/node": "^30.2.0",
"@octokit/rest": "^18.5.3",
@@ -25,6 +25,7 @@ import catalog from './plugins/catalog';
import scaffolder from './plugins/scaffolder';
import proxy from './plugins/proxy';
import techdocs from './plugins/techdocs';
import search from './plugins/search';
import { PluginEnvironment } from './types';
function makeCreateEnv(config: Config) {
@@ -57,6 +58,7 @@ async function main() {
const authEnv = useHotMemoize(module, () => createEnv('auth'));
const proxyEnv = useHotMemoize(module, () => createEnv('proxy'));
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
const searchEnv = useHotMemoize(module, () => createEnv('search'));
const appEnv = useHotMemoize(module, () => createEnv('app'));
const apiRouter = Router();
@@ -65,6 +67,7 @@ async function main() {
apiRouter.use('/auth', await auth(authEnv));
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
apiRouter.use('/proxy', await proxy(proxyEnv));
apiRouter.use('/search', await search(searchEnv));
apiRouter.use(notFoundHandler());
const service = createServiceBuilder(module)
@@ -0,0 +1,38 @@
import { useHotCleanup } from '@backstage/backend-common';
import { createRouter } from '@backstage/plugin-search-backend';
import {
IndexBuilder,
LunrSearchEngine,
} from '@backstage/plugin-search-backend-node';
import { PluginEnvironment } from '../types';
import { DefaultCatalogCollator } from '@backstage/plugin-catalog-backend';
export default async function createPlugin({
logger,
discovery,
}: PluginEnvironment) {
// Initialize a connection to a search engine.
const searchEngine = new LunrSearchEngine({ logger });
const indexBuilder = new IndexBuilder({ logger, searchEngine });
// Collators are responsible for gathering documents known to plugins. This
// particular collator gathers entities from the software catalog.
indexBuilder.addCollator({
defaultRefreshIntervalSeconds: 600,
collator: new DefaultCatalogCollator({ discovery }),
});
// The scheduler controls when documents are gathered from collators and sent
// to the search engine for indexing.
const { scheduler } = await indexBuilder.build();
// A 3 second delay gives the backend server a chance to initialize before
// any collators are executed, which may attempt requests against the API.
setTimeout(() => scheduler.start(), 3000);
useHotCleanup(module, () => scheduler.stop());
return await createRouter({
engine: indexBuilder.getSearchEngine(),
logger,
});
}