Merge branch 'backstage:master' into fix
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
GitHub discovery processor adds support for discovering the default GitHub branch
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search-backend-module-pg': patch
|
||||
---
|
||||
|
||||
Sanitize special characters before building search query for postgres
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-git-release-manager': patch
|
||||
---
|
||||
|
||||
Expose internal constants, helpers and components to make it easier for users to build custom features for GRM.
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
---
|
||||
|
||||
Memoize the context value in `EntityListProvider`.
|
||||
|
||||
This removes quite a few unnecessary rerenders of the inner components.
|
||||
|
||||
When running the full `CatalogPage` test:
|
||||
|
||||
- Before: 98 table render calls total, 16 seconds runtime
|
||||
- After: 57 table render calls total, 14 seconds runtime
|
||||
|
||||
This doesn't account for all of the slowness, but does give a minor difference in perceived speed in the browser too.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
Fixed a bug where OAuth state parameters would be serialized as the string `'undefined'`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Added GitLabDiscoveryProcessor, which allows catalog discovery from a GitLab instance
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-kubernetes-backend': patch
|
||||
---
|
||||
|
||||
Fixes bug reading ExternalId from k8s backend config
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/create-app': minor
|
||||
---
|
||||
|
||||
Updated the default create-app `EntityPage` to include orphan and processing error alerts for all entity types. Previously these were only shown for entities with the `Component` kind. The `EntityPage` in Backstage applications should be updated following [d027681](https://github.com/backstage/backstage/pull/6899/commits/d0276817123ba131c9211de30d229839f13d7775). This also adds the `EntityLinkCard` for API entities.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-search': patch
|
||||
---
|
||||
|
||||
Fix search page to respond to searches made from sidebar search
|
||||
@@ -142,6 +142,7 @@ maintainership
|
||||
makefile
|
||||
md
|
||||
memcache
|
||||
memoize
|
||||
memoized
|
||||
microservice
|
||||
microservices
|
||||
@@ -215,6 +216,7 @@ repo
|
||||
Repo
|
||||
repos
|
||||
rerender
|
||||
rerenders
|
||||
Reusability
|
||||
reusability
|
||||
roadmaps
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
id: discovery
|
||||
title: GitLab Discovery
|
||||
sidebar_label: Discovery
|
||||
# prettier-ignore
|
||||
description: Automatically discovering catalog entities from repositories in GitLab
|
||||
---
|
||||
|
||||
The GitLab integration has a special discovery processor for discovering catalog
|
||||
entities from GitLab. The processor will crawl the GitLab instance and register
|
||||
entities matching the configured path. This can be useful as an alternative to
|
||||
static locations or manually adding things to the catalog.
|
||||
|
||||
To use the discovery processor, you'll need a GitLab integration
|
||||
[set up](locations.md) with a `token`. Then you can add a location target to the
|
||||
catalog configuration:
|
||||
|
||||
```yaml
|
||||
catalog:
|
||||
locations:
|
||||
- type: gitlab-discovery
|
||||
target: https://gitlab.com/group/subgroup/blob/main/catalog-info.yaml
|
||||
```
|
||||
|
||||
Note the `gitlab-discovery` type, as this is not a regular `url` processor.
|
||||
|
||||
The target is composed of three parts:
|
||||
|
||||
- The base URL, `https://gitlab.com` in this case
|
||||
- The group path, `group/subgroup` in this case. This is optional: If you omit
|
||||
this path the processor will scan the entire GitLab instance instead.
|
||||
- The path within each repository to find the catalog YAML file. This will
|
||||
usually be `/blob/main/catalog-info.yaml`, `/blob/master/catalog-info.yaml` or
|
||||
a similar variation for catalog files stored in the root directory of each
|
||||
repository. If you want to use the repository's default branch use the `*`
|
||||
wildcard, e.g.: `/blob/*/catalog-info.yaml`
|
||||
@@ -211,20 +211,8 @@ const cicdCard = (
|
||||
</EntitySwitch>
|
||||
);
|
||||
|
||||
const errorsContent = (
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={isRollbarAvailable}>
|
||||
<EntityRollbarContent />
|
||||
</EntitySwitch.Case>
|
||||
|
||||
<EntitySwitch.Case>
|
||||
<EntitySentryContent />
|
||||
</EntitySwitch.Case>
|
||||
</EntitySwitch>
|
||||
);
|
||||
|
||||
const overviewContent = (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
const entityWarningContent = (
|
||||
<>
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={isOrphan}>
|
||||
<Grid item xs={12}>
|
||||
@@ -240,7 +228,24 @@ const overviewContent = (
|
||||
</Grid>
|
||||
</EntitySwitch.Case>
|
||||
</EntitySwitch>
|
||||
</>
|
||||
);
|
||||
|
||||
const errorsContent = (
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={isRollbarAvailable}>
|
||||
<EntityRollbarContent />
|
||||
</EntitySwitch.Case>
|
||||
|
||||
<EntitySwitch.Case>
|
||||
<EntitySentryContent />
|
||||
</EntitySwitch.Case>
|
||||
</EntitySwitch>
|
||||
);
|
||||
|
||||
const overviewContent = (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
{entityWarningContent}
|
||||
<Grid item md={8} xs={12}>
|
||||
<EntityAboutCard variant="gridItem" />
|
||||
</Grid>
|
||||
@@ -448,6 +453,7 @@ const apiPage = (
|
||||
<EntityLayoutWrapper>
|
||||
<EntityLayout.Route path="/" title="Overview">
|
||||
<Grid container spacing={3}>
|
||||
{entityWarningContent}
|
||||
<Grid item xs={12}>
|
||||
<EntityAboutCard />
|
||||
</Grid>
|
||||
@@ -478,6 +484,7 @@ const userPage = (
|
||||
<EntityLayoutWrapper>
|
||||
<EntityLayout.Route path="/" title="Overview">
|
||||
<Grid container spacing={3}>
|
||||
{entityWarningContent}
|
||||
<Grid item xs={12} md={6}>
|
||||
<EntityUserProfileCard variant="gridItem" />
|
||||
</Grid>
|
||||
@@ -493,6 +500,7 @@ const groupPage = (
|
||||
<EntityLayoutWrapper>
|
||||
<EntityLayout.Route path="/" title="Overview">
|
||||
<Grid container spacing={3}>
|
||||
{entityWarningContent}
|
||||
<Grid item xs={12} md={6}>
|
||||
<EntityGroupProfileCard variant="gridItem" />
|
||||
</Grid>
|
||||
@@ -511,6 +519,7 @@ const systemPage = (
|
||||
<EntityLayoutWrapper>
|
||||
<EntityLayout.Route path="/" title="Overview">
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
{entityWarningContent}
|
||||
<Grid item md={6}>
|
||||
<EntityAboutCard variant="gridItem" />
|
||||
</Grid>
|
||||
@@ -535,6 +544,7 @@ const domainPage = (
|
||||
<EntityLayoutWrapper>
|
||||
<EntityLayout.Route path="/" title="Overview">
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
{entityWarningContent}
|
||||
<Grid item md={6}>
|
||||
<EntityAboutCard variant="gridItem" />
|
||||
</Grid>
|
||||
|
||||
+15
-2
@@ -82,8 +82,8 @@ const cicdContent = (
|
||||
</EntitySwitch>
|
||||
);
|
||||
|
||||
const overviewContent = (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
const entityWarningContent = (
|
||||
<>
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={isOrphan}>
|
||||
<Grid item xs={12}>
|
||||
@@ -99,7 +99,12 @@ const overviewContent = (
|
||||
</Grid>
|
||||
</EntitySwitch.Case>
|
||||
</EntitySwitch>
|
||||
</>
|
||||
);
|
||||
|
||||
const overviewContent = (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
{entityWarningContent}
|
||||
<Grid item md={6}>
|
||||
<EntityAboutCard variant="gridItem" />
|
||||
</Grid>
|
||||
@@ -214,9 +219,13 @@ const apiPage = (
|
||||
<EntityLayout>
|
||||
<EntityLayout.Route path="/" title="Overview">
|
||||
<Grid container spacing={3}>
|
||||
{entityWarningContent}
|
||||
<Grid item md={6}>
|
||||
<EntityAboutCard />
|
||||
</Grid>
|
||||
<Grid item md={4} xs={12}>
|
||||
<EntityLinksCard />
|
||||
</Grid>
|
||||
<Grid container item md={12}>
|
||||
<Grid item md={6}>
|
||||
<EntityProvidingComponentsCard />
|
||||
@@ -242,6 +251,7 @@ const userPage = (
|
||||
<EntityLayout>
|
||||
<EntityLayout.Route path="/" title="Overview">
|
||||
<Grid container spacing={3}>
|
||||
{entityWarningContent}
|
||||
<Grid item xs={12} md={6}>
|
||||
<EntityUserProfileCard variant="gridItem" />
|
||||
</Grid>
|
||||
@@ -257,6 +267,7 @@ const groupPage = (
|
||||
<EntityLayout>
|
||||
<EntityLayout.Route path="/" title="Overview">
|
||||
<Grid container spacing={3}>
|
||||
{entityWarningContent}
|
||||
<Grid item xs={12} md={6}>
|
||||
<EntityGroupProfileCard variant="gridItem" />
|
||||
</Grid>
|
||||
@@ -275,6 +286,7 @@ const systemPage = (
|
||||
<EntityLayout>
|
||||
<EntityLayout.Route path="/" title="Overview">
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
{entityWarningContent}
|
||||
<Grid item md={6}>
|
||||
<EntityAboutCard variant="gridItem" />
|
||||
</Grid>
|
||||
@@ -299,6 +311,7 @@ const domainPage = (
|
||||
<EntityLayout>
|
||||
<EntityLayout.Route path="/" title="Overview">
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
{entityWarningContent}
|
||||
<Grid item md={6}>
|
||||
<EntityAboutCard variant="gridItem" />
|
||||
</Grid>
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"jose": "^1.27.1",
|
||||
"jwt-decode": "^3.1.0",
|
||||
"knex": "^0.95.1",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^2.0.2",
|
||||
"minimatch": "^3.0.3",
|
||||
"morgan": "^1.10.0",
|
||||
|
||||
@@ -15,9 +15,39 @@
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import { verifyNonce, encodeState } from './helpers';
|
||||
import { verifyNonce, encodeState, readState } from './helpers';
|
||||
|
||||
describe('OAuthProvider Utils', () => {
|
||||
describe('encodeState', () => {
|
||||
it('should serialized values', () => {
|
||||
const state = {
|
||||
nonce: '123',
|
||||
env: 'development',
|
||||
origin: 'https://example.com',
|
||||
};
|
||||
|
||||
const encoded = encodeState(state);
|
||||
expect(encoded).toBe(
|
||||
Buffer.from(
|
||||
'nonce=123&env=development&origin=https%3A%2F%2Fexample.com',
|
||||
).toString('hex'),
|
||||
);
|
||||
|
||||
expect(readState(encoded)).toEqual(state);
|
||||
});
|
||||
|
||||
it('should not include undefined values', () => {
|
||||
const state = { nonce: '123', env: 'development', origin: undefined };
|
||||
|
||||
const encoded = encodeState(state);
|
||||
expect(encoded).toBe(
|
||||
Buffer.from('nonce=123&env=development').toString('hex'),
|
||||
);
|
||||
|
||||
expect(readState(encoded)).toEqual(state);
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyNonce', () => {
|
||||
it('should throw error if cookie nonce missing', () => {
|
||||
const state = { nonce: 'NONCE', env: 'development' };
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import express from 'express';
|
||||
import { OAuthState } from './types';
|
||||
import pickBy from 'lodash/pickBy';
|
||||
|
||||
export const readState = (stateString: string): OAuthState => {
|
||||
const state = Object.fromEntries(
|
||||
@@ -34,7 +35,9 @@ export const readState = (stateString: string): OAuthState => {
|
||||
};
|
||||
|
||||
export const encodeState = (state: OAuthState): string => {
|
||||
const stateString = new URLSearchParams(state).toString();
|
||||
const stateString = new URLSearchParams(
|
||||
pickBy(state, value => value !== undefined),
|
||||
).toString();
|
||||
|
||||
return Buffer.from(stateString, 'utf-8').toString('hex');
|
||||
};
|
||||
|
||||
@@ -813,6 +813,27 @@ export class GithubOrgReaderProcessor implements CatalogProcessor {
|
||||
): Promise<boolean>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "GitLabDiscoveryProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export class GitLabDiscoveryProcessor implements CatalogProcessor {
|
||||
// (undocumented)
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
options: {
|
||||
logger: Logger_2;
|
||||
},
|
||||
): GitLabDiscoveryProcessor;
|
||||
// (undocumented)
|
||||
readLocation(
|
||||
location: LocationSpec,
|
||||
_optional: boolean,
|
||||
emit: CatalogProcessorEmit,
|
||||
): Promise<boolean>;
|
||||
// (undocumented)
|
||||
updateLastActivity(): Promise<string | undefined>;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "HigherOrderOperation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
/*
|
||||
* 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 { ConfigReader } from '@backstage/config';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { LocationSpec } from '@backstage/catalog-model';
|
||||
import { GitLabDiscoveryProcessor, parseUrl } from './GitLabDiscoveryProcessor';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { rest } from 'msw';
|
||||
import { GitLabProject } from './gitlab';
|
||||
|
||||
const server = setupServer();
|
||||
|
||||
const PROJECTS_URL = 'https://gitlab.fake/api/v4/projects';
|
||||
const GROUP_PROJECTS_URL =
|
||||
'https://gitlab.fake/api/v4/groups/group%2Fsubgroup/projects';
|
||||
|
||||
const PROJECT_LOCATION: LocationSpec = {
|
||||
type: 'gitlab-discovery',
|
||||
target: 'https://gitlab.fake/blob/*/catalog-info.yaml',
|
||||
};
|
||||
const PROJECT_LOCATION_MASTER_BRANCH: LocationSpec = {
|
||||
type: 'gitlab-discovery',
|
||||
target: 'https://gitlab.fake/blob/master/catalog-info.yaml',
|
||||
};
|
||||
const GROUP_LOCATION: LocationSpec = {
|
||||
type: 'gitlab-discovery',
|
||||
target: 'https://gitlab.fake/group/subgroup/blob/*/catalog-info.yaml',
|
||||
};
|
||||
|
||||
function setupFakeServer(
|
||||
url: string,
|
||||
callback: (request: { page: number; include_subgroups: boolean }) => {
|
||||
data: GitLabProject[];
|
||||
nextPage?: number;
|
||||
},
|
||||
) {
|
||||
server.use(
|
||||
rest.get(url, (req, res, ctx) => {
|
||||
if (req.headers.get('private-token') !== 'test-token') {
|
||||
return res(ctx.status(401), ctx.json({}));
|
||||
}
|
||||
const page = req.url.searchParams.get('page');
|
||||
const include_subgroups = req.url.searchParams.get('include_subgroups');
|
||||
const response = callback({
|
||||
page: parseInt(page!, 10),
|
||||
include_subgroups: include_subgroups === 'true',
|
||||
});
|
||||
|
||||
// Filter the fake results based on the `last_activity_after` parameter
|
||||
const last_activity_after = req.url.searchParams.get(
|
||||
'last_activity_after',
|
||||
);
|
||||
const filteredData = response.data.filter(
|
||||
v =>
|
||||
!last_activity_after ||
|
||||
Date.parse(v.last_activity_at) >= Date.parse(last_activity_after),
|
||||
);
|
||||
|
||||
return res(
|
||||
ctx.set('x-next-page', response.nextPage?.toString() ?? ''),
|
||||
ctx.json(filteredData),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function getConfig(): any {
|
||||
return {
|
||||
backend: {
|
||||
cache: { store: 'memory' },
|
||||
},
|
||||
integrations: {
|
||||
gitlab: [
|
||||
{
|
||||
host: 'gitlab.fake',
|
||||
apiBaseUrl: 'https://gitlab.fake/api/v4',
|
||||
token: 'test-token',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getProcessor(config?: any): GitLabDiscoveryProcessor {
|
||||
return GitLabDiscoveryProcessor.fromConfig(
|
||||
new ConfigReader(config || getConfig()),
|
||||
{
|
||||
logger: getVoidLogger(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
describe('GitlabDiscoveryProcessor', () => {
|
||||
beforeAll(() => {
|
||||
server.listen();
|
||||
jest.useFakeTimers('modern');
|
||||
jest.setSystemTime(new Date('2001-01-01T12:34:56Z'));
|
||||
});
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('parseUrl', () => {
|
||||
it('parses well formed URLs', () => {
|
||||
expect(
|
||||
parseUrl('https://gitlab.com/group/subgroup/blob/master/catalog.yaml'),
|
||||
).toEqual({
|
||||
group: 'group/subgroup',
|
||||
host: 'gitlab.com',
|
||||
branch: 'master',
|
||||
catalogPath: 'catalog.yaml',
|
||||
});
|
||||
expect(
|
||||
parseUrl('https://gitlab.com/blob/*/subfolder/catalog.yaml'),
|
||||
).toEqual({
|
||||
group: undefined,
|
||||
host: 'gitlab.com',
|
||||
branch: '*',
|
||||
catalogPath: 'subfolder/catalog.yaml',
|
||||
});
|
||||
});
|
||||
|
||||
it('throws on incorrectly formed URLs', () => {
|
||||
expect(() => parseUrl('https://gitlab.com')).toThrow();
|
||||
expect(() => parseUrl('https://gitlab.com//')).toThrow();
|
||||
expect(() => parseUrl('https://gitlab.com/foo')).toThrow();
|
||||
expect(() => parseUrl('https://gitlab.com//foo')).toThrow();
|
||||
expect(() => parseUrl('https://gitlab.com/org/teams')).toThrow();
|
||||
expect(() => parseUrl('https://gitlab.com/org//teams')).toThrow();
|
||||
expect(() =>
|
||||
parseUrl('https://gitlab.com/org//teams/blob/catalog.yaml'),
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handles repositories', () => {
|
||||
it('pages through all repositories', async () => {
|
||||
const processor = getProcessor();
|
||||
setupFakeServer(PROJECTS_URL, request => {
|
||||
switch (request.page) {
|
||||
case 1:
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
archived: false,
|
||||
default_branch: 'main',
|
||||
last_activity_at: '2021-08-05T11:03:05.774Z',
|
||||
web_url: 'https://gitlab.fake/1',
|
||||
},
|
||||
],
|
||||
nextPage: 2,
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
id: 2,
|
||||
archived: false,
|
||||
default_branch: 'master',
|
||||
last_activity_at: '2021-08-05T11:03:05.774Z',
|
||||
web_url: 'https://gitlab.fake/2',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
archived: true, // ARCHIVED
|
||||
default_branch: 'master',
|
||||
last_activity_at: '2021-08-05T11:03:05.774Z',
|
||||
web_url: 'https://gitlab.fake/3',
|
||||
},
|
||||
],
|
||||
};
|
||||
default:
|
||||
throw new Error('Invalid request');
|
||||
}
|
||||
});
|
||||
|
||||
const result: any[] = [];
|
||||
await processor.readLocation(PROJECT_LOCATION, false, e => {
|
||||
result.push(e);
|
||||
});
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'location',
|
||||
location: {
|
||||
type: 'url',
|
||||
target: 'https://gitlab.fake/1/-/blob/main/catalog-info.yaml',
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
{
|
||||
type: 'location',
|
||||
location: {
|
||||
type: 'url',
|
||||
target: 'https://gitlab.fake/2/-/blob/master/catalog-info.yaml',
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('can force a branch name', async () => {
|
||||
const processor = getProcessor();
|
||||
setupFakeServer(PROJECTS_URL, request => {
|
||||
switch (request.page) {
|
||||
case 1:
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
archived: false,
|
||||
default_branch: 'main',
|
||||
last_activity_at: '2021-08-05T11:03:05.774Z',
|
||||
web_url: 'https://gitlab.fake/1',
|
||||
},
|
||||
],
|
||||
};
|
||||
default:
|
||||
throw new Error('Invalid request');
|
||||
}
|
||||
});
|
||||
|
||||
const result: any[] = [];
|
||||
await processor.readLocation(PROJECT_LOCATION_MASTER_BRANCH, false, e => {
|
||||
result.push(e);
|
||||
});
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'location',
|
||||
location: {
|
||||
type: 'url',
|
||||
target: 'https://gitlab.fake/1/-/blob/master/catalog-info.yaml',
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('can filter based on group', async () => {
|
||||
const processor = getProcessor();
|
||||
setupFakeServer(GROUP_PROJECTS_URL, request => {
|
||||
if (!request.include_subgroups) {
|
||||
throw new Error('include_subgroups should be set');
|
||||
}
|
||||
switch (request.page) {
|
||||
case 1:
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
archived: false,
|
||||
default_branch: 'main',
|
||||
last_activity_at: '2021-08-05T11:03:05.774Z',
|
||||
web_url: 'https://gitlab.fake/1',
|
||||
},
|
||||
],
|
||||
};
|
||||
default:
|
||||
throw new Error('Invalid request');
|
||||
}
|
||||
});
|
||||
|
||||
const result: any[] = [];
|
||||
await processor.readLocation(GROUP_LOCATION, false, e => {
|
||||
result.push(e);
|
||||
});
|
||||
// If everything was set up correctly, we should have received the fake repo specified above
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('uses the previous scan timestamp to filter', async () => {
|
||||
const processor = getProcessor();
|
||||
setupFakeServer(PROJECTS_URL, request => {
|
||||
switch (request.page) {
|
||||
case 1:
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
archived: false,
|
||||
default_branch: 'main',
|
||||
last_activity_at: '2000-01-01T00:00:00Z',
|
||||
web_url: 'https://gitlab.fake/1',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
archived: false,
|
||||
default_branch: 'main',
|
||||
last_activity_at: '2002-01-01T00:00:00Z',
|
||||
web_url: 'https://gitlab.fake/2',
|
||||
},
|
||||
],
|
||||
};
|
||||
default:
|
||||
throw new Error('Invalid request');
|
||||
}
|
||||
});
|
||||
|
||||
const result: any[] = [];
|
||||
|
||||
// First scan should find all repos, since no last activity was cached
|
||||
await processor.readLocation(PROJECT_LOCATION, false, e => {
|
||||
result.push(e);
|
||||
});
|
||||
expect(result).toHaveLength(2);
|
||||
|
||||
// Second scan should have used the mocked Date to set the last scanned time to 2001
|
||||
// This should result in only the second repo being scanned, since that has a timestamp of 2002
|
||||
const result2: any[] = [];
|
||||
await processor.readLocation(PROJECT_LOCATION, false, e => {
|
||||
result2.push(e);
|
||||
});
|
||||
expect(result2).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handles failure', () => {
|
||||
it('invalid token', async () => {
|
||||
// Setup an empty fake gitlab, since we don't care about actual results
|
||||
setupFakeServer(PROJECTS_URL, _ => {
|
||||
return {
|
||||
data: [],
|
||||
};
|
||||
});
|
||||
|
||||
const config = getConfig();
|
||||
config.integrations.gitlab[0].token = 'invalid';
|
||||
await expect(
|
||||
getProcessor(config).readLocation(PROJECT_LOCATION, false, _ => {}),
|
||||
).rejects.toThrow(/Unauthorized/);
|
||||
});
|
||||
|
||||
it('missing integration', async () => {
|
||||
const config = getConfig();
|
||||
delete config.integrations;
|
||||
await expect(
|
||||
getProcessor(config).readLocation(PROJECT_LOCATION, false, _ => {}),
|
||||
).rejects.toThrow(/no GitLab integration/);
|
||||
});
|
||||
|
||||
it('location type', async () => {
|
||||
const incorrectLocation: LocationSpec = {
|
||||
type: 'something-that-is-not-gitlab-discovery',
|
||||
target: 'https://gitlab.fake/oh-dear',
|
||||
};
|
||||
|
||||
await expect(
|
||||
getProcessor().readLocation(incorrectLocation, false, _ => {}),
|
||||
).resolves.toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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 { LocationSpec } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { Logger } from 'winston';
|
||||
import * as results from './results';
|
||||
import { CatalogProcessor, CatalogProcessorEmit } from './types';
|
||||
import { GitLabClient, GitLabProject, paginated } from './gitlab';
|
||||
import {
|
||||
CacheClient,
|
||||
CacheManager,
|
||||
PluginCacheManager,
|
||||
} from '@backstage/backend-common';
|
||||
|
||||
/**
|
||||
* Extracts repositories out of an GitLab instance.
|
||||
*/
|
||||
export class GitLabDiscoveryProcessor implements CatalogProcessor {
|
||||
private readonly integrations: ScmIntegrations;
|
||||
private readonly logger: Logger;
|
||||
private readonly cache: CacheClient;
|
||||
|
||||
static fromConfig(config: Config, options: { logger: Logger }) {
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
const pluginCache =
|
||||
CacheManager.fromConfig(config).forPlugin('gitlab-discovery');
|
||||
|
||||
return new GitLabDiscoveryProcessor({
|
||||
...options,
|
||||
integrations,
|
||||
pluginCache,
|
||||
});
|
||||
}
|
||||
|
||||
private constructor(options: {
|
||||
integrations: ScmIntegrations;
|
||||
pluginCache: PluginCacheManager;
|
||||
logger: Logger;
|
||||
}) {
|
||||
this.integrations = options.integrations;
|
||||
this.cache = options.pluginCache.getClient();
|
||||
this.logger = options.logger;
|
||||
}
|
||||
|
||||
async readLocation(
|
||||
location: LocationSpec,
|
||||
_optional: boolean,
|
||||
emit: CatalogProcessorEmit,
|
||||
): Promise<boolean> {
|
||||
if (location.type !== 'gitlab-discovery') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { group, host, branch, catalogPath } = parseUrl(location.target);
|
||||
|
||||
const integration = this.integrations.gitlab.byUrl(`https://${host}`);
|
||||
if (!integration) {
|
||||
throw new Error(
|
||||
`There is no GitLab integration that matches ${host}. Please add a configuration entry for it under integrations.gitlab`,
|
||||
);
|
||||
}
|
||||
|
||||
const client = new GitLabClient({
|
||||
config: integration.config,
|
||||
logger: this.logger,
|
||||
});
|
||||
const startTimestamp = Date.now();
|
||||
this.logger.debug(`Reading GitLab projects from ${location.target}`);
|
||||
|
||||
const projects = paginated(options => client.listProjects(options), {
|
||||
group,
|
||||
last_activity_after: await this.updateLastActivity(),
|
||||
page: 1,
|
||||
});
|
||||
|
||||
const result: Result = {
|
||||
scanned: 0,
|
||||
matches: [],
|
||||
};
|
||||
for await (const project of projects) {
|
||||
result.scanned++;
|
||||
if (!project.archived) {
|
||||
result.matches.push(project);
|
||||
}
|
||||
}
|
||||
|
||||
for (const project of result.matches) {
|
||||
const project_branch = branch === '*' ? project.default_branch : branch;
|
||||
|
||||
emit(
|
||||
results.location(
|
||||
{
|
||||
type: 'url',
|
||||
// The format expected by the GitLabUrlReader:
|
||||
// https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath
|
||||
//
|
||||
// This unfortunately will trigger another API call in `getGitLabFileFetchUrl` to get the project ID.
|
||||
// The alternative is using the `buildRawUrl` function, which does not support subgroups, so providing a raw
|
||||
// URL here won't work either.
|
||||
target: `${project.web_url}/-/blob/${project_branch}/${catalogPath}`,
|
||||
},
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1);
|
||||
this.logger.debug(
|
||||
`Read ${result.scanned} GitLab repositories in ${duration} seconds`,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async updateLastActivity(): Promise<string | undefined> {
|
||||
const lastActivity = await this.cache.get('last-activity');
|
||||
await this.cache.set('last-activity', new Date().toISOString());
|
||||
return lastActivity as string | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
type Result = {
|
||||
scanned: number;
|
||||
matches: GitLabProject[];
|
||||
};
|
||||
|
||||
/*
|
||||
* Helpers
|
||||
*/
|
||||
|
||||
export function parseUrl(urlString: string): {
|
||||
group?: string;
|
||||
host: string;
|
||||
branch: string;
|
||||
catalogPath: string;
|
||||
} {
|
||||
const url = new URL(urlString);
|
||||
const path = url.pathname.substr(1).split('/');
|
||||
|
||||
// (/group/subgroup)/blob/branch|*/filepath
|
||||
const blobIndex = path.findIndex(p => p === 'blob');
|
||||
if (blobIndex !== -1 && path.length > blobIndex + 2) {
|
||||
const group =
|
||||
blobIndex > 0 ? path.slice(0, blobIndex).join('/') : undefined;
|
||||
|
||||
return {
|
||||
group,
|
||||
host: url.host,
|
||||
branch: decodeURIComponent(path[blobIndex + 1]),
|
||||
catalogPath: decodeURIComponent(path.slice(blobIndex + 2).join('/')),
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Failed to parse ${urlString}`);
|
||||
}
|
||||
@@ -35,7 +35,8 @@ describe('GithubDiscoveryProcessor', () => {
|
||||
org: 'foo',
|
||||
host: 'github.com',
|
||||
repoSearchPath: /^proj$/,
|
||||
catalogPath: '/blob/master/catalog.yaml',
|
||||
branch: 'master',
|
||||
catalogPath: '/catalog.yaml',
|
||||
});
|
||||
expect(
|
||||
parseUrl('https://github.com/foo/proj*/blob/master/catalog.yaml'),
|
||||
@@ -43,14 +44,21 @@ describe('GithubDiscoveryProcessor', () => {
|
||||
org: 'foo',
|
||||
host: 'github.com',
|
||||
repoSearchPath: /^proj.*$/,
|
||||
catalogPath: '/blob/master/catalog.yaml',
|
||||
branch: 'master',
|
||||
catalogPath: '/catalog.yaml',
|
||||
});
|
||||
expect(parseUrl('https://github.com/foo')).toEqual({
|
||||
org: 'foo',
|
||||
host: 'github.com',
|
||||
repoSearchPath: /^.*$/,
|
||||
branch: '-',
|
||||
catalogPath: '/catalog-info.yaml',
|
||||
});
|
||||
});
|
||||
|
||||
it('throws on incorrectly formed URLs', () => {
|
||||
expect(() => parseUrl('https://github.com')).toThrow();
|
||||
expect(() => parseUrl('https://github.com//')).toThrow();
|
||||
expect(() => parseUrl('https://github.com/foo')).toThrow();
|
||||
expect(() => parseUrl('https://github.com//foo')).toThrow();
|
||||
expect(() => parseUrl('https://github.com/org/teams')).toThrow();
|
||||
expect(() => parseUrl('https://github.com/org//teams')).toThrow();
|
||||
@@ -125,11 +133,17 @@ describe('GithubDiscoveryProcessor', () => {
|
||||
name: 'backstage',
|
||||
url: 'https://github.com/backstage/backstage',
|
||||
isArchived: false,
|
||||
defaultBranchRef: {
|
||||
name: 'master',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'demo',
|
||||
url: 'https://github.com/backstage/demo',
|
||||
isArchived: false,
|
||||
defaultBranchRef: {
|
||||
name: 'main',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -168,16 +182,25 @@ describe('GithubDiscoveryProcessor', () => {
|
||||
name: 'backstage',
|
||||
url: 'https://github.com/backstage/backstage',
|
||||
isArchived: false,
|
||||
defaultBranchRef: {
|
||||
name: 'main',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'techdocs-cli',
|
||||
url: 'https://github.com/backstage/techdocs-cli',
|
||||
isArchived: false,
|
||||
defaultBranchRef: {
|
||||
name: 'main',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'techdocs-container',
|
||||
url: 'https://github.com/backstage/techdocs-container',
|
||||
isArchived: false,
|
||||
defaultBranchRef: {
|
||||
name: 'main',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -215,21 +238,33 @@ describe('GithubDiscoveryProcessor', () => {
|
||||
name: 'abstest',
|
||||
url: 'https://github.com/backstage/abctest',
|
||||
isArchived: false,
|
||||
defaultBranchRef: {
|
||||
name: 'main',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'test',
|
||||
url: 'https://github.com/backstage/test',
|
||||
isArchived: false,
|
||||
defaultBranchRef: {
|
||||
name: 'main',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'test-archived',
|
||||
url: 'https://github.com/backstage/test',
|
||||
isArchived: true,
|
||||
defaultBranchRef: {
|
||||
name: 'main',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'testxyz',
|
||||
url: 'https://github.com/backstage/testxyz',
|
||||
isArchived: false,
|
||||
defaultBranchRef: {
|
||||
name: 'main',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -28,7 +28,18 @@ import { CatalogProcessor, CatalogProcessorEmit } from './types';
|
||||
|
||||
/**
|
||||
* Extracts repositories out of a GitHub org.
|
||||
*/
|
||||
*
|
||||
* The following will create locations for all projects which have a catalog-info.yaml
|
||||
* on the default branch. The first is shorthand for the second.
|
||||
*
|
||||
* target: "https://github.com/backstage"
|
||||
* or
|
||||
* target: https://github.com/backstage/*\/blob/-/catalog-info.yaml
|
||||
*
|
||||
* You may also explicitly specify the source branch:
|
||||
*
|
||||
* target: https://github.com/backstage/*\/blob/main/catalog-info.yaml
|
||||
**/
|
||||
export class GithubDiscoveryProcessor implements CatalogProcessor {
|
||||
private readonly integrations: ScmIntegrations;
|
||||
private readonly logger: Logger;
|
||||
@@ -65,7 +76,7 @@ export class GithubDiscoveryProcessor implements CatalogProcessor {
|
||||
);
|
||||
}
|
||||
|
||||
const { org, repoSearchPath, catalogPath, host } = parseUrl(
|
||||
const { org, repoSearchPath, catalogPath, branch, host } = parseUrl(
|
||||
location.target,
|
||||
);
|
||||
|
||||
@@ -97,11 +108,14 @@ export class GithubDiscoveryProcessor implements CatalogProcessor {
|
||||
);
|
||||
|
||||
for (const repository of matching) {
|
||||
const path = `/blob/${
|
||||
branch === '-' ? repository.defaultBranchRef.name : branch
|
||||
}${catalogPath}`;
|
||||
emit(
|
||||
results.location(
|
||||
{
|
||||
type: 'url',
|
||||
target: `${repository.url}${catalogPath}`,
|
||||
target: `${repository.url}${path}`,
|
||||
},
|
||||
// Not all locations may actually exist, since the user defined them as a wildcard pattern.
|
||||
// Thus, we emit them as optional and let the downstream processor find them while not outputting
|
||||
@@ -123,19 +137,31 @@ export function parseUrl(urlString: string): {
|
||||
org: string;
|
||||
repoSearchPath: RegExp;
|
||||
catalogPath: string;
|
||||
branch: string;
|
||||
host: string;
|
||||
} {
|
||||
const url = new URL(urlString);
|
||||
const path = url.pathname.substr(1).split('/');
|
||||
|
||||
// /backstage/techdocs-*/blob/master/catalog-info.yaml
|
||||
// can also be
|
||||
// /backstage
|
||||
if (path.length > 2 && path[0].length && path[1].length) {
|
||||
return {
|
||||
org: decodeURIComponent(path[0]),
|
||||
repoSearchPath: escapeRegExp(decodeURIComponent(path[1])),
|
||||
catalogPath: `/${decodeURIComponent(path.slice(2).join('/'))}`,
|
||||
branch: decodeURIComponent(path[3]),
|
||||
catalogPath: `/${decodeURIComponent(path.slice(4).join('/'))}`,
|
||||
host: url.host,
|
||||
};
|
||||
} else if (path.length === 1 && path[0].length) {
|
||||
return {
|
||||
org: decodeURIComponent(path[0]),
|
||||
host: url.host,
|
||||
repoSearchPath: escapeRegExp('*'),
|
||||
catalogPath: '/catalog-info.yaml',
|
||||
branch: '-',
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Failed to parse ${urlString}`);
|
||||
|
||||
@@ -201,11 +201,17 @@ describe('github', () => {
|
||||
name: 'backstage',
|
||||
url: 'https://github.com/backstage/backstage',
|
||||
isArchived: false,
|
||||
defaultBranchRef: {
|
||||
name: 'main',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'demo',
|
||||
url: 'https://github.com/backstage/demo',
|
||||
isArchived: true,
|
||||
defaultBranchRef: {
|
||||
name: 'main',
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
@@ -221,11 +227,17 @@ describe('github', () => {
|
||||
name: 'backstage',
|
||||
url: 'https://github.com/backstage/backstage',
|
||||
isArchived: false,
|
||||
defaultBranchRef: {
|
||||
name: 'main',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'demo',
|
||||
url: 'https://github.com/backstage/demo',
|
||||
isArchived: true,
|
||||
defaultBranchRef: {
|
||||
name: 'main',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -60,6 +60,9 @@ export type Repository = {
|
||||
name: string;
|
||||
url: string;
|
||||
isArchived: boolean;
|
||||
defaultBranchRef: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type Connection<T> = {
|
||||
@@ -252,6 +255,9 @@ export async function getOrganizationRepositories(
|
||||
name
|
||||
url
|
||||
isArchived
|
||||
defaultBranchRef {
|
||||
name
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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 fetch from 'cross-fetch';
|
||||
import {
|
||||
getGitLabRequestOptions,
|
||||
GitLabIntegrationConfig,
|
||||
} from '@backstage/integration';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
export class GitLabClient {
|
||||
private readonly config: GitLabIntegrationConfig;
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(options: { config: GitLabIntegrationConfig; logger: Logger }) {
|
||||
this.config = options.config;
|
||||
this.logger = options.logger;
|
||||
}
|
||||
|
||||
async listProjects(options?: ListOptions): Promise<PagedResponse<any>> {
|
||||
if (options?.group) {
|
||||
return this.pagedRequest(
|
||||
`${this.config.apiBaseUrl}/groups/${encodeURIComponent(
|
||||
options?.group,
|
||||
)}/projects`,
|
||||
{
|
||||
...options,
|
||||
include_subgroups: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.pagedRequest(`${this.config.apiBaseUrl}/projects`, options);
|
||||
}
|
||||
|
||||
private async pagedRequest(
|
||||
endpoint: string,
|
||||
options?: ListOptions,
|
||||
): Promise<PagedResponse<any>> {
|
||||
const request = new URL(endpoint);
|
||||
for (const key in options) {
|
||||
if (options[key]) {
|
||||
request.searchParams.append(key, options[key]!.toString());
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.debug(`Fetching: ${request.toString()}`);
|
||||
const response = await fetch(
|
||||
request.toString(),
|
||||
getGitLabRequestOptions(this.config),
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Unexpected response when fetching ${request.toString()}. Expected 200 but got ${
|
||||
response.status
|
||||
} - ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
return response.json().then(items => {
|
||||
const nextPage = response.headers.get('x-next-page');
|
||||
|
||||
return {
|
||||
items,
|
||||
nextPage: nextPage ? Number(nextPage) : null,
|
||||
} as PagedResponse<any>;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type ListOptions = {
|
||||
[key: string]: string | number | boolean | undefined;
|
||||
group?: string;
|
||||
per_page?: number | undefined;
|
||||
page?: number | undefined;
|
||||
};
|
||||
|
||||
export type PagedResponse<T> = {
|
||||
items: T[];
|
||||
nextPage?: number;
|
||||
};
|
||||
|
||||
export async function* paginated(
|
||||
request: (options: ListOptions) => Promise<PagedResponse<any>>,
|
||||
options: ListOptions,
|
||||
) {
|
||||
let res;
|
||||
do {
|
||||
res = await request(options);
|
||||
options.page = res.nextPage;
|
||||
for (const item of res.items) {
|
||||
yield item;
|
||||
}
|
||||
} while (res.nextPage);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { GitLabClient, paginated } from './client';
|
||||
export type { GitLabProject } from './types';
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type GitLabProject = {
|
||||
id: number;
|
||||
default_branch: string;
|
||||
archived: boolean;
|
||||
last_activity_at: string;
|
||||
web_url: string;
|
||||
};
|
||||
@@ -26,6 +26,7 @@ export { FileReaderProcessor } from './FileReaderProcessor';
|
||||
export { GithubDiscoveryProcessor } from './GithubDiscoveryProcessor';
|
||||
export { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor';
|
||||
export { GithubMultiOrgReaderProcessor } from './GithubMultiOrgReaderProcessor';
|
||||
export { GitLabDiscoveryProcessor } from './GitLabDiscoveryProcessor';
|
||||
export { LocationEntityProcessor } from './LocationEntityProcessor';
|
||||
export { PlaceholderProcessor } from './PlaceholderProcessor';
|
||||
export type { PlaceholderResolver } from './PlaceholderProcessor';
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
FileReaderProcessor,
|
||||
GithubDiscoveryProcessor,
|
||||
GithubOrgReaderProcessor,
|
||||
GitLabDiscoveryProcessor,
|
||||
PlaceholderProcessor,
|
||||
PlaceholderResolver,
|
||||
UrlReaderProcessor,
|
||||
@@ -395,6 +396,7 @@ export class NextCatalogBuilder {
|
||||
BitbucketDiscoveryProcessor.fromConfig(config, { logger }),
|
||||
GithubDiscoveryProcessor.fromConfig(config, { logger }),
|
||||
GithubOrgReaderProcessor.fromConfig(config, { logger }),
|
||||
GitLabDiscoveryProcessor.fromConfig(config, { logger }),
|
||||
new UrlReaderProcessor({ reader, logger }),
|
||||
CodeOwnersProcessor.fromConfig(config, { logger, reader }),
|
||||
new AnnotateLocationEntityProcessor({ integrations }),
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
FileReaderProcessor,
|
||||
GithubDiscoveryProcessor,
|
||||
GithubOrgReaderProcessor,
|
||||
GitLabDiscoveryProcessor,
|
||||
HigherOrderOperation,
|
||||
HigherOrderOperations,
|
||||
LocationEntityProcessor,
|
||||
@@ -316,6 +317,7 @@ export class CatalogBuilder {
|
||||
BitbucketDiscoveryProcessor.fromConfig(config, { logger }),
|
||||
GithubDiscoveryProcessor.fromConfig(config, { logger }),
|
||||
GithubOrgReaderProcessor.fromConfig(config, { logger }),
|
||||
GitLabDiscoveryProcessor.fromConfig(config, { logger }),
|
||||
new UrlReaderProcessor({ reader, logger }),
|
||||
CodeOwnersProcessor.fromConfig(config, { logger, reader }),
|
||||
new LocationEntityProcessor({ integrations }),
|
||||
|
||||
@@ -22,6 +22,7 @@ import React, {
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
@@ -197,18 +198,29 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>({
|
||||
[],
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
filters: outputState.appliedFilters,
|
||||
entities: outputState.entities,
|
||||
backendEntities: outputState.backendEntities,
|
||||
updateFilters,
|
||||
queryParameters: outputState.queryParameters,
|
||||
loading,
|
||||
error,
|
||||
}),
|
||||
[
|
||||
outputState.appliedFilters,
|
||||
outputState.entities,
|
||||
outputState.backendEntities,
|
||||
updateFilters,
|
||||
outputState.queryParameters,
|
||||
loading,
|
||||
error,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<EntityListContext.Provider
|
||||
value={{
|
||||
filters: outputState.appliedFilters,
|
||||
entities: outputState.entities,
|
||||
backendEntities: outputState.backendEntities,
|
||||
updateFilters,
|
||||
queryParameters: outputState.queryParameters,
|
||||
loading,
|
||||
error,
|
||||
}}
|
||||
>
|
||||
<EntityListContext.Provider value={value}>
|
||||
{children}
|
||||
</EntityListContext.Provider>
|
||||
);
|
||||
|
||||
@@ -7,9 +7,145 @@
|
||||
|
||||
import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { default as React_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "calverRegexp" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const calverRegexp: RegExp;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "GetCommitResult" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "createMockCommit" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const createMockCommit: (
|
||||
overrides: Partial<GetCommitResult['commit']>,
|
||||
) => GetCommitResult;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "GetTagResult" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "createMockTag" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const createMockTag: (overrides: Partial<GetTagResult['tag']>) => GetTagResult;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "DifferProps" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "Differ" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const Differ: ({ current, next, icon }: DifferProps) => JSX.Element;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "DISABLE_CACHE" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const DISABLE_CACHE: {
|
||||
readonly headers: {
|
||||
readonly 'If-None-Match': '';
|
||||
};
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "Divider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const Divider: () => JSX.Element;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "SemverTagParts" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "getBumpedSemverTagParts" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
function getBumpedSemverTagParts(
|
||||
tagParts: SemverTagParts,
|
||||
semverBumpLevel: keyof typeof SEMVER_PARTS,
|
||||
): {
|
||||
bumpedTagParts: {
|
||||
prefix: string;
|
||||
major: number;
|
||||
minor: number;
|
||||
patch: number;
|
||||
};
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "getBumpedTag" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
function getBumpedTag({
|
||||
project,
|
||||
tag,
|
||||
bumpLevel,
|
||||
}: {
|
||||
project: Project;
|
||||
tag: string;
|
||||
bumpLevel: keyof typeof SEMVER_PARTS;
|
||||
}):
|
||||
| {
|
||||
bumpedTag: string;
|
||||
tagParts: CalverTagParts;
|
||||
error: undefined;
|
||||
}
|
||||
| {
|
||||
bumpedTag: string;
|
||||
tagParts: {
|
||||
prefix: string;
|
||||
major: number;
|
||||
minor: number;
|
||||
patch: number;
|
||||
};
|
||||
error: undefined;
|
||||
}
|
||||
| {
|
||||
error: AlertError;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "getCalverTagParts" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
function getCalverTagParts(tag: string):
|
||||
| {
|
||||
error: AlertError;
|
||||
tagParts?: undefined;
|
||||
}
|
||||
| {
|
||||
tagParts: CalverTagParts;
|
||||
error?: undefined;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "getSemverTagParts" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
function getSemverTagParts(tag: string):
|
||||
| {
|
||||
error: AlertError;
|
||||
tagParts?: undefined;
|
||||
}
|
||||
| {
|
||||
tagParts: SemverTagParts;
|
||||
error?: undefined;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "getShortCommitHash" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
function getShortCommitHash(hash: string): string;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "getTagParts" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
function getTagParts({ project, tag }: { project: Project; tag: string }):
|
||||
| {
|
||||
error: AlertError;
|
||||
tagParts?: undefined;
|
||||
}
|
||||
| {
|
||||
tagParts: CalverTagParts;
|
||||
error?: undefined;
|
||||
}
|
||||
| {
|
||||
tagParts: SemverTagParts;
|
||||
error?: undefined;
|
||||
};
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "GitReleaseApi" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "gitReleaseManagerApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
@@ -32,5 +168,382 @@ export const gitReleaseManagerPlugin: BackstagePlugin<
|
||||
{}
|
||||
>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "InfoCardPlus" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const InfoCardPlus: ({
|
||||
children,
|
||||
}: {
|
||||
children?: React_2.ReactNode;
|
||||
}) => JSX.Element;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "internals" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const internals: {
|
||||
components: typeof components;
|
||||
constants: typeof constants;
|
||||
helpers: typeof helpers;
|
||||
testHelpers: typeof testHelpers;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "isCalverTagParts" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
function isCalverTagParts(
|
||||
project: Project,
|
||||
_tagParts: unknown,
|
||||
): _tagParts is CalverTagParts;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "isProjectValid" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
function isProjectValid(project: any): project is Project;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "LinearProgressWithLabel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
function LinearProgressWithLabel(props: {
|
||||
progress: number;
|
||||
responseSteps: ResponseStep[];
|
||||
}): JSX.Element;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "mockApiClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
const mockApiClient: GitReleaseApi;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "mockBumpedTag" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const mockBumpedTag = 'rc-2020.01.01_1337';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "mockCalverProject" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const mockCalverProject: Project;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "mockDefaultBranch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const mockDefaultBranch = 'mock_defaultBranch';
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "getReleaseCandidateGitInfo" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "mockNextGitInfoCalver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const mockNextGitInfoCalver: ReturnType<typeof getReleaseCandidateGitInfo>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "mockNextGitInfoSemver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const mockNextGitInfoSemver: ReturnType<typeof getReleaseCandidateGitInfo>;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "mockReleaseBranch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const mockReleaseBranch: {
|
||||
name: string;
|
||||
links: {
|
||||
html: string;
|
||||
};
|
||||
commit: {
|
||||
sha: string;
|
||||
commit: {
|
||||
tree: {
|
||||
sha: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "mockReleaseCandidateCalver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const mockReleaseCandidateCalver: {
|
||||
targetCommitish: string;
|
||||
tagName: string;
|
||||
prerelease: boolean;
|
||||
id: number;
|
||||
htmlUrl: string;
|
||||
body?: string | null | undefined;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "mockReleaseCandidateSemver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const mockReleaseCandidateSemver: {
|
||||
targetCommitish: string;
|
||||
tagName: string;
|
||||
prerelease: boolean;
|
||||
id: number;
|
||||
htmlUrl: string;
|
||||
body?: string | null | undefined;
|
||||
};
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "ReleaseStats" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "mockReleaseStats" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const mockReleaseStats: ReleaseStats;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "mockReleaseVersionCalver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const mockReleaseVersionCalver: {
|
||||
targetCommitish: string;
|
||||
tagName: string;
|
||||
prerelease: boolean;
|
||||
id: number;
|
||||
htmlUrl: string;
|
||||
body?: string | null | undefined;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "mockReleaseVersionSemver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const mockReleaseVersionSemver: {
|
||||
targetCommitish: string;
|
||||
tagName: string;
|
||||
prerelease: boolean;
|
||||
id: number;
|
||||
htmlUrl: string;
|
||||
body?: string | null | undefined;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "mockSearchCalver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const mockSearchCalver: string;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "mockSearchSemver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const mockSearchSemver: string;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "mockSelectedPatchCommit" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const mockSelectedPatchCommit: {
|
||||
htmlUrl: string;
|
||||
sha: string;
|
||||
author: {
|
||||
htmlUrl?: string | undefined;
|
||||
login?: string | undefined;
|
||||
};
|
||||
commit: {
|
||||
message: string;
|
||||
};
|
||||
firstParentSha?: string | undefined;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "mockSemverProject" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const mockSemverProject: Project;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "mockTagParts" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const mockTagParts: CalverTagParts;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "mockUser" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const mockUser: {
|
||||
username: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "NoLatestRelease" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const NoLatestRelease: () => JSX.Element;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "DialogProps" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "ResponseStepDialog" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const ResponseStepDialog: ({
|
||||
progress,
|
||||
responseSteps,
|
||||
title,
|
||||
}: DialogProps) => JSX.Element;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "ResponseStepListProps" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "ResponseStepList" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const ResponseStepList: ({
|
||||
responseSteps,
|
||||
animationDelay,
|
||||
loading,
|
||||
denseList,
|
||||
children,
|
||||
}: PropsWithChildren<ResponseStepListProps>) => JSX.Element;
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "ResponseStepListItemProps" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-missing-release-tag) "ResponseStepListItem" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const ResponseStepListItem: ({
|
||||
responseStep,
|
||||
animationDelay,
|
||||
}: ResponseStepListItemProps) => JSX.Element;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "SEMVER_PARTS" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const SEMVER_PARTS: {
|
||||
major: 'major';
|
||||
minor: 'minor';
|
||||
patch: 'patch';
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "semverRegexp" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const semverRegexp: RegExp;
|
||||
|
||||
declare namespace stats {
|
||||
export { mockReleaseStats };
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "TAG_OBJECT_MESSAGE" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const TAG_OBJECT_MESSAGE =
|
||||
'Tag generated by your friendly neighborhood Backstage Release Manager';
|
||||
|
||||
// Warning: (ae-missing-release-tag) "TEST_IDS" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const TEST_IDS: {
|
||||
info: {
|
||||
info: string;
|
||||
infoFeaturePlus: string;
|
||||
};
|
||||
createRc: {
|
||||
cta: string;
|
||||
semverSelect: string;
|
||||
};
|
||||
promoteRc: {
|
||||
mockedPromoteRcBody: string;
|
||||
notRcWarning: string;
|
||||
promoteRc: string;
|
||||
cta: string;
|
||||
};
|
||||
patch: {
|
||||
error: string;
|
||||
loading: string;
|
||||
notPrerelease: string;
|
||||
body: string;
|
||||
};
|
||||
form: {
|
||||
owner: {
|
||||
loading: string;
|
||||
select: string;
|
||||
error: string;
|
||||
empty: string;
|
||||
};
|
||||
repo: {
|
||||
loading: string;
|
||||
select: string;
|
||||
error: string;
|
||||
empty: string;
|
||||
};
|
||||
versioningStrategy: {
|
||||
radioGroup: string;
|
||||
};
|
||||
};
|
||||
components: {
|
||||
divider: string;
|
||||
noLatestRelease: string;
|
||||
circularProgress: string;
|
||||
responseStepListDialogContent: string;
|
||||
responseStepListItem: string;
|
||||
responseStepListItemIconSuccess: string;
|
||||
responseStepListItemIconFailure: string;
|
||||
responseStepListItemIconLink: string;
|
||||
responseStepListItemIconDefault: string;
|
||||
differ: {
|
||||
current: string;
|
||||
next: string;
|
||||
icons: {
|
||||
tag: string;
|
||||
branch: string;
|
||||
github: string;
|
||||
slack: string;
|
||||
versioning: string;
|
||||
};
|
||||
};
|
||||
linearProgressWithLabel: string;
|
||||
};
|
||||
};
|
||||
|
||||
declare namespace testHelpers_2 {
|
||||
export {
|
||||
createMockTag,
|
||||
createMockCommit,
|
||||
mockUser,
|
||||
mockSemverProject,
|
||||
mockCalverProject,
|
||||
mockSearchCalver,
|
||||
mockSearchSemver,
|
||||
mockDefaultBranch,
|
||||
mockNextGitInfoSemver,
|
||||
mockNextGitInfoCalver,
|
||||
mockTagParts,
|
||||
mockBumpedTag,
|
||||
mockReleaseCandidateCalver,
|
||||
mockReleaseVersionCalver,
|
||||
mockReleaseCandidateSemver,
|
||||
mockReleaseVersionSemver,
|
||||
mockReleaseBranch,
|
||||
mockSelectedPatchCommit,
|
||||
mockApiClient,
|
||||
};
|
||||
}
|
||||
|
||||
declare namespace testIds {
|
||||
export { TEST_IDS };
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "validateTagName" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const validateTagName: ({
|
||||
project,
|
||||
tagName,
|
||||
}: {
|
||||
project: Project;
|
||||
tagName?: string | undefined;
|
||||
}) =>
|
||||
| {
|
||||
tagNameError: null;
|
||||
}
|
||||
| {
|
||||
tagNameError: AlertError | undefined;
|
||||
};
|
||||
|
||||
// Warning: (ae-missing-release-tag) "VERSIONING_STRATEGIES" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
const VERSIONING_STRATEGIES: {
|
||||
semver: 'semver';
|
||||
calver: 'calver';
|
||||
};
|
||||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/components/ResponseStepDialog/LinearProgressWithLabel.d.ts:5:5 - (ae-forgotten-export) The symbol "ResponseStep" needs to be exported by the entry point index.d.ts
|
||||
// src/helpers/getBumpedTag.d.ts:14:5 - (ae-forgotten-export) The symbol "Project" needs to be exported by the entry point index.d.ts
|
||||
// src/helpers/getBumpedTag.d.ts:19:5 - (ae-forgotten-export) The symbol "CalverTagParts" needs to be exported by the entry point index.d.ts
|
||||
// src/helpers/getBumpedTag.d.ts:31:5 - (ae-forgotten-export) The symbol "AlertError" needs to be exported by the entry point index.d.ts
|
||||
// src/index.d.ts:4:5 - (ae-forgotten-export) The symbol "components" needs to be exported by the entry point index.d.ts
|
||||
// src/index.d.ts:5:5 - (ae-forgotten-export) The symbol "constants" needs to be exported by the entry point index.d.ts
|
||||
// src/index.d.ts:6:5 - (ae-forgotten-export) The symbol "helpers" needs to be exported by the entry point index.d.ts
|
||||
// src/index.d.ts:7:5 - (ae-forgotten-export) The symbol "testHelpers" needs to be exported by the entry point index.d.ts
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { Differ } from './Differ';
|
||||
export { Divider } from './Divider';
|
||||
export { InfoCardPlus } from './InfoCardPlus';
|
||||
export { LinearProgressWithLabel } from './ResponseStepDialog/LinearProgressWithLabel';
|
||||
export { NoLatestRelease } from './NoLatestRelease';
|
||||
export { ResponseStepDialog } from './ResponseStepDialog/ResponseStepDialog';
|
||||
export { ResponseStepList } from './ResponseStepDialog/ResponseStepList';
|
||||
export { ResponseStepListItem } from './ResponseStepDialog/ResponseStepListItem';
|
||||
@@ -21,6 +21,14 @@ import { Project } from '../contexts/ProjectContext';
|
||||
import { SEMVER_PARTS } from '../constants/constants';
|
||||
import { SemverTagParts } from './tagParts/getSemverTagParts';
|
||||
|
||||
/**
|
||||
* Calculates the next version for the project
|
||||
*
|
||||
* For calendar versioning this means a bump in patch
|
||||
*
|
||||
* For semantic versioning this means either a minor or a patch bump
|
||||
* depending on the value of `bumpLevel`
|
||||
*/
|
||||
export function getBumpedTag({
|
||||
project,
|
||||
tag,
|
||||
@@ -74,6 +82,10 @@ function getBumpedSemverTag(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the next semantic version, taking into account
|
||||
* whether or not it's a minor or patch
|
||||
*/
|
||||
export function getBumpedSemverTagParts(
|
||||
tagParts: SemverTagParts,
|
||||
semverBumpLevel: keyof typeof SEMVER_PARTS,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { calverRegexp, getCalverTagParts } from './tagParts/getCalverTagParts';
|
||||
export { getBumpedSemverTagParts, getBumpedTag } from './getBumpedTag';
|
||||
export { getSemverTagParts, semverRegexp } from './tagParts/getSemverTagParts';
|
||||
export { getShortCommitHash } from './getShortCommitHash';
|
||||
export { getTagParts } from './tagParts/getTagParts';
|
||||
export { isCalverTagParts } from './isCalverTagParts';
|
||||
export { isProjectValid } from './isProjectValid';
|
||||
export { validateTagName } from './tagParts/validateTagName';
|
||||
@@ -18,6 +18,10 @@ import { getCalverTagParts } from './getCalverTagParts';
|
||||
import { getSemverTagParts } from './getSemverTagParts';
|
||||
import { Project } from '../../contexts/ProjectContext';
|
||||
|
||||
/**
|
||||
* Tag parts are the individual parts of a version, e.g. <major>.<minor>.<patch>
|
||||
* are the parts of a semantic version
|
||||
*/
|
||||
export function getTagParts({
|
||||
project,
|
||||
tag,
|
||||
|
||||
@@ -19,3 +19,12 @@ export {
|
||||
GitReleaseManagerPage,
|
||||
gitReleaseManagerApiRef,
|
||||
} from './plugin';
|
||||
|
||||
import { components, constants, helpers, testHelpers } from './plugin';
|
||||
|
||||
export const internals = {
|
||||
components,
|
||||
constants,
|
||||
helpers,
|
||||
testHelpers,
|
||||
};
|
||||
|
||||
@@ -21,6 +21,10 @@ describe('git-release-manager', () => {
|
||||
expect(Object.keys(plugin)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"gitReleaseManagerApiRef",
|
||||
"constants",
|
||||
"helpers",
|
||||
"components",
|
||||
"testHelpers",
|
||||
"gitReleaseManagerPlugin",
|
||||
"GitReleaseManagerPage",
|
||||
]
|
||||
|
||||
@@ -14,10 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { gitReleaseManagerApiRef } from './api/serviceApiRef';
|
||||
|
||||
import { GitReleaseClient } from './api/GitReleaseClient';
|
||||
import { rootRouteRef } from './routes';
|
||||
import {
|
||||
configApiRef,
|
||||
createPlugin,
|
||||
@@ -26,7 +22,15 @@ import {
|
||||
createRoutableExtension,
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
export { gitReleaseManagerApiRef };
|
||||
import { GitReleaseClient } from './api/GitReleaseClient';
|
||||
import { gitReleaseManagerApiRef } from './api/serviceApiRef';
|
||||
import { rootRouteRef } from './routes';
|
||||
import * as constants from './constants/constants';
|
||||
import * as helpers from './helpers';
|
||||
import * as components from './components';
|
||||
import * as testHelpers from './test-helpers';
|
||||
|
||||
export { gitReleaseManagerApiRef, constants, helpers, components, testHelpers };
|
||||
|
||||
export const gitReleaseManagerPlugin = createPlugin({
|
||||
id: 'git-release-manager',
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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 * as stats from './stats';
|
||||
import * as testHelpers from './test-helpers';
|
||||
import * as testIds from './test-ids';
|
||||
|
||||
export { stats, testHelpers, testIds };
|
||||
@@ -115,6 +115,14 @@ describe('ConfigClusterLocator', () => {
|
||||
authProvider: 'aws',
|
||||
skipTLSVerify: true,
|
||||
},
|
||||
{
|
||||
assumeRole: 'SomeRole',
|
||||
name: 'cluster2',
|
||||
externalId: 'SomeExternalId',
|
||||
url: 'http://localhost:8081',
|
||||
authProvider: 'aws',
|
||||
skipTLSVerify: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -127,6 +135,7 @@ describe('ConfigClusterLocator', () => {
|
||||
assumeRole: undefined,
|
||||
name: 'cluster1',
|
||||
serviceAccountToken: 'token',
|
||||
externalId: undefined,
|
||||
url: 'http://localhost:8080',
|
||||
authProvider: 'aws',
|
||||
skipTLSVerify: false,
|
||||
@@ -134,11 +143,21 @@ describe('ConfigClusterLocator', () => {
|
||||
{
|
||||
assumeRole: 'SomeRole',
|
||||
name: 'cluster2',
|
||||
externalId: undefined,
|
||||
serviceAccountToken: undefined,
|
||||
url: 'http://localhost:8081',
|
||||
authProvider: 'aws',
|
||||
skipTLSVerify: true,
|
||||
},
|
||||
{
|
||||
assumeRole: 'SomeRole',
|
||||
name: 'cluster2',
|
||||
externalId: 'SomeExternalId',
|
||||
url: 'http://localhost:8081',
|
||||
serviceAccountToken: undefined,
|
||||
authProvider: 'aws',
|
||||
skipTLSVerify: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,7 +44,9 @@ export class ConfigClusterLocator implements KubernetesClustersSupplier {
|
||||
}
|
||||
case 'aws': {
|
||||
const assumeRole = c.getOptionalString('assumeRole');
|
||||
return { assumeRole, ...clusterDetails };
|
||||
const externalId = c.getOptionalString('externalId');
|
||||
|
||||
return { assumeRole, externalId, ...clusterDetails };
|
||||
}
|
||||
case 'serviceAccount': {
|
||||
return clusterDetails;
|
||||
|
||||
@@ -65,6 +65,17 @@ describe('PgSearchEngine', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should sanitize query term', async () => {
|
||||
const actualTranslatedQuery = searchEngine.translator({
|
||||
term: 'H&e|l!l*o W\0o(r)l:d',
|
||||
pageCursor: '',
|
||||
}) as PgSearchQuery;
|
||||
|
||||
expect(actualTranslatedQuery).toMatchObject({
|
||||
pgTerm: '("Hello" | "Hello":*)&("World" | "World":*)',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return translated query with filters', async () => {
|
||||
const actualTranslatedQuery = searchEngine.translator({
|
||||
term: 'testTerm',
|
||||
|
||||
@@ -50,7 +50,7 @@ export class PgSearchEngine implements SearchEngine {
|
||||
return {
|
||||
pgTerm: query.term
|
||||
.split(/\s/)
|
||||
.map(p => p.trim())
|
||||
.map(p => p.replace(/[\0()|&:*!]/g, '').trim())
|
||||
.filter(p => p !== '')
|
||||
.map(p => `(${JSON.stringify(p)} | ${JSON.stringify(p)}:*)`)
|
||||
.join('&'),
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ChangeEvent, useState } from 'react';
|
||||
import React, { ChangeEvent, useEffect, useState } from 'react';
|
||||
import { useDebounce } from 'react-use';
|
||||
import { InputBase, InputAdornment, IconButton } from '@material-ui/core';
|
||||
import SearchIcon from '@material-ui/icons/Search';
|
||||
@@ -31,6 +31,10 @@ export const SearchBar = ({ className, debounceTime = 0 }: Props) => {
|
||||
const { term, setTerm } = useSearch();
|
||||
const [value, setValue] = useState<string>(term);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(prevValue => (prevValue !== term ? term : prevValue));
|
||||
}, [term]);
|
||||
|
||||
useDebounce(() => setTerm(value), debounceTime, [value]);
|
||||
|
||||
const handleQuery = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
|
||||
@@ -54,7 +54,7 @@ export const SearchContextProvider = ({
|
||||
term: '',
|
||||
pageCursor: '',
|
||||
filters: {},
|
||||
types: ['*'],
|
||||
types: [],
|
||||
},
|
||||
children,
|
||||
}: PropsWithChildren<{ initialState?: SettableSearchContext }>) => {
|
||||
|
||||
@@ -18,7 +18,7 @@ import React from 'react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { useLocation, useOutlet } from 'react-router';
|
||||
|
||||
import { useSearch, SearchContextProvider } from '../SearchContext';
|
||||
import { useSearch } from '../SearchContext';
|
||||
import { SearchPage } from './';
|
||||
|
||||
jest.mock('react-router', () => ({
|
||||
@@ -29,6 +29,11 @@ jest.mock('react-router', () => ({
|
||||
useOutlet: jest.fn().mockReturnValue('Route Children'),
|
||||
}));
|
||||
|
||||
const setTermMock = jest.fn();
|
||||
const setTypesMock = jest.fn();
|
||||
const setFiltersMock = jest.fn();
|
||||
const setPageCursorMock = jest.fn();
|
||||
|
||||
jest.mock('../SearchContext', () => ({
|
||||
...jest.requireActual('../SearchContext'),
|
||||
SearchContextProvider: jest
|
||||
@@ -36,9 +41,13 @@ jest.mock('../SearchContext', () => ({
|
||||
.mockImplementation(({ children }) => children),
|
||||
useSearch: jest.fn().mockReturnValue({
|
||||
term: '',
|
||||
setTerm: (term: any) => setTermMock(term),
|
||||
types: [],
|
||||
setTypes: (types: any) => setTypesMock(types),
|
||||
filters: {},
|
||||
setFilters: (filters: any) => setFiltersMock(filters),
|
||||
pageCursor: '',
|
||||
setPageCursor: (pageCursor: any) => setPageCursorMock(pageCursor),
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -58,7 +67,7 @@ describe('SearchPage', () => {
|
||||
window.history.replaceState = origReplaceState;
|
||||
});
|
||||
|
||||
it('uses initial term state from location', async () => {
|
||||
it('sets term state from location', async () => {
|
||||
// Given this initial location.search value...
|
||||
const expectedFilterField = 'anyKey';
|
||||
const expectedFilterValue = 'anyValue';
|
||||
@@ -75,13 +84,11 @@ describe('SearchPage', () => {
|
||||
// When we render the page...
|
||||
await renderInTestApp(<SearchPage />);
|
||||
|
||||
// Then search context should be initialized with these values...
|
||||
const calls = (SearchContextProvider as jest.Mock).mock.calls[0];
|
||||
const actualInitialState = calls[0].initialState;
|
||||
expect(actualInitialState.term).toEqual(expectedTerm);
|
||||
expect(actualInitialState.types).toEqual(expectedTypes);
|
||||
expect(actualInitialState.pageCursor).toEqual(expectedPageCursor);
|
||||
expect(actualInitialState.filters).toStrictEqual(expectedFilters);
|
||||
// Then search context should be set with these values...
|
||||
expect(setTermMock).toHaveBeenCalledWith(expectedTerm);
|
||||
expect(setTypesMock).toHaveBeenCalledWith(expectedTypes);
|
||||
expect(setPageCursorMock).toHaveBeenCalledWith(expectedPageCursor);
|
||||
expect(setFiltersMock).toHaveBeenCalledWith(expectedFilters);
|
||||
});
|
||||
|
||||
it('renders provided router element', async () => {
|
||||
@@ -103,6 +110,10 @@ describe('SearchPage', () => {
|
||||
types: ['software-catalog'],
|
||||
pageCursor: 'page2-or-something',
|
||||
filters: { anyKey: 'anyValue' },
|
||||
setTerm: setTermMock,
|
||||
setTypes: setTypesMock,
|
||||
setFilters: setFiltersMock,
|
||||
setPageCursor: setPageCursorMock,
|
||||
});
|
||||
const expectedLocation = encodeURI(
|
||||
'?query=bieber&types[]=software-catalog&pageCursor=page2-or-something&filters[anyKey]=anyValue',
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { usePrevious } from 'react-use';
|
||||
import qs from 'qs';
|
||||
import { useLocation, useOutlet } from 'react-router';
|
||||
import { SearchContextProvider, useSearch } from '../SearchContext';
|
||||
@@ -22,46 +23,72 @@ import { JsonObject } from '@backstage/config';
|
||||
import { LegacySearchPage } from '../LegacySearchPage';
|
||||
|
||||
export const UrlUpdater = () => {
|
||||
const { term, types, pageCursor, filters } = useSearch();
|
||||
const location = useLocation();
|
||||
const {
|
||||
term,
|
||||
setTerm,
|
||||
types,
|
||||
setTypes,
|
||||
pageCursor,
|
||||
setPageCursor,
|
||||
filters,
|
||||
setFilters,
|
||||
} = useSearch();
|
||||
|
||||
const newParams = qs.stringify(
|
||||
{
|
||||
query: term,
|
||||
types,
|
||||
pageCursor,
|
||||
filters,
|
||||
},
|
||||
{ arrayFormat: 'brackets' },
|
||||
);
|
||||
const newUrl = `${window.location.pathname}?${newParams}`;
|
||||
const prevQueryParams = usePrevious(location.search);
|
||||
useEffect(() => {
|
||||
// Only respond to changes to url query params
|
||||
if (location.search === prevQueryParams) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We directly manipulate window history here in order to not re-render
|
||||
// infinitely (state => location => state => etc). The intention of this
|
||||
// code is just to ensure the right query/filters are loaded when a user
|
||||
// clicks the "back" button after clicking a result.
|
||||
window.history.replaceState(null, document.title, newUrl);
|
||||
const query =
|
||||
qs.parse(location.search.substring(1), { arrayLimit: 0 }) || {};
|
||||
|
||||
if (query.filters) {
|
||||
setFilters(query.filters as JsonObject);
|
||||
}
|
||||
|
||||
if (query.query) {
|
||||
setTerm(query.query as string);
|
||||
}
|
||||
|
||||
if (query.pageCursor) {
|
||||
setPageCursor(query.pageCursor as string);
|
||||
}
|
||||
|
||||
if (query.types) {
|
||||
setTypes(query.types as string[]);
|
||||
}
|
||||
}, [prevQueryParams, location, setTerm, setTypes, setPageCursor, setFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
const newParams = qs.stringify(
|
||||
{
|
||||
query: term,
|
||||
types,
|
||||
pageCursor,
|
||||
filters,
|
||||
},
|
||||
{ arrayFormat: 'brackets' },
|
||||
);
|
||||
const newUrl = `${window.location.pathname}?${newParams}`;
|
||||
|
||||
// We directly manipulate window history here in order to not re-render
|
||||
// infinitely (state => location => state => etc). The intention of this
|
||||
// code is just to ensure the right query/filters are loaded when a user
|
||||
// clicks the "back" button after clicking a result.
|
||||
window.history.replaceState(null, document.title, newUrl);
|
||||
}, [term, types, pageCursor, filters]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const SearchPage = () => {
|
||||
const location = useLocation();
|
||||
const outlet = useOutlet();
|
||||
const query = qs.parse(location.search.substring(1), { arrayLimit: 0 }) || {};
|
||||
const filters = (query.filters as JsonObject) || {};
|
||||
const queryString = (query.query as string) || '';
|
||||
const pageCursor = (query.pageCursor as string) || '';
|
||||
const types = (query.types as string[]) || [];
|
||||
|
||||
const initialState = {
|
||||
term: queryString || '',
|
||||
types,
|
||||
pageCursor,
|
||||
filters,
|
||||
};
|
||||
|
||||
return (
|
||||
<SearchContextProvider initialState={initialState}>
|
||||
<SearchContextProvider>
|
||||
<UrlUpdater />
|
||||
{outlet || <LegacySearchPage />}
|
||||
</SearchContextProvider>
|
||||
|
||||
Reference in New Issue
Block a user