Merge branch 'master' of github.com:spotify/backstage into shmidt-i/location-update-results

This commit is contained in:
Ivan Shmidt
2020-05-29 10:03:47 +02:00
102 changed files with 814 additions and 1275 deletions
@@ -15,3 +15,4 @@
*/
require('jest-fetch-mock').enableMocks();
export {};
+4 -1
View File
@@ -14,8 +14,11 @@
* limitations under the License.
*/
import { PluginEnvironment } from './types';
describe('test', () => {
it('unbreaks the test runner', () => {
expect(true).toBeTruthy();
const unbreaker = {} as PluginEnvironment;
expect(unbreaker).toBeTruthy();
});
});
+8 -8
View File
@@ -17,7 +17,7 @@
import {
Entity,
FieldFormatEntityPolicy,
ForeignRootFieldsEntityPolicy,
NoForeignRootFieldsEntityPolicy,
ReservedFieldsEntityPolicy,
SchemaValidEntityPolicy,
} from './entity';
@@ -29,10 +29,10 @@ import { EntityPolicy } from './types';
class AllEntityPolicies implements EntityPolicy {
constructor(private readonly policies: EntityPolicy[]) {}
async apply(entity: Entity): Promise<Entity> {
async enforce(entity: Entity): Promise<Entity> {
let result = entity;
for (const policy of this.policies) {
result = await policy.apply(entity);
result = await policy.enforce(entity);
}
return result;
}
@@ -43,10 +43,10 @@ class AllEntityPolicies implements EntityPolicy {
class AnyEntityPolicy implements EntityPolicy {
constructor(private readonly policies: EntityPolicy[]) {}
async apply(entity: Entity): Promise<Entity> {
async enforce(entity: Entity): Promise<Entity> {
for (const policy of this.policies) {
try {
return await policy.apply(entity);
return await policy.enforce(entity);
} catch {
continue;
}
@@ -62,7 +62,7 @@ export class EntityPolicies implements EntityPolicy {
return EntityPolicies.allOf([
EntityPolicies.allOf([
new SchemaValidEntityPolicy(),
new ForeignRootFieldsEntityPolicy(),
new NoForeignRootFieldsEntityPolicy(),
new FieldFormatEntityPolicy(),
new ReservedFieldsEntityPolicy(),
]),
@@ -82,7 +82,7 @@ export class EntityPolicies implements EntityPolicy {
this.policy = policy;
}
apply(entity: Entity): Promise<Entity> {
return this.policy.apply(entity);
enforce(entity: Entity): Promise<Entity> {
return this.policy.enforce(entity);
}
}
+4 -4
View File
@@ -32,9 +32,9 @@ export type Entity = {
kind: string;
/**
* Optional metadata related to the entity.
* Metadata related to the entity.
*/
metadata?: EntityMeta;
metadata: EntityMeta;
/**
* The specification data describing the entity itself.
@@ -86,9 +86,9 @@ export type EntityMeta = {
* The name of the entity.
*
* Must be uniqe within the catalog at any given point in time, for any
* given namespace, for any given kind.
* given namespace + kind pair.
*/
name?: string;
name: string;
/**
* The namespace that the entity belongs to.
@@ -42,64 +42,64 @@ describe('FieldFormatEntityPolicy', () => {
});
it('works for the happy path', async () => {
await expect(policy.apply(data)).resolves.toBe(data);
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects bad apiVersion', async () => {
data.apiVersion = 7;
await expect(policy.apply(data)).rejects.toThrow(/apiVersion/);
await expect(policy.enforce(data)).rejects.toThrow(/apiVersion/);
data.apiVersion = 'a#b';
await expect(policy.apply(data)).rejects.toThrow(/apiVersion/);
await expect(policy.enforce(data)).rejects.toThrow(/apiVersion/);
});
it('rejects bad kind', async () => {
data.kind = 7;
await expect(policy.apply(data)).rejects.toThrow(/kind/);
await expect(policy.enforce(data)).rejects.toThrow(/kind/);
data.kind = 'a#b';
await expect(policy.apply(data)).rejects.toThrow(/kind/);
await expect(policy.enforce(data)).rejects.toThrow(/kind/);
});
it('handles missing metadata gracefully', async () => {
delete data.medatata;
await expect(policy.apply(data)).resolves.toBe(data);
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('handles missing spec gracefully', async () => {
delete data.spec;
await expect(policy.apply(data)).resolves.toBe(data);
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects bad name', async () => {
data.metadata.name = 7;
await expect(policy.apply(data)).rejects.toThrow(/name.*7/);
await expect(policy.enforce(data)).rejects.toThrow(/name.*7/);
data.metadata.name = 'a'.repeat(1000);
await expect(policy.apply(data)).rejects.toThrow(/name.*aaaa/);
await expect(policy.enforce(data)).rejects.toThrow(/name.*aaaa/);
});
it('rejects bad namespace', async () => {
data.metadata.namespace = 7;
await expect(policy.apply(data)).rejects.toThrow(/namespace.*7/);
await expect(policy.enforce(data)).rejects.toThrow(/namespace.*7/);
data.metadata.namespace = 'a'.repeat(1000);
await expect(policy.apply(data)).rejects.toThrow(/namespace.*aaaa/);
await expect(policy.enforce(data)).rejects.toThrow(/namespace.*aaaa/);
});
it('rejects bad label key', async () => {
data.metadata.labels['a#b'] = 'value';
await expect(policy.apply(data)).rejects.toThrow(/label.*a#b/i);
await expect(policy.enforce(data)).rejects.toThrow(/label.*a#b/i);
});
it('rejects bad label value', async () => {
data.metadata.labels.a = 'a#b';
await expect(policy.apply(data)).rejects.toThrow(/label.*a#b/i);
await expect(policy.enforce(data)).rejects.toThrow(/label.*a#b/i);
});
it('rejects bad annotation key', async () => {
data.metadata.annotations['a#b'] = 'value';
await expect(policy.apply(data)).rejects.toThrow(/annotation.*a#b/i);
await expect(policy.enforce(data)).rejects.toThrow(/annotation.*a#b/i);
});
it('rejects bad annotation value', async () => {
data.metadata.annotations.a = 7;
await expect(policy.apply(data)).rejects.toThrow(/annotation.*7/i);
await expect(policy.enforce(data)).rejects.toThrow(/annotation.*7/i);
});
});
@@ -32,7 +32,7 @@ export class FieldFormatEntityPolicy implements EntityPolicy {
this.validators = validators;
}
async apply(entity: Entity): Promise<Entity> {
async enforce(entity: Entity): Promise<Entity> {
function require(
field: string,
value: any,
@@ -65,23 +65,20 @@ export class FieldFormatEntityPolicy implements EntityPolicy {
require('apiVersion', entity.apiVersion, this.validators.isValidApiVersion);
require('kind', entity.kind, this.validators.isValidKind);
optional(
'metadata.name',
entity.metadata?.name,
this.validators.isValidEntityName,
);
require('metadata.name', entity.metadata.name, this.validators
.isValidEntityName);
optional(
'metadata.namespace',
entity.metadata?.namespace,
entity.metadata.namespace,
this.validators.isValidNamespace,
);
for (const [k, v] of Object.entries(entity.metadata?.labels ?? [])) {
for (const [k, v] of Object.entries(entity.metadata.labels ?? [])) {
require(`labels.${k}`, k, this.validators.isValidLabelKey);
require(`labels.${k}`, v, this.validators.isValidLabelValue);
}
for (const [k, v] of Object.entries(entity.metadata?.annotations ?? [])) {
for (const [k, v] of Object.entries(entity.metadata.annotations ?? [])) {
require(`annotations.${k}`, k, this.validators.isValidAnnotationKey);
require(`annotations.${k}`, v, this.validators.isValidAnnotationValue);
}
@@ -15,11 +15,11 @@
*/
import yaml from 'yaml';
import { ForeignRootFieldsEntityPolicy } from './ForeignRootFieldsEntityPolicy';
import { NoForeignRootFieldsEntityPolicy } from './NoForeignRootFieldsEntityPolicy';
describe('ForeignRootFieldsEntityPolicy', () => {
describe('NoForeignRootFieldsEntityPolicy', () => {
let data: any;
let policy: ForeignRootFieldsEntityPolicy;
let policy: NoForeignRootFieldsEntityPolicy;
beforeEach(() => {
data = yaml.parse(`
@@ -38,15 +38,15 @@ describe('ForeignRootFieldsEntityPolicy', () => {
spec:
custom: stuff
`);
policy = new ForeignRootFieldsEntityPolicy();
policy = new NoForeignRootFieldsEntityPolicy();
});
it('works for the happy path', async () => {
await expect(policy.apply(data)).resolves.toBe(data);
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects unknown root fields', async () => {
data.spec2 = {};
await expect(policy.apply(data)).rejects.toThrow(/spec2/i);
await expect(policy.enforce(data)).rejects.toThrow(/spec2/i);
});
});
@@ -22,14 +22,14 @@ const defaultKnownFields = ['apiVersion', 'kind', 'metadata', 'spec'];
/**
* Ensures that there are no foreign root fields in the entity.
*/
export class ForeignRootFieldsEntityPolicy implements EntityPolicy {
export class NoForeignRootFieldsEntityPolicy implements EntityPolicy {
private readonly knownFields: string[];
constructor(knownFields: string[] = defaultKnownFields) {
this.knownFields = knownFields;
}
async apply(entity: Entity): Promise<Entity> {
async enforce(entity: Entity): Promise<Entity> {
for (const field of Object.keys(entity)) {
if (!this.knownFields.includes(field)) {
throw new Error(`Unknown field ${field}`);
@@ -42,21 +42,23 @@ describe('ReservedFieldsEntityPolicy', () => {
});
it('works for the happy path', async () => {
await expect(policy.apply(data)).resolves.toBe(data);
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects reserved keys in the spec root', async () => {
data.spec.apiVersion = 'a/b';
await expect(policy.apply(data)).rejects.toThrow(/spec.*apiVersion/i);
await expect(policy.enforce(data)).rejects.toThrow(/spec.*apiVersion/i);
});
it('rejects reserved keys in labels', async () => {
data.metadata.labels.apiVersion = 'a';
await expect(policy.apply(data)).rejects.toThrow(/label.*apiVersion/i);
await expect(policy.enforce(data)).rejects.toThrow(/label.*apiVersion/i);
});
it('rejects reserved keys in annotations', async () => {
data.metadata.annotations.apiVersion = 'a';
await expect(policy.apply(data)).rejects.toThrow(/annotation.*apiVersion/i);
await expect(policy.enforce(data)).rejects.toThrow(
/annotation.*apiVersion/i,
);
});
});
@@ -43,19 +43,19 @@ export class ReservedFieldsEntityPolicy implements EntityPolicy {
];
}
async apply(entity: Entity): Promise<Entity> {
async enforce(entity: Entity): Promise<Entity> {
for (const field of this.reservedFields) {
if (entity.spec?.hasOwnProperty(field)) {
throw new Error(
`The spec may not contain the field ${field}, because it has reserved meaning`,
);
}
if (entity.metadata?.labels?.hasOwnProperty(field)) {
if (entity.metadata.labels?.hasOwnProperty(field)) {
throw new Error(
`A label may not have the field ${field}, because it has reserved meaning`,
);
}
if (entity.metadata?.annotations?.hasOwnProperty(field)) {
if (entity.metadata.annotations?.hasOwnProperty(field)) {
throw new Error(
`An annotation may not have the field ${field}, because it has reserved meaning`,
);
@@ -43,7 +43,7 @@ describe('SchemaValidEntityPolicy', () => {
});
it('works for the happy path', async () => {
await expect(policy.apply(data)).resolves.toBe(data);
await expect(policy.enforce(data)).resolves.toBe(data);
});
//
@@ -51,113 +51,113 @@ describe('SchemaValidEntityPolicy', () => {
//
it('rejects wrong root type', async () => {
await expect(policy.apply((7 as unknown) as Entity)).rejects.toThrow(
await expect(policy.enforce((7 as unknown) as Entity)).rejects.toThrow(
/object/,
);
});
it('rejects missing apiVersion', async () => {
delete data.apiVersion;
await expect(policy.apply(data)).rejects.toThrow(/apiVersion/);
await expect(policy.enforce(data)).rejects.toThrow(/apiVersion/);
});
it('rejects bad apiVersion type', async () => {
data.apiVersion = 7;
await expect(policy.apply(data)).rejects.toThrow(/apiVersion/);
await expect(policy.enforce(data)).rejects.toThrow(/apiVersion/);
});
it('rejects missing kind', async () => {
delete data.kind;
await expect(policy.apply(data)).rejects.toThrow(/kind/);
await expect(policy.enforce(data)).rejects.toThrow(/kind/);
});
it('rejects bad kind type', async () => {
data.kind = 7;
await expect(policy.apply(data)).rejects.toThrow(/kind/);
await expect(policy.enforce(data)).rejects.toThrow(/kind/);
});
//
// metadata
//
it('accepts missing metadata', async () => {
delete data.medatata;
await expect(policy.apply(data)).resolves.toBe(data);
it('rejects missing metadata', async () => {
delete data.metadata;
await expect(policy.enforce(data)).rejects.toThrow(/metadata/);
});
it('rejects bad metadata type', async () => {
data.metadata = 7;
await expect(policy.apply(data)).rejects.toThrow(/metadata/);
await expect(policy.enforce(data)).rejects.toThrow(/metadata/);
});
it('accepts missing uid', async () => {
delete data.metadata.uid;
await expect(policy.apply(data)).resolves.toBe(data);
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects bad uid type', async () => {
data.metadata.uid = 7;
await expect(policy.apply(data)).rejects.toThrow(/uid/);
await expect(policy.enforce(data)).rejects.toThrow(/uid/);
});
it('accepts missing etag', async () => {
delete data.metadata.etag;
await expect(policy.apply(data)).resolves.toBe(data);
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects bad etag type', async () => {
data.metadata.etag = 7;
await expect(policy.apply(data)).rejects.toThrow(/etag/);
await expect(policy.enforce(data)).rejects.toThrow(/etag/);
});
it('accepts missing generation', async () => {
delete data.metadata.generation;
await expect(policy.apply(data)).resolves.toBe(data);
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects bad generation type', async () => {
data.metadata.generation = 'a';
await expect(policy.apply(data)).rejects.toThrow(/generation/);
await expect(policy.enforce(data)).rejects.toThrow(/generation/);
});
it('accepts missing name', async () => {
it('rejects missing name', async () => {
delete data.metadata.name;
await expect(policy.apply(data)).resolves.toBe(data);
await expect(policy.enforce(data)).rejects.toThrow(/name/);
});
it('rejects bad name type', async () => {
data.metadata.name = 7;
await expect(policy.apply(data)).rejects.toThrow(/name/);
await expect(policy.enforce(data)).rejects.toThrow(/name/);
});
it('accepts missing namespace', async () => {
delete data.metadata.namespace;
await expect(policy.apply(data)).resolves.toBe(data);
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects bad namespace type', async () => {
data.metadata.namespace = 7;
await expect(policy.apply(data)).rejects.toThrow(/namespace/);
await expect(policy.enforce(data)).rejects.toThrow(/namespace/);
});
it('accepts missing labels', async () => {
delete data.metadata.labels;
await expect(policy.apply(data)).resolves.toBe(data);
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects bad labels type', async () => {
data.metadata.labels = 7;
await expect(policy.apply(data)).rejects.toThrow(/labels/);
await expect(policy.enforce(data)).rejects.toThrow(/labels/);
});
it('accepts missing annotations', async () => {
delete data.metadata.annotations;
await expect(policy.apply(data)).resolves.toBe(data);
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects bad annotations type', async () => {
data.metadata.annotations = 7;
await expect(policy.apply(data)).rejects.toThrow(/annotations/);
await expect(policy.enforce(data)).rejects.toThrow(/annotations/);
});
//
@@ -166,11 +166,11 @@ describe('SchemaValidEntityPolicy', () => {
it('accepts missing spec', async () => {
delete data.spec;
await expect(policy.apply(data)).resolves.toBe(data);
await expect(policy.enforce(data)).resolves.toBe(data);
});
it('rejects non-object spec', async () => {
data.spec = 7;
await expect(policy.apply(data)).rejects.toThrow(/spec/);
await expect(policy.enforce(data)).rejects.toThrow(/spec/);
});
});
@@ -47,12 +47,12 @@ const DEFAULT_ENTITY_SCHEMA = yup.object({
'The generation must be an integer greater than zero',
value => value === undefined || (value === (value | 0) && value > 0),
),
name: yup.string().notRequired(),
name: yup.string().required(),
namespace: yup.string().notRequired(),
labels: yup.object<Record<string, string>>().notRequired(),
annotations: yup.object<Record<string, string>>().notRequired(),
})
.notRequired(),
.required(),
spec: yup.object({}).notRequired(),
});
@@ -70,7 +70,7 @@ export class SchemaValidEntityPolicy implements EntityPolicy {
this.schema = schema;
}
async apply(entity: Entity): Promise<Entity> {
async enforce(entity: Entity): Promise<Entity> {
try {
return await this.schema.validate(entity, { strict: true });
} catch (e) {
@@ -15,6 +15,6 @@
*/
export { FieldFormatEntityPolicy } from './FieldFormatEntityPolicy';
export { ForeignRootFieldsEntityPolicy } from './ForeignRootFieldsEntityPolicy';
export { NoForeignRootFieldsEntityPolicy } from './NoForeignRootFieldsEntityPolicy';
export { ReservedFieldsEntityPolicy } from './ReservedFieldsEntityPolicy';
export { SchemaValidEntityPolicy } from './SchemaValidEntityPolicy';
@@ -50,7 +50,7 @@ export class ComponentV1beta1Policy implements EntityPolicy {
});
}
async apply(envelope: Entity): Promise<Entity> {
async enforce(envelope: Entity): Promise<Entity> {
if (
envelope.apiVersion !== 'backstage.io/v1beta1' ||
envelope.kind !== 'Component'
+1 -1
View File
@@ -28,5 +28,5 @@ export type EntityPolicy = {
* @returns The incoming entity, or a mutated version of the same
* @throws An error if the entity should be rejected
*/
apply(entity: Entity): Promise<Entity>;
enforce(entity: Entity): Promise<Entity>;
};
@@ -21,7 +21,7 @@ import privateExports, {
defaultSystemIcons,
} from '@backstage/core-api';
import ErrorPage from '../layout/ErrorPage';
import { ErrorPage } from '../layout/ErrorPage';
import { lightTheme, darkTheme } from '@backstage/theme';
const { PrivateAppImpl } = privateExports;
@@ -16,7 +16,7 @@
import React from 'react';
import CodeSnippet from './CodeSnippet';
import InfoCard from '../../layout/InfoCard';
import { InfoCard } from '../../layout/InfoCard';
export default {
title: 'CodeSnippet',
@@ -16,33 +16,10 @@
import React from 'react';
import CopyTextButton from '.';
import {
ApiProvider,
errorApiRef,
ApiRegistry,
ErrorApi,
} from '@backstage/core-api';
export default {
title: 'CopyTextButton',
component: CopyTextButton,
decorators: [
(storyFn: () => JSX.Element) => {
// TODO: move this to common storybook config, requires core package to be separate from components
const registry = ApiRegistry.from([
[
errorApiRef,
{
post(error) {
// eslint-disable-next-line no-alert
window.alert(`Component posted error, ${error}`);
},
} as ErrorApi,
],
]);
return <ApiProvider apis={registry} children={storyFn()} />;
},
],
};
export const Default = () => (
@@ -16,8 +16,8 @@
import React, { FC } from 'react';
import { makeStyles } from '@material-ui/core';
import InfoCard from '../../layout/InfoCard';
import { Props as BottomLinkProps } from '../../layout/BottomLink';
import { InfoCard } from '../../layout/InfoCard';
import { BottomLinkProps } from '../../layout/BottomLink';
import CircleProgress from './CircleProgress';
type Props = {
@@ -36,7 +36,7 @@ const useStyles = makeStyles({
},
});
const ProgressCard: FC<Props> = (props) => {
const ProgressCard: FC<Props> = props => {
const classes = useStyles(props);
const { title, subheader, progress, deepLink, variant } = props;
@@ -24,7 +24,7 @@ import {
StatusWarning,
} from './Status';
import Table from '../Table';
import InfoCard from '../../layout/InfoCard';
import { InfoCard } from '../../layout/InfoCard';
export default {
title: 'Status',
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { FC } from 'react';
import InfoCard from '../../layout/InfoCard';
import { InfoCard } from '../../layout/InfoCard';
import { Grid } from '@material-ui/core';
import StructuredMetadataTable from '.';
@@ -17,7 +17,7 @@
import React from 'react';
import TrendLine from '.';
import Table from '../Table';
import InfoCard from '../../layout/InfoCard';
import { InfoCard } from '../../layout/InfoCard';
export default {
title: 'TrendLine',
-39
View File
@@ -1,39 +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 { SvgIconProps } from '@material-ui/core';
import PeopleIcon from '@material-ui/icons/People';
import PersonIcon from '@material-ui/icons/Person';
import React, { FC } from 'react';
import { useApp } from '@backstage/core-api';
import { IconComponent, SystemIconKey, SystemIcons } from './types';
export const defaultSystemIcons: SystemIcons = {
user: PersonIcon,
group: PeopleIcon,
};
const overridableSystemIcon = (key: SystemIconKey): IconComponent => {
const Component: FC<SvgIconProps> = props => {
const app = useApp();
const Icon = app.getSystemIcon(key);
return <Icon {...props} />;
};
return Component;
};
export const UserIcon = overridableSystemIcon('user');
export const GroupIcon = overridableSystemIcon('group');
+3 -15
View File
@@ -16,22 +16,11 @@
export * from '@backstage/core-api';
export * from './api';
export { default as Page } from './layout/Page';
export { gradients, pageTheme } from './layout/Page';
export type { PageTheme } from './layout/Page';
export * from './api-wrappers';
export * from './layout';
export { default as CodeSnippet } from './components/CodeSnippet';
export { default as Content } from './layout/Content/Content';
export { default as ContentHeader } from './layout/ContentHeader/ContentHeader';
export { default as DismissableBanner } from './components/DismissableBanner';
export { default as Header } from './layout/Header/Header';
export { default as HeaderLabel } from './layout/HeaderLabel';
export { default as HomepageTimer } from './layout/HomepageTimer';
export { default as InfoCard } from './layout/InfoCard';
export { CardTab, TabbedCard } from './layout/TabbedCard';
export { default as ErrorBoundary } from './layout/ErrorBoundary';
export * from './layout/Sidebar';
export * from './layout/LoginPage';
export { AlertDisplay } from './components/AlertDisplay';
export { default as HorizontalScrollGrid } from './components/HorizontalScrollGrid';
export { default as ProgressCard } from './components/ProgressBars/ProgressCard';
@@ -50,4 +39,3 @@ export { default as TrendLine } from './components/TrendLine';
export { FeatureCalloutCircular } from './components/FeatureDiscovery/FeatureCalloutCircular';
export * from './components/Status';
export { default as WarningPanel } from './components/WarningPanel';
export type { IconComponent } from './icons';
@@ -17,7 +17,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import BottomLink from './BottomLink';
import { BottomLink } from './BottomLink';
const minProps = {
title: 'A deepLink title',
@@ -41,13 +41,13 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
},
}));
export type Props = {
export type BottomLinkProps = {
link: string;
title: string;
onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
};
const BottomLink: FC<Props> = ({ link, title, onClick }) => {
export const BottomLink: FC<BottomLinkProps> = ({ link, title, onClick }) => {
const classes = useStyles();
return (
@@ -68,5 +68,3 @@ const BottomLink: FC<Props> = ({ link, title, onClick }) => {
</div>
);
};
export default BottomLink;
+2 -2
View File
@@ -14,5 +14,5 @@
* limitations under the License.
*/
export { default } from './BottomLink';
export type { Props } from './BottomLink';
export { BottomLink } from './BottomLink';
export type { BottomLinkProps } from './BottomLink';
+1 -3
View File
@@ -42,7 +42,7 @@ type Props = {
className?: string;
};
const Content: FC<Props> = ({
export const Content: FC<Props> = ({
className,
stretch,
noPadding,
@@ -62,5 +62,3 @@ const Content: FC<Props> = ({
</article>
);
};
export default Content;
@@ -14,5 +14,4 @@
* limitations under the License.
*/
export * from './icons';
export * from './types';
export { Content } from './Content';
@@ -16,7 +16,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import ContentHeader from './ContentHeader';
import { ContentHeader } from './ContentHeader';
import { wrapInThemedTestApp } from '@backstage/test-utils';
jest.mock('react-helmet', () => {
@@ -75,7 +75,7 @@ type ContentHeaderProps = {
description?: string;
};
const ContentHeader: FC<ContentHeaderProps> = ({
export const ContentHeader: FC<ContentHeaderProps> = ({
description,
title,
titleComponent: TitleComponent = undefined,
@@ -105,5 +105,3 @@ const ContentHeader: FC<ContentHeaderProps> = ({
</Fragment>
);
};
export default ContentHeader;
@@ -14,8 +14,4 @@
* limitations under the License.
*/
describe('dummy', () => {
it('dummy', () => {
expect(1).toBe(1);
});
});
export { ContentHeader } from './ContentHeader';
@@ -26,7 +26,7 @@ type State = {
errorInfo?: ErrorInfo;
};
const ErrorBoundary: ComponentClass<
export const ErrorBoundary: ComponentClass<
Props,
State
> = class ErrorBoundary extends Component<Props, State> {
@@ -57,8 +57,6 @@ const ErrorBoundary: ComponentClass<
}
};
export default ErrorBoundary;
type EProps = {
error?: Error;
slackChannel?: string;
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { default } from './ErrorBoundary';
export { ErrorBoundary } from './ErrorBoundary';
@@ -16,7 +16,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import ErrorPage from './ErrorPage';
import { ErrorPage } from './ErrorPage';
import { wrapInThemedTestApp } from '@backstage/test-utils';
describe('<ErrorPage/>', () => {
@@ -18,7 +18,7 @@ import React from 'react';
import { Typography, Link, Grid } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import { BackstageTheme } from '@backstage/theme';
import MicDrop from './MicDrop';
import { MicDrop } from './MicDrop';
import { useHistory } from 'react-router';
interface IErrorPageProps {
@@ -38,7 +38,7 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
},
}));
const ErrorPage = ({ status, statusMessage }: IErrorPageProps) => {
export const ErrorPage = ({ status, statusMessage }: IErrorPageProps) => {
const classes = useStyles();
const history = useHistory();
@@ -63,5 +63,3 @@ const ErrorPage = ({ status, statusMessage }: IErrorPageProps) => {
</Grid>
);
};
export default ErrorPage;
@@ -26,7 +26,7 @@ const useStyles = makeStyles({
},
});
const MicDrop = () => {
export const MicDrop = () => {
const classes = useStyles();
return (
<svg
@@ -158,5 +158,3 @@ const MicDrop = () => {
</svg>
);
};
export default MicDrop;
+1 -1
View File
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { default } from './ErrorPage';
export { ErrorPage } from './ErrorPage';
@@ -14,9 +14,9 @@
* limitations under the License.
*/
import React from 'react';
import Header from '.';
import HeaderLabel from '../HeaderLabel';
import Page, { pageTheme } from '../Page';
import { Header } from '.';
import { HeaderLabel } from '../HeaderLabel';
import { Page, pageTheme } from '../Page';
export default {
title: 'Header',
@@ -17,7 +17,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { wrapInThemedTestApp } from '@backstage/test-utils';
import Header from './Header';
import { Header } from './Header';
jest.mock('react-helmet', () => {
return {
+1 -3
View File
@@ -20,7 +20,7 @@ import { Typography, Tooltip, makeStyles } from '@material-ui/core';
import { BackstageTheme } from '@backstage/theme';
import { Theme } from '../Page/Page';
import Waves from './Waves';
import { Waves } from './Waves';
const useStyles = makeStyles<BackstageTheme>(theme => ({
header: {
@@ -193,5 +193,3 @@ export const Header: FC<Props> = ({
</Fragment>
);
};
export default Header;
@@ -17,7 +17,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { pageTheme } from '../Page/PageThemeProvider';
import Waves from './Waves';
import { Waves } from './Waves';
describe('<Waves/>', () => {
it('should render svg', () => {
+1 -3
View File
@@ -35,7 +35,7 @@ type Props = {
theme: PageTheme;
};
const Waves: FC<Props> = ({ theme }) => {
export const Waves: FC<Props> = ({ theme }) => {
const classes = useStyles();
const [color1, color2] = theme.gradient.colors;
@@ -139,5 +139,3 @@ const Waves: FC<Props> = ({ theme }) => {
</svg>
);
};
export default Waves;
+1 -1
View File
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { default } from './Header';
export { Header } from './Header';
@@ -17,7 +17,7 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { wrapInThemedTestApp, Keyboard } from '@backstage/test-utils';
import HeaderActionMenu from './HeaderActionMenu';
import { HeaderActionMenu } from './HeaderActionMenu';
describe('<ComponentContextMenu />', () => {
it('renders without any items and without exploding', () => {
@@ -24,7 +24,7 @@ import {
Popover,
ListItemTextProps,
} from '@material-ui/core';
import { default as KebabMenuIcon } from './MenuVertical';
import { VerticalMenuIcon } from './VerticalMenuIcon';
type ActionItemProps = {
label?: ListItemTextProps['primary'];
@@ -66,7 +66,9 @@ export type HeaderActionMenuProps = {
actionItems: ActionItemProps[];
};
const HeaderActionMenu: FC<HeaderActionMenuProps> = ({ actionItems }) => {
export const HeaderActionMenu: FC<HeaderActionMenuProps> = ({
actionItems,
}) => {
const [open, setOpen] = React.useState(false);
const anchorElRef = React.useRef(null);
@@ -84,7 +86,7 @@ const HeaderActionMenu: FC<HeaderActionMenuProps> = ({ actionItems }) => {
padding: 0,
}}
>
<KebabMenuIcon titleAccess="menu" style={{ fontSize: 40 }} />
<VerticalMenuIcon titleAccess="menu" style={{ fontSize: 40 }} />
</IconButton>
<Popover
open={open}
@@ -104,5 +106,3 @@ const HeaderActionMenu: FC<HeaderActionMenuProps> = ({ actionItems }) => {
</Fragment>
);
};
export default HeaderActionMenu;
@@ -17,11 +17,9 @@
import React from 'react';
import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';
const SvgMenuVertical = (props: SvgIconProps) =>
export const VerticalMenuIcon = (props: SvgIconProps) =>
React.createElement(
SvgIcon,
props,
<path d="M11 3a1 1 0 00-1 1v2a1 1 0 001 1h2a1 1 0 001-1V4a1 1 0 00-1-1h-2zm0 7a1 1 0 00-1 1v2a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 00-1-1h-2zm0 7a1 1 0 00-1 1v2a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 00-1-1h-2z" />,
);
export default SvgMenuVertical;
@@ -14,8 +14,4 @@
* limitations under the License.
*/
import { ComponentType } from 'react';
import { SvgIconProps } from '@material-ui/core';
export type IconComponent = ComponentType<SvgIconProps>;
export type SystemIconKey = 'user' | 'group';
export type SystemIcons = { [key in SystemIconKey]: IconComponent };
export { HeaderActionMenu } from './HeaderActionMenu';
@@ -17,7 +17,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { wrapInThemedTestApp } from '@backstage/test-utils';
import HeaderLabel from './HeaderLabel';
import { HeaderLabel } from './HeaderLabel';
describe('<HeaderLabel />', () => {
it('should have a label', () => {
@@ -56,7 +56,7 @@ type HeaderLabelProps = {
url?: string;
};
const HeaderLabel: FC<HeaderLabelProps> = ({ label, value, url }) => {
export const HeaderLabel: FC<HeaderLabelProps> = ({ label, value, url }) => {
const classes = useStyles();
const content = (
<HeaderLabelContent
@@ -71,5 +71,3 @@ const HeaderLabel: FC<HeaderLabelProps> = ({ label, value, url }) => {
</span>
);
};
export default HeaderLabel;
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { default } from './HeaderLabel';
export { HeaderLabel } from './HeaderLabel';
@@ -15,7 +15,7 @@
*/
import React, { FC } from 'react';
import HeaderLabel from '../HeaderLabel';
import { HeaderLabel } from '../HeaderLabel';
const timeFormat = { hour: '2-digit', minute: '2-digit' };
const utcOptions = { timeZone: 'UTC', ...timeFormat };
@@ -44,7 +44,7 @@ function getTimes() {
return { timeNY, timeUTC, timeTYO, timeSTO };
}
const HomePageTimer: FC<{}> = () => {
export const HomepageTimer: FC<{}> = () => {
const [{ timeNY, timeUTC, timeTYO, timeSTO }, setTimes] = React.useState(
defaultTimes,
);
@@ -70,5 +70,3 @@ const HomePageTimer: FC<{}> = () => {
</>
);
};
export default HomePageTimer;
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { default } from './HomepageTimer';
export { HomepageTimer } from './HomepageTimer';
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { FC } from 'react';
import InfoCard from '.';
import { InfoCard } from '.';
import { Grid } from '@material-ui/core';
const cardContentStyle = { height: 200, width: 500 };
@@ -17,7 +17,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { wrapInTestApp } from '@backstage/test-utils';
import InfoCard from './InfoCard';
import { InfoCard } from './InfoCard';
const minProps = {
title: 'Some title',
@@ -25,8 +25,8 @@ import {
makeStyles,
} from '@material-ui/core';
import classNames from 'classnames';
import ErrorBoundary from '../ErrorBoundary';
import BottomLink, { Props as BottomLinkProps } from '../BottomLink';
import { ErrorBoundary } from '../ErrorBoundary';
import { BottomLink, BottomLinkProps } from '../BottomLink';
const useStyles = makeStyles(theme => ({
header: {
@@ -137,7 +137,7 @@ type Props = {
noPadding?: boolean;
};
const InfoCard: FC<Props> = ({
export const InfoCard: FC<Props> = ({
title,
subheader,
divider,
@@ -214,5 +214,3 @@ const InfoCard: FC<Props> = ({
</Card>
);
};
export default InfoCard;
+1 -1
View File
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { default } from './InfoCard';
export { InfoCard } from './InfoCard';
@@ -16,10 +16,11 @@
import React, { FC, useState } from 'react';
import GitHubIcon from '@material-ui/icons/GitHub';
import Page from '../Page';
import Header from '../Header';
import Content from '../Content/Content';
import ContentHeader from '../ContentHeader/ContentHeader';
import { Page } from '../Page';
import { Header } from '../Header';
import { Content } from '../Content';
import { ContentHeader } from '../ContentHeader';
import { InfoCard } from '../InfoCard/InfoCard';
import {
Grid,
Typography,
@@ -29,7 +30,6 @@ import {
ListItem,
Link,
} from '@material-ui/core';
import InfoCard from '../InfoCard/InfoCard';
enum AuthType {
GitHub,
+1 -3
View File
@@ -35,7 +35,7 @@ type Props = {
theme?: PageTheme;
};
const Page: FC<Props> = ({ theme = pageTheme.home, children }) => {
export const Page: FC<Props> = ({ theme = pageTheme.home, children }) => {
const classes = useStyles();
return (
<Theme.Provider value={theme}>
@@ -43,5 +43,3 @@ const Page: FC<Props> = ({ theme = pageTheme.home, children }) => {
</Theme.Provider>
);
};
export default Page;
+1 -1
View File
@@ -14,6 +14,6 @@
* limitations under the License.
*/
export { default } from './Page';
export { Page } from './Page';
export { gradients, pageTheme } from './PageThemeProvider';
export type { PageTheme } from './PageThemeProvider';
+3 -3
View File
@@ -16,10 +16,10 @@
import { makeStyles } from '@material-ui/core';
import clsx from 'clsx';
import React, { FC, useRef, useState } from 'react';
import React, { FC, useRef, useState, useContext } from 'react';
import { sidebarConfig, SidebarContext } from './config';
import { BackstageTheme } from '@backstage/theme';
import { useSidebarPinState } from '../../hooks/useSidebarPinState';
import { SidebarPinStateContext } from './Page';
const useStyles = makeStyles<BackstageTheme>(theme => ({
root: {
@@ -76,7 +76,7 @@ export const Sidebar: FC<Props> = ({
const classes = useStyles();
const [state, setState] = useState(State.Closed);
const hoverTimerRef = useRef<number>();
const { isPinned } = useSidebarPinState();
const { isPinned } = useContext(SidebarPinStateContext);
const handleOpen = () => {
if (isPinned) {
+1 -1
View File
@@ -22,12 +22,12 @@ import {
Typography,
Badge,
} from '@material-ui/core';
import { IconComponent } from '@backstage/core-api';
import SearchIcon from '@material-ui/icons/Search';
import clsx from 'clsx';
import React, { FC, useContext, useState, KeyboardEventHandler } from 'react';
import { NavLink } from 'react-router-dom';
import { sidebarConfig, SidebarContext } from './config';
import { IconComponent } from '../../icons';
const useStyles = makeStyles<Theme>(theme => {
const {
+1 -1
View File
@@ -18,7 +18,7 @@ import { makeStyles } from '@material-ui/core';
import React, { createContext, FC, useEffect, useState } from 'react';
import { sidebarConfig } from './config';
import { BackstageTheme } from '@backstage/theme';
import { LocalStorage } from '../../data/localStorage';
import { LocalStorage } from './localStorage';
const useStyles = makeStyles<BackstageTheme, { isPinned: boolean }>({
root: {
+18 -6
View File
@@ -14,10 +14,22 @@
* limitations under the License.
*/
export * from './Bar';
export * from './Page';
export * from './Items';
export * from './Intro';
export * from './UserBadge';
export * from './config';
export { Sidebar } from './Bar';
export { SidebarPage, SidebarPinStateContext } from './Page';
export type { SidebarPinStateContextType } from './Page';
export {
SidebarDivider,
SidebarItem,
SidebarSearchField,
SidebarSpace,
SidebarSpacer,
} from './Items';
export { IntroCard, SidebarIntro } from './Intro';
export { SidebarUserBadge } from './UserBadge';
export {
SIDEBAR_INTRO_LOCAL_STORAGE,
SidebarContext,
sidebarConfig,
} from './config';
export type { SidebarContextType } from './config';
export { SidebarThemeToggle } from './SidebarThemeToggle';
@@ -26,8 +26,8 @@ import {
Tab,
TabProps,
} from '@material-ui/core';
import BottomLink, { Props as BottomLinkProps } from '../BottomLink';
import ErrorBoundary from '../ErrorBoundary/ErrorBoundary';
import { BottomLink, BottomLinkProps } from '../BottomLink';
import { ErrorBoundary } from '../ErrorBoundary';
const useTabsStyles = makeStyles(theme => ({
root: {
+1 -1
View File
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export * from './TabbedCard';
export { CardTab, TabbedCard } from './TabbedCard';
@@ -13,16 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useContext } from 'react';
import { SidebarPinStateContext } from '../layout/Sidebar';
export function useSidebarPinState() {
const { isPinned, toggleSidebarPinState } = useContext(
SidebarPinStateContext,
);
return {
isPinned,
toggleSidebarPinState,
};
}
export * from './Content';
export * from './ContentHeader';
export * from './ErrorBoundary';
export * from './Header';
export * from './HeaderLabel';
export * from './HomepageTimer';
export * from './InfoCard';
export * from './LoginPage';
export * from './Page';
export * from './Sidebar';
export * from './TabbedCard';
+16
View File
@@ -0,0 +1,16 @@
import {
ApiRegistry,
alertApiRef,
errorApiRef,
AlertApiForwarder,
ErrorApiForwarder,
ErrorAlerter,
} from '@backstage/core';
const builder = ApiRegistry.builder();
const alertApi = builder.add(alertApiRef, new AlertApiForwarder());
builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder()));
export const apis = builder.build();
+11 -7
View File
@@ -3,14 +3,18 @@ import { addDecorator, addParameters } from '@storybook/react';
import { lightTheme, darkTheme } from '@backstage/theme';
import { CssBaseline, ThemeProvider } from '@material-ui/core';
import { useDarkMode } from 'storybook-dark-mode';
import { Content } from '@backstage/core';
import { Content, ApiProvider, AlertDisplay } from '@backstage/core';
import { apis } from './apis';
addDecorator((story) => (
<ThemeProvider theme={useDarkMode() ? darkTheme : lightTheme}>
<CssBaseline>
<Content>{story()}</Content>
</CssBaseline>
</ThemeProvider>
addDecorator(story => (
<ApiProvider apis={apis}>
<ThemeProvider theme={useDarkMode() ? darkTheme : lightTheme}>
<CssBaseline>
<AlertDisplay />
<Content>{story()}</Content>
</CssBaseline>
</ThemeProvider>
</ApiProvider>
));
addParameters({