diff --git a/.changeset/beige-wolves-kneel.md b/.changeset/beige-wolves-kneel.md
new file mode 100644
index 0000000000..2c768a3f03
--- /dev/null
+++ b/.changeset/beige-wolves-kneel.md
@@ -0,0 +1,5 @@
+---
+'@backstage/backend-test-utils': patch
+---
+
+Lazy-load `testcontainers` module in order to avoid side-effects.
diff --git a/.changeset/eight-worms-report.md b/.changeset/eight-worms-report.md
new file mode 100644
index 0000000000..1dc12db0e4
--- /dev/null
+++ b/.changeset/eight-worms-report.md
@@ -0,0 +1,5 @@
+---
+'@backstage/backend-common': patch
+---
+
+Use the default CSP policy provided by `helmet` directly rather than a copy.
diff --git a/.changeset/eighty-dancers-heal.md b/.changeset/eighty-dancers-heal.md
new file mode 100644
index 0000000000..9286e75d2c
--- /dev/null
+++ b/.changeset/eighty-dancers-heal.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-auth-backend': patch
+---
+
+Update `auth0` and `onelogin` providers to allow for `authHandler` and `signIn.resolver` configuration.
diff --git a/.changeset/four-phones-shave.md b/.changeset/four-phones-shave.md
new file mode 100644
index 0000000000..1ef58bdadb
--- /dev/null
+++ b/.changeset/four-phones-shave.md
@@ -0,0 +1,29 @@
+---
+'@backstage/plugin-auth-backend': minor
+---
+
+Avoid ever returning OAuth refresh tokens back to the client, and always exchange refresh tokens for a new one when available for all providers.
+
+This comes with a breaking change to the TypeScript API for custom auth providers. The `refresh` method of `OAuthHandlers` implementation must now return a `{ response, refreshToken }` object rather than a direct response. Existing `refresh` implementations are typically migrated by changing an existing return expression that looks like this:
+
+```ts
+return await this.handleResult({
+ fullProfile,
+ params,
+ accessToken,
+ refreshToken,
+});
+```
+
+Into the following:
+
+```ts
+return {
+ response: await this.handleResult({
+ fullProfile,
+ params,
+ accessToken,
+ }),
+ refreshToken,
+};
+```
diff --git a/.changeset/four-waves-tan.md b/.changeset/four-waves-tan.md
new file mode 100644
index 0000000000..444891d31b
--- /dev/null
+++ b/.changeset/four-waves-tan.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-search': patch
+---
+
+Internal cleanup of the exports structure
diff --git a/.changeset/happy-moose-worry.md b/.changeset/happy-moose-worry.md
new file mode 100644
index 0000000000..66f0728f7d
--- /dev/null
+++ b/.changeset/happy-moose-worry.md
@@ -0,0 +1,5 @@
+---
+'@backstage/create-app': patch
+---
+
+Removed unused templating asset.
diff --git a/.changeset/honest-chefs-mate.md b/.changeset/honest-chefs-mate.md
new file mode 100644
index 0000000000..dfc1ff16fb
--- /dev/null
+++ b/.changeset/honest-chefs-mate.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-app-backend': patch
+---
+
+Set `X-Frame-Options: deny` rather than the default `sameorigin` for all content served by the `app-backend`.`
diff --git a/.changeset/modern-waves-lay.md b/.changeset/modern-waves-lay.md
new file mode 100644
index 0000000000..c0d9804379
--- /dev/null
+++ b/.changeset/modern-waves-lay.md
@@ -0,0 +1,5 @@
+---
+'@backstage/create-app': patch
+---
+
+Add a comment to the default backend about the fallback 404 handler.
diff --git a/.changeset/search-en-jul-hemma.md b/.changeset/search-en-jul-hemma.md
new file mode 100644
index 0000000000..9fae9fcaa5
--- /dev/null
+++ b/.changeset/search-en-jul-hemma.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-search': patch
+---
+
+Introduces a `` variant, which operates on the same part of a search query as the existing ``, but in a more opinionated way (as a single-select control surface suitable for faceted search UIs).
+
+Check the [search plugin storybook](https://backstage.io/storybook/?path=/story/plugins-search-searchtype--accordion) to see how it can be used.
diff --git a/.changeset/search-odd-starfishes-raise.md b/.changeset/search-odd-starfishes-raise.md
new file mode 100644
index 0000000000..73627cf457
--- /dev/null
+++ b/.changeset/search-odd-starfishes-raise.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-search': patch
+---
+
+Captures the search term entered in the SearchBarBase as a `search` event.
diff --git a/.changeset/search-over-tysta-skogor.md b/.changeset/search-over-tysta-skogor.md
new file mode 100644
index 0000000000..5398ae7f5f
--- /dev/null
+++ b/.changeset/search-over-tysta-skogor.md
@@ -0,0 +1,58 @@
+---
+'@backstage/create-app': patch
+---
+
+The `` filter in the composed `SearchPage.tsx` was replaced with the `` variant.
+
+This is an entirely optional change; if you wish to display a control surface for search `types` as a single-select accordion (as opposed to the current multi-select of checkboxes), you can make the following (or similar) changes to your search page layout:
+
+```diff
+--- a/packages/app/src/components/search/SearchPage.tsx
++++ b/packages/app/src/components/search/SearchPage.tsx
+@@ -11,7 +11,7 @@ import {
+ SearchType,
+ DefaultResultListItem,
+ } from '@backstage/plugin-search';
+-import { Content, Header, Page } from '@backstage/core-components';
++import { CatalogIcon, Content, DocsIcon, Header, Page } from '@backstage/core-components';
+
+ const useStyles = makeStyles((theme: Theme) => ({
+ bar: {
+@@ -19,6 +19,7 @@ const useStyles = makeStyles((theme: Theme) => ({
+ },
+ filters: {
+ padding: theme.spacing(2),
++ marginTop: theme.spacing(2),
+ },
+ filter: {
+ '& + &': {
+@@ -41,12 +42,23 @@ const SearchPage = () => {
+
+
+
++ ,
++ },
++ {
++ value: 'techdocs',
++ name: 'Documentation',
++ icon: ,
++ },
++ ]}
++ />
+
+-
+ ({
},
filters: {
padding: theme.spacing(2),
+ marginTop: theme.spacing(2),
},
}));
@@ -64,12 +67,23 @@ const SearchPage = () => {
{!isMobile && (
+ ,
+ },
+ {
+ value: 'techdocs',
+ name: 'Documentation',
+ icon: ,
+ },
+ ]}
+ />
-
| undefined,
-): CspOptions | undefined {
- const result: CspOptions = { ...DEFAULT_CSP };
+): ContentSecurityPolicyOptions['directives'] {
+ const result: ContentSecurityPolicyOptions['directives'] =
+ helmet.contentSecurityPolicy.getDefaultDirectives();
if (directives) {
for (const [key, value] of Object.entries(directives)) {
diff --git a/packages/backend-common/src/service/lib/config.ts b/packages/backend-common/src/service/lib/config.ts
index d69b00f04c..91eeff13aa 100644
--- a/packages/backend-common/src/service/lib/config.ts
+++ b/packages/backend-common/src/service/lib/config.ts
@@ -42,8 +42,6 @@ export type CertificateAttributes = {
/**
* A map from CSP directive names to their values.
- *
- * Added here since helmet doesn't export this type publicly.
*/
export type CspOptions = Record;
diff --git a/packages/backend-test-utils/src/database/startMysqlContainer.ts b/packages/backend-test-utils/src/database/startMysqlContainer.ts
index 9601854cb3..fa99389cbc 100644
--- a/packages/backend-test-utils/src/database/startMysqlContainer.ts
+++ b/packages/backend-test-utils/src/database/startMysqlContainer.ts
@@ -15,7 +15,6 @@
*/
import createConnection, { Knex } from 'knex';
-import { GenericContainer } from 'testcontainers';
import { v4 as uuid } from 'uuid';
async function waitForMysqlReady(
@@ -50,6 +49,9 @@ export async function startMysqlContainer(image: string) {
const user = 'root';
const password = uuid();
+ // Lazy-load to avoid side-effect of importing testcontainers
+ const { GenericContainer } = await import('testcontainers');
+
const container = await new GenericContainer(image)
.withExposedPorts(3306)
.withEnv('MYSQL_ROOT_PASSWORD', password)
diff --git a/packages/backend-test-utils/src/database/startPostgresContainer.ts b/packages/backend-test-utils/src/database/startPostgresContainer.ts
index 89a0417e22..81358e01d3 100644
--- a/packages/backend-test-utils/src/database/startPostgresContainer.ts
+++ b/packages/backend-test-utils/src/database/startPostgresContainer.ts
@@ -15,7 +15,6 @@
*/
import createConnection, { Knex } from 'knex';
-import { GenericContainer } from 'testcontainers';
import { v4 as uuid } from 'uuid';
async function waitForPostgresReady(
@@ -50,6 +49,9 @@ export async function startPostgresContainer(image: string) {
const user = 'postgres';
const password = uuid();
+ // Lazy-load to avoid side-effect of importing testcontainers
+ const { GenericContainer } = await import('testcontainers');
+
const container = await new GenericContainer(image)
.withExposedPorts(5432)
.withEnv('POSTGRES_PASSWORD', password)
diff --git a/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx
index 95c8c64c62..cf380b6f5c 100644
--- a/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx
+++ b/packages/create-app/templates/default-app/packages/app/src/components/search/SearchPage.tsx
@@ -11,7 +11,13 @@ import {
SearchType,
DefaultResultListItem,
} from '@backstage/plugin-search';
-import { Content, Header, Page } from '@backstage/core-components';
+import {
+ CatalogIcon,
+ Content,
+ DocsIcon,
+ Header,
+ Page,
+} from '@backstage/core-components';
const useStyles = makeStyles((theme: Theme) => ({
bar: {
@@ -19,6 +25,7 @@ const useStyles = makeStyles((theme: Theme) => ({
},
filters: {
padding: theme.spacing(2),
+ marginTop: theme.spacing(2),
},
filter: {
'& + &': {
@@ -41,12 +48,23 @@ const SearchPage = () => {
+ ,
+ },
+ {
+ value: 'techdocs',
+ name: 'Documentation',
+ icon: ,
+ },
+ ]}
+ />
-
-
-
-
-
-
-
- Backstage
-
-
-
-
-
-
-
diff --git a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx
index 32a6ec0f77..936edc07da 100644
--- a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx
+++ b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx
@@ -62,6 +62,7 @@ paths:
kind: 'API',
metadata: {
name: 'my-name',
+ title: 'My Name',
},
spec: {
type: 'openapi',
@@ -88,7 +89,7 @@ paths:
);
await waitFor(() => {
- expect(getByText(/my-name/i)).toBeInTheDocument();
+ expect(getByText(/My Name/i)).toBeInTheDocument();
expect(getByText(/OpenAPI/)).toBeInTheDocument();
expect(getByText(/Raw/i)).toBeInTheDocument();
expect(getByText(/List all artists/i)).toBeInTheDocument();
@@ -101,6 +102,7 @@ paths:
kind: 'API',
metadata: {
name: 'my-name',
+ title: 'My Name',
},
spec: {
type: 'custom-type',
@@ -118,7 +120,7 @@ paths:
,
);
- expect(getByText(/my-name/i)).toBeInTheDocument();
+ expect(getByText(/My Name/i)).toBeInTheDocument();
expect(getByText(/custom-type/i)).toBeInTheDocument();
expect(
getAllByText(
diff --git a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx
index 2ad5a4aca7..6f3e155816 100644
--- a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx
+++ b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.tsx
@@ -39,10 +39,11 @@ export const ApiDefinitionCard = (_: Props) => {
}
const definitionWidget = getApiDefinitionWidget(entity);
+ const entityTitle = entity.metadata.title ?? entity.metadata.name;
if (definitionWidget) {
return (
-
+
{definitionWidget.component(entity.spec.definition)}
@@ -58,7 +59,7 @@ export const ApiDefinitionCard = (_: Props) => {
return (
diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json
index fffc34ee17..58469b27fe 100644
--- a/plugins/app-backend/package.json
+++ b/plugins/app-backend/package.json
@@ -38,6 +38,7 @@
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"fs-extra": "9.1.0",
+ "helmet": "^4.0.0",
"winston": "^3.2.1",
"yn": "^4.0.0"
},
diff --git a/plugins/app-backend/src/service/router.ts b/plugins/app-backend/src/service/router.ts
index 297029dffc..171684a964 100644
--- a/plugins/app-backend/src/service/router.ts
+++ b/plugins/app-backend/src/service/router.ts
@@ -16,6 +16,7 @@
import { notFoundHandler, resolvePackagePath } from '@backstage/backend-common';
import { Config } from '@backstage/config';
+import helmet from 'helmet';
import express from 'express';
import Router from 'express-promise-router';
import fs from 'fs-extra';
@@ -89,6 +90,8 @@ export async function createRouter(
const router = Router();
+ router.use(helmet.frameguard({ action: 'deny' }));
+
// Use a separate router for static content so that a fallback can be provided by backend
const staticRouter = Router();
staticRouter.use(express.static(resolvePath(appDistDir, 'static')));
diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md
index 9ed523c678..af46d874df 100644
--- a/plugins/auth-backend/api-report.md
+++ b/plugins/auth-backend/api-report.md
@@ -27,10 +27,13 @@ export class AtlassianAuthProvider implements OAuthHandlers {
// (undocumented)
handler(req: express.Request): Promise<{
response: OAuthResponse;
- refreshToken: string;
+ refreshToken: string | undefined;
}>;
// (undocumented)
- refresh(req: OAuthRefreshRequest): Promise;
+ refresh(req: OAuthRefreshRequest): Promise<{
+ response: OAuthResponse;
+ refreshToken: string | undefined;
+ }>;
// Warning: (ae-forgotten-export) The symbol "RedirectInfo" needs to be exported by the entry point index.d.ts
//
// (undocumented)
@@ -47,6 +50,14 @@ export type AtlassianProviderOptions = {
};
};
+// @public (undocumented)
+export type Auth0ProviderOptions = {
+ authHandler?: AuthHandler;
+ signIn?: {
+ resolver: SignInResolver;
+ };
+};
+
// @public
export type AuthHandler = (
input: AuthResult,
@@ -219,6 +230,11 @@ export const createAtlassianProvider: (
options?: AtlassianProviderOptions | undefined,
) => AuthProviderFactory;
+// @public (undocumented)
+export const createAuth0Provider: (
+ options?: Auth0ProviderOptions | undefined,
+) => AuthProviderFactory;
+
// Warning: (ae-missing-release-tag) "createAwsAlbProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -282,6 +298,11 @@ export const createOktaProvider: (
_options?: OktaProviderOptions | undefined,
) => AuthProviderFactory;
+// @public (undocumented)
+export const createOneLoginProvider: (
+ options?: OneLoginProviderOptions | undefined,
+) => AuthProviderFactory;
+
// Warning: (ae-missing-release-tag) "createOriginFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -470,7 +491,10 @@ export interface OAuthHandlers {
// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
- refresh?(req: OAuthRefreshRequest): Promise;
+ refresh?(req: OAuthRefreshRequest): Promise<{
+ response: OAuthResponse;
+ refreshToken?: string;
+ }>;
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
@@ -485,7 +509,6 @@ export type OAuthProviderInfo = {
idToken?: string;
expiresInSeconds?: number;
scope: string;
- refreshToken?: string;
};
// Warning: (ae-missing-release-tag) "OAuthProviderOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -572,6 +595,14 @@ export type OktaProviderOptions = {
};
};
+// @public (undocumented)
+export type OneLoginProviderOptions = {
+ authHandler?: AuthHandler;
+ signIn?: {
+ resolver: SignInResolver;
+ };
+};
+
// Warning: (ae-missing-release-tag) "postMessageResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -677,11 +708,11 @@ export type WebMessageResponse =
//
// src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts
// src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts
-// src/providers/github/provider.d.ts:71:58 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag
-// src/providers/github/provider.d.ts:71:90 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag
-// src/providers/github/provider.d.ts:71:89 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
-// src/providers/github/provider.d.ts:71:67 - (tsdoc-malformed-html-name) Invalid HTML element: Expecting an HTML name
-// src/providers/github/provider.d.ts:71:68 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
-// src/providers/github/provider.d.ts:78:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts
+// src/providers/github/provider.d.ts:74:58 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag
+// src/providers/github/provider.d.ts:74:90 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag
+// src/providers/github/provider.d.ts:74:89 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
+// src/providers/github/provider.d.ts:74:67 - (tsdoc-malformed-html-name) Invalid HTML element: Expecting an HTML name
+// src/providers/github/provider.d.ts:74:68 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
+// src/providers/github/provider.d.ts:81:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts
// src/providers/types.d.ts:100:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts
```
diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts
index a3fc77bc02..98f4cd9fa1 100644
--- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts
+++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts
@@ -57,7 +57,10 @@ describe('OAuthAdapter', () => {
};
}
async refresh() {
- return mockResponseData;
+ return {
+ response: mockResponseData,
+ refreshToken: 'token',
+ };
}
}
const providerInstance = new MyAuthProvider();
@@ -257,7 +260,10 @@ describe('OAuthAdapter', () => {
});
it('correctly populates incomplete identities', async () => {
- const mockRefresh = jest.fn, [express.Request]>();
+ const mockRefresh = jest.fn<
+ Promise<{ response: OAuthResponse }>,
+ [express.Request]
+ >();
const oauthProvider = new OAuthAdapter(
{
@@ -291,10 +297,12 @@ describe('OAuthAdapter', () => {
// Without a token
mockRefresh.mockResolvedValueOnce({
- ...mockResponseData,
- backstageIdentity: {
- id: 'foo',
- token: '',
+ response: {
+ ...mockResponseData,
+ backstageIdentity: {
+ id: 'foo',
+ token: '',
+ },
},
});
await oauthProvider.refresh(mockRequest, mockResponse);
@@ -315,10 +323,12 @@ describe('OAuthAdapter', () => {
// With a token
mockRefresh.mockResolvedValueOnce({
- ...mockResponseData,
- backstageIdentity: {
- id: 'foo',
- token: `z.${mkTokenBody({ sub: 'user:my-ns/foo' })}.z`,
+ response: {
+ ...mockResponseData,
+ backstageIdentity: {
+ id: 'foo',
+ token: `z.${mkTokenBody({ sub: 'user:my-ns/foo' })}.z`,
+ },
},
});
await oauthProvider.refresh(mockRequest, mockResponse);
diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts
index 4d5d507aa1..ce4b52ef4f 100644
--- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts
+++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts
@@ -212,19 +212,15 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
const forwardReq = Object.assign(req, { scope, refreshToken });
// get new access_token
- const response = await this.handlers.refresh(
- forwardReq as OAuthRefreshRequest,
- );
+ const { response, refreshToken: newRefreshToken } =
+ await this.handlers.refresh(forwardReq as OAuthRefreshRequest);
const backstageIdentity = await this.populateIdentity(
response.backstageIdentity,
);
- if (
- response.providerInfo.refreshToken &&
- response.providerInfo.refreshToken !== refreshToken
- ) {
- this.setRefreshTokenCookie(res, response.providerInfo.refreshToken);
+ if (newRefreshToken && newRefreshToken !== refreshToken) {
+ this.setRefreshTokenCookie(res, newRefreshToken);
}
res.status(200).json({ ...response, backstageIdentity });
diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts
index cd1439b399..f54d04d15b 100644
--- a/plugins/auth-backend/src/lib/oauth/types.ts
+++ b/plugins/auth-backend/src/lib/oauth/types.ts
@@ -79,10 +79,6 @@ export type OAuthProviderInfo = {
* Scopes granted for the access token.
*/
scope: string;
- /**
- * A refresh token issued for the signed in user
- */
- refreshToken?: string;
};
export type OAuthState = {
@@ -130,7 +126,10 @@ export interface OAuthHandlers {
* @param {string} refreshToken
* @param {string} scope
*/
- refresh?(req: OAuthRefreshRequest): Promise;
+ refresh?(req: OAuthRefreshRequest): Promise<{
+ response: OAuthResponse;
+ refreshToken?: string;
+ }>;
/**
* (Optional) Sign out of the auth provider.
diff --git a/plugins/auth-backend/src/providers/atlassian/provider.test.ts b/plugins/auth-backend/src/providers/atlassian/provider.test.ts
index 7bed582f81..29241dd17b 100644
--- a/plugins/auth-backend/src/providers/atlassian/provider.test.ts
+++ b/plugins/auth-backend/src/providers/atlassian/provider.test.ts
@@ -78,20 +78,22 @@ describe('createAtlassianProvider', () => {
refreshToken: 'wacka',
},
});
- const { response } = await provider.handler({} as any);
- expect(response).toEqual({
- providerInfo: {
- accessToken: 'accessToken',
- expiresInSeconds: 123,
- idToken: 'idToken',
- scope: 'scope',
- refreshToken: 'wacka',
- },
- profile: {
- email: 'conrad@example.com',
- displayName: 'Conrad',
- picture: 'http://google.com/lols',
+ const result = await provider.handler({} as any);
+ expect(result).toEqual({
+ response: {
+ providerInfo: {
+ accessToken: 'accessToken',
+ expiresInSeconds: 123,
+ idToken: 'idToken',
+ scope: 'scope',
+ },
+ profile: {
+ email: 'conrad@example.com',
+ displayName: 'Conrad',
+ picture: 'http://google.com/lols',
+ },
},
+ refreshToken: 'wacka',
});
});
@@ -127,20 +129,22 @@ describe('createAtlassianProvider', () => {
],
});
- const response = await provider.refresh({} as any);
+ const result = await provider.refresh({} as any);
- expect(response).toEqual({
- profile: {
- displayName: 'Mocked User',
- email: 'mockuser@gmail.com',
- picture: 'http://google.com/lols',
- },
- providerInfo: {
- accessToken: 'a.b.c',
- idToken: 'my-id',
- refreshToken: 'dont-forget-to-send-refresh',
- scope: 'read_user',
+ expect(result).toEqual({
+ response: {
+ profile: {
+ displayName: 'Mocked User',
+ email: 'mockuser@gmail.com',
+ picture: 'http://google.com/lols',
+ },
+ providerInfo: {
+ accessToken: 'a.b.c',
+ idToken: 'my-id',
+ scope: 'read_user',
+ },
},
+ refreshToken: 'dont-forget-to-send-refresh',
});
});
});
diff --git a/plugins/auth-backend/src/providers/atlassian/provider.ts b/plugins/auth-backend/src/providers/atlassian/provider.ts
index e19f29a3a4..ba402eace0 100644
--- a/plugins/auth-backend/src/providers/atlassian/provider.ts
+++ b/plugins/auth-backend/src/providers/atlassian/provider.ts
@@ -107,9 +107,7 @@ export class AtlassianAuthProvider implements OAuthHandlers {
});
}
- async handler(
- req: express.Request,
- ): Promise<{ response: OAuthResponse; refreshToken: string }> {
+ async handler(req: express.Request) {
const { result } = await executeFrameHandlerStrategy(
req,
this._strategy,
@@ -117,7 +115,7 @@ export class AtlassianAuthProvider implements OAuthHandlers {
return {
response: await this.handleResult(result),
- refreshToken: result.refreshToken ?? '',
+ refreshToken: result.refreshToken,
};
}
@@ -128,7 +126,6 @@ export class AtlassianAuthProvider implements OAuthHandlers {
providerInfo: {
idToken: result.params.id_token,
accessToken: result.accessToken,
- refreshToken: result.refreshToken,
scope: result.params.scope,
expiresInSeconds: result.params.expires_in,
},
@@ -152,28 +149,27 @@ export class AtlassianAuthProvider implements OAuthHandlers {
return response;
}
- async refresh(req: OAuthRefreshRequest): Promise {
- const {
- accessToken,
- params,
- refreshToken: newRefreshToken,
- } = await executeRefreshTokenStrategy(
- this._strategy,
- req.refreshToken,
- req.scope,
- );
+ async refresh(req: OAuthRefreshRequest) {
+ const { accessToken, params, refreshToken } =
+ await executeRefreshTokenStrategy(
+ this._strategy,
+ req.refreshToken,
+ req.scope,
+ );
const fullProfile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
);
- return this.handleResult({
- fullProfile,
- params,
- accessToken,
- refreshToken: newRefreshToken,
- });
+ return {
+ response: await this.handleResult({
+ fullProfile,
+ params,
+ accessToken,
+ }),
+ refreshToken,
+ };
}
}
diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts
index 7aa98c3e65..b99bbe86dd 100644
--- a/plugins/auth-backend/src/providers/auth0/provider.ts
+++ b/plugins/auth-backend/src/providers/auth0/provider.ts
@@ -36,7 +36,15 @@ import {
makeProfileInfo,
PassportDoneCallback,
} from '../../lib/passport';
-import { RedirectInfo, AuthProviderFactory } from '../types';
+import {
+ RedirectInfo,
+ AuthProviderFactory,
+ AuthHandler,
+ SignInResolver,
+} from '../types';
+import { CatalogIdentityClient } from '../../lib/catalog';
+import { TokenIssuer } from '../../identity';
+import { Logger } from 'winston';
type PrivateInfo = {
refreshToken: string;
@@ -44,12 +52,27 @@ type PrivateInfo = {
export type Auth0AuthProviderOptions = OAuthProviderOptions & {
domain: string;
+ signInResolver?: SignInResolver;
+ authHandler: AuthHandler;
+ tokenIssuer: TokenIssuer;
+ catalogIdentityClient: CatalogIdentityClient;
+ logger: Logger;
};
export class Auth0AuthProvider implements OAuthHandlers {
private readonly _strategy: Auth0Strategy;
+ private readonly signInResolver?: SignInResolver;
+ private readonly authHandler: AuthHandler;
+ private readonly tokenIssuer: TokenIssuer;
+ private readonly catalogIdentityClient: CatalogIdentityClient;
+ private readonly logger: Logger;
constructor(options: Auth0AuthProviderOptions) {
+ this.signInResolver = options.signInResolver;
+ this.authHandler = options.authHandler;
+ this.tokenIssuer = options.tokenIssuer;
+ this.catalogIdentityClient = options.catalogIdentityClient;
+ this.logger = options.logger;
this._strategy = new Auth0Strategy(
{
clientID: options.clientId,
@@ -90,88 +113,144 @@ export class Auth0AuthProvider implements OAuthHandlers {
});
}
- async handler(
- req: express.Request,
- ): Promise<{ response: OAuthResponse; refreshToken: string }> {
+ async handler(req: express.Request) {
const { result, privateInfo } = await executeFrameHandlerStrategy<
OAuthResult,
PrivateInfo
>(req, this._strategy);
- const profile = makeProfileInfo(result.fullProfile, result.params.id_token);
-
return {
- response: await this.populateIdentity({
- profile,
- providerInfo: {
- idToken: result.params.id_token,
- accessToken: result.accessToken,
- scope: result.params.scope,
- expiresInSeconds: result.params.expires_in,
- },
- }),
+ response: await this.handleResult(result),
refreshToken: privateInfo.refreshToken,
};
}
- async refresh(req: OAuthRefreshRequest): Promise {
- const { accessToken, params } = await executeRefreshTokenStrategy(
- this._strategy,
- req.refreshToken,
- req.scope,
- );
+ async refresh(req: OAuthRefreshRequest) {
+ const { accessToken, refreshToken, params } =
+ await executeRefreshTokenStrategy(
+ this._strategy,
+ req.refreshToken,
+ req.scope,
+ );
const fullProfile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
);
- const profile = makeProfileInfo(fullProfile, params.id_token);
- return this.populateIdentity({
- providerInfo: {
+ return {
+ response: await this.handleResult({
+ fullProfile,
+ params,
accessToken,
- idToken: params.id_token,
- expiresInSeconds: params.expires_in,
- scope: params.scope,
- },
- profile,
- });
+ }),
+ refreshToken,
+ };
}
- // Use this function to grab the user profile info from the token
- // Then populate the profile with it
- private async populateIdentity(
- response: OAuthResponse,
- ): Promise {
- const { profile } = response;
+ private async handleResult(result: OAuthResult) {
+ const { profile } = await this.authHandler(result);
- if (!profile.email) {
- throw new Error('Profile does not contain an email');
+ const response: OAuthResponse = {
+ providerInfo: {
+ idToken: result.params.id_token,
+ accessToken: result.accessToken,
+ scope: result.params.scope,
+ expiresInSeconds: result.params.expires_in,
+ },
+ profile,
+ };
+
+ if (this.signInResolver) {
+ response.backstageIdentity = await this.signInResolver(
+ {
+ result,
+ profile,
+ },
+ {
+ tokenIssuer: this.tokenIssuer,
+ catalogIdentityClient: this.catalogIdentityClient,
+ logger: this.logger,
+ },
+ );
}
- const id = profile.email.split('@')[0];
-
- return { ...response, backstageIdentity: { id, token: '' } };
+ return response;
}
}
-export type Auth0ProviderOptions = {};
+const defaultSignInResolver: SignInResolver = async info => {
+ const { profile } = info;
+ if (!profile.email) {
+ throw new Error('Profile does not contain an email');
+ }
+
+ const id = profile.email.split('@')[0];
+
+ return { id, token: '' };
+};
+
+/** @public */
+export type Auth0ProviderOptions = {
+ /**
+ * The profile transformation function used to verify and convert the auth response
+ * into the profile that will be presented to the user.
+ */
+ authHandler?: AuthHandler;
+
+ /**
+ * Configure sign-in for this provider, without it the provider can not be used to sign users in.
+ */
+ signIn?: {
+ /**
+ * Maps an auth result to a Backstage identity for the user.
+ */
+ resolver: SignInResolver;
+ };
+};
+
+/** @public */
export const createAuth0Provider = (
- _options?: Auth0ProviderOptions,
+ options?: Auth0ProviderOptions,
): AuthProviderFactory => {
- return ({ providerId, globalConfig, config, tokenIssuer }) =>
+ return ({
+ providerId,
+ globalConfig,
+ config,
+ tokenIssuer,
+ catalogApi,
+ logger,
+ }) =>
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
const clientId = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const domain = envConfig.getString('domain');
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
+ const catalogIdentityClient = new CatalogIdentityClient({
+ catalogApi,
+ tokenIssuer,
+ });
+
+ const authHandler: AuthHandler = options?.authHandler
+ ? options.authHandler
+ : async ({ fullProfile, params }) => ({
+ profile: makeProfileInfo(fullProfile, params.id_token),
+ });
+
+ const signInResolver = options?.signIn?.resolver ?? defaultSignInResolver;
+
const provider = new Auth0AuthProvider({
clientId,
clientSecret,
callbackUrl,
domain,
+ authHandler,
+ signInResolver,
+ tokenIssuer,
+ catalogIdentityClient,
+ logger,
});
return OAuthAdapter.fromConfig(globalConfig, provider, {
diff --git a/plugins/auth-backend/src/providers/bitbucket/provider.ts b/plugins/auth-backend/src/providers/bitbucket/provider.ts
index 3a518a6baa..5ad9a739d9 100644
--- a/plugins/auth-backend/src/providers/bitbucket/provider.ts
+++ b/plugins/auth-backend/src/providers/bitbucket/provider.ts
@@ -138,9 +138,7 @@ export class BitbucketAuthProvider implements OAuthHandlers {
});
}
- async handler(
- req: express.Request,
- ): Promise<{ response: OAuthResponse; refreshToken: string }> {
+ async handler(req: express.Request) {
const { result, privateInfo } = await executeFrameHandlerStrategy<
OAuthResult,
PrivateInfo
@@ -152,22 +150,25 @@ export class BitbucketAuthProvider implements OAuthHandlers {
};
}
- async refresh(req: OAuthRefreshRequest): Promise {
- const { accessToken, params } = await executeRefreshTokenStrategy(
- this._strategy,
- req.refreshToken,
- req.scope,
- );
+ async refresh(req: OAuthRefreshRequest) {
+ const { accessToken, refreshToken, params } =
+ await executeRefreshTokenStrategy(
+ this._strategy,
+ req.refreshToken,
+ req.scope,
+ );
const fullProfile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
);
- return this.handleResult({
- fullProfile,
- params,
- accessToken,
- refreshToken: req.refreshToken,
- });
+ return {
+ response: await this.handleResult({
+ fullProfile,
+ params,
+ accessToken,
+ }),
+ refreshToken,
+ };
}
private async handleResult(result: BitbucketOAuthResult) {
diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts
index e418ab22c2..b11ac7f5a1 100644
--- a/plugins/auth-backend/src/providers/github/provider.test.ts
+++ b/plugins/auth-backend/src/providers/github/provider.test.ts
@@ -316,24 +316,26 @@ describe('GithubAuthProvider', () => {
],
});
- const response = await provider.refresh({} as any);
+ const result = await provider.refresh({} as any);
- expect(response).toEqual({
- backstageIdentity: {
- id: 'mockuser',
- token: 'token-for-mockuser',
- },
- profile: {
- displayName: 'Mocked User',
- email: 'mockuser@gmail.com',
- picture: undefined,
- },
- providerInfo: {
- accessToken: 'a.b.c',
- refreshToken: 'dont-forget-to-send-refresh',
- expiresInSeconds: 123,
- scope: 'read_user',
+ expect(result).toEqual({
+ response: {
+ backstageIdentity: {
+ id: 'mockuser',
+ token: 'token-for-mockuser',
+ },
+ profile: {
+ displayName: 'Mocked User',
+ email: 'mockuser@gmail.com',
+ picture: undefined,
+ },
+ providerInfo: {
+ accessToken: 'a.b.c',
+ expiresInSeconds: 123,
+ scope: 'read_user',
+ },
},
+ refreshToken: 'dont-forget-to-send-refresh',
});
});
});
diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts
index c7e82cc5ff..b832f4f77b 100644
--- a/plugins/auth-backend/src/providers/github/provider.ts
+++ b/plugins/auth-backend/src/providers/github/provider.ts
@@ -129,26 +129,26 @@ export class GithubAuthProvider implements OAuthHandlers {
};
}
- async refresh(req: OAuthRefreshRequest): Promise {
- const {
- accessToken,
- refreshToken: newRefreshToken,
- params,
- } = await executeRefreshTokenStrategy(
- this._strategy,
- req.refreshToken,
- req.scope,
- );
+ async refresh(req: OAuthRefreshRequest) {
+ const { accessToken, refreshToken, params } =
+ await executeRefreshTokenStrategy(
+ this._strategy,
+ req.refreshToken,
+ req.scope,
+ );
const fullProfile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
);
- return this.handleResult({
- fullProfile,
- params,
- accessToken,
- refreshToken: newRefreshToken,
- });
+
+ return {
+ response: await this.handleResult({
+ fullProfile,
+ params,
+ accessToken,
+ }),
+ refreshToken,
+ };
}
private async handleResult(result: GithubOAuthResult) {
@@ -158,7 +158,6 @@ export class GithubAuthProvider implements OAuthHandlers {
const response: OAuthResponse = {
providerInfo: {
accessToken: result.accessToken,
- refreshToken: result.refreshToken, // GitHub expires the old refresh token when used
scope: result.params.scope,
expiresInSeconds:
expiresInStr === undefined ? undefined : Number(expiresInStr),
diff --git a/plugins/auth-backend/src/providers/gitlab/provider.test.ts b/plugins/auth-backend/src/providers/gitlab/provider.test.ts
index d1a84b43e8..f90de3c75b 100644
--- a/plugins/auth-backend/src/providers/gitlab/provider.test.ts
+++ b/plugins/auth-backend/src/providers/gitlab/provider.test.ts
@@ -184,23 +184,25 @@ describe('GitlabAuthProvider', () => {
],
});
- const response = await provider.refresh({} as any);
+ const result = await provider.refresh({} as any);
- expect(response).toEqual({
- backstageIdentity: {
- id: 'mockuser',
- },
- profile: {
- displayName: 'Mocked User',
- email: 'mockuser@gmail.com',
- picture: 'http://gitlab.com/lols',
- },
- providerInfo: {
- accessToken: 'a.b.c',
- idToken: 'my-id',
- refreshToken: 'dont-forget-to-send-refresh',
- scope: 'read_user',
+ expect(result).toEqual({
+ response: {
+ backstageIdentity: {
+ id: 'mockuser',
+ },
+ profile: {
+ displayName: 'Mocked User',
+ email: 'mockuser@gmail.com',
+ picture: 'http://gitlab.com/lols',
+ },
+ providerInfo: {
+ accessToken: 'a.b.c',
+ idToken: 'my-id',
+ scope: 'read_user',
+ },
},
+ refreshToken: 'dont-forget-to-send-refresh',
});
});
});
diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts
index 6ca405d15f..dedbb7bd5b 100644
--- a/plugins/auth-backend/src/providers/gitlab/provider.ts
+++ b/plugins/auth-backend/src/providers/gitlab/provider.ts
@@ -132,9 +132,7 @@ export class GitlabAuthProvider implements OAuthHandlers {
});
}
- async handler(
- req: express.Request,
- ): Promise<{ response: OAuthResponse; refreshToken: string }> {
+ async handler(req: express.Request) {
const { result, privateInfo } = await executeFrameHandlerStrategy<
OAuthResult,
PrivateInfo
@@ -146,28 +144,26 @@ export class GitlabAuthProvider implements OAuthHandlers {
};
}
- async refresh(req: OAuthRefreshRequest): Promise {
- const {
- accessToken,
- refreshToken: newRefreshToken,
- params,
- } = await executeRefreshTokenStrategy(
- this._strategy,
- req.refreshToken,
- req.scope,
- );
+ async refresh(req: OAuthRefreshRequest) {
+ const { accessToken, refreshToken, params } =
+ await executeRefreshTokenStrategy(
+ this._strategy,
+ req.refreshToken,
+ req.scope,
+ );
const fullProfile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
);
-
- return this.handleResult({
- fullProfile,
- params,
- accessToken,
- refreshToken: newRefreshToken,
- });
+ return {
+ response: await this.handleResult({
+ fullProfile,
+ params,
+ accessToken,
+ }),
+ refreshToken,
+ };
}
private async handleResult(result: OAuthResult): Promise {
@@ -177,7 +173,6 @@ export class GitlabAuthProvider implements OAuthHandlers {
providerInfo: {
idToken: result.params.id_token,
accessToken: result.accessToken,
- refreshToken: result.refreshToken, // GitLab expires the old refresh token when used
scope: result.params.scope,
expiresInSeconds: result.params.expires_in,
},
diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts
index d85fc2de2d..10123bfe6f 100644
--- a/plugins/auth-backend/src/providers/google/provider.ts
+++ b/plugins/auth-backend/src/providers/google/provider.ts
@@ -113,9 +113,7 @@ export class GoogleAuthProvider implements OAuthHandlers {
});
}
- async handler(
- req: express.Request,
- ): Promise<{ response: OAuthResponse; refreshToken: string }> {
+ async handler(req: express.Request) {
const { result, privateInfo } = await executeFrameHandlerStrategy<
OAuthResult,
PrivateInfo
@@ -127,22 +125,26 @@ export class GoogleAuthProvider implements OAuthHandlers {
};
}
- async refresh(req: OAuthRefreshRequest): Promise {
- const { accessToken, params } = await executeRefreshTokenStrategy(
- this._strategy,
- req.refreshToken,
- req.scope,
- );
+ async refresh(req: OAuthRefreshRequest) {
+ const { accessToken, refreshToken, params } =
+ await executeRefreshTokenStrategy(
+ this._strategy,
+ req.refreshToken,
+ req.scope,
+ );
const fullProfile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
);
- return this.handleResult({
- fullProfile,
- params,
- accessToken,
- refreshToken: req.refreshToken,
- });
+
+ return {
+ response: await this.handleResult({
+ fullProfile,
+ params,
+ accessToken,
+ }),
+ refreshToken,
+ };
}
private async handleResult(result: OAuthResult) {
diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts
index 9589e97265..3f71dd2c26 100644
--- a/plugins/auth-backend/src/providers/index.ts
+++ b/plugins/auth-backend/src/providers/index.ts
@@ -14,6 +14,10 @@
* limitations under the License.
*/
+export * from './atlassian';
+export * from './auth0';
+export * from './aws-alb';
+export * from './bitbucket';
export * from './github';
export * from './gitlab';
export * from './google';
@@ -21,9 +25,7 @@ export * from './microsoft';
export * from './oauth2';
export * from './oidc';
export * from './okta';
-export * from './bitbucket';
-export * from './atlassian';
-export * from './aws-alb';
+export * from './onelogin';
export * from './saml';
export { factories as defaultAuthProviderFactories } from './factories';
diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts
index b21218c066..7e928a2786 100644
--- a/plugins/auth-backend/src/providers/microsoft/provider.ts
+++ b/plugins/auth-backend/src/providers/microsoft/provider.ts
@@ -104,9 +104,7 @@ export class MicrosoftAuthProvider implements OAuthHandlers {
});
}
- async handler(
- req: express.Request,
- ): Promise<{ response: OAuthResponse; refreshToken: string }> {
+ async handler(req: express.Request) {
const { result, privateInfo } = await executeFrameHandlerStrategy<
OAuthResult,
PrivateInfo
@@ -118,24 +116,27 @@ export class MicrosoftAuthProvider implements OAuthHandlers {
};
}
- async refresh(req: OAuthRefreshRequest): Promise {
- const { accessToken, params } = await executeRefreshTokenStrategy(
- this._strategy,
- req.refreshToken,
- req.scope,
- );
+ async refresh(req: OAuthRefreshRequest) {
+ const { accessToken, refreshToken, params } =
+ await executeRefreshTokenStrategy(
+ this._strategy,
+ req.refreshToken,
+ req.scope,
+ );
const fullProfile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
);
- return this.handleResult({
- fullProfile,
- params,
- accessToken,
- refreshToken: req.refreshToken,
- });
+ return {
+ response: await this.handleResult({
+ fullProfile,
+ params,
+ accessToken,
+ }),
+ refreshToken,
+ };
}
private async handleResult(result: OAuthResult) {
diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts
index 2f9c739860..dc4afbac48 100644
--- a/plugins/auth-backend/src/providers/oauth2/provider.ts
+++ b/plugins/auth-backend/src/providers/oauth2/provider.ts
@@ -127,9 +127,7 @@ export class OAuth2AuthProvider implements OAuthHandlers {
});
}
- async handler(
- req: express.Request,
- ): Promise<{ response: OAuthResponse; refreshToken: string }> {
+ async handler(req: express.Request) {
const { result, privateInfo } = await executeFrameHandlerStrategy<
OAuthResult,
PrivateInfo
@@ -141,29 +139,27 @@ export class OAuth2AuthProvider implements OAuthHandlers {
};
}
- async refresh(req: OAuthRefreshRequest): Promise {
+ async refresh(req: OAuthRefreshRequest) {
const refreshTokenResponse = await executeRefreshTokenStrategy(
this._strategy,
req.refreshToken,
req.scope,
);
- const {
- accessToken,
- params,
- refreshToken: updatedRefreshToken,
- } = refreshTokenResponse;
+ const { accessToken, params, refreshToken } = refreshTokenResponse;
const fullProfile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
);
- return this.handleResult({
- fullProfile,
- params,
- accessToken,
- refreshToken: updatedRefreshToken,
- });
+ return {
+ response: await this.handleResult({
+ fullProfile,
+ params,
+ accessToken,
+ }),
+ refreshToken,
+ };
}
private async handleResult(result: OAuthResult) {
@@ -175,7 +171,6 @@ export class OAuth2AuthProvider implements OAuthHandlers {
accessToken: result.accessToken,
scope: result.params.scope,
expiresInSeconds: result.params.expires_in,
- refreshToken: result.refreshToken,
},
profile,
};
diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts
index fe4c042500..e5bfd14f40 100644
--- a/plugins/auth-backend/src/providers/oidc/provider.ts
+++ b/plugins/auth-backend/src/providers/oidc/provider.ts
@@ -112,34 +112,31 @@ export class OidcAuthProvider implements OAuthHandlers {
return await executeRedirectStrategy(req, strategy, options);
}
- async handler(
- req: express.Request,
- ): Promise<{ response: OAuthResponse; refreshToken?: string }> {
+ async handler(req: express.Request) {
const { strategy } = await this.implementation;
- const strategyResponse = await executeFrameHandlerStrategy<
+ const { result, privateInfo } = await executeFrameHandlerStrategy<
OidcAuthResult,
PrivateInfo
>(req, strategy);
- const {
- result: { userinfo, tokenset },
- privateInfo,
- } = strategyResponse;
- const identityResponse = await this.handleResult({ tokenset, userinfo });
return {
- response: identityResponse,
+ response: await this.handleResult(result),
refreshToken: privateInfo.refreshToken,
};
}
- async refresh(req: OAuthRefreshRequest): Promise {
+ async refresh(req: OAuthRefreshRequest) {
const { client } = await this.implementation;
const tokenset = await client.refresh(req.refreshToken);
if (!tokenset.access_token) {
throw new Error('Refresh failed');
}
- const profile = await client.userinfo(tokenset.access_token);
- return this.handleResult({ tokenset, userinfo: profile });
+ const userinfo = await client.userinfo(tokenset.access_token);
+
+ return {
+ response: await this.handleResult({ tokenset, userinfo }),
+ refreshToken: tokenset.refresh_token,
+ };
}
private async setupStrategy(options: Options): Promise {
@@ -190,7 +187,6 @@ export class OidcAuthProvider implements OAuthHandlers {
providerInfo: {
idToken: result.tokenset.id_token,
accessToken: result.tokenset.access_token!,
- refreshToken: result.tokenset.refresh_token,
scope: result.tokenset.scope!,
expiresInSeconds: result.tokenset.expires_in,
},
diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts
index 69bbbd8c84..1c74e171ad 100644
--- a/plugins/auth-backend/src/providers/okta/provider.ts
+++ b/plugins/auth-backend/src/providers/okta/provider.ts
@@ -133,9 +133,7 @@ export class OktaAuthProvider implements OAuthHandlers {
});
}
- async handler(
- req: express.Request,
- ): Promise<{ response: OAuthResponse; refreshToken: string }> {
+ async handler(req: express.Request) {
const { result, privateInfo } = await executeFrameHandlerStrategy<
OAuthResult,
PrivateInfo
@@ -147,7 +145,7 @@ export class OktaAuthProvider implements OAuthHandlers {
};
}
- async refresh(req: OAuthRefreshRequest): Promise {
+ async refresh(req: OAuthRefreshRequest) {
const { accessToken, refreshToken, params } =
await executeRefreshTokenStrategy(
this._strategy,
@@ -160,12 +158,14 @@ export class OktaAuthProvider implements OAuthHandlers {
accessToken,
);
- return this.handleResult({
- fullProfile,
- params,
- accessToken,
+ return {
+ response: await this.handleResult({
+ fullProfile,
+ params,
+ accessToken,
+ }),
refreshToken,
- });
+ };
}
private async handleResult(result: OAuthResult) {
@@ -177,7 +177,6 @@ export class OktaAuthProvider implements OAuthHandlers {
accessToken: result.accessToken,
scope: result.params.scope,
expiresInSeconds: result.params.expires_in,
- refreshToken: result.refreshToken,
},
profile,
};
diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts
index 66e8b0bfc5..4b2201e576 100644
--- a/plugins/auth-backend/src/providers/onelogin/provider.ts
+++ b/plugins/auth-backend/src/providers/onelogin/provider.ts
@@ -36,7 +36,15 @@ import {
executeFetchUserProfileStrategy,
PassportDoneCallback,
} from '../../lib/passport';
-import { RedirectInfo, AuthProviderFactory } from '../types';
+import {
+ RedirectInfo,
+ AuthProviderFactory,
+ AuthHandler,
+ SignInResolver,
+} from '../types';
+import { CatalogIdentityClient } from '../../lib/catalog';
+import { Logger } from 'winston';
+import { TokenIssuer } from '../../identity';
type PrivateInfo = {
refreshToken: string;
@@ -44,12 +52,27 @@ type PrivateInfo = {
export type Options = OAuthProviderOptions & {
issuer: string;
+ signInResolver?: SignInResolver;
+ authHandler: AuthHandler;
+ tokenIssuer: TokenIssuer;
+ catalogIdentityClient: CatalogIdentityClient;
+ logger: Logger;
};
export class OneLoginProvider implements OAuthHandlers {
private readonly _strategy: any;
+ private readonly signInResolver?: SignInResolver;
+ private readonly authHandler: AuthHandler;
+ private readonly tokenIssuer: TokenIssuer;
+ private readonly catalogIdentityClient: CatalogIdentityClient;
+ private readonly logger: Logger;
constructor(options: Options) {
+ this.signInResolver = options.signInResolver;
+ this.authHandler = options.authHandler;
+ this.tokenIssuer = options.tokenIssuer;
+ this.catalogIdentityClient = options.catalogIdentityClient;
+ this.logger = options.logger;
this._strategy = new OneLoginStrategy(
{
issuer: options.issuer,
@@ -89,86 +112,144 @@ export class OneLoginProvider implements OAuthHandlers {
});
}
- async handler(
- req: express.Request,
- ): Promise<{ response: OAuthResponse; refreshToken: string }> {
+ async handler(req: express.Request) {
const { result, privateInfo } = await executeFrameHandlerStrategy<
OAuthResult,
PrivateInfo
>(req, this._strategy);
- const profile = makeProfileInfo(result.fullProfile, result.params.id_token);
-
return {
- response: await this.populateIdentity({
- profile,
- providerInfo: {
- idToken: result.params.id_token,
- accessToken: result.accessToken,
- scope: result.params.scope,
- expiresInSeconds: result.params.expires_in,
- },
- }),
+ response: await this.handleResult(result),
refreshToken: privateInfo.refreshToken,
};
}
- async refresh(req: OAuthRefreshRequest): Promise {
- const { accessToken, params } = await executeRefreshTokenStrategy(
- this._strategy,
- req.refreshToken,
- req.scope,
- );
+ async refresh(req: OAuthRefreshRequest) {
+ const { accessToken, refreshToken, params } =
+ await executeRefreshTokenStrategy(
+ this._strategy,
+ req.refreshToken,
+ req.scope,
+ );
const fullProfile = await executeFetchUserProfileStrategy(
this._strategy,
accessToken,
);
- const profile = makeProfileInfo(fullProfile, params.id_token);
- return this.populateIdentity({
- providerInfo: {
+ return {
+ response: await this.handleResult({
+ fullProfile,
+ params,
accessToken,
- idToken: params.id_token,
- expiresInSeconds: params.expires_in,
- scope: params.scope,
- },
- profile,
- });
+ }),
+ refreshToken,
+ };
}
- private async populateIdentity(
- response: OAuthResponse,
- ): Promise {
- const { profile } = response;
+ private async handleResult(result: OAuthResult) {
+ const { profile } = await this.authHandler(result);
- if (!profile.email) {
- throw new Error('OIDC profile contained no email');
+ const response: OAuthResponse = {
+ providerInfo: {
+ idToken: result.params.id_token,
+ accessToken: result.accessToken,
+ scope: result.params.scope,
+ expiresInSeconds: result.params.expires_in,
+ },
+ profile,
+ };
+
+ if (this.signInResolver) {
+ response.backstageIdentity = await this.signInResolver(
+ {
+ result,
+ profile,
+ },
+ {
+ tokenIssuer: this.tokenIssuer,
+ catalogIdentityClient: this.catalogIdentityClient,
+ logger: this.logger,
+ },
+ );
}
- const id = profile.email.split('@')[0];
-
- return { ...response, backstageIdentity: { id, token: '' } };
+ return response;
}
}
-export type OneLoginProviderOptions = {};
+const defaultSignInResolver: SignInResolver = async info => {
+ const { profile } = info;
+ if (!profile.email) {
+ throw new Error('OIDC profile contained no email');
+ }
+
+ const id = profile.email.split('@')[0];
+
+ return { id, token: '' };
+};
+
+/** @public */
+export type OneLoginProviderOptions = {
+ /**
+ * The profile transformation function used to verify and convert the auth response
+ * into the profile that will be presented to the user.
+ */
+ authHandler?: AuthHandler;
+
+ /**
+ * Configure sign-in for this provider, without it the provider can not be used to sign users in.
+ */
+ signIn?: {
+ /**
+ * Maps an auth result to a Backstage identity for the user.
+ */
+ resolver: SignInResolver;
+ };
+};
+
+/** @public */
export const createOneLoginProvider = (
- _options?: OneLoginProviderOptions,
+ options?: OneLoginProviderOptions,
): AuthProviderFactory => {
- return ({ providerId, globalConfig, config, tokenIssuer }) =>
+ return ({
+ providerId,
+ globalConfig,
+ config,
+ tokenIssuer,
+ catalogApi,
+ logger,
+ }) =>
OAuthEnvironmentHandler.mapConfig(config, envConfig => {
const clientId = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const issuer = envConfig.getString('issuer');
const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`;
+ const catalogIdentityClient = new CatalogIdentityClient({
+ catalogApi,
+ tokenIssuer,
+ });
+
+ const authHandler: AuthHandler = options?.authHandler
+ ? options.authHandler
+ : async ({ fullProfile, params }) => ({
+ profile: makeProfileInfo(fullProfile, params.id_token),
+ });
+
+ const signInResolver = options?.signIn?.resolver ?? defaultSignInResolver;
+
const provider = new OneLoginProvider({
clientId,
clientSecret,
callbackUrl,
issuer,
+ authHandler,
+ signInResolver,
+ tokenIssuer,
+ catalogIdentityClient,
+ logger,
});
return OAuthAdapter.fromConfig(globalConfig, provider, {
diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md
index 8c8594ca1d..0d5b1a36a5 100644
--- a/plugins/search/api-report.md
+++ b/plugins/search/api-report.md
@@ -213,16 +213,32 @@ export const SearchResult: ({
// @public (undocumented)
export const SearchResultPager: () => JSX.Element;
-// Warning: (ae-forgotten-export) The symbol "SearchTypeProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "SearchType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
-export const SearchType: ({
- values,
- className,
- name,
- defaultValue,
-}: SearchTypeProps) => JSX.Element;
+export const SearchType: {
+ (props: SearchTypeProps): JSX.Element;
+ Accordion(props: SearchTypeAccordionProps): JSX.Element;
+};
+
+// @public (undocumented)
+export type SearchTypeAccordionProps = {
+ name: string;
+ types: Array<{
+ value: string;
+ name: string;
+ icon: JSX.Element;
+ }>;
+ defaultValue?: string;
+};
+
+// @public (undocumented)
+export type SearchTypeProps = {
+ className?: string;
+ name: string;
+ values?: string[];
+ defaultValue?: string[] | string | null;
+};
// Warning: (ae-missing-release-tag) "SidebarSearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
diff --git a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx
index 0445a35943..f5fa6689ef 100644
--- a/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx
+++ b/plugins/search/src/components/DefaultResultListItem/DefaultResultListItem.stories.tsx
@@ -14,17 +14,28 @@
* limitations under the License.
*/
-import React from 'react';
+import { Button } from '@backstage/core-components';
import { Grid } from '@material-ui/core';
import FindInPageIcon from '@material-ui/icons/FindInPage';
import GroupIcon from '@material-ui/icons/Group';
-import { Button } from '@backstage/core-components';
-import { DefaultResultListItem } from '../index';
+import React from 'react';
import { MemoryRouter } from 'react-router';
+import { DefaultResultListItem } from './DefaultResultListItem';
export default {
title: 'Plugins/Search/DefaultResultListItem',
component: DefaultResultListItem,
+ decorators: [
+ (Story: () => JSX.Element) => (
+
+
+
+
+
+
+
+ ),
+ ],
};
const mockSearchResult = {
@@ -35,54 +46,34 @@ const mockSearchResult = {
};
export const Default = () => {
- return (
-
-
-
-
-
-
-
- );
+ return ;
};
export const WithIcon = () => {
return (
-
-
-
- }
- />
-
-
-
+ }
+ />
);
};
export const WithSecondaryAction = () => {
return (
-
-
-
- }
- style={{ textTransform: 'lowercase' }}
- >
- {mockSearchResult.owner}
-
- }
- />
-
-
-
+ }
+ style={{ textTransform: 'lowercase' }}
+ >
+ {mockSearchResult.owner}
+
+ }
+ />
);
};
diff --git a/plugins/search/src/components/LegacySearchPage/LegacySearchPage.tsx b/plugins/search/src/components/LegacySearchPage/LegacySearchPage.tsx
index c8f2e538cb..ec0fc3acb9 100644
--- a/plugins/search/src/components/LegacySearchPage/LegacySearchPage.tsx
+++ b/plugins/search/src/components/LegacySearchPage/LegacySearchPage.tsx
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { Grid } from '@material-ui/core';
import React, { useEffect, useState } from 'react';
import { useDebounce } from 'react-use';
diff --git a/plugins/search/src/components/SearchBar/SearchBar.stories.tsx b/plugins/search/src/components/SearchBar/SearchBar.stories.tsx
index d00f391bc7..3887df814f 100644
--- a/plugins/search/src/components/SearchBar/SearchBar.stories.tsx
+++ b/plugins/search/src/components/SearchBar/SearchBar.stories.tsx
@@ -14,88 +14,58 @@
* limitations under the License.
*/
-import React from 'react';
-import { Paper, Grid, makeStyles } from '@material-ui/core';
-import { SearchBar, SearchContext } from '../index';
-import { MemoryRouter } from 'react-router';
+import { Grid, makeStyles, Paper } from '@material-ui/core';
+import React, { ComponentType } from 'react';
+import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories';
+import { SearchBar } from './SearchBar';
export default {
title: 'Plugins/Search/SearchBar',
component: SearchBar,
-};
-
-const defaultValue = {
- term: '',
- setTerm: () => {},
+ decorators: [
+ (Story: ComponentType<{}>) => (
+
+
+
+
+
+
+
+ ),
+ ],
};
export const Default = () => {
return (
-
- {/* @ts-ignore (defaultValue requires more than what is used here) */}
-
-
-
-
-
-
-
-
-
-
+
+
+
);
};
export const CustomPlaceholder = () => {
return (
-
- {/* @ts-ignore (defaultValue requires more than what is used here) */}
-
-
-
-
-
-
-
-
-
-
+
+
+
);
};
export const Focused = () => {
return (
-
- {/* @ts-ignore (defaultValue requires more than what is used here) */}
-
-
-
-
- {/* decision up to adopter, read https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-autofocus.md#no-autofocus */}
- {/* eslint-disable-next-line jsx-a11y/no-autofocus */}
-
-
-
-
-
-
+
+ {/* decision up to adopter, read https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-autofocus.md#no-autofocus */}
+ {/* eslint-disable-next-line jsx-a11y/no-autofocus */}
+
+
);
};
export const WithoutClearButton = () => {
return (
-
- {/* @ts-ignore (defaultValue requires more than what is used here) */}
-
-
-
-
-
-
-
-
-
-
+
+
+
);
};
@@ -112,17 +82,8 @@ const useStyles = makeStyles({
export const CustomStyles = () => {
const classes = useStyles();
return (
-
- {/* @ts-ignore (defaultValue requires more than what is used here) */}
-
-
-
-
-
-
-
-
-
-
+
+
+
);
};
diff --git a/plugins/search/src/components/SearchBar/SearchBar.test.tsx b/plugins/search/src/components/SearchBar/SearchBar.test.tsx
index b3f30473ac..cb4008d494 100644
--- a/plugins/search/src/components/SearchBar/SearchBar.test.tsx
+++ b/plugins/search/src/components/SearchBar/SearchBar.test.tsx
@@ -20,10 +20,10 @@ import userEvent from '@testing-library/user-event';
import { SearchContextProvider } from '../SearchContext';
import { SearchBar } from './SearchBar';
-import { configApiRef } from '@backstage/core-plugin-api';
+import { configApiRef, analyticsApiRef } from '@backstage/core-plugin-api';
import { ApiProvider, ConfigReader } from '@backstage/core-app-api';
import { searchApiRef } from '../../apis';
-import { TestApiRegistry } from '@backstage/test-utils';
+import { MockAnalyticsApi, TestApiRegistry } from '@backstage/test-utils';
jest.mock('@backstage/core-plugin-api', () => ({
...jest.requireActual('@backstage/core-plugin-api'),
@@ -38,9 +38,16 @@ describe('SearchBar', () => {
};
const query = jest.fn().mockResolvedValue({});
+ const analyticsApiSpy = new MockAnalyticsApi();
+ let apiRegistry: TestApiRegistry;
- const apiRegistry = TestApiRegistry.from(
- [configApiRef, new ConfigReader({ app: { title: 'Mock title' } })],
+ apiRegistry = TestApiRegistry.from(
+ [
+ configApiRef,
+ new ConfigReader({
+ app: { title: 'Mock title' },
+ }),
+ ],
[searchApiRef, { query }],
);
@@ -210,4 +217,128 @@ describe('SearchBar', () => {
expect.objectContaining({ term: value }),
);
});
+
+ it('does not capture analytics event if not enabled in app', async () => {
+ jest.useFakeTimers();
+
+ const debounceTime = 600;
+
+ render(
+
+
+
+
+ ,
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByRole('textbox', { name })).toBeInTheDocument();
+ });
+
+ const textbox = screen.getByRole('textbox', { name });
+
+ const value = 'value';
+
+ userEvent.type(textbox, value);
+
+ act(() => {
+ jest.advanceTimersByTime(debounceTime);
+ });
+
+ await waitFor(() => expect(textbox).toHaveValue(value));
+
+ expect(analyticsApiSpy.getEvents()).toHaveLength(0);
+ });
+
+ it('captures analytics events if enabled in app', async () => {
+ jest.useFakeTimers();
+
+ const debounceTime = 600;
+
+ apiRegistry = TestApiRegistry.from(
+ [analyticsApiRef, analyticsApiSpy],
+ [
+ configApiRef,
+ new ConfigReader({
+ app: {
+ title: 'Mock title',
+ analytics: {
+ ga: {
+ trackingId: 'xyz123',
+ },
+ },
+ },
+ }),
+ ],
+ [searchApiRef, { query }],
+ );
+
+ render(
+
+
+
+
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByRole('textbox', { name })).toBeInTheDocument();
+ });
+
+ const textbox = screen.getByRole('textbox', { name });
+
+ const value = 'value';
+
+ userEvent.type(textbox, value);
+
+ expect(analyticsApiSpy.getEvents()).toHaveLength(0);
+
+ act(() => {
+ jest.advanceTimersByTime(debounceTime);
+ });
+
+ await waitFor(() => expect(textbox).toHaveValue(value));
+
+ expect(analyticsApiSpy.getEvents()).toHaveLength(1);
+ expect(analyticsApiSpy.getEvents()[0]).toEqual({
+ action: 'search',
+ context: {
+ extension: 'App',
+ pluginId: 'root',
+ routeRef: 'unknown',
+ searchTypes: 'software-catalog,techdocs',
+ },
+ subject: 'value',
+ });
+
+ userEvent.clear(textbox);
+
+ // make sure new term is captured
+ userEvent.type(textbox, 'new value');
+
+ act(() => {
+ jest.advanceTimersByTime(debounceTime);
+ });
+
+ await waitFor(() => expect(textbox).toHaveValue('new value'));
+
+ expect(analyticsApiSpy.getEvents()).toHaveLength(2);
+ expect(analyticsApiSpy.getEvents()[1]).toEqual({
+ action: 'search',
+ context: {
+ extension: 'App',
+ pluginId: 'root',
+ routeRef: 'unknown',
+ searchTypes: 'software-catalog,techdocs',
+ },
+ subject: 'new value',
+ });
+ });
});
diff --git a/plugins/search/src/components/SearchBar/SearchBar.tsx b/plugins/search/src/components/SearchBar/SearchBar.tsx
index 95bf1570e1..aa4b1e728a 100644
--- a/plugins/search/src/components/SearchBar/SearchBar.tsx
+++ b/plugins/search/src/components/SearchBar/SearchBar.tsx
@@ -33,6 +33,7 @@ import SearchIcon from '@material-ui/icons/Search';
import ClearButton from '@material-ui/icons/Clear';
import { useSearch } from '../SearchContext';
+import { TrackSearch } from '../SearchTracker';
/**
* Props for {@link SearchBarBase}.
@@ -119,18 +120,20 @@ export const SearchBarBase = ({
);
return (
-
+
+
+
);
};
@@ -150,8 +153,11 @@ export const SearchBar = ({ onChange, ...props }: SearchBarProps) => {
const { term, setTerm } = useSearch();
const handleChange = (newValue: string) => {
- setTerm(newValue);
- if (onChange) onChange(newValue);
+ if (onChange) {
+ onChange(newValue);
+ } else {
+ setTerm(newValue);
+ }
};
return ;
diff --git a/plugins/search/src/components/SearchContext/SearchContext.tsx b/plugins/search/src/components/SearchContext/SearchContext.tsx
index dd9310d782..300a191983 100644
--- a/plugins/search/src/components/SearchContext/SearchContext.tsx
+++ b/plugins/search/src/components/SearchContext/SearchContext.tsx
@@ -15,7 +15,7 @@
*/
import { JsonObject } from '@backstage/types';
-import { useApi } from '@backstage/core-plugin-api';
+import { useApi, AnalyticsContext } from '@backstage/core-plugin-api';
import { SearchResultSet } from '@backstage/search-common';
import React, {
createContext,
@@ -130,7 +130,11 @@ export const SearchContextProvider = ({
fetchPreviousPage: hasPreviousPage ? fetchPreviousPage : undefined,
};
- return ;
+ return (
+
+
+
+ );
};
export const useSearch = () => {
diff --git a/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx b/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx
new file mode 100644
index 0000000000..e969e0d8cc
--- /dev/null
+++ b/plugins/search/src/components/SearchContext/SearchContextForStorybook.stories.tsx
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021 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 { ApiProvider } from '@backstage/core-app-api';
+import { SearchResultSet } from '@backstage/search-common';
+import { TestApiRegistry } from '@backstage/test-utils';
+import React, { ComponentProps } from 'react';
+import { searchApiRef } from '../../apis';
+import { SearchContextProvider as RealSearchContextProvider } from './SearchContext';
+
+type QueryResultProps = {
+ mockedResults?: SearchResultSet;
+};
+
+/**
+ * Utility context provider only for use in Storybook stories. You should use
+ * the real `` exported by `@backstage/plugin-search` in
+ * your app instead of this! In some cases (like the search page) it may
+ * already be provided on your behalf.
+ */
+export const SearchContextProvider = (
+ props: ComponentProps & QueryResultProps,
+) => {
+ const { mockedResults, ...contextProps } = props;
+ const query: any = () => Promise.resolve(mockedResults || {});
+ const apiRegistry = TestApiRegistry.from([searchApiRef, { query }]);
+
+ return (
+
+
+
+ );
+};
diff --git a/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx b/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx
index 190856fca8..0ea1a66bf9 100644
--- a/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx
+++ b/plugins/search/src/components/SearchFilter/SearchFilter.stories.tsx
@@ -14,56 +14,45 @@
* limitations under the License.
*/
-import React from 'react';
import { Grid, Paper } from '@material-ui/core';
-import { SearchFilter, SearchContext } from '../index';
-import { MemoryRouter } from 'react-router';
+import React, { ComponentType } from 'react';
+import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories';
+import { SearchFilter } from './SearchFilter';
export default {
title: 'Plugins/Search/SearchFilter',
component: SearchFilter,
-};
-
-const defaultValue = {
- filters: {},
+ decorators: [
+ (Story: ComponentType<{}>) => (
+
+
+
+
+
+
+
+ ),
+ ],
};
export const CheckBoxFilter = () => {
return (
-
- {/* @ts-ignore (defaultValue requires more than what is used here) */}
-
-
-
-
-
-
-
-
-
-
+
+
+
);
};
export const SelectFilter = () => {
return (
-
- {/* @ts-ignore (defaultValue requires more than what is used here) */}
-
-
-
-
-
-
-
-
-
-
+
+
+
);
};
diff --git a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx
index 6fa93c6588..bfea548134 100644
--- a/plugins/search/src/components/SearchModal/SearchModal.stories.tsx
+++ b/plugins/search/src/components/SearchModal/SearchModal.stories.tsx
@@ -14,62 +14,52 @@
* limitations under the License.
*/
-import React, { ComponentType } from 'react';
-import { Button } from '@material-ui/core';
-import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
import { wrapInTestApp } from '@backstage/test-utils';
-import { SearchModal } from '../index';
-import { useSearch, SearchContextProvider } from '../SearchContext';
-import { searchApiRef } from '../../apis';
+import { Button } from '@material-ui/core';
+import React, { ComponentType } from 'react';
import { rootRouteRef } from '../../plugin';
+import { useSearch } from '../SearchContext';
+import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories';
+import { SearchModal } from './SearchModal';
-const mockSearchApi = {
- query: () =>
- Promise.resolve({
- results: [
- {
- type: 'custom-result-item',
- document: {
- location: 'search/search-result-1',
- title: 'Search Result 1',
- text: 'some text from the search result',
- },
- },
- {
- type: 'no-custom-result-item',
- document: {
- location: 'search/search-result-2',
- title: 'Search Result 2',
- text: 'some text from the search result',
- },
- },
- {
- type: 'no-custom-result-item',
- document: {
- location: 'search/search-result-3',
- title: 'Search Result 3',
- text: 'some text from the search result',
- },
- },
- ],
- }),
+const mockResults = {
+ results: [
+ {
+ type: 'custom-result-item',
+ document: {
+ location: 'search/search-result-1',
+ title: 'Search Result 1',
+ text: 'some text from the search result',
+ },
+ },
+ {
+ type: 'no-custom-result-item',
+ document: {
+ location: 'search/search-result-2',
+ title: 'Search Result 2',
+ text: 'some text from the search result',
+ },
+ },
+ {
+ type: 'no-custom-result-item',
+ document: {
+ location: 'search/search-result-3',
+ title: 'Search Result 3',
+ text: 'some text from the search result',
+ },
+ },
+ ],
};
-const apiRegistry = () => ApiRegistry.from([[searchApiRef, mockSearchApi]]);
-
export default {
title: 'Plugins/Search/SearchModal',
component: SearchModal,
decorators: [
(Story: ComponentType<{}>) =>
wrapInTestApp(
- <>
-
-
-
-
-
- >,
+
+
+ ,
{ mountedRoutes: { '/search': rootRouteRef } },
),
],
diff --git a/plugins/search/src/components/SearchModal/SearchModal.test.tsx b/plugins/search/src/components/SearchModal/SearchModal.test.tsx
index ff9622dfb9..d3209f9a9b 100644
--- a/plugins/search/src/components/SearchModal/SearchModal.test.tsx
+++ b/plugins/search/src/components/SearchModal/SearchModal.test.tsx
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import React from 'react';
import { screen } from '@testing-library/react';
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx
index ef1eaf650a..8b4da51592 100644
--- a/plugins/search/src/components/SearchModal/SearchModal.tsx
+++ b/plugins/search/src/components/SearchModal/SearchModal.tsx
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import React from 'react';
import {
Dialog,
diff --git a/plugins/search/src/components/SearchPage/SearchPage.test.tsx b/plugins/search/src/components/SearchPage/SearchPage.test.tsx
index c9980b3bf7..9486196584 100644
--- a/plugins/search/src/components/SearchPage/SearchPage.test.tsx
+++ b/plugins/search/src/components/SearchPage/SearchPage.test.tsx
@@ -18,7 +18,7 @@ import { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
import { useLocation, useOutlet } from 'react-router';
import { useSearch } from '../SearchContext';
-import { SearchPage } from './';
+import { SearchPage } from './SearchPage';
jest.mock('react-router', () => ({
...jest.requireActual('react-router'),
diff --git a/plugins/search/src/components/SearchResult/SearchResult.stories.tsx b/plugins/search/src/components/SearchResult/SearchResult.stories.tsx
index aebaecb8b4..5efbecbe2b 100644
--- a/plugins/search/src/components/SearchResult/SearchResult.stories.tsx
+++ b/plugins/search/src/components/SearchResult/SearchResult.stories.tsx
@@ -14,83 +14,83 @@
* limitations under the License.
*/
-import React from 'react';
-import { List, ListItem } from '@material-ui/core';
-import { SearchResult, SearchContext, DefaultResultListItem } from '../index';
-import { MemoryRouter } from 'react-router';
import { Link } from '@backstage/core-components';
+import { List, ListItem } from '@material-ui/core';
+import React, { ComponentType } from 'react';
+import { MemoryRouter } from 'react-router';
+import { DefaultResultListItem } from '../DefaultResultListItem';
+import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories';
+import { SearchResult } from './SearchResult';
+
+const mockResults = {
+ results: [
+ {
+ type: 'custom-result-item',
+ document: {
+ location: 'search/search-result-1',
+ title: 'Search Result 1',
+ text: 'some text from the search result',
+ },
+ },
+ {
+ type: 'no-custom-result-item',
+ document: {
+ location: 'search/search-result-2',
+ title: 'Search Result 2',
+ text: 'some text from the search result',
+ },
+ },
+ {
+ type: 'no-custom-result-item',
+ document: {
+ location: 'search/search-result-3',
+ title: 'Search Result 3',
+ text: 'some text from the search result',
+ },
+ },
+ ],
+};
export default {
title: 'Plugins/Search/SearchResult',
component: SearchResult,
-};
-
-const defaultValue = {
- result: {
- loading: false,
- error: '',
- value: {
- results: [
- {
- type: 'custom-result-item',
- document: {
- location: 'search/search-result-1',
- title: 'Search Result 1',
- text: 'some text from the search result',
- },
- },
- {
- type: 'no-custom-result-item',
- document: {
- location: 'search/search-result-2',
- title: 'Search Result 2',
- text: 'some text from the search result',
- },
- },
- {
- type: 'no-custom-result-item',
- document: {
- location: 'search/search-result-3',
- title: 'Search Result 3',
- text: 'some text from the search result',
- },
- },
- ],
- },
- },
+ decorators: [
+ (Story: ComponentType<{}>) => (
+
+
+
+
+
+ ),
+ ],
};
export const Default = () => {
return (
-
- {/* @ts-ignore (defaultValue requires more than what is used here) */}
-
-
- {({ results }) => (
-
- {results.map(({ type, document }) => {
- switch (type) {
- case 'custom-result-item':
- return (
-
- );
- default:
- return (
-
-
- {document.title} - {document.text}
-
-
- );
- }
- })}
-
- )}
-
-
-
+
+ {({ results }) => (
+
+ {results.map(({ type, document }) => {
+ switch (type) {
+ case 'custom-result-item':
+ return (
+
+ );
+ default:
+ return (
+
+
+ {document.title} - {document.text}
+
+
+ );
+ }
+ })}
+
+ )}
+
);
};
diff --git a/plugins/search/src/components/SearchTracker/SearchTracker.tsx b/plugins/search/src/components/SearchTracker/SearchTracker.tsx
new file mode 100644
index 0000000000..b9da9b5560
--- /dev/null
+++ b/plugins/search/src/components/SearchTracker/SearchTracker.tsx
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2021 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, { useEffect } from 'react';
+import { useAnalytics } from '@backstage/core-plugin-api';
+import { useSearch } from '../SearchContext';
+
+/**
+ * Capture search event on term change.
+ */
+export const TrackSearch = ({ children }: { children: React.ReactChild }) => {
+ const analytics = useAnalytics();
+ const { term } = useSearch();
+
+ useEffect(() => {
+ if (term) {
+ // Capture analytics search event with search term provided as value
+ analytics.captureEvent('search', term);
+ }
+ }, [analytics, term]);
+
+ return <>{children}>;
+};
diff --git a/plugins/search/src/components/index.tsx b/plugins/search/src/components/SearchTracker/index.ts
similarity index 54%
rename from plugins/search/src/components/index.tsx
rename to plugins/search/src/components/SearchTracker/index.ts
index e413e9505d..5e6a75aa25 100644
--- a/plugins/search/src/components/index.tsx
+++ b/plugins/search/src/components/SearchTracker/index.ts
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Backstage Authors
+ * Copyright 2021 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.
@@ -13,17 +13,4 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-export * from './DefaultResultListItem';
-export * from './Filters';
-export * from './SearchBar';
-export * from './SearchContext';
-export * from './SearchFilter';
-export * from './SearchModal';
-export * from './SearchPage';
-export * from './SearchResult';
-export * from './SearchResultPager';
-export * from './SearchType';
-export * from './SidebarSearch';
-export * from './SidebarSearchModal';
-export * from './HomePageComponent';
+export { TrackSearch } from './SearchTracker';
diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx
new file mode 100644
index 0000000000..6b4171a024
--- /dev/null
+++ b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2021 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 { ApiProvider } from '@backstage/core-app-api';
+import { TestApiRegistry } from '@backstage/test-utils';
+import { act, render } from '@testing-library/react';
+import user from '@testing-library/user-event';
+
+import { searchApiRef } from '../../apis';
+import { SearchContext, SearchContextProvider } from '../SearchContext';
+import { SearchType } from './SearchType';
+
+describe('SearchType.Accordion', () => {
+ const query = jest.fn();
+ const mockApis = TestApiRegistry.from([searchApiRef, { query }]);
+
+ const contextSpy = {
+ result: { loading: false, value: { results: [] } },
+ term: '',
+ types: [],
+ filters: {},
+ toggleModal: jest.fn(),
+ setTerm: jest.fn(),
+ setTypes: jest.fn(),
+ setFilters: jest.fn(),
+ setPageCursor: jest.fn(),
+ };
+
+ const expectedLabel = 'Expected Label';
+ const expectedType = {
+ value: 'expected-type',
+ name: 'Expected Type',
+ icon: <>>,
+ };
+
+ beforeEach(() => {
+ query.mockResolvedValue({ results: [] });
+ });
+
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('should render as expected', async () => {
+ const { getByText } = render(
+
+
+
+
+ ,
+ );
+
+ // The given label should be rendered.
+ expect(getByText(expectedLabel)).toBeInTheDocument();
+
+ // "Collapse" is visible by default (element is not collapsed)
+ expect(getByText('Collapse')).toBeInTheDocument();
+
+ // The default "all" type should be rendered.
+ expect(getByText('All')).toBeInTheDocument();
+
+ // The given type is also visible
+ expect(getByText(expectedType.name)).toBeInTheDocument();
+
+ await act(() => Promise.resolve());
+ });
+
+ it('should set entire types array when a type is selected', () => {
+ const { getByText } = render(
+
+
+ ,
+ );
+
+ user.click(getByText(expectedType.name));
+
+ expect(contextSpy.setTypes).toHaveBeenCalledWith([expectedType.value]);
+ });
+
+ it('should reset types array when all is selected', () => {
+ const { getByText } = render(
+
+
+ ,
+ );
+
+ user.click(getByText('All'));
+
+ expect(contextSpy.setTypes).toHaveBeenCalledWith([]);
+ });
+
+ it('should reset page cursor when a new type is selected', () => {
+ const { getByText } = render(
+
+
+ ,
+ );
+
+ user.click(getByText(expectedType.name));
+
+ expect(contextSpy.setPageCursor).toHaveBeenCalledWith(undefined);
+ });
+
+ it('should collapse when a new type is selected', () => {
+ const { getByText, queryByText } = render(
+
+
+ ,
+ );
+
+ user.click(getByText(expectedType.name));
+
+ expect(queryByText('Collapse')).not.toBeInTheDocument();
+ });
+});
diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx
new file mode 100644
index 0000000000..57e6609bab
--- /dev/null
+++ b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2021 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, { cloneElement, Fragment, useEffect, useState } from 'react';
+import { useSearch } from '../SearchContext';
+import {
+ Accordion,
+ AccordionSummary,
+ AccordionDetails,
+ Card,
+ CardContent,
+ CardHeader,
+ Divider,
+ List,
+ ListItem,
+ ListItemIcon,
+ ListItemText,
+ makeStyles,
+} from '@material-ui/core';
+import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+import AllIcon from '@material-ui/icons/FontDownload';
+
+const useStyles = makeStyles(theme => ({
+ card: {
+ backgroundColor: 'rgba(0, 0, 0, .11)',
+ },
+ cardContent: {
+ paddingTop: theme.spacing(1),
+ },
+ icon: {
+ color: theme.palette.common.black,
+ },
+ list: {
+ width: '100%',
+ },
+ listItemIcon: {
+ width: '24px',
+ height: '24px',
+ },
+ accordion: {
+ backgroundColor: theme.palette.background.paper,
+ },
+ accordionSummary: {
+ minHeight: 'auto',
+ '&.Mui-expanded': {
+ minHeight: 'auto',
+ },
+ },
+ accordionSummaryContent: {
+ margin: theme.spacing(2, 0),
+ '&.Mui-expanded': {
+ margin: theme.spacing(2, 0),
+ },
+ },
+ accordionDetails: {
+ padding: theme.spacing(0, 0, 1),
+ },
+}));
+
+/**
+ * @public
+ */
+export type SearchTypeAccordionProps = {
+ name: string;
+ types: Array<{
+ value: string;
+ name: string;
+ icon: JSX.Element;
+ }>;
+ defaultValue?: string;
+};
+
+export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => {
+ const classes = useStyles();
+ const { setPageCursor, setTypes, types } = useSearch();
+ const [expanded, setExpanded] = useState(true);
+ const { defaultValue, name, types: givenTypes } = props;
+
+ const toggleExpanded = () => setExpanded(prevState => !prevState);
+ const handleClick = (type: string) => {
+ return () => {
+ setTypes(type !== '' ? [type] : []);
+ setPageCursor(undefined);
+ setExpanded(false);
+ };
+ };
+
+ // Handle any provided defaultValue
+ useEffect(() => {
+ if (defaultValue) {
+ setTypes([defaultValue]);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const definedTypes = [
+ {
+ value: '',
+ name: 'All',
+ icon: ,
+ },
+ ...givenTypes,
+ ];
+ const selected = types[0] || '';
+
+ return (
+
+
+
+
+ }
+ IconButtonProps={{ size: 'small' }}
+ >
+ {expanded
+ ? 'Collapse'
+ : definedTypes.filter(t => t.value === selected)[0]!.name}
+
+
+
+ {definedTypes.map(type => (
+
+
+
+
+ {cloneElement(type.icon, {
+ className: classes.listItemIcon,
+ })}
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+};
diff --git a/plugins/search/src/components/SearchType/SearchType.stories.tsx b/plugins/search/src/components/SearchType/SearchType.stories.tsx
index da662d8fef..833573f5ee 100644
--- a/plugins/search/src/components/SearchType/SearchType.stories.tsx
+++ b/plugins/search/src/components/SearchType/SearchType.stories.tsx
@@ -13,24 +13,51 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import React, { useState } from 'react';
-import { SearchType } from '../index';
-import { SearchContext } from '../SearchContext';
+import { Grid, Paper } from '@material-ui/core';
+import CatalogIcon from '@material-ui/icons/MenuBook';
+import DocsIcon from '@material-ui/icons/Description';
+import UsersGroupsIcon from '@material-ui/icons/Person';
+import React, { ComponentType } from 'react';
+import { SearchContextProvider } from '../SearchContext/SearchContextForStorybook.stories';
+import { SearchType } from './SearchType';
export default {
title: 'Plugins/Search/SearchType',
component: SearchType,
+ decorators: [
+ (Story: ComponentType<{}>) => (
+
+
+
+
+
+
+
+ ),
+ ],
};
const values = ['value-1', 'value-2', 'value-3'];
export const Default = () => {
- const [types, setTypes] = useState([]);
-
return (
-
+
-
+
+ );
+};
+
+export const Accordion = () => {
+ return (
+ },
+ { value: 'value-2', name: 'Value Two', icon: },
+ { value: 'value-3', name: 'Value Three', icon: },
+ ]}
+ />
);
};
diff --git a/plugins/search/src/components/SearchType/SearchType.tsx b/plugins/search/src/components/SearchType/SearchType.tsx
index 5290caa0b7..f42802819a 100644
--- a/plugins/search/src/components/SearchType/SearchType.tsx
+++ b/plugins/search/src/components/SearchType/SearchType.tsx
@@ -25,6 +25,10 @@ import {
} from '@material-ui/core';
import React, { ChangeEvent } from 'react';
import { useEffectOnce } from 'react-use';
+import {
+ SearchTypeAccordion,
+ SearchTypeAccordionProps,
+} from './SearchType.Accordion';
import { useSearch } from '../SearchContext';
const useStyles = makeStyles(theme => ({
@@ -41,6 +45,9 @@ const useStyles = makeStyles(theme => ({
},
}));
+/**
+ * @public
+ */
export type SearchTypeProps = {
className?: string;
name: string;
@@ -48,12 +55,8 @@ export type SearchTypeProps = {
defaultValue?: string[] | string | null;
};
-const SearchType = ({
- values = [],
- className,
- name,
- defaultValue,
-}: SearchTypeProps) => {
+const SearchType = (props: SearchTypeProps) => {
+ const { className, defaultValue, name, values = [] } = props;
const classes = useStyles();
const { types, setTypes } = useSearch();
@@ -112,4 +115,14 @@ const SearchType = ({
);
};
+/**
+ * A control surface for the search query's "types" property, displayed as a
+ * single-select collapsible accordion suitable for use in faceted search UIs.
+ * @public
+ */
+SearchType.Accordion = (props: SearchTypeAccordionProps) => {
+ return ;
+};
+
export { SearchType };
+export type { SearchTypeAccordionProps };
diff --git a/plugins/search/src/components/SearchType/index.ts b/plugins/search/src/components/SearchType/index.ts
index 400f3d2a46..e605276455 100644
--- a/plugins/search/src/components/SearchType/index.ts
+++ b/plugins/search/src/components/SearchType/index.ts
@@ -15,3 +15,4 @@
*/
export { SearchType } from './SearchType';
+export type { SearchTypeAccordionProps, SearchTypeProps } from './SearchType';
diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts
index 5bfdcf4318..2cd9130721 100644
--- a/plugins/search/src/index.ts
+++ b/plugins/search/src/index.ts
@@ -22,30 +22,30 @@
export { searchApiRef } from './apis';
export type { SearchApi } from './apis';
-export {
- Filters,
- FiltersButton,
- SearchBar,
- SearchBarBase,
- SearchContextProvider,
- SearchFilter,
- SearchFilterNext,
- SearchModal,
- SearchPage as Router,
- SearchResultPager,
- SearchType,
- SidebarSearch,
- useSearch,
-} from './components';
+
+export { Filters, FiltersButton } from './components/Filters';
+export type { FiltersState } from './components/Filters';
+export type { HomePageSearchBarProps } from './components/HomePageComponent';
+export { SearchBar, SearchBarBase } from './components/SearchBar';
export type {
- SearchModalProps,
- SidebarSearchModalProps,
- HomePageSearchBarProps,
- SidebarSearchProps,
- FiltersState,
- SearchBarProps,
SearchBarBaseProps,
-} from './components';
+ SearchBarProps,
+} from './components/SearchBar';
+export { SearchContextProvider, useSearch } from './components/SearchContext';
+export { SearchFilter, SearchFilterNext } from './components/SearchFilter';
+export { SearchModal } from './components/SearchModal';
+export type { SearchModalProps } from './components/SearchModal';
+export { SearchPage as Router } from './components/SearchPage';
+export { SearchResultPager } from './components/SearchResultPager';
+export { SearchType } from './components/SearchType';
+export type {
+ SearchTypeAccordionProps,
+ SearchTypeProps,
+} from './components/SearchType';
+export { SidebarSearch } from './components/SidebarSearch';
+export type { SidebarSearchProps } from './components/SidebarSearch';
+export type { SidebarSearchModalProps } from './components/SidebarSearchModal';
+
export {
DefaultResultListItem,
HomePageSearchBar,
diff --git a/yarn.lock b/yarn.lock
index 1e3570b3d6..4ddb332fb3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5814,22 +5814,22 @@
integrity sha512-64WWqE40U/WwWV8iIQBseTU+b2t+SdJSyQoCLdVPCKM9uf7KOjRivVwXe4KlWoV3y7duNSGuB2UgWhkXzscVmQ==
"@storybook/addon-a11y@^6.3.4":
- version "6.3.12"
- resolved "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-6.3.12.tgz#2f930fc84fc275a4ed43a716fc09cc12caf4e110"
- integrity sha512-q1NdRHFJV6sLEEJw0hatCc5ZIthELqM/AWdrEWDyhcJNyiq7Tq4nKqQBMTQSYwHiUAmxVgw7i4oa1vM2M51/3g==
+ version "6.4.9"
+ resolved "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-6.4.9.tgz#95cd51ad7a71e4c0a39259df2eb10c382bf324db"
+ integrity sha512-9LwFprh7A3KWmQRTqnyh/wvQ1SX/BewzhBPWqJzq0fGnx9fG35zt0VFtXV13i157wTRDqSO1g92Dgxw9l3S8/A==
dependencies:
- "@storybook/addons" "6.3.12"
- "@storybook/api" "6.3.12"
- "@storybook/channels" "6.3.12"
- "@storybook/client-api" "6.3.12"
- "@storybook/client-logger" "6.3.12"
- "@storybook/components" "6.3.12"
- "@storybook/core-events" "6.3.12"
- "@storybook/theming" "6.3.12"
+ "@storybook/addons" "6.4.9"
+ "@storybook/api" "6.4.9"
+ "@storybook/channels" "6.4.9"
+ "@storybook/client-logger" "6.4.9"
+ "@storybook/components" "6.4.9"
+ "@storybook/core-events" "6.4.9"
+ "@storybook/csf" "0.0.2--canary.87bc651.0"
+ "@storybook/theming" "6.4.9"
axe-core "^4.2.0"
core-js "^3.8.2"
global "^4.4.0"
- lodash "^4.17.20"
+ lodash "^4.17.21"
react-sizeme "^3.0.1"
regenerator-runtime "^0.13.7"
ts-dedent "^2.0.0"
@@ -5911,7 +5911,7 @@
global "^4.4.0"
regenerator-runtime "^0.13.7"
-"@storybook/addons@6.3.12", "@storybook/addons@^6.1.11":
+"@storybook/addons@6.3.12":
version "6.3.12"
resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.3.12.tgz#8773dcc113c5086dfff722388b7b65580e43b65b"
integrity sha512-UgoMyr7Qr0FS3ezt8u6hMEcHgyynQS9ucr5mAwZky3wpXRPFyUTmMto9r4BBUdqyUvTUj/LRKIcmLBfj+/l0Fg==
@@ -5941,6 +5941,23 @@
global "^4.4.0"
regenerator-runtime "^0.13.7"
+"@storybook/addons@6.4.9", "@storybook/addons@^6.1.11":
+ version "6.4.9"
+ resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.4.9.tgz#43b5dabf6781d863fcec0a0b293c236b4d5d4433"
+ integrity sha512-y+oiN2zd+pbRWwkf6aQj4tPDFn+rQkrv7fiVoMxsYub+kKyZ3CNOuTSJH+A1A+eBL6DmzocChUyO6jvZFuh6Dg==
+ dependencies:
+ "@storybook/api" "6.4.9"
+ "@storybook/channels" "6.4.9"
+ "@storybook/client-logger" "6.4.9"
+ "@storybook/core-events" "6.4.9"
+ "@storybook/csf" "0.0.2--canary.87bc651.0"
+ "@storybook/router" "6.4.9"
+ "@storybook/theming" "6.4.9"
+ "@types/webpack-env" "^1.16.0"
+ core-js "^3.8.2"
+ global "^4.4.0"
+ regenerator-runtime "^0.13.7"
+
"@storybook/api@6.3.11":
version "6.3.11"
resolved "https://registry.npmjs.org/@storybook/api/-/api-6.3.11.tgz#ea3806a0570da65bfb5b39e4edb90289b5ba701e"
@@ -6019,6 +6036,29 @@
ts-dedent "^2.0.0"
util-deprecate "^1.0.2"
+"@storybook/api@6.4.9":
+ version "6.4.9"
+ resolved "https://registry.npmjs.org/@storybook/api/-/api-6.4.9.tgz#6187d08658629580f0a583f2069d55b34964b34a"
+ integrity sha512-U+YKcDQg8xal9sE5eSMXB9vcqk8fD1pSyewyAjjbsW5hV0B3L3i4u7z/EAD9Ujbnor+Cvxq+XGvp+Qnc5Gd40A==
+ dependencies:
+ "@storybook/channels" "6.4.9"
+ "@storybook/client-logger" "6.4.9"
+ "@storybook/core-events" "6.4.9"
+ "@storybook/csf" "0.0.2--canary.87bc651.0"
+ "@storybook/router" "6.4.9"
+ "@storybook/semver" "^7.3.2"
+ "@storybook/theming" "6.4.9"
+ core-js "^3.8.2"
+ fast-deep-equal "^3.1.3"
+ global "^4.4.0"
+ lodash "^4.17.21"
+ memoizerific "^1.11.3"
+ regenerator-runtime "^0.13.7"
+ store2 "^2.12.0"
+ telejson "^5.3.2"
+ ts-dedent "^2.0.0"
+ util-deprecate "^1.0.2"
+
"@storybook/builder-webpack4@6.3.11":
version "6.3.11"
resolved "https://registry.npmjs.org/@storybook/builder-webpack4/-/builder-webpack4-6.3.11.tgz#b1b62a41b2fbd951733e86aaa4730dc2541b4221"
@@ -6148,6 +6188,15 @@
ts-dedent "^2.0.0"
util-deprecate "^1.0.2"
+"@storybook/channels@6.4.9":
+ version "6.4.9"
+ resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.4.9.tgz#132c574d3fb2e6aaa9c52312c592794699b9d8ec"
+ integrity sha512-DNW1qDg+1WFS2aMdGh658WJXh8xBXliO5KAn0786DKcWCsKjfsPPQg/QCHczHK0+s5SZyzQT5aOBb4kTRHELQA==
+ dependencies:
+ core-js "^3.8.2"
+ ts-dedent "^2.0.0"
+ util-deprecate "^1.0.2"
+
"@storybook/client-api@6.3.11":
version "6.3.11"
resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.3.11.tgz#3e6548bf6e83a2958db701cf59740a2519eea771"
@@ -6220,6 +6269,14 @@
core-js "^3.8.2"
global "^4.4.0"
+"@storybook/client-logger@6.4.9":
+ version "6.4.9"
+ resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.4.9.tgz#ef6af30fac861fea69c8917120ed06b4c2f0b54e"
+ integrity sha512-BVagmmHcuKDZ/XROADfN3tiolaDW2qG0iLmDhyV1gONnbGE6X5Qm19Jt2VYu3LvjKF1zMPSWm4mz7HtgdwKbuQ==
+ dependencies:
+ core-js "^3.8.2"
+ global "^4.4.0"
+
"@storybook/components@6.3.11":
version "6.3.11"
resolved "https://registry.npmjs.org/@storybook/components/-/components-6.3.11.tgz#a7d015fc9808d0200d033d8f694db79277770030"
@@ -6310,6 +6367,36 @@
ts-dedent "^2.0.0"
util-deprecate "^1.0.2"
+"@storybook/components@6.4.9":
+ version "6.4.9"
+ resolved "https://registry.npmjs.org/@storybook/components/-/components-6.4.9.tgz#caed59eb3f09d1646da748186f718a0e54fb8fd7"
+ integrity sha512-uOUR97S6kjptkMCh15pYNM1vAqFXtpyneuonmBco5vADJb3ds0n2a8NeVd+myIbhIXn55x0OHKiSwBH/u7swCQ==
+ dependencies:
+ "@popperjs/core" "^2.6.0"
+ "@storybook/client-logger" "6.4.9"
+ "@storybook/csf" "0.0.2--canary.87bc651.0"
+ "@storybook/theming" "6.4.9"
+ "@types/color-convert" "^2.0.0"
+ "@types/overlayscrollbars" "^1.12.0"
+ "@types/react-syntax-highlighter" "11.0.5"
+ color-convert "^2.0.1"
+ core-js "^3.8.2"
+ fast-deep-equal "^3.1.3"
+ global "^4.4.0"
+ lodash "^4.17.21"
+ markdown-to-jsx "^7.1.3"
+ memoizerific "^1.11.3"
+ overlayscrollbars "^1.13.1"
+ polished "^4.0.5"
+ prop-types "^15.7.2"
+ react-colorful "^5.1.2"
+ react-popper-tooltip "^3.1.1"
+ react-syntax-highlighter "^13.5.3"
+ react-textarea-autosize "^8.3.0"
+ regenerator-runtime "^0.13.7"
+ ts-dedent "^2.0.0"
+ util-deprecate "^1.0.2"
+
"@storybook/core-client@6.3.11":
version "6.3.11"
resolved "https://registry.npmjs.org/@storybook/core-client/-/core-client-6.3.11.tgz#beede8dfb0b0d86945f0b15ef574c6ed8af37576"
@@ -6408,6 +6495,13 @@
dependencies:
core-js "^3.8.2"
+"@storybook/core-events@6.4.9":
+ version "6.4.9"
+ resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.4.9.tgz#7febedb8d263fbd6e4a69badbfcdce0101e6f782"
+ integrity sha512-YhU2zJr6wzvh5naYYuy/0UKNJ/SaXu73sIr0Tx60ur3bL08XkRg7eZ9vBhNBTlAa35oZqI0iiGCh0ljiX7yEVQ==
+ dependencies:
+ core-js "^3.8.2"
+
"@storybook/core-server@6.3.11":
version "6.3.11"
resolved "https://registry.npmjs.org/@storybook/core-server/-/core-server-6.3.11.tgz#e6cfdab8de72254007de50473186734c26bbcbc7"
@@ -6483,6 +6577,13 @@
dependencies:
lodash "^4.17.15"
+"@storybook/csf@0.0.2--canary.87bc651.0":
+ version "0.0.2--canary.87bc651.0"
+ resolved "https://registry.npmjs.org/@storybook/csf/-/csf-0.0.2--canary.87bc651.0.tgz#c7b99b3a344117ef67b10137b6477a3d2750cf44"
+ integrity sha512-ajk1Uxa+rBpFQHKrCcTmJyQBXZ5slfwHVEaKlkuFaW77it8RgbPJp/ccna3sgoi8oZ7FkkOyvv1Ve4SmwFqRqw==
+ dependencies:
+ lodash "^4.17.15"
+
"@storybook/manager-webpack4@6.3.11":
version "6.3.11"
resolved "https://registry.npmjs.org/@storybook/manager-webpack4/-/manager-webpack4-6.3.11.tgz#477a796da09a771b2d3e1adfe217a79de37057af"
@@ -6627,6 +6728,23 @@
qs "^6.10.0"
ts-dedent "^2.0.0"
+"@storybook/router@6.4.9":
+ version "6.4.9"
+ resolved "https://registry.npmjs.org/@storybook/router/-/router-6.4.9.tgz#7cc3f85494f4e14d38925e2802145df69a071201"
+ integrity sha512-GT2KtVHo/mBjxDBFB5ZtVJVf8vC+3p5kRlQC4jao68caVp7H24ikPOkcY54VnQwwe4A1aXpGbJXUyTisEPFlhQ==
+ dependencies:
+ "@storybook/client-logger" "6.4.9"
+ core-js "^3.8.2"
+ fast-deep-equal "^3.1.3"
+ global "^4.4.0"
+ history "5.0.0"
+ lodash "^4.17.21"
+ memoizerific "^1.11.3"
+ qs "^6.10.0"
+ react-router "^6.0.0"
+ react-router-dom "^6.0.0"
+ ts-dedent "^2.0.0"
+
"@storybook/semver@^7.3.2":
version "7.3.2"
resolved "https://registry.npmjs.org/@storybook/semver/-/semver-7.3.2.tgz#f3b9c44a1c9a0b933c04e66d0048fcf2fa10dac0"
@@ -6705,6 +6823,24 @@
resolve-from "^5.0.0"
ts-dedent "^2.0.0"
+"@storybook/theming@6.4.9":
+ version "6.4.9"
+ resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.4.9.tgz#8ece44007500b9a592e71eca693fbeac90803b0d"
+ integrity sha512-Do6GH6nKjxfnBg6djcIYAjss5FW9SRKASKxLYxX2RyWJBpz0m/8GfcGcRyORy0yFTk6jByA3Hs+WFH3GnEbWkw==
+ dependencies:
+ "@emotion/core" "^10.1.1"
+ "@emotion/is-prop-valid" "^0.8.6"
+ "@emotion/styled" "^10.0.27"
+ "@storybook/client-logger" "6.4.9"
+ core-js "^3.8.2"
+ deep-object-diff "^1.1.0"
+ emotion-theming "^10.0.27"
+ global "^4.4.0"
+ memoizerific "^1.11.3"
+ polished "^4.0.5"
+ resolve-from "^5.0.0"
+ ts-dedent "^2.0.0"
+
"@storybook/ui@6.3.11":
version "6.3.11"
resolved "https://registry.npmjs.org/@storybook/ui/-/ui-6.3.11.tgz#fc04d6ab50b78dc5f3d8fdc1eade0c78e4c4a4e9"
@@ -7886,15 +8022,7 @@
resolved "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.0.tgz#f946cefb5c05c64f460090f6be97bd50460c8898"
integrity sha512-RNBIyVwa/1v2r8/SqK8tadH2sJlFRAo5Ghac/cOcCv4Kp94m0I03UmAh9WVhCqS9ZdB84dF3x47p9aTw8E4c4A==
-"@types/node-fetch@^2.5.0", "@types/node-fetch@^2.5.7":
- version "2.5.8"
- resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.8.tgz#e199c835d234c7eb0846f6618012e558544ee2fb"
- integrity sha512-fbjI6ja0N5ZA8TV53RUqzsKNkl9fv8Oj3T7zxW7FGv1GSH7gwJaNF8dzCjrqKaxKeUpTz4yT1DaJFq/omNpGfw==
- dependencies:
- "@types/node" "*"
- form-data "^3.0.0"
-
-"@types/node-fetch@^2.5.12":
+"@types/node-fetch@^2.5.0", "@types/node-fetch@^2.5.12", "@types/node-fetch@^2.5.7":
version "2.5.12"
resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz#8a6f779b1d4e60b7a57fb6fd48d84fb545b9cc66"
integrity sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==
@@ -10006,12 +10134,7 @@ aws4@^1.11.0, aws4@^1.8.0:
resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
-axe-core@^4.0.2:
- version "4.1.3"
- resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.1.3.tgz#64a4c85509e0991f5168340edc4bedd1ceea6966"
- integrity sha512-vwPpH4Aj4122EW38mxO/fxhGKtwWTMLDIJfZ1He0Edbtjcfna/R3YB67yVhezUMzqc3Jr3+Ii50KRntlENL4xQ==
-
-axe-core@^4.2.0:
+axe-core@^4.0.2, axe-core@^4.2.0:
version "4.3.1"
resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.3.1.tgz#0c6a076e4a1c3e0544ba6a9479158f9be7a7928e"
integrity sha512-3WVgVPs/7OnKU3s+lqMtkv3wQlg3WxK1YifmpJSDO0E1aPBrZWlrrTO6cxRqCXLuX2aYgCljqXIQd0VnRidV0g==
@@ -16645,6 +16768,13 @@ highlight.js@^10.7.2, highlight.js@~10.7.0:
resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==
+history@5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/history/-/history-5.0.0.tgz#0cabbb6c4bbf835addb874f8259f6d25101efd08"
+ integrity sha512-3NyRMKIiFSJmIPdq7FxkNMJkQ7ZEtVblOQ38VtKaA0zZMW1Eo6Q6W8oDKEflr1kNNTItSnk4JMCO1deeSgbLLg==
+ dependencies:
+ "@babel/runtime" "^7.7.6"
+
history@^5.0.0:
version "5.1.0"
resolved "https://registry.npmjs.org/history/-/history-5.1.0.tgz#2e93c09c064194d38d52ed62afd0afc9d9b01ece"
@@ -16652,6 +16782,13 @@ history@^5.0.0:
dependencies:
"@babel/runtime" "^7.7.6"
+history@^5.2.0:
+ version "5.2.0"
+ resolved "https://registry.npmjs.org/history/-/history-5.2.0.tgz#7cdd31cf9bac3c5d31f09c231c9928fad0007b7c"
+ integrity sha512-uPSF6lAJb3nSePJ43hN3eKj1dTWpN9gMod0ZssbFTIsen+WehTmEadgL+kg78xLJFdRfrrC//SavDzmRVdE+Ig==
+ dependencies:
+ "@babel/runtime" "^7.7.6"
+
hmac-drbg@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@@ -23948,13 +24085,13 @@ promzard@^0.3.0:
read "1"
prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
- version "15.7.2"
- resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
- integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
+ version "15.8.0"
+ resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.0.tgz#d237e624c45a9846e469f5f31117f970017ff588"
+ integrity sha512-fDGekdaHh65eI3lMi5OnErU6a8Ighg2KjcjQxO7m8VHyWjcPyj5kiOgV1LQDOOOgVy3+5FgjXvdSSX7B8/5/4g==
dependencies:
loose-envify "^1.4.0"
object-assign "^4.1.1"
- react-is "^16.8.1"
+ react-is "^16.13.1"
property-expr@^2.0.4:
version "2.0.4"
@@ -24549,7 +24686,7 @@ react-inspector@^5.1.0, react-inspector@^5.1.1:
is-dom "^1.0.0"
prop-types "^15.0.0"
-react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0:
+react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0:
version "16.13.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -24651,6 +24788,14 @@ react-router-dom@6.0.0-beta.0, react-router-dom@^6.0.0-beta.0:
prop-types "^15.7.2"
react-router "6.0.0-beta.0"
+react-router-dom@^6.0.0:
+ version "6.2.1"
+ resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.2.1.tgz#32ec81829152fbb8a7b045bf593a22eadf019bec"
+ integrity sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA==
+ dependencies:
+ history "^5.2.0"
+ react-router "6.2.1"
+
react-router@6.0.0-beta.0, react-router@^6.0.0-beta.0:
version "6.0.0-beta.0"
resolved "https://registry.npmjs.org/react-router/-/react-router-6.0.0-beta.0.tgz#3e11f39b6ded4412c2fed9e4f989dd4c8156724d"
@@ -24658,6 +24803,13 @@ react-router@6.0.0-beta.0, react-router@^6.0.0-beta.0:
dependencies:
prop-types "^15.7.2"
+react-router@6.2.1, react-router@^6.0.0:
+ version "6.2.1"
+ resolved "https://registry.npmjs.org/react-router/-/react-router-6.2.1.tgz#be2a97a6006ce1d9123c28934e604faef51448a3"
+ integrity sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg==
+ dependencies:
+ history "^5.2.0"
+
react-side-effect@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.0.tgz#1ce4a8b4445168c487ed24dab886421f74d380d3"
@@ -26785,9 +26937,9 @@ stacktrace-js@^2.0.2:
stacktrace-gps "^3.0.4"
start-server-and-test@^1.10.11:
- version "1.13.1"
- resolved "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-1.13.1.tgz#c06eb18c3f31d610724722b7eecbdf2550b03582"
- integrity sha512-wZjksmjG5scEHXmV/3HWzImxNzUgaNQ6W8kkqL2GbiOldM+nqiqh7niimlC9ZGNopTGj16kheWZnZtSWgdBZNQ==
+ version "1.14.0"
+ resolved "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-1.14.0.tgz#c57f04f73eac15dd51733b551d775b40837fdde3"
+ integrity sha512-on5ELuxO2K0t8EmNj9MtVlFqwBMxfWOhu4U7uZD1xccVpFlOQKR93CSe0u98iQzfNxRyaNTb/CdadbNllplTsw==
dependencies:
bluebird "3.7.2"
check-more-types "2.24.0"