Merge branch 'master' into lintMod
This commit is contained in:
@@ -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',
|
||||
});
|
||||
Reference in New Issue
Block a user