diff --git a/.changeset/late-papayas-push.md b/.changeset/late-papayas-push.md
new file mode 100644
index 0000000000..07422e1b1b
--- /dev/null
+++ b/.changeset/late-papayas-push.md
@@ -0,0 +1,5 @@
+---
+'@backstage/frontend-plugin-api': patch
+---
+
+Add support for `SidebarGroup` on the sidebar item extension.
diff --git a/.changeset/olive-cars-rescue.md b/.changeset/olive-cars-rescue.md
new file mode 100644
index 0000000000..fd6c8fe331
--- /dev/null
+++ b/.changeset/olive-cars-rescue.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-search': patch
+---
+
+Create an experimental search plugin that is compatible with the declarative integration system, it is exported from `/alpha` subpath.
diff --git a/.changeset/purple-forks-fetch.md b/.changeset/purple-forks-fetch.md
new file mode 100644
index 0000000000..022fc32e71
--- /dev/null
+++ b/.changeset/purple-forks-fetch.md
@@ -0,0 +1,5 @@
+---
+'@backstage/frontend-app-api': patch
+---
+
+Register default api implementations when creating declarative integrated apps.
diff --git a/.changeset/tasty-avocados-hope.md b/.changeset/tasty-avocados-hope.md
new file mode 100644
index 0000000000..0772b55683
--- /dev/null
+++ b/.changeset/tasty-avocados-hope.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog': patch
+---
+
+Migrate catalog api to declarative integration system, it is exported from `/alpha` subpath.
diff --git a/packages/app-next/src/examples/pagesPlugin.tsx b/packages/app-next/src/examples/pagesPlugin.tsx
index c805442305..290c6b118e 100644
--- a/packages/app-next/src/examples/pagesPlugin.tsx
+++ b/packages/app-next/src/examples/pagesPlugin.tsx
@@ -48,6 +48,9 @@ const IndexPage = createPageExtension({
GraphiQL
+
+ Search
+
);
};
diff --git a/packages/frontend-app-api/src/extensions/CoreNav.tsx b/packages/frontend-app-api/src/extensions/CoreNav.tsx
index e66bee8212..71ab71a6aa 100644
--- a/packages/frontend-app-api/src/extensions/CoreNav.tsx
+++ b/packages/frontend-app-api/src/extensions/CoreNav.tsx
@@ -19,6 +19,8 @@ import {
createExtension,
coreExtensionData,
createExtensionInput,
+ useRouteRef,
+ NavTarget,
} from '@backstage/frontend-plugin-api';
import { makeStyles } from '@material-ui/core';
import {
@@ -29,7 +31,6 @@ import {
SidebarDivider,
SidebarItem,
} from '@backstage/core-components';
-import { GraphiQLIcon } from '@backstage/plugin-graphiql';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import LogoIcon from '../../../app/src/components/Root/LogoIcon';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
@@ -63,25 +64,33 @@ const SidebarLogo = () => {
);
};
+const SidebarNavItem = (props: NavTarget) => {
+ const { icon: Icon, title, routeRef } = props;
+ const to = useRouteRef(routeRef)();
+ // TODO: Support opening modal, for example, the search one
+ return ;
+};
+
export const CoreNav = createExtension({
id: 'core.nav',
at: 'core.layout/nav',
inputs: {
items: createExtensionInput({
- path: coreExtensionData.navTarget,
+ target: coreExtensionData.navTarget,
}),
},
output: {
element: coreExtensionData.reactElement,
},
- factory({ bind }) {
+ factory({ bind, inputs }) {
bind({
- // TODO: set base path using the logic from AppRouter
element: (
-
+ {inputs.items.map((item, index) => (
+
+ ))}
),
});
diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx
index bb50dea075..dc7db79022 100644
--- a/packages/frontend-app-api/src/wiring/createApp.tsx
+++ b/packages/frontend-app-api/src/wiring/createApp.tsx
@@ -49,6 +49,7 @@ import {
featureFlagsApiRef,
attachComponentData,
useRouteRef,
+ identityApiRef,
} from '@backstage/core-plugin-api';
import { getAvailablePlugins } from './discovery';
import {
@@ -62,6 +63,8 @@ import {
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { AppThemeProvider } from '../../../core-app-api/src/app/AppThemeProvider';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
+import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy';
+// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { AppContextProvider } from '../../../core-app-api/src/app/AppContext';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { LocalStorageFeatureFlags } from '../../../core-app-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags';
@@ -366,13 +369,13 @@ function createApiHolder(
): ApiHolder {
const factoryRegistry = new ApiFactoryRegistry();
- const apiFactories =
+ const pluginApis =
coreExtension.attachments
.get('apis')
?.map(e => e.getData(coreExtensionData.apiFactory))
.filter((x): x is AnyApiFactory => !!x) ?? [];
- for (const factory of apiFactories) {
+ for (const factory of [...defaultApis, ...pluginApis]) {
factoryRegistry.register('default', factory);
}
@@ -383,6 +386,38 @@ function createApiHolder(
factory: () => new LocalStorageFeatureFlags(),
});
+ factoryRegistry.register('static', {
+ api: identityApiRef,
+ deps: {},
+ factory: () => {
+ const appIdentityProxy = new AppIdentityProxy();
+ // TODO: Remove this when sign-in page is migrated
+ appIdentityProxy.setTarget(
+ {
+ getUserId: () => 'guest',
+ getIdToken: async () => undefined,
+ getProfile: () => ({
+ email: 'guest@example.com',
+ displayName: 'Guest',
+ }),
+ getProfileInfo: async () => ({
+ email: 'guest@example.com',
+ displayName: 'Guest',
+ }),
+ getBackstageIdentity: async () => ({
+ type: 'user',
+ userEntityRef: 'user:default/guest',
+ ownershipEntityRefs: ['user:default/guest'],
+ }),
+ getCredentials: async () => ({}),
+ signOut: async () => {},
+ },
+ { signOutTargetUrl: '/' },
+ );
+ return appIdentityProxy;
+ },
+ });
+
factoryRegistry.register('static', {
api: appThemeApiRef,
deps: {},
diff --git a/plugins/catalog/alpha-api-report.md b/plugins/catalog/alpha-api-report.md
index 27de408f86..631c43e73c 100644
--- a/plugins/catalog/alpha-api-report.md
+++ b/plugins/catalog/alpha-api-report.md
@@ -3,8 +3,13 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
+import { BackstagePlugin } from '@backstage/frontend-plugin-api';
+import { Extension } from '@backstage/frontend-plugin-api';
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
+// @alpha (undocumented)
+export const CatalogApi: Extension<{}>;
+
// @alpha (undocumented)
export const catalogTranslationRef: TranslationRef<
'catalog',
@@ -14,5 +19,9 @@ export const catalogTranslationRef: TranslationRef<
}
>;
+// @alpha (undocumented)
+const _default: BackstagePlugin;
+export default _default;
+
// (No @packageDocumentation comment for this package)
```
diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json
index 412c7d0716..4ef0db4910 100644
--- a/plugins/catalog/package.json
+++ b/plugins/catalog/package.json
@@ -51,6 +51,7 @@
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
+ "@backstage/frontend-plugin-api": "workspace:^",
"@backstage/integration-react": "workspace:^",
"@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
diff --git a/plugins/catalog/src/alpha.ts b/plugins/catalog/src/alpha.ts
index 3509c1fb7f..d9c2af338d 100644
--- a/plugins/catalog/src/alpha.ts
+++ b/plugins/catalog/src/alpha.ts
@@ -15,3 +15,40 @@
*/
export * from './translation';
+
+import {
+ DiscoveryApi,
+ FetchApi,
+ discoveryApiRef,
+ fetchApiRef,
+} from '@backstage/core-plugin-api';
+import { CatalogClient } from '@backstage/catalog-client';
+import {
+ createApiExtension,
+ createPlugin,
+} from '@backstage/frontend-plugin-api';
+import { catalogApiRef } from '@backstage/plugin-catalog-react';
+
+/** @alpha */
+export const CatalogApi = createApiExtension({
+ factory: {
+ api: catalogApiRef,
+ deps: {
+ discoveryApi: discoveryApiRef,
+ fetchApi: fetchApiRef,
+ },
+ factory: ({
+ discoveryApi,
+ fetchApi,
+ }: {
+ discoveryApi: DiscoveryApi;
+ fetchApi: FetchApi;
+ }) => new CatalogClient({ discoveryApi, fetchApi }),
+ },
+});
+
+/** @alpha */
+export default createPlugin({
+ id: 'catalog',
+ extensions: [CatalogApi],
+});
diff --git a/plugins/search/alpha-api-report.md b/plugins/search/alpha-api-report.md
new file mode 100644
index 0000000000..5799ff5cd6
--- /dev/null
+++ b/plugins/search/alpha-api-report.md
@@ -0,0 +1,28 @@
+## API Report File for "@backstage/plugin-search"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { BackstagePlugin } from '@backstage/frontend-plugin-api';
+import { Extension } from '@backstage/frontend-plugin-api';
+
+// @alpha (undocumented)
+const _default: BackstagePlugin;
+export default _default;
+
+// @alpha (undocumented)
+export const SearchApi: Extension<{}>;
+
+// @alpha (undocumented)
+export const SearchNavItem: Extension<{
+ title: string;
+}>;
+
+// @alpha (undocumented)
+export const SearchPage: Extension<{
+ path: string;
+ noTrack: boolean;
+}>;
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/search/package.json b/plugins/search/package.json
index 77c3bd4727..8b90867a5a 100644
--- a/plugins/search/package.json
+++ b/plugins/search/package.json
@@ -6,9 +6,22 @@
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
- "access": "public",
- "main": "dist/index.esm.js",
- "types": "dist/index.d.ts"
+ "access": "public"
+ },
+ "exports": {
+ ".": "./src/index.ts",
+ "./alpha": "./src/alpha.tsx",
+ "./package.json": "./package.json"
+ },
+ "typesVersions": {
+ "*": {
+ "alpha": [
+ "src/alpha.tsx"
+ ],
+ "package.json": [
+ "package.json"
+ ]
+ }
},
"backstage": {
"role": "frontend-plugin"
@@ -38,6 +51,7 @@
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
+ "@backstage/frontend-plugin-api": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
"@backstage/plugin-search-common": "workspace:^",
"@backstage/plugin-search-react": "workspace:^",
diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx
new file mode 100644
index 0000000000..2a9a714abd
--- /dev/null
+++ b/plugins/search/src/alpha.tsx
@@ -0,0 +1,245 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+
+import { makeStyles, Theme, Grid, Paper } from '@material-ui/core';
+import SearchIcon from '@material-ui/icons/Search';
+
+import {
+ CatalogIcon,
+ Content,
+ DocsIcon,
+ Header,
+ Page,
+ useSidebarPinState,
+} from '@backstage/core-components';
+import {
+ createRouteRef,
+ useApi,
+ DiscoveryApi,
+ IdentityApi,
+ discoveryApiRef,
+ identityApiRef,
+} from '@backstage/core-plugin-api';
+
+import {
+ createPlugin,
+ createApiExtension,
+ createPageExtension,
+ createExtensionInput,
+ createNavItemExtension,
+ createSchemaFromZod,
+} from '@backstage/frontend-plugin-api';
+
+import {
+ catalogApiRef,
+ CATALOG_FILTER_EXISTS,
+} from '@backstage/plugin-catalog-react';
+
+import {
+ DefaultResultListItem,
+ SearchBar,
+ SearchFilter,
+ SearchPagination,
+ SearchResult as SearchResults,
+ SearchResultPager,
+ useSearch,
+ SearchContextProvider,
+} from '@backstage/plugin-search-react';
+import { SearchResult } from '@backstage/plugin-search-common';
+import { searchApiRef } from '@backstage/plugin-search-react';
+import { searchResultItemExtensionData } from '@backstage/plugin-search-react/alpha';
+
+import { SearchClient } from './apis';
+import { SearchType } from './components/SearchType';
+import { UrlUpdater } from './components/SearchPage/SearchPage';
+
+/** @alpha */
+export const SearchApi = createApiExtension({
+ factory: {
+ api: searchApiRef,
+ deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
+ factory: ({
+ identityApi,
+ discoveryApi,
+ }: {
+ identityApi: IdentityApi;
+ discoveryApi: DiscoveryApi;
+ }) => new SearchClient({ discoveryApi, identityApi }),
+ },
+});
+
+const useSearchPageStyles = makeStyles((theme: Theme) => ({
+ filter: {
+ '& + &': {
+ marginTop: theme.spacing(2.5),
+ },
+ },
+ filters: {
+ padding: theme.spacing(2),
+ marginTop: theme.spacing(2),
+ },
+}));
+
+const searchRouteRef = createRouteRef({ id: 'plugin.search.page' });
+
+/** @alpha */
+export const SearchPage = createPageExtension({
+ id: 'plugin.search.page',
+ routeRef: searchRouteRef,
+ configSchema: createSchemaFromZod(z =>
+ z.object({
+ path: z.string().default('/search'),
+ noTrack: z.boolean().default(false),
+ }),
+ ),
+ inputs: {
+ items: createExtensionInput({
+ item: searchResultItemExtensionData,
+ }),
+ },
+ loader: async ({ config, inputs }) => {
+ const getResultItemComponent = (result: SearchResult) => {
+ const value = inputs.items.find(({ item }) => item?.predicate?.(result));
+ return value?.item.component ?? DefaultResultListItem;
+ };
+
+ const Component = () => {
+ const classes = useSearchPageStyles();
+ const { isMobile } = useSidebarPinState();
+ const { types } = useSearch();
+ const catalogApi = useApi(catalogApiRef);
+
+ return (
+
+ {!isMobile && }
+
+
+
+
+
+ {!isMobile && (
+
+ ,
+ },
+ {
+ value: 'techdocs',
+ name: 'Documentation',
+ icon: ,
+ },
+ {
+ value: 'adr',
+ name: 'Architecture Decision Records',
+ icon: ,
+ },
+ ]}
+ />
+
+ {types.includes('techdocs') && (
+ {
+ // Return a list of entities which are documented.
+ const { items } = await catalogApi.getEntities({
+ fields: ['metadata.name'],
+ filter: {
+ 'metadata.annotations.backstage.io/techdocs-ref':
+ CATALOG_FILTER_EXISTS,
+ },
+ });
+
+ const names = items.map(
+ entity => entity.metadata.name,
+ );
+ names.sort();
+ return names;
+ }}
+ />
+ )}
+
+
+
+
+ )}
+
+
+
+ {({ results }) =>
+ results.map((result, index) => {
+ const SearchResultListItem =
+ getResultItemComponent(result);
+ return (
+
+ );
+ })
+ }
+
+
+
+
+
+
+ );
+ };
+
+ return (
+
+
+
+
+ );
+ },
+});
+
+/** @alpha */
+export const SearchNavItem = createNavItemExtension({
+ id: 'plugin.search.nav.index',
+ routeRef: searchRouteRef,
+ title: 'Search',
+ icon: SearchIcon,
+});
+
+/** @alpha */
+export default createPlugin({
+ id: 'plugin.search',
+ extensions: [SearchApi, SearchPage, SearchNavItem],
+});
diff --git a/plugins/search/src/components/SearchPage/UrlUpdater.tsx b/plugins/search/src/components/SearchPage/UrlUpdater.tsx
new file mode 100644
index 0000000000..d44195b6e2
--- /dev/null
+++ b/plugins/search/src/components/SearchPage/UrlUpdater.tsx
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { useEffect } from 'react';
+import usePrevious from 'react-use/lib/usePrevious';
+import qs from 'qs';
+import { useLocation } from 'react-router-dom';
+import { useSearch } from '@backstage/plugin-search-react';
+import { JsonObject } from '@backstage/types';
+
+export const UrlUpdater = () => {
+ const location = useLocation();
+ const {
+ term,
+ setTerm,
+ types,
+ setTypes,
+ pageCursor,
+ setPageCursor,
+ filters,
+ setFilters,
+ } = useSearch();
+
+ const prevQueryParams = usePrevious(location.search);
+
+ useEffect(() => {
+ // Only respond to changes to url query params
+ if (location.search === prevQueryParams) {
+ return;
+ }
+
+ const query =
+ qs.parse(location.search.substring(1), { arrayLimit: 0 }) || {};
+
+ if (query.filters) {
+ setFilters(query.filters as JsonObject);
+ }
+
+ if (query.query) {
+ setTerm(query.query as string);
+ }
+
+ if (query.pageCursor) {
+ setPageCursor(query.pageCursor as string);
+ }
+
+ if (query.types) {
+ setTypes(query.types as string[]);
+ }
+ }, [prevQueryParams, location, setTerm, setTypes, setPageCursor, setFilters]);
+
+ useEffect(() => {
+ const newParams = qs.stringify(
+ {
+ query: term,
+ types,
+ pageCursor,
+ filters,
+ },
+ { arrayFormat: 'brackets' },
+ );
+ const newUrl = `${window.location.pathname}?${newParams}`;
+
+ // We directly manipulate window history here in order to not re-render
+ // infinitely (state => location => state => etc). The intention of this
+ // code is just to ensure the right query/filters are loaded when a user
+ // clicks the "back" button after clicking a result.
+ window.history.replaceState(null, document.title, newUrl);
+ }, [term, types, pageCursor, filters]);
+
+ return null;
+};
diff --git a/yarn.lock b/yarn.lock
index e429806816..2fe1ebc6f3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6023,6 +6023,7 @@ __metadata:
"@backstage/core-plugin-api": "workspace:^"
"@backstage/dev-utils": "workspace:^"
"@backstage/errors": "workspace:^"
+ "@backstage/frontend-plugin-api": "workspace:^"
"@backstage/integration-react": "workspace:^"
"@backstage/plugin-catalog-common": "workspace:^"
"@backstage/plugin-catalog-react": "workspace:^"
@@ -9117,6 +9118,7 @@ __metadata:
"@backstage/core-plugin-api": "workspace:^"
"@backstage/dev-utils": "workspace:^"
"@backstage/errors": "workspace:^"
+ "@backstage/frontend-plugin-api": "workspace:^"
"@backstage/plugin-catalog-react": "workspace:^"
"@backstage/plugin-search-common": "workspace:^"
"@backstage/plugin-search-react": "workspace:^"