Merge branch 'master' into lintMod
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-cost-insights': patch
|
||||
---
|
||||
|
||||
Fixed date calculations incorrectly converting to UTC in some cases. This should be a transparent change.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/integration': patch
|
||||
---
|
||||
|
||||
Fix gitlab handling of paths with spaces
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': minor
|
||||
---
|
||||
|
||||
Make use of the `resolveUrl` facility of the `integration` package.
|
||||
|
||||
Also rename the `LocationRefProcessor` to `LocationEntityProcessor`, to match the file name. This constitutes an interface change since the class is exported, but it is unlikely to be consumed outside of the package since it sits comfortably with the other default processors inside the catalog builder.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'example-app': patch
|
||||
'@backstage/plugin-rollbar': patch
|
||||
---
|
||||
|
||||
Migrated to new composability API, exporting the plugin instance as `rollbarPlugin`, the entity page content as `EntityRollbarContent`, and entity conditional as `isRollbarAvailable`. Updated the `EntityPage` for the `example-app` to include a composite `ErrorsSwitcher` component that works with both `Sentry` & `Rollbar`. Also removed the unused and undocumented `RollbarHome` related components.
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
'@backstage/integration': patch
|
||||
---
|
||||
|
||||
Add a `resolveUrl` method to integrations, that works like the two-argument URL
|
||||
constructor. The reason for using this is that Azure have their paths in a
|
||||
query parameter, rather than the pathname of the URL.
|
||||
|
||||
The implementation is optional (when not present, the URL constructor is used),
|
||||
so this does not imply a breaking change.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/integration': major
|
||||
---
|
||||
|
||||
#4322 Bitbucket own hosted v5.11.1 branchUrl fix and enabled error tracing… #4347
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/catalog-model': patch
|
||||
---
|
||||
|
||||
Implement matchEntityWithRef for client side filtering of entities by ref matching
|
||||
+1
-1
@@ -41,7 +41,7 @@
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.11.0",
|
||||
"@changesets/cli": "^2.14.0",
|
||||
"@octokit/openapi-types": "^2.2.0",
|
||||
"@spotify/eslint-config-oss": "^1.0.1",
|
||||
"@spotify/prettier-config": "^9.0.0",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ApiEntity,
|
||||
Entity,
|
||||
@@ -64,6 +65,10 @@ import {
|
||||
isPluginApplicableToEntity as isPagerDutyAvailable,
|
||||
PagerDutyCard,
|
||||
} from '@backstage/plugin-pagerduty';
|
||||
import {
|
||||
isRollbarAvailable,
|
||||
Router as RollbarRouter,
|
||||
} from '@backstage/plugin-rollbar';
|
||||
import { Router as SentryRouter } from '@backstage/plugin-sentry';
|
||||
import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs';
|
||||
import { Button, Grid } from '@material-ui/core';
|
||||
@@ -153,6 +158,15 @@ const RecentCICDRunsSwitcher = ({ entity }: { entity: Entity }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const ErrorsSwitcher = ({ entity }: { entity: Entity }) => {
|
||||
switch (true) {
|
||||
case isRollbarAvailable(entity):
|
||||
return <RollbarRouter entity={entity} />;
|
||||
default:
|
||||
return <SentryRouter entity={entity} />;
|
||||
}
|
||||
};
|
||||
|
||||
const ComponentOverviewContent = ({ entity }: { entity: Entity }) => (
|
||||
<Grid container spacing={3} alignItems="stretch">
|
||||
<Grid item md={6}>
|
||||
@@ -212,9 +226,9 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
element={<CICDSwitcher entity={entity} />}
|
||||
/>
|
||||
<EntityPageLayout.Content
|
||||
path="/sentry"
|
||||
title="Sentry"
|
||||
element={<SentryRouter entity={entity} />}
|
||||
path="/errors/*"
|
||||
title="Errors"
|
||||
element={<ErrorsSwitcher entity={entity} />}
|
||||
/>
|
||||
<EntityPageLayout.Content
|
||||
path="/api/*"
|
||||
@@ -267,9 +281,9 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
element={<LighthouseRouter entity={entity} />}
|
||||
/>
|
||||
<EntityPageLayout.Content
|
||||
path="/sentry"
|
||||
title="Sentry"
|
||||
element={<SentryRouter entity={entity} />}
|
||||
path="/errors/*"
|
||||
title="Errors"
|
||||
element={<ErrorsSwitcher entity={entity} />}
|
||||
/>
|
||||
<EntityPageLayout.Content
|
||||
path="/docs/*"
|
||||
|
||||
@@ -76,6 +76,8 @@ function withRetries(count: number, fn: () => Promise<void>) {
|
||||
}
|
||||
|
||||
describe('UrlReaders', () => {
|
||||
jest.setTimeout(30_000);
|
||||
|
||||
it(
|
||||
'should read data from azure',
|
||||
withRetries(3, async () => {
|
||||
|
||||
@@ -27,6 +27,7 @@ export type {
|
||||
} from './Entity';
|
||||
export * from './policies';
|
||||
export {
|
||||
compareEntityToRef,
|
||||
getEntityName,
|
||||
parseEntityName,
|
||||
parseEntityRef,
|
||||
|
||||
@@ -16,7 +16,12 @@
|
||||
|
||||
import { ENTITY_DEFAULT_NAMESPACE } from './constants';
|
||||
import { Entity } from './Entity';
|
||||
import { parseEntityName, parseEntityRef, serializeEntityRef } from './ref';
|
||||
import {
|
||||
compareEntityToRef,
|
||||
parseEntityName,
|
||||
parseEntityRef,
|
||||
serializeEntityRef,
|
||||
} from './ref';
|
||||
|
||||
describe('ref', () => {
|
||||
describe('parseEntityName', () => {
|
||||
@@ -381,4 +386,320 @@ describe('ref', () => {
|
||||
).toEqual({ kind: 'a', namespace: 'b', name: 'c/x' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareEntityToRef', () => {
|
||||
const entityWithNamespace: Entity = {
|
||||
apiVersion: 'a',
|
||||
kind: 'K',
|
||||
metadata: {
|
||||
name: 'n',
|
||||
namespace: 'ns',
|
||||
},
|
||||
};
|
||||
const entityWithoutNamespace: Entity = {
|
||||
apiVersion: 'a',
|
||||
kind: 'K',
|
||||
metadata: {
|
||||
name: 'n',
|
||||
},
|
||||
};
|
||||
|
||||
it('handles matching string refs', () => {
|
||||
expect(compareEntityToRef(entityWithNamespace, 'K:ns/n')).toBe(true);
|
||||
expect(compareEntityToRef(entityWithNamespace, 'k:nS/N')).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(entityWithNamespace, 'K:n', {
|
||||
defaultNamespace: 'ns',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(entityWithNamespace, 'K:n', {
|
||||
defaultNamespace: 'Ns',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(entityWithNamespace, 'ns/n', { defaultKind: 'K' }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(entityWithNamespace, 'n', {
|
||||
defaultKind: 'K',
|
||||
defaultNamespace: 'ns',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(entityWithNamespace, 'N', {
|
||||
defaultKind: 'k',
|
||||
defaultNamespace: 'nS',
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(compareEntityToRef(entityWithoutNamespace, 'K:default/n')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(compareEntityToRef(entityWithoutNamespace, 'K:deFault/n')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
compareEntityToRef(entityWithoutNamespace, 'K:n', {
|
||||
defaultNamespace: 'default',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(entityWithoutNamespace, 'K:n', {
|
||||
defaultNamespace: 'deFault',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(compareEntityToRef(entityWithoutNamespace, 'K:default/n')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(compareEntityToRef(entityWithoutNamespace, 'K:n')).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(entityWithoutNamespace, 'default/n', {
|
||||
defaultKind: 'K',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(entityWithoutNamespace, 'n', {
|
||||
defaultKind: 'K',
|
||||
defaultNamespace: 'default',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(entityWithoutNamespace, 'n', {
|
||||
defaultKind: 'K',
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('handles mismatching string refs', () => {
|
||||
expect(compareEntityToRef(entityWithNamespace, 'X:ns/n')).toBe(false);
|
||||
expect(
|
||||
compareEntityToRef(entityWithoutNamespace, 'ns/n', {
|
||||
defaultKind: 'X',
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(compareEntityToRef(entityWithNamespace, 'K:xx/n')).toBe(false);
|
||||
expect(
|
||||
compareEntityToRef(entityWithoutNamespace, 'K:n', {
|
||||
defaultNamespace: 'xx',
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(compareEntityToRef(entityWithNamespace, 'K:ns/x')).toBe(false);
|
||||
expect(
|
||||
compareEntityToRef(entityWithoutNamespace, 'x', {
|
||||
defaultKind: 'K',
|
||||
defaultNamespace: 'ns',
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('handles matching compound refs', () => {
|
||||
expect(
|
||||
compareEntityToRef(entityWithNamespace, {
|
||||
kind: 'K',
|
||||
namespace: 'ns',
|
||||
name: 'n',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(entityWithNamespace, {
|
||||
kind: 'k',
|
||||
namespace: 'Ns',
|
||||
name: 'N',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(
|
||||
entityWithNamespace,
|
||||
{ kind: 'K', name: 'n' },
|
||||
{
|
||||
defaultNamespace: 'ns',
|
||||
},
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(
|
||||
entityWithNamespace,
|
||||
{ namespace: 'ns', name: 'n' },
|
||||
{ defaultKind: 'K' },
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(entityWithNamespace, 'n', {
|
||||
defaultKind: 'K',
|
||||
defaultNamespace: 'ns',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(entityWithNamespace, 'N', {
|
||||
defaultKind: 'k',
|
||||
defaultNamespace: 'nS',
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
compareEntityToRef(entityWithoutNamespace, {
|
||||
kind: 'K',
|
||||
namespace: 'default',
|
||||
name: 'n',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(entityWithoutNamespace, {
|
||||
kind: 'k',
|
||||
namespace: 'deFault',
|
||||
name: 'N',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(
|
||||
entityWithoutNamespace,
|
||||
{ kind: 'K', name: 'n' },
|
||||
{
|
||||
defaultNamespace: 'default',
|
||||
},
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(entityWithoutNamespace, { kind: 'K', name: 'n' }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(
|
||||
entityWithoutNamespace,
|
||||
{ namespace: 'default', name: 'n' },
|
||||
{
|
||||
defaultKind: 'K',
|
||||
},
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(
|
||||
entityWithoutNamespace,
|
||||
{ name: 'n' },
|
||||
{
|
||||
defaultKind: 'K',
|
||||
defaultNamespace: 'default',
|
||||
},
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(
|
||||
entityWithoutNamespace,
|
||||
{ name: 'N' },
|
||||
{
|
||||
defaultKind: 'k',
|
||||
defaultNamespace: 'defAult',
|
||||
},
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
compareEntityToRef(
|
||||
entityWithoutNamespace,
|
||||
{ name: 'n' },
|
||||
{
|
||||
defaultKind: 'K',
|
||||
},
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('handles mismatching compound refs', () => {
|
||||
expect(
|
||||
compareEntityToRef(entityWithNamespace, {
|
||||
kind: 'X',
|
||||
namespace: 'ns',
|
||||
name: 'n',
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
compareEntityToRef(
|
||||
entityWithNamespace,
|
||||
{
|
||||
namespace: 'ns',
|
||||
name: 'n',
|
||||
},
|
||||
{ defaultKind: 'X' },
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
compareEntityToRef(entityWithoutNamespace, {
|
||||
kind: 'X',
|
||||
namespace: 'default',
|
||||
name: 'n',
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
compareEntityToRef(
|
||||
entityWithoutNamespace,
|
||||
{
|
||||
namespace: 'default',
|
||||
name: 'n',
|
||||
},
|
||||
{ defaultKind: 'X' },
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
compareEntityToRef(entityWithNamespace, {
|
||||
kind: 'K',
|
||||
namespace: 'xx',
|
||||
name: 'n',
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
compareEntityToRef(
|
||||
entityWithNamespace,
|
||||
{
|
||||
kind: 'K',
|
||||
name: 'n',
|
||||
},
|
||||
{ defaultNamespace: 'xx' },
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
compareEntityToRef(entityWithoutNamespace, {
|
||||
kind: 'K',
|
||||
namespace: 'xx',
|
||||
name: 'n',
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
compareEntityToRef(
|
||||
entityWithoutNamespace,
|
||||
{
|
||||
kind: 'K',
|
||||
name: 'n',
|
||||
},
|
||||
{ defaultNamespace: 'xx' },
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
compareEntityToRef(entityWithNamespace, {
|
||||
kind: 'K',
|
||||
namespace: 'ns',
|
||||
name: 'x',
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
compareEntityToRef(entityWithoutNamespace, {
|
||||
kind: 'K',
|
||||
namespace: 'default',
|
||||
name: 'x',
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
compareEntityToRef(
|
||||
entityWithoutNamespace,
|
||||
{
|
||||
kind: 'K',
|
||||
name: 'x',
|
||||
},
|
||||
{ defaultNamespace: 'default' },
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,27 @@ import { EntityName, EntityRef } from '../types';
|
||||
import { ENTITY_DEFAULT_NAMESPACE } from './constants';
|
||||
import { Entity } from './Entity';
|
||||
|
||||
function parseRefString(
|
||||
ref: string,
|
||||
): {
|
||||
kind?: string;
|
||||
namespace?: string;
|
||||
name: string;
|
||||
} {
|
||||
const match = /^([^:/]+:)?([^:/]+\/)?([^:/]+)$/.exec(ref.trim());
|
||||
if (!match) {
|
||||
throw new TypeError(
|
||||
`Entity reference "${ref}" was not on the form [<kind>:][<namespace>/]<name>`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
kind: match[1]?.slice(0, -1),
|
||||
namespace: match[2]?.slice(0, -1),
|
||||
name: match[3],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the kind, namespace and name that form the name triplet of the
|
||||
* given entity.
|
||||
@@ -121,17 +142,11 @@ export function parseEntityRef(
|
||||
}
|
||||
|
||||
if (typeof ref === 'string') {
|
||||
const match = /^([^:/]+:)?([^:/]+\/)?([^:/]+)$/.exec(ref.trim());
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`Entity reference "${ref}" was not on the form [<kind>:][<namespace>/]<name>`,
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = parseRefString(ref);
|
||||
return {
|
||||
kind: match[1]?.slice(0, -1) ?? context.defaultKind,
|
||||
namespace: match[2]?.slice(0, -1) ?? context.defaultNamespace,
|
||||
name: match[3],
|
||||
kind: parsed.kind ?? context.defaultKind,
|
||||
namespace: parsed.namespace ?? context.defaultNamespace,
|
||||
name: parsed.name,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -196,3 +211,53 @@ export function serializeEntityRef(
|
||||
|
||||
return `${kind ? `${kind}:` : ''}${namespace ? `${namespace}/` : ''}${name}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares an entity to either a string reference or a compound reference.
|
||||
*
|
||||
* The comparison is case insensitive, and all of kind, namespace, and name
|
||||
* must match (after applying the optional context to the ref).
|
||||
*
|
||||
* @param entity The entity to match
|
||||
* @param ref A string or compound entity ref
|
||||
* @param context An optional context of default kind and namespace, that apply
|
||||
* to the ref if given
|
||||
* @returns True if matching, false otherwise
|
||||
*/
|
||||
export function compareEntityToRef(
|
||||
entity: Entity,
|
||||
ref: EntityRef | EntityName,
|
||||
context?: EntityRefContext,
|
||||
): boolean {
|
||||
const entityKind = entity.kind;
|
||||
const entityNamespace = entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE;
|
||||
const entityName = entity.metadata.name;
|
||||
|
||||
let refKind: string | undefined;
|
||||
let refNamespace: string | undefined;
|
||||
let refName: string;
|
||||
if (typeof ref === 'string') {
|
||||
const parsed = parseRefString(ref);
|
||||
refKind = parsed.kind || context?.defaultKind;
|
||||
refNamespace =
|
||||
parsed.namespace || context?.defaultNamespace || ENTITY_DEFAULT_NAMESPACE;
|
||||
refName = parsed.name;
|
||||
} else {
|
||||
refKind = ref.kind || context?.defaultKind;
|
||||
refNamespace =
|
||||
ref.namespace || context?.defaultNamespace || ENTITY_DEFAULT_NAMESPACE;
|
||||
refName = ref.name;
|
||||
}
|
||||
|
||||
if (!refKind || !refNamespace) {
|
||||
throw new Error(
|
||||
`Entity reference or context did not contain kind and namespace`,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
entityKind.toLowerCase() === refKind.toLowerCase() &&
|
||||
entityNamespace.toLowerCase() === refNamespace.toLowerCase() &&
|
||||
entityName.toLowerCase() === refName.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,4 +73,19 @@ describe('ScmIntegrations', () => {
|
||||
expect(i.byHost('github.local')).toBe(github);
|
||||
expect(i.byHost('gitlab.local')).toBe(gitlab);
|
||||
});
|
||||
|
||||
it('can resolveUrl using fallback', () => {
|
||||
expect(
|
||||
i.resolveUrl({
|
||||
url: '../b.yaml',
|
||||
base: 'https://no-matching-integration.com/x/a.yaml',
|
||||
}),
|
||||
).toBe('https://no-matching-integration.com/b.yaml');
|
||||
expect(
|
||||
i.resolveUrl({
|
||||
url: 'https://absolute.com/path',
|
||||
base: 'https://no-matching-integration.com/x/a.yaml',
|
||||
}),
|
||||
).toBe('https://absolute.com/path');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,4 +81,13 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
|
||||
.map(i => i.byHost(host))
|
||||
.find(Boolean);
|
||||
}
|
||||
|
||||
resolveUrl(options: { url: string; base: string }): string {
|
||||
const resolve = this.byUrl(options.base)?.resolveUrl;
|
||||
if (!resolve) {
|
||||
return new URL(options.url, options.base).toString();
|
||||
}
|
||||
|
||||
return resolve(options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,4 +41,52 @@ describe('AzureIntegration', () => {
|
||||
expect(integration.type).toBe('azure');
|
||||
expect(integration.title).toBe('h.com');
|
||||
});
|
||||
|
||||
describe('resolveUrl', () => {
|
||||
it('works for valid urls', () => {
|
||||
const integration = new AzureIntegration({
|
||||
host: 'dev.azure.com',
|
||||
} as any);
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: '../a.yaml',
|
||||
base:
|
||||
'https://dev.azure.com/organization/project/_git/repository?path=%2Ffolder%2Fcatalog-info.yaml',
|
||||
}),
|
||||
).toBe(
|
||||
'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml',
|
||||
);
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: './a.yaml',
|
||||
base: 'https://dev.azure.com/organization/project/_git/repository',
|
||||
}),
|
||||
).toBe(
|
||||
'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml',
|
||||
);
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: 'https://absolute.com/path',
|
||||
base:
|
||||
'https://dev.azure.com/organization/project/_git/repository?path=%2Fcatalog-info.yaml',
|
||||
}),
|
||||
).toBe('https://absolute.com/path');
|
||||
});
|
||||
|
||||
it('falls back to regular URL resolution if not in a repo', () => {
|
||||
const integration = new AzureIntegration({
|
||||
host: 'dev.azure.com',
|
||||
} as any);
|
||||
|
||||
expect(
|
||||
integration.resolveUrl({
|
||||
url: './test',
|
||||
base: 'https://dev.azure.com/organization/project/_git',
|
||||
}),
|
||||
).toBe('https://dev.azure.com/organization/project/test');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import parseGitUrl from 'git-url-parse';
|
||||
import { basicIntegrations } from '../helpers';
|
||||
import { ScmIntegration, ScmIntegrationsFactory } from '../types';
|
||||
import { AzureIntegrationConfig, readAzureIntegrationConfigs } from './config';
|
||||
@@ -42,4 +43,39 @@ export class AzureIntegration implements ScmIntegration {
|
||||
get config(): AzureIntegrationConfig {
|
||||
return this.integrationConfig;
|
||||
}
|
||||
|
||||
/*
|
||||
* Azure repo URLs on the form with a `path` query param are treated specially.
|
||||
*
|
||||
* Example base URL: https://dev.azure.com/organization/project/_git/repository?path=%2Fcatalog-info.yaml
|
||||
*/
|
||||
resolveUrl(options: { url: string; base: string }): string {
|
||||
const { url, base } = options;
|
||||
|
||||
// If we can parse the url, it is absolute - then return it verbatim
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new URL(url);
|
||||
return url;
|
||||
} catch {
|
||||
// Ignore intentionally - looks like a relative path
|
||||
}
|
||||
|
||||
const parsed = parseGitUrl(base);
|
||||
const { organization, owner, name, filepath } = parsed;
|
||||
|
||||
// If not an actual file path within a repo, treat the URL as raw
|
||||
if (!organization || !owner || !name) {
|
||||
return new URL(url, base).toString();
|
||||
}
|
||||
|
||||
const path = filepath?.replace(/^\//, '') || '';
|
||||
const mockBaseUrl = new URL(`https://a.com/${path}`);
|
||||
const updatedPath = new URL(url, mockBaseUrl).pathname;
|
||||
|
||||
const newUrl = new URL(base);
|
||||
newUrl.searchParams.set('path', updatedPath);
|
||||
|
||||
return newUrl.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ describe('bitbucket core', () => {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
@@ -250,5 +251,40 @@ describe('bitbucket core', () => {
|
||||
);
|
||||
expect(defaultBranch).toEqual('main');
|
||||
});
|
||||
|
||||
it('return default branch for Bitbucket Server for bitbucket version 5.11', async () => {
|
||||
const defaultBranchResponse = {
|
||||
displayId: 'main',
|
||||
};
|
||||
worker.use(
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(404),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(defaultBranchResponse),
|
||||
),
|
||||
),
|
||||
rest.get(
|
||||
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches/default',
|
||||
(_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/json'),
|
||||
ctx.json(defaultBranchResponse),
|
||||
),
|
||||
),
|
||||
);
|
||||
const config: BitbucketIntegrationConfig = {
|
||||
host: 'bitbucket.mycompany.net',
|
||||
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
|
||||
};
|
||||
const defaultBranch = await getBitbucketDefaultBranch(
|
||||
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/README.md',
|
||||
config,
|
||||
);
|
||||
expect(defaultBranch).toEqual('main');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,11 +32,19 @@ export async function getBitbucketDefaultBranch(
|
||||
|
||||
const isHosted = resource === 'bitbucket.org';
|
||||
// Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp184
|
||||
const branchUrl = isHosted
|
||||
let branchUrl = isHosted
|
||||
? `${config.apiBaseUrl}/repositories/${project}/${repoName}`
|
||||
: `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/default-branch`;
|
||||
|
||||
const response = await fetch(branchUrl, getBitbucketRequestOptions(config));
|
||||
let response = await fetch(branchUrl, getBitbucketRequestOptions(config));
|
||||
|
||||
if (response.status === 404 && !isHosted) {
|
||||
// First try the new format, and then if it gets specifically a 404 it should try the old format
|
||||
// (to support old Atlassian Bitbucket v5.11.1 format )
|
||||
branchUrl = `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/branches/default`;
|
||||
response = await fetch(branchUrl, getBitbucketRequestOptions(config));
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const message = `Failed to retrieve default branch from ${branchUrl}, ${response.status} ${response.statusText}`;
|
||||
throw new Error(message);
|
||||
|
||||
@@ -49,23 +49,23 @@ describe('gitlab core', () => {
|
||||
{
|
||||
config: configWithNoToken,
|
||||
url:
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml',
|
||||
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path%20with%20spaces/to/file.yaml',
|
||||
result:
|
||||
'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch',
|
||||
'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%20with%20spaces%2Fto%2Ffile.yaml/raw?ref=branch',
|
||||
},
|
||||
{
|
||||
config: configWithToken,
|
||||
url:
|
||||
'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml',
|
||||
'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path%20with%20spaces/to/file.yaml',
|
||||
result:
|
||||
'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch',
|
||||
'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%20with%20spaces%2Fto%2Ffile.yaml/raw?ref=branch',
|
||||
},
|
||||
{
|
||||
config: configWithNoToken,
|
||||
url:
|
||||
'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/to/file.yaml', // Repo not in subgroup
|
||||
'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path%20with%20spaces/to/file.yaml', // Repo not in subgroup
|
||||
result:
|
||||
'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch',
|
||||
'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%20with%20spaces%2Fto%2Ffile.yaml/raw?ref=branch',
|
||||
},
|
||||
// Raw URLs
|
||||
{
|
||||
|
||||
@@ -109,7 +109,7 @@ export function buildProjectUrl(target: string, projectID: Number): URL {
|
||||
'/api/v4/projects',
|
||||
projectID,
|
||||
'repository/files',
|
||||
encodeURIComponent(filePath.join('/')),
|
||||
encodeURIComponent(decodeURIComponent(filePath.join('/'))),
|
||||
'raw',
|
||||
].join('/');
|
||||
url.search = `?ref=${branch}`;
|
||||
|
||||
@@ -34,6 +34,18 @@ export interface ScmIntegration {
|
||||
* differentiate between different integrations.
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* Works like the two-argument form of the URL constructor, resolving an
|
||||
* absolute or relative URL in relation to a base URL.
|
||||
*
|
||||
* If this method is not implemented, the URL constructor is used instead for
|
||||
* URLs that match this integration.
|
||||
*
|
||||
* @param options.url The (absolute or relative) URL or path to resolve
|
||||
* @param options.base The base URL onto which this resolution happens
|
||||
*/
|
||||
resolveUrl?(options: { url: string; base: string }): string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,6 +81,15 @@ export interface ScmIntegrationRegistry
|
||||
bitbucket: ScmIntegrationsGroup<BitbucketIntegration>;
|
||||
github: ScmIntegrationsGroup<GitHubIntegration>;
|
||||
gitlab: ScmIntegrationsGroup<GitLabIntegration>;
|
||||
|
||||
/**
|
||||
* Works like the two-argument form of the URL constructor, resolving an
|
||||
* absolute or relative URL in relation to a base URL.
|
||||
*
|
||||
* @param options.url The (absolute or relative) URL or path to resolve
|
||||
* @param options.base The base URL onto which this resolution happens
|
||||
*/
|
||||
resolveUrl(options: { url: string; base: string }): string;
|
||||
}
|
||||
|
||||
export type ScmIntegrationsFactory<T extends ScmIntegration> = (options: {
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"@backstage/backend-common": "^0.5.1",
|
||||
"@backstage/catalog-model": "^0.7.0",
|
||||
"@backstage/config": "^0.1.2",
|
||||
"@backstage/integration": "^0.3.1",
|
||||
"@octokit/graphql": "^4.5.8",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/ldapjs": "^1.0.9",
|
||||
|
||||
@@ -15,30 +15,73 @@
|
||||
*/
|
||||
|
||||
import { LocationSpec } from '@backstage/catalog-model';
|
||||
import { toAbsoluteUrl } from './LocationEntityProcessor';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
ScmIntegrations,
|
||||
ScmIntegrationRegistry,
|
||||
} from '@backstage/integration';
|
||||
import path from 'path';
|
||||
import { toAbsoluteUrl } from './LocationEntityProcessor';
|
||||
|
||||
describe('LocationEntityProcessor', () => {
|
||||
describe('toAbsoluteUrl', () => {
|
||||
it('handles files', () => {
|
||||
const integrations = ({} as unknown) as ScmIntegrationRegistry;
|
||||
const base: LocationSpec = {
|
||||
type: 'file',
|
||||
target: `some${path.sep}path${path.sep}catalog-info.yaml`,
|
||||
};
|
||||
expect(toAbsoluteUrl(base, `.${path.sep}c`)).toBe(
|
||||
expect(toAbsoluteUrl(integrations, base, `.${path.sep}c`)).toBe(
|
||||
`some${path.sep}path${path.sep}c`,
|
||||
);
|
||||
expect(toAbsoluteUrl(base, `${path.sep}c`)).toBe(`${path.sep}c`);
|
||||
expect(toAbsoluteUrl(integrations, base, `${path.sep}c`)).toBe(
|
||||
`${path.sep}c`,
|
||||
);
|
||||
});
|
||||
|
||||
it('handles urls', () => {
|
||||
const integrations = ScmIntegrations.fromConfig(new ConfigReader({}));
|
||||
const base: LocationSpec = {
|
||||
type: 'url',
|
||||
target: 'http://a.com/b/catalog-info.yaml',
|
||||
};
|
||||
expect(toAbsoluteUrl(base, './c/d')).toBe('http://a.com/b/c/d');
|
||||
expect(toAbsoluteUrl(base, 'c/d')).toBe('http://a.com/b/c/d');
|
||||
expect(toAbsoluteUrl(base, 'http://b.com/z')).toBe('http://b.com/z');
|
||||
jest.spyOn(integrations, 'resolveUrl');
|
||||
|
||||
expect(toAbsoluteUrl(integrations, base, './c/d')).toBe(
|
||||
'http://a.com/b/c/d',
|
||||
);
|
||||
expect(toAbsoluteUrl(integrations, base, 'c/d')).toBe(
|
||||
'http://a.com/b/c/d',
|
||||
);
|
||||
expect(toAbsoluteUrl(integrations, base, 'http://b.com/z')).toBe(
|
||||
'http://b.com/z',
|
||||
);
|
||||
|
||||
expect(integrations.resolveUrl).toBeCalledTimes(3);
|
||||
});
|
||||
|
||||
it('handles azure urls specifically', () => {
|
||||
const integrations = ScmIntegrations.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
azure: [{ host: 'dev.azure.com' }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
toAbsoluteUrl(
|
||||
integrations,
|
||||
{
|
||||
type: 'url',
|
||||
target:
|
||||
'https://dev.azure.com/organization/project/_git/repository?path=%2Fcatalog-info.yaml',
|
||||
},
|
||||
'./a.yaml',
|
||||
),
|
||||
).toBe(
|
||||
'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,11 +15,16 @@
|
||||
*/
|
||||
|
||||
import { Entity, LocationEntity, LocationSpec } from '@backstage/catalog-model';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import path from 'path';
|
||||
import * as result from './results';
|
||||
import { CatalogProcessor, CatalogProcessorEmit } from './types';
|
||||
import path from 'path';
|
||||
|
||||
export function toAbsoluteUrl(base: LocationSpec, target: string): string {
|
||||
export function toAbsoluteUrl(
|
||||
integrations: ScmIntegrationRegistry,
|
||||
base: LocationSpec,
|
||||
target: string,
|
||||
): string {
|
||||
try {
|
||||
if (base.type === 'file') {
|
||||
if (target.startsWith('.')) {
|
||||
@@ -27,13 +32,19 @@ export function toAbsoluteUrl(base: LocationSpec, target: string): string {
|
||||
}
|
||||
return target;
|
||||
}
|
||||
return new URL(target, base.target).toString();
|
||||
return integrations.resolveUrl({ url: target, base: base.target });
|
||||
} catch (e) {
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
export class LocationRefProcessor implements CatalogProcessor {
|
||||
type Options = {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
};
|
||||
|
||||
export class LocationEntityProcessor implements CatalogProcessor {
|
||||
constructor(private readonly options: Options) {}
|
||||
|
||||
async postProcessEntity(
|
||||
entity: Entity,
|
||||
location: LocationSpec,
|
||||
@@ -47,7 +58,7 @@ export class LocationRefProcessor implements CatalogProcessor {
|
||||
emit(
|
||||
result.inputError(
|
||||
location,
|
||||
`LocationRefProcessor cannot handle ${type} type location with target ${location.target} that ends with a path separator`,
|
||||
`LocationEntityProcessor cannot handle ${type} type location with target ${location.target} that ends with a path separator`,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -61,7 +72,11 @@ export class LocationRefProcessor implements CatalogProcessor {
|
||||
}
|
||||
|
||||
for (const maybeRelativeTarget of targets) {
|
||||
const target = toAbsoluteUrl(location, maybeRelativeTarget);
|
||||
const target = toAbsoluteUrl(
|
||||
this.options.integrations,
|
||||
location,
|
||||
maybeRelativeTarget,
|
||||
);
|
||||
emit(result.location({ type, target }, false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export { CodeOwnersProcessor } from './CodeOwnersProcessor';
|
||||
export { FileReaderProcessor } from './FileReaderProcessor';
|
||||
export { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor';
|
||||
export { LdapOrgReaderProcessor } from './LdapOrgReaderProcessor';
|
||||
export { LocationRefProcessor } from './LocationEntityProcessor';
|
||||
export { LocationEntityProcessor } from './LocationEntityProcessor';
|
||||
export { MicrosoftGraphOrgReaderProcessor } from './MicrosoftGraphOrgReaderProcessor';
|
||||
export { PlaceholderProcessor } from './PlaceholderProcessor';
|
||||
export type { PlaceholderResolver } from './PlaceholderProcessor';
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
Validators,
|
||||
} from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import lodash from 'lodash';
|
||||
import { Logger } from 'winston';
|
||||
import {
|
||||
@@ -46,8 +47,8 @@ import {
|
||||
HigherOrderOperation,
|
||||
HigherOrderOperations,
|
||||
LdapOrgReaderProcessor,
|
||||
LocationEntityProcessor,
|
||||
LocationReaders,
|
||||
LocationRefProcessor,
|
||||
MicrosoftGraphOrgReaderProcessor,
|
||||
PlaceholderProcessor,
|
||||
PlaceholderResolver,
|
||||
@@ -280,6 +281,7 @@ export class CatalogBuilder {
|
||||
|
||||
private buildProcessors(): CatalogProcessor[] {
|
||||
const { config, logger, reader } = this.env;
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
|
||||
this.checkDeprecatedReaderProcessors();
|
||||
|
||||
@@ -306,7 +308,7 @@ export class CatalogBuilder {
|
||||
MicrosoftGraphOrgReaderProcessor.fromConfig(config, { logger }),
|
||||
new UrlReaderProcessor({ reader, logger }),
|
||||
new CodeOwnersProcessor({ reader, logger }),
|
||||
new LocationRefProcessor(),
|
||||
new LocationEntityProcessor({ integrations }),
|
||||
new AnnotateLocationEntityProcessor(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,13 +76,13 @@ describe('getPreviousPeriodTotalCost', () => {
|
||||
change: changeOf(MockAggregatedDailyCosts),
|
||||
trendline: trendlineOf(MockAggregatedDailyCosts),
|
||||
};
|
||||
const exclusiveEndDate = '2020-09-30';
|
||||
const inclusiveEndDate = '2020-09-30';
|
||||
expect(
|
||||
getPreviousPeriodTotalCost(
|
||||
mockGroupDailyCost.aggregation,
|
||||
Duration.P30D,
|
||||
exclusiveEndDate,
|
||||
inclusiveEndDate,
|
||||
),
|
||||
).toEqual(100_000);
|
||||
).toEqual(96_600);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
GrowthType,
|
||||
MetricData,
|
||||
Duration,
|
||||
DEFAULT_DATE_FORMAT,
|
||||
DateAggregation,
|
||||
} from '../types';
|
||||
import dayjs, { OpUnitType } from 'dayjs';
|
||||
@@ -73,10 +72,7 @@ export function getPreviousPeriodTotalCost(
|
||||
inclusiveEndDate: string,
|
||||
): number {
|
||||
const dayjsDuration = dayjs.duration(duration);
|
||||
const startDate = inclusiveStartDateOf(
|
||||
duration,
|
||||
dayjs(inclusiveEndDate).add(1, 'day').format(DEFAULT_DATE_FORMAT),
|
||||
);
|
||||
const startDate = inclusiveStartDateOf(duration, inclusiveEndDate);
|
||||
// dayjs doesn't allow adding an ISO 8601 period to dates.
|
||||
const [amount, type]: [number, OpUnitType] = dayjsDuration.days()
|
||||
? [dayjsDuration.days(), 'day']
|
||||
|
||||
@@ -24,23 +24,21 @@ export const DEFAULT_DURATION = Duration.P30D;
|
||||
* Derive the start date of a given period, assuming two repeating intervals.
|
||||
*
|
||||
* @param duration see comment on Duration enum
|
||||
* @param exclusiveEndDate from CostInsightsApi.getLastCompleteBillingDate + 1 day
|
||||
* @param inclusiveEndDate from CostInsightsApi.getLastCompleteBillingDate
|
||||
*/
|
||||
export function inclusiveStartDateOf(
|
||||
duration: Duration,
|
||||
exclusiveEndDate: string,
|
||||
inclusiveEndDate: string,
|
||||
): string {
|
||||
switch (duration) {
|
||||
case Duration.P7D:
|
||||
case Duration.P30D:
|
||||
case Duration.P90D:
|
||||
return moment(exclusiveEndDate)
|
||||
.utc()
|
||||
return moment(inclusiveEndDate)
|
||||
.subtract(moment.duration(duration).add(moment.duration(duration)))
|
||||
.format(DEFAULT_DATE_FORMAT);
|
||||
case Duration.P3M:
|
||||
return moment(exclusiveEndDate)
|
||||
.utc()
|
||||
return moment(inclusiveEndDate)
|
||||
.startOf('quarter')
|
||||
.subtract(moment.duration(duration).add(moment.duration(duration)))
|
||||
.format(DEFAULT_DATE_FORMAT);
|
||||
@@ -57,13 +55,9 @@ export function exclusiveEndDateOf(
|
||||
case Duration.P7D:
|
||||
case Duration.P30D:
|
||||
case Duration.P90D:
|
||||
return moment(inclusiveEndDate)
|
||||
.utc()
|
||||
.add(1, 'day')
|
||||
.format(DEFAULT_DATE_FORMAT);
|
||||
return moment(inclusiveEndDate).add(1, 'day').format(DEFAULT_DATE_FORMAT);
|
||||
case Duration.P3M:
|
||||
return moment(quarterEndDate(inclusiveEndDate))
|
||||
.utc()
|
||||
.add(1, 'day')
|
||||
.format(DEFAULT_DATE_FORMAT);
|
||||
default:
|
||||
@@ -76,7 +70,6 @@ export function inclusiveEndDateOf(
|
||||
inclusiveEndDate: string,
|
||||
): string {
|
||||
return moment(exclusiveEndDateOf(duration, inclusiveEndDate))
|
||||
.utc()
|
||||
.subtract(1, 'day')
|
||||
.format(DEFAULT_DATE_FORMAT);
|
||||
}
|
||||
@@ -94,7 +87,7 @@ export function intervalsOf(
|
||||
}
|
||||
|
||||
export function quarterEndDate(inclusiveEndDate: string): string {
|
||||
const endDate = moment(inclusiveEndDate).utc();
|
||||
const endDate = moment(inclusiveEndDate);
|
||||
const endOfQuarter = endDate.endOf('quarter').format(DEFAULT_DATE_FORMAT);
|
||||
if (endOfQuarter === inclusiveEndDate) {
|
||||
return endDate.format(DEFAULT_DATE_FORMAT);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import moment from 'moment';
|
||||
import pluralize from 'pluralize';
|
||||
import { Duration, DEFAULT_DATE_FORMAT } from '../types';
|
||||
import { Duration } from '../types';
|
||||
import { inclusiveEndDateOf, inclusiveStartDateOf } from '../utils/duration';
|
||||
|
||||
export type Period = {
|
||||
@@ -92,11 +92,8 @@ export function formatPercent(n: number): string {
|
||||
}
|
||||
|
||||
export function formatLastTwoLookaheadQuarters(inclusiveEndDate: string) {
|
||||
const exclusiveEndDate = moment(inclusiveEndDate)
|
||||
.add(1, 'day')
|
||||
.format(DEFAULT_DATE_FORMAT);
|
||||
const start = moment(
|
||||
inclusiveStartDateOf(Duration.P3M, exclusiveEndDate),
|
||||
inclusiveStartDateOf(Duration.P3M, inclusiveEndDate),
|
||||
).format('[Q]Q YYYY');
|
||||
const end = moment(inclusiveEndDateOf(Duration.P3M, inclusiveEndDate)).format(
|
||||
'[Q]Q YYYY',
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
getDefaultState as getDefaultLoadingState,
|
||||
} from '../utils/loading';
|
||||
import { findAlways } from '../utils/assert';
|
||||
import { inclusiveStartDateOf } from './duration';
|
||||
import { inclusiveEndDateOf, inclusiveStartDateOf } from './duration';
|
||||
|
||||
type mockAlertRenderer<T> = (alert: T) => T;
|
||||
type mockEntityRenderer<T> = (entity: T) => T;
|
||||
@@ -228,8 +228,9 @@ export function aggregationFor(
|
||||
baseline: number,
|
||||
): DateAggregation[] {
|
||||
const { duration, endDate } = parseIntervals(intervals);
|
||||
const inclusiveEndDate = inclusiveEndDateOf(duration, endDate);
|
||||
const days = dayjs(endDate).diff(
|
||||
inclusiveStartDateOf(duration, endDate),
|
||||
inclusiveStartDateOf(duration, inclusiveEndDate),
|
||||
'day',
|
||||
);
|
||||
|
||||
@@ -244,7 +245,7 @@ export function aggregationFor(
|
||||
return [...Array(days).keys()].reduce(
|
||||
(values: DateAggregation[], i: number): DateAggregation[] => {
|
||||
const last = values.length ? values[values.length - 1].amount : baseline;
|
||||
const date = dayjs(inclusiveStartDateOf(duration, endDate))
|
||||
const date = dayjs(inclusiveStartDateOf(duration, inclusiveEndDate))
|
||||
.add(i, 'day')
|
||||
.format(DEFAULT_DATE_FORMAT);
|
||||
const amount = Math.max(0, last + nextDelta());
|
||||
|
||||
@@ -19,17 +19,7 @@ yarn add @backstage/plugin-rollbar
|
||||
export { plugin as Rollbar } from '@backstage/plugin-rollbar';
|
||||
```
|
||||
|
||||
4. Add plugin API to your Backstage instance:
|
||||
|
||||
```ts
|
||||
// packages/app/src/api.ts
|
||||
import { RollbarClient, rollbarApiRef } from '@backstage/plugin-rollbar';
|
||||
|
||||
// ...
|
||||
builder.add(rollbarApiRef, new RollbarClient({ discoveryApi }));
|
||||
```
|
||||
|
||||
5. Add to the app `EntityPage` component:
|
||||
4. Add to the app `EntityPage` component:
|
||||
|
||||
```ts
|
||||
// packages/app/src/components/catalog/EntityPage.tsx
|
||||
@@ -48,7 +38,7 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => (
|
||||
);
|
||||
```
|
||||
|
||||
6. Setup the `app.config.yaml` and account token environment variable
|
||||
5. Setup the `app.config.yaml` and account token environment variable
|
||||
|
||||
```yaml
|
||||
# app.config.yaml
|
||||
@@ -59,7 +49,7 @@ rollbar:
|
||||
$env: ROLLBAR_ACCOUNT_TOKEN
|
||||
```
|
||||
|
||||
7. Annotate entities with the rollbar project slug
|
||||
6. Annotate entities with the rollbar project slug
|
||||
|
||||
```yaml
|
||||
# pump-station-catalog-component.yaml
|
||||
@@ -71,7 +61,7 @@ metadata:
|
||||
rollbar.com/project-slug: project-name
|
||||
```
|
||||
|
||||
8. Run app with `yarn start` and navigate to `/rollbar` or a catalog entity
|
||||
7. Run app with `yarn start` and navigate to `/rollbar` or a catalog entity
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
@@ -15,6 +15,6 @@
|
||||
*/
|
||||
|
||||
import { createDevApp } from '@backstage/dev-utils';
|
||||
import { plugin } from '../src/plugin';
|
||||
import { rollbarPlugin } from '../src/plugin';
|
||||
|
||||
createDevApp().registerPlugin(plugin).render();
|
||||
createDevApp().registerPlugin(rollbarPlugin).render();
|
||||
|
||||
@@ -14,14 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import React from 'react';
|
||||
import { RollbarProject } from '../RollbarProject/RollbarProject';
|
||||
|
||||
type Props = {
|
||||
entity: Entity;
|
||||
/** @deprecated The entity is now grabbed from context instead */
|
||||
entity?: Entity;
|
||||
};
|
||||
|
||||
export const EntityPageRollbar = ({ entity }: Props) => {
|
||||
export const EntityPageRollbar = (_props: Props) => {
|
||||
const { entity } = useEntity();
|
||||
|
||||
return <RollbarProject entity={entity} />;
|
||||
};
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 React from 'react';
|
||||
import {
|
||||
ApiProvider,
|
||||
ApiRegistry,
|
||||
ConfigApi,
|
||||
configApiRef,
|
||||
} from '@backstage/core';
|
||||
import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog-react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { render } from '@testing-library/react';
|
||||
import { RollbarApi, rollbarApiRef } from '../../api/RollbarApi';
|
||||
import { RollbarProject } from '../../api/types';
|
||||
import { RollbarHome } from './RollbarHome';
|
||||
|
||||
describe('RollbarHome component', () => {
|
||||
const projects: RollbarProject[] = [
|
||||
{ id: 123, name: 'abc', accountId: 1, status: 'enabled' },
|
||||
{ id: 456, name: 'xyz', accountId: 1, status: 'enabled' },
|
||||
];
|
||||
const rollbarApi: Partial<RollbarApi> = {
|
||||
getAllProjects: () => Promise.resolve(projects),
|
||||
};
|
||||
const config: Partial<ConfigApi> = {
|
||||
getString: () => 'foo',
|
||||
getOptionalString: () => 'foo',
|
||||
};
|
||||
|
||||
const renderWrapped = (children: React.ReactNode) =>
|
||||
render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider
|
||||
apis={ApiRegistry.from([
|
||||
[rollbarApiRef, rollbarApi],
|
||||
[configApiRef, config],
|
||||
[
|
||||
catalogApiRef,
|
||||
({
|
||||
async getEntities() {
|
||||
return { items: [] };
|
||||
},
|
||||
} as Partial<CatalogApi>) as CatalogApi,
|
||||
],
|
||||
])}
|
||||
>
|
||||
{children}
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
it('should render rollbar landing page', async () => {
|
||||
const rendered = renderWrapped(<RollbarHome />);
|
||||
expect(rendered.getByText(/Rollbar/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { Content, Header, Page } from '@backstage/core';
|
||||
import { RollbarProjectTable } from '../RollbarProjectTable/RollbarProjectTable';
|
||||
import { useRollbarEntities } from '../../hooks/useRollbarEntities';
|
||||
|
||||
export const RollbarHome = () => {
|
||||
const { entities, loading, error } = useRollbarEntities();
|
||||
|
||||
return (
|
||||
<Page themeId="tool">
|
||||
<Header
|
||||
title="Rollbar"
|
||||
subtitle="Real-time error tracking & debugging tools for developers"
|
||||
/>
|
||||
<Content>
|
||||
<RollbarProjectTable
|
||||
entities={entities || []}
|
||||
loading={loading}
|
||||
error={error}
|
||||
/>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -14,8 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import React from 'react';
|
||||
import { useTopActiveItems } from '../../hooks/useTopActiveItems';
|
||||
import { RollbarTopItemsTable } from '../RollbarTopItemsTable/RollbarTopItemsTable';
|
||||
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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,
|
||||
ApiRegistry,
|
||||
ConfigApi,
|
||||
configApiRef,
|
||||
} from '@backstage/core';
|
||||
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { render } from '@testing-library/react';
|
||||
import * as React from 'react';
|
||||
import { RollbarApi, rollbarApiRef } from '../../api/RollbarApi';
|
||||
import { RollbarTopActiveItem } from '../../api/types';
|
||||
import { RollbarProjectPage } from './RollbarProjectPage';
|
||||
|
||||
describe('RollbarProjectPage component', () => {
|
||||
const items: RollbarTopActiveItem[] = [
|
||||
{
|
||||
item: {
|
||||
id: 9898989,
|
||||
counter: 1234,
|
||||
environment: 'production',
|
||||
framework: 2,
|
||||
lastOccurrenceTimestamp: new Date().getTime() / 1000,
|
||||
level: 50,
|
||||
occurrences: 100,
|
||||
projectId: 12345,
|
||||
title: 'error occurred',
|
||||
uniqueOccurrences: 10,
|
||||
},
|
||||
counts: [10, 10, 10, 10, 10, 50],
|
||||
},
|
||||
];
|
||||
const rollbarApi: Partial<RollbarApi> = {
|
||||
getTopActiveItems: () => Promise.resolve(items),
|
||||
};
|
||||
const config: Partial<ConfigApi> = {
|
||||
getString: () => 'foo',
|
||||
getOptionalString: () => 'foo',
|
||||
};
|
||||
|
||||
const renderWrapped = (children: React.ReactNode) =>
|
||||
render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider
|
||||
apis={ApiRegistry.from([
|
||||
[rollbarApiRef, rollbarApi],
|
||||
[configApiRef, config],
|
||||
[
|
||||
catalogApiRef,
|
||||
({
|
||||
async getEntityByName() {
|
||||
return {
|
||||
metadata: { name: 'foo' },
|
||||
spec: { owner: 'bar', lifecycle: 'experimental' },
|
||||
} as any;
|
||||
},
|
||||
} as Partial<CatalogApi>) as CatalogApi,
|
||||
],
|
||||
])}
|
||||
>
|
||||
{children}
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
it('should render rollbar project page', async () => {
|
||||
const rendered = renderWrapped(<RollbarProjectPage />);
|
||||
expect(rendered.getByText(/Rollbar/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { Content, Header, HeaderLabel, Page } from '@backstage/core';
|
||||
import { useCatalogEntity } from '../../hooks/useCatalogEntity';
|
||||
import { RollbarProject } from '../RollbarProject/RollbarProject';
|
||||
|
||||
export const RollbarProjectPage = () => {
|
||||
const { entity } = useCatalogEntity();
|
||||
|
||||
return (
|
||||
<Page themeId="tool">
|
||||
<Header title={entity?.metadata?.name} subtitle="Rollbar Project">
|
||||
<HeaderLabel label="Owner" value={entity?.spec?.owner} />
|
||||
<HeaderLabel label="Lifecycle" value={entity?.spec?.lifecycle} />
|
||||
</Header>
|
||||
<Content>
|
||||
{entity ? <RollbarProject entity={entity} /> : 'Loading'}
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { Link as RouterLink, generatePath } from 'react-router-dom';
|
||||
import { Table, TableColumn } from '@backstage/core';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Link } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { entityRouteRef } from '../../routes';
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{
|
||||
title: 'Name',
|
||||
field: 'metadata.name',
|
||||
type: 'string',
|
||||
highlight: true,
|
||||
render: (entity: any) => (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={generatePath(entityRouteRef.path, {
|
||||
optionalNamespaceAndName: [
|
||||
entity.metadata.namespace,
|
||||
entity.metadata.name,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(':'),
|
||||
kind: entity.kind,
|
||||
})}
|
||||
>
|
||||
{entity.metadata.name}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
field: 'metadata.description',
|
||||
},
|
||||
];
|
||||
|
||||
type Props = {
|
||||
entities: Entity[];
|
||||
loading: boolean;
|
||||
error?: any;
|
||||
};
|
||||
|
||||
export const RollbarProjectTable = ({ entities, loading, error }: Props) => {
|
||||
if (error) {
|
||||
return (
|
||||
<div>
|
||||
<Alert severity="error">
|
||||
Error encountered while fetching rollbar projects. {error.toString()}
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
isLoading={loading}
|
||||
columns={columns}
|
||||
options={{
|
||||
search: true,
|
||||
paging: true,
|
||||
pageSize: 10,
|
||||
showEmptyDataSourceMessage: !loading,
|
||||
}}
|
||||
title="Projects"
|
||||
data={entities}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -17,8 +17,8 @@
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { render } from '@testing-library/react';
|
||||
import * as React from 'react';
|
||||
import { RollbarTopItemsTable } from './RollbarTopItemsTable';
|
||||
import { RollbarTopActiveItem } from '../../api/types';
|
||||
import { RollbarTopItemsTable } from './RollbarTopItemsTable';
|
||||
|
||||
const items: RollbarTopActiveItem[] = [
|
||||
{
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Table, TableColumn } from '@backstage/core';
|
||||
import { Box, Link, Typography } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import React from 'react';
|
||||
import {
|
||||
RollbarFrameworkId,
|
||||
RollbarLevel,
|
||||
|
||||
@@ -14,29 +14,36 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Routes, Route } from 'react-router';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { MissingAnnotationEmptyState } from '@backstage/core';
|
||||
import { catalogRouteRef } from '../routes';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import React from 'react';
|
||||
import { Route, Routes } from 'react-router';
|
||||
import { ROLLBAR_ANNOTATION } from '../constants';
|
||||
import { rootRouteRef } from '../plugin';
|
||||
import { EntityPageRollbar } from './EntityPageRollbar/EntityPageRollbar';
|
||||
|
||||
export const isPluginApplicableToEntity = (entity: Entity) =>
|
||||
Boolean(entity.metadata.annotations?.[ROLLBAR_ANNOTATION]);
|
||||
|
||||
type Props = {
|
||||
entity: Entity;
|
||||
/** @deprecated The entity is now grabbed from context instead */
|
||||
entity?: Entity;
|
||||
};
|
||||
|
||||
export const Router = ({ entity }: Props) =>
|
||||
!isPluginApplicableToEntity(entity) ? (
|
||||
<MissingAnnotationEmptyState annotation={ROLLBAR_ANNOTATION} />
|
||||
) : (
|
||||
export const Router = (_props: Props) => {
|
||||
const { entity } = useEntity();
|
||||
|
||||
if (!isPluginApplicableToEntity(entity)) {
|
||||
<MissingAnnotationEmptyState annotation={ROLLBAR_ANNOTATION} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
path={`/${catalogRouteRef.path}`}
|
||||
path={`/${rootRouteRef.path}`}
|
||||
element={<EntityPageRollbar entity={entity} />}
|
||||
/>
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { TrendGraph } from './TrendGraph';
|
||||
|
||||
describe('TrendGraph component', () => {
|
||||
|
||||
@@ -14,10 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { plugin } from './plugin';
|
||||
export * from './api';
|
||||
export * from './routes';
|
||||
export { Router } from './components/Router';
|
||||
export { RollbarProjectPage } from './components/RollbarProjectPage/RollbarProjectPage';
|
||||
export { EntityPageRollbar } from './components/EntityPageRollbar/EntityPageRollbar';
|
||||
export {
|
||||
isPluginApplicableToEntity,
|
||||
isPluginApplicableToEntity as isRollbarAvailable,
|
||||
Router,
|
||||
} from './components/Router';
|
||||
export { ROLLBAR_ANNOTATION } from './constants';
|
||||
export {
|
||||
EntityRollbarContent,
|
||||
rollbarPlugin as plugin,
|
||||
rollbarPlugin,
|
||||
} from './plugin';
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { plugin } from './plugin';
|
||||
import { rollbarPlugin } from './plugin';
|
||||
|
||||
describe('rollbar', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(plugin).toBeDefined();
|
||||
expect(rollbarPlugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,17 +15,21 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
createPlugin,
|
||||
createApiFactory,
|
||||
createPlugin,
|
||||
createRoutableExtension,
|
||||
createRouteRef,
|
||||
discoveryApiRef,
|
||||
} from '@backstage/core';
|
||||
import { rootRouteRef, entityRouteRef } from './routes';
|
||||
import { RollbarHome } from './components/RollbarHome/RollbarHome';
|
||||
import { RollbarProjectPage } from './components/RollbarProjectPage/RollbarProjectPage';
|
||||
import { rollbarApiRef } from './api/RollbarApi';
|
||||
import { RollbarClient } from './api/RollbarClient';
|
||||
|
||||
export const plugin = createPlugin({
|
||||
export const rootRouteRef = createRouteRef({
|
||||
path: '',
|
||||
title: 'Rollbar',
|
||||
});
|
||||
|
||||
export const rollbarPlugin = createPlugin({
|
||||
id: 'rollbar',
|
||||
apis: [
|
||||
createApiFactory({
|
||||
@@ -34,8 +38,14 @@ export const plugin = createPlugin({
|
||||
factory: ({ discoveryApi }) => new RollbarClient({ discoveryApi }),
|
||||
}),
|
||||
],
|
||||
register({ router }) {
|
||||
router.addRoute(rootRouteRef, RollbarHome);
|
||||
router.addRoute(entityRouteRef, RollbarProjectPage);
|
||||
routes: {
|
||||
entityContent: rootRouteRef,
|
||||
},
|
||||
});
|
||||
|
||||
export const EntityRollbarContent = rollbarPlugin.provide(
|
||||
createRoutableExtension({
|
||||
component: () => import('./components/Router').then(m => m.Router),
|
||||
mountPoint: rootRouteRef,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { createRouteRef } from '@backstage/core';
|
||||
|
||||
const NoIcon = () => null;
|
||||
|
||||
export const rootRouteRef = createRouteRef({
|
||||
icon: NoIcon,
|
||||
path: '/rollbar',
|
||||
title: 'Rollbar Home',
|
||||
});
|
||||
|
||||
export const entityRouteRef = createRouteRef({
|
||||
path: '/rollbar/:optionalNamespaceAndName',
|
||||
title: 'Rollbar',
|
||||
});
|
||||
|
||||
export const catalogRouteRef = createRouteRef({
|
||||
icon: NoIcon,
|
||||
path: '',
|
||||
title: 'Rollbar',
|
||||
});
|
||||
@@ -2655,21 +2655,22 @@
|
||||
resolved "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-5.0.0.tgz#3ba791f37b90e7f6170d252b63aacfcae943c039"
|
||||
integrity sha512-WmKrB/575EJCzbeSJR3YQ5sET5FaizeljLRw1382qVUeGqzuWBgIS+AF5a0FO51uQTrDpoRgvuHC2IWVsgwkkA==
|
||||
|
||||
"@changesets/apply-release-plan@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-4.0.0.tgz#e78efb56a4e459a8dab814ba43045f2ace0f27c9"
|
||||
integrity sha512-MrcUd8wIlQ4S/PznzqJVsmnEpUGfPEkCGF54iqt8G05GEqi/zuxpoTfebcScpj5zeiDyxFIcA9RbeZ3pvJJxoA==
|
||||
"@changesets/apply-release-plan@^4.2.0":
|
||||
version "4.2.0"
|
||||
resolved "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-4.2.0.tgz#f1005815f27c3238f66507e90c6ae14d8fc62b41"
|
||||
integrity sha512-/vt6UwgQldhOw93Gb8llI5OuYGlJt2+U45AfcXsoxzl8gZzCmChGm3vUaQJYbmtL8TbL8OOVXHRIKJJidMNPKw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.4.4"
|
||||
"@changesets/config" "^1.2.0"
|
||||
"@babel/runtime" "^7.10.4"
|
||||
"@changesets/config" "^1.5.0"
|
||||
"@changesets/get-version-range-type" "^0.3.2"
|
||||
"@changesets/git" "^1.0.5"
|
||||
"@changesets/types" "^3.1.0"
|
||||
"@changesets/types" "^3.3.0"
|
||||
"@manypkg/get-packages" "^1.0.1"
|
||||
detect-indent "^6.0.0"
|
||||
fs-extra "^7.0.1"
|
||||
lodash.startcase "^4.4.0"
|
||||
outdent "^0.5.0"
|
||||
prettier "^1.18.2"
|
||||
prettier "^1.19.1"
|
||||
resolve-from "^5.0.0"
|
||||
semver "^5.4.1"
|
||||
|
||||
@@ -2685,23 +2686,35 @@
|
||||
"@manypkg/get-packages" "^1.0.1"
|
||||
semver "^5.4.1"
|
||||
|
||||
"@changesets/cli@^2.11.0":
|
||||
version "2.12.0"
|
||||
resolved "https://registry.npmjs.org/@changesets/cli/-/cli-2.12.0.tgz#26124b051e6ce6dcc5aa4595588c8cb2ce3e4363"
|
||||
integrity sha512-dGdFkg75zsaEObsG8gwMLglS6sJVjXWwgVTAzEIjqIoWVnKwqZqccTb4gn0noq47uCwy7SqxiAJqGibIy9UOKw==
|
||||
"@changesets/assemble-release-plan@^4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-4.1.0.tgz#091e5e768dfe51835937e71d1ebaca1c9d6de55b"
|
||||
integrity sha512-dMqe2L5Pn4UGTW89kOuuCuZD3pQFZj1Sxv92ZW4S98sXGsxcb2PdW+PeHbQ7tawkCYCOvzhXxAlN4OdF2DlDKQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.4"
|
||||
"@changesets/apply-release-plan" "^4.0.0"
|
||||
"@changesets/assemble-release-plan" "^4.0.0"
|
||||
"@changesets/config" "^1.4.0"
|
||||
"@changesets/errors" "^0.1.4"
|
||||
"@changesets/get-dependents-graph" "^1.1.3"
|
||||
"@changesets/get-dependents-graph" "^1.2.0"
|
||||
"@changesets/types" "^3.3.0"
|
||||
"@manypkg/get-packages" "^1.0.1"
|
||||
semver "^5.4.1"
|
||||
|
||||
"@changesets/cli@^2.14.0":
|
||||
version "2.14.0"
|
||||
resolved "https://registry.npmjs.org/@changesets/cli/-/cli-2.14.0.tgz#b8d1d33d832c640ce0b95333bbd8d5ac5b9c9824"
|
||||
integrity sha512-rbQMRDXl1cXOglnjUvYyrFLlYBbS0YZdfxZfW3ZbGLzLoS4n50+B9fLSE9oW20hQuL3zAnyLyacb9cwNhF2lig==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.4"
|
||||
"@changesets/apply-release-plan" "^4.2.0"
|
||||
"@changesets/assemble-release-plan" "^4.1.0"
|
||||
"@changesets/config" "^1.5.0"
|
||||
"@changesets/errors" "^0.1.4"
|
||||
"@changesets/get-dependents-graph" "^1.2.0"
|
||||
"@changesets/get-release-plan" "^2.0.1"
|
||||
"@changesets/git" "^1.0.6"
|
||||
"@changesets/git" "^1.1.0"
|
||||
"@changesets/logger" "^0.0.5"
|
||||
"@changesets/pre" "^1.0.4"
|
||||
"@changesets/read" "^0.4.6"
|
||||
"@changesets/types" "^3.2.0"
|
||||
"@changesets/types" "^3.3.0"
|
||||
"@changesets/write" "^0.1.3"
|
||||
"@manypkg/get-packages" "^1.0.1"
|
||||
"@types/semver" "^6.0.0"
|
||||
@@ -2721,7 +2734,7 @@
|
||||
term-size "^2.1.0"
|
||||
tty-table "^2.8.10"
|
||||
|
||||
"@changesets/config@^1.2.0", "@changesets/config@^1.4.0":
|
||||
"@changesets/config@^1.2.0":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmjs.org/@changesets/config/-/config-1.4.0.tgz#c157a4121f198b749f2bbc2e9015b6e976ece7d6"
|
||||
integrity sha512-eoTOcJ6py7jBDY8rUXwEGxR5UtvUX+p//0NhkVpPGcnvIeITHq+DOIsuWyGzWcb+1FaYkof3CCr32/komZTu4Q==
|
||||
@@ -2734,6 +2747,19 @@
|
||||
fs-extra "^7.0.1"
|
||||
micromatch "^4.0.2"
|
||||
|
||||
"@changesets/config@^1.5.0":
|
||||
version "1.5.0"
|
||||
resolved "https://registry.npmjs.org/@changesets/config/-/config-1.5.0.tgz#6d58b01e45916318d3f39e9cde86a5d69dc58ed8"
|
||||
integrity sha512-Bl9nLVYcwFCpd9jpzcOsExZk1NuTYX20D2YWHCdS1xll3W0yOdSUlWLUCCfugN1l3+yTR6iDW6q9o6vpCevWvA==
|
||||
dependencies:
|
||||
"@changesets/errors" "^0.1.4"
|
||||
"@changesets/get-dependents-graph" "^1.2.0"
|
||||
"@changesets/logger" "^0.0.5"
|
||||
"@changesets/types" "^3.3.0"
|
||||
"@manypkg/get-packages" "^1.0.1"
|
||||
fs-extra "^7.0.1"
|
||||
micromatch "^4.0.2"
|
||||
|
||||
"@changesets/errors@^0.1.4":
|
||||
version "0.1.4"
|
||||
resolved "https://registry.npmjs.org/@changesets/errors/-/errors-0.1.4.tgz#f79851746c43679a66b383fdff4c012f480f480d"
|
||||
@@ -2752,6 +2778,17 @@
|
||||
fs-extra "^7.0.1"
|
||||
semver "^5.4.1"
|
||||
|
||||
"@changesets/get-dependents-graph@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-1.2.0.tgz#6986927a5fec60bc6fcc76f966efae2ac30c05ed"
|
||||
integrity sha512-96NInEKpEZH8KvmXyh42PynXVAdq3kQ9VjAeswHtJ3umUCeTF42b/KVXaov+5P1RNnaUVtRuEwzs4syGuowDTw==
|
||||
dependencies:
|
||||
"@changesets/types" "^3.3.0"
|
||||
"@manypkg/get-packages" "^1.0.1"
|
||||
chalk "^2.1.0"
|
||||
fs-extra "^7.0.1"
|
||||
semver "^5.4.1"
|
||||
|
||||
"@changesets/get-release-plan@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-2.0.1.tgz#b95d8f1a3cc719ff4b42b9b9aae72458d8787c13"
|
||||
@@ -2770,7 +2807,7 @@
|
||||
resolved "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.3.2.tgz#8131a99035edd11aa7a44c341cbb05e668618c67"
|
||||
integrity sha512-SVqwYs5pULYjYT4op21F2pVbcrca4qA/bAA3FmFXKMN7Y+HcO8sbZUTx3TAy2VXulP2FACd1aC7f2nTuqSPbqg==
|
||||
|
||||
"@changesets/git@^1.0.5", "@changesets/git@^1.0.6":
|
||||
"@changesets/git@^1.0.5":
|
||||
version "1.0.6"
|
||||
resolved "https://registry.npmjs.org/@changesets/git/-/git-1.0.6.tgz#057e627e5d3fcb74bf6c18d7284e130ba5a7632e"
|
||||
integrity sha512-e0M06XuME3W5lGhz+CO0vLc60u+hLk/pYjOx/6GXEWuQrwtGgeycFIfRgRt8qTs664o1oKtVHBbd7ItpoWgFfA==
|
||||
@@ -2782,6 +2819,18 @@
|
||||
is-subdir "^1.1.1"
|
||||
spawndamnit "^2.0.0"
|
||||
|
||||
"@changesets/git@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/@changesets/git/-/git-1.1.0.tgz#ecaa8058ac87b450c09da0e74b68e6854cd0742c"
|
||||
integrity sha512-f/2rQynT+JiAL/V0V/GQdXhLkcb86ELg3UwH3fQO4wVdfUbE9NHIHq9ohJdH1Ymh0Lv48F/b38aWZ5v2sKiF3w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.4"
|
||||
"@changesets/errors" "^0.1.4"
|
||||
"@changesets/types" "^3.1.1"
|
||||
"@manypkg/get-packages" "^1.0.1"
|
||||
is-subdir "^1.1.1"
|
||||
spawndamnit "^2.0.0"
|
||||
|
||||
"@changesets/logger@^0.0.5":
|
||||
version "0.0.5"
|
||||
resolved "https://registry.npmjs.org/@changesets/logger/-/logger-0.0.5.tgz#68305dd5a643e336be16a2369cb17cdd8ed37d4c"
|
||||
@@ -2827,6 +2876,11 @@
|
||||
resolved "https://registry.npmjs.org/@changesets/types/-/types-3.2.0.tgz#d8306d7219c3b19b6d860ddeb9d7374e2dd6b035"
|
||||
integrity sha512-rAmPtOyXpisEEE25CchKNUAf2ApyAeuZ/h78YDoqKZaCk5tUD0lgYZGPIRV9WTPoqNjJULIym37ogc6pkax5jg==
|
||||
|
||||
"@changesets/types@^3.3.0":
|
||||
version "3.3.0"
|
||||
resolved "https://registry.npmjs.org/@changesets/types/-/types-3.3.0.tgz#04cd8184b2d2da760667bd89bf9b930938dbd96e"
|
||||
integrity sha512-rJamRo+OD/MQekImfIk07JZwYSB18iU6fYL8xOg0gfAiTh1a1+OlR1fPIxm55I7RsWw812is2YcPPwXdIewrhA==
|
||||
|
||||
"@changesets/write@^0.1.3":
|
||||
version "0.1.3"
|
||||
resolved "https://registry.npmjs.org/@changesets/write/-/write-0.1.3.tgz#00ae575af50274773d7493e77fb96838a08ad8ad"
|
||||
@@ -7686,9 +7740,9 @@
|
||||
integrity sha512-MBSp62AjB1KrSOI3gX9GekddXU5YYQAVA93+aSl78biBqoSzxg876aQY2KJK5Gnfbpqq7O2cadVX5kPAtBqIXw==
|
||||
|
||||
"@types/zen-observable@^0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d"
|
||||
integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg==
|
||||
version "0.8.2"
|
||||
resolved "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.2.tgz#808c9fa7e4517274ed555fa158f2de4b4f468e71"
|
||||
integrity sha512-HrCIVMLjE1MOozVoD86622S7aunluLb2PJdPfb3nYiEtohm8mIB/vyv0Fd37AdeMFrTUQXEunw78YloMA3Qilg==
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^v4.14.0":
|
||||
version "4.14.0"
|
||||
@@ -21260,7 +21314,7 @@ prepend-http@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
|
||||
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
|
||||
|
||||
prettier@^1.18.2:
|
||||
prettier@^1.18.2, prettier@^1.19.1:
|
||||
version "1.19.1"
|
||||
resolved "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
|
||||
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
|
||||
|
||||
Reference in New Issue
Block a user