Fix validation file

This commit is contained in:
Marek Calus
2020-10-20 08:24:55 +02:00
232 changed files with 2709 additions and 1567 deletions
+1 -1
View File
@@ -34,8 +34,8 @@
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@octokit/rest": "^18.0.0",
"@roadiehq/backstage-plugin-github-insights": "^0.2.7",
"@roadiehq/backstage-plugin-github-pull-requests": "^0.5.1",
"@roadiehq/backstage-plugin-github-insights": "^0.2.2",
"@roadiehq/backstage-plugin-travis-ci": "^0.2.3",
"dayjs": "^1.9.1",
"history": "^5.0.0",
+17
View File
@@ -48,6 +48,23 @@
}
</style>
<title><%= app.title %></title>
<% if (app.googleAnalyticsTrackingId && typeof app.googleAnalyticsTrackingId
=== 'string') { %>
<script
async
src="https://www.googletagmanager.com/gtag/js?id=<%= app.googleAnalyticsTrackingId %>"
></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', '<%= app.googleAnalyticsTrackingId %>');
</script>
<% } %>
</head>
<body style="margin: 0;">
<noscript>You need to enable JavaScript to run this app.</noscript>
@@ -28,6 +28,7 @@ import {
Group,
inclusiveStartDateOf,
Maybe,
MetricData,
ProductCost,
Project,
ProjectGrowthAlert,
@@ -116,19 +117,33 @@ export class ExampleCostInsightsClient implements CostInsightsApi {
return projects;
}
async getGroupDailyCost(
group: string,
metric: string | null,
async getDailyMetricData(
metric: string,
intervals: string,
): Promise<Cost> {
): Promise<MetricData> {
const aggregation = aggregationFor(
durationOf(intervals),
metric ? 0.3 : 8_000,
);
const groupDailyCost: Cost = await this.request(
{ group, metric, intervals },
100_000,
).map(entry => ({ ...entry, amount: Math.round(entry.amount) }));
const cost: MetricData = await this.request(
{ metric, intervals },
{
format: 'number',
aggregation: aggregation,
change: changeOf(aggregation),
trendline: trendlineOf(aggregation),
},
);
return cost;
}
async getGroupDailyCost(group: string, intervals: string): Promise<Cost> {
const aggregation = aggregationFor(durationOf(intervals), 8_000);
const groupDailyCost: Cost = await this.request(
{ group, intervals },
{
id: metric, // costs with null ids will appear as "All Projects" in Cost Overview panel
aggregation: aggregation,
change: changeOf(aggregation),
trendline: trendlineOf(aggregation),
@@ -138,17 +153,10 @@ export class ExampleCostInsightsClient implements CostInsightsApi {
return groupDailyCost;
}
async getProjectDailyCost(
project: string,
metric: string | null,
intervals: string,
): Promise<Cost> {
const aggregation = aggregationFor(
durationOf(intervals),
metric ? 0.1 : 1_500,
);
async getProjectDailyCost(project: string, intervals: string): Promise<Cost> {
const aggregation = aggregationFor(durationOf(intervals), 1_500);
const projectDailyCost: Cost = await this.request(
{ project, metric, intervals },
{ project, intervals },
{
id: 'project-a',
aggregation: aggregation,
@@ -17,7 +17,7 @@
import { mergeDatabaseConfig } from './config';
describe('config', () => {
describe(mergeDatabaseConfig, () => {
describe('mergeDatabaseConfig', () => {
it('does not mutate the input object', () => {
const input = {
original: 'key',
@@ -26,7 +26,7 @@ describe('database connection', () => {
},
]);
describe(createDatabaseClient, () => {
describe('createDatabaseClient', () => {
it('returns a postgres connection', () => {
expect(
createDatabaseClient(
@@ -44,7 +44,7 @@ describe('postgres', () => {
},
]);
describe(buildPgDatabaseConfig, () => {
describe('buildPgDatabaseConfig', () => {
it('builds a postgres config', () => {
const mockConnection = createMockConnection();
@@ -125,7 +125,7 @@ describe('postgres', () => {
});
});
describe(getPgConnectionConfig, () => {
describe('getPgConnectionConfig', () => {
it('returns the connection object back', () => {
const mockConnection = createMockConnection();
const config = createConfig(mockConnection);
@@ -163,7 +163,7 @@ describe('postgres', () => {
});
});
describe(createPgDatabaseClient, () => {
describe('createPgDatabaseClient', () => {
it('creates a postgres knex instance', () => {
expect(
createPgDatabaseClient(
@@ -188,8 +188,8 @@ describe('postgres', () => {
});
});
describe(parsePgConnectionString, () => {
it('parses a connection string uri ', () => {
describe('parsePgConnectionString', () => {
it('parses a connection string uri', () => {
expect(
parsePgConnectionString(
'postgresql://postgres:pass@foobar:5432/dbname?ssl=true',
@@ -32,7 +32,7 @@ describe('sqlite3', () => {
},
]);
describe(buildSqliteDatabaseConfig, () => {
describe('buildSqliteDatabaseConfig', () => {
it('buidls a string connection', () => {
expect(buildSqliteDatabaseConfig(createConfig(':memory:'))).toEqual({
client: 'sqlite3',
@@ -72,7 +72,7 @@ describe('sqlite3', () => {
});
});
describe(createSqliteDatabaseClient, () => {
describe('createSqliteDatabaseClient', () => {
it('creates an in memory knex instance', () => {
expect(
createSqliteDatabaseClient(
-2
View File
@@ -14,8 +14,6 @@
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/camelcase */
import { resolve as resolvePath } from 'path';
/**
+6 -1
View File
@@ -68,7 +68,12 @@ export default async function createPlugin({
) as RepoVisibilityOptions;
const githubToken = githubConfig.getString('token');
const githubClient = new Octokit({ auth: githubToken });
const githubHost =
githubConfig.getOptionalString('host') ?? 'https://github.com';
const githubClient = new Octokit({
auth: githubToken,
baseUrl: githubHost,
});
const githubPublisher = new GithubPublisher({
client: githubClient,
token: githubToken,
+2 -2
View File
@@ -22,11 +22,11 @@
"dependencies": {
"@backstage/config": "^0.1.1-alpha.25",
"@types/json-schema": "^7.0.5",
"@types/yup": "^0.28.2",
"@types/yup": "^0.29.8",
"json-schema": "^0.2.5",
"lodash": "^4.17.15",
"uuid": "^8.0.0",
"yup": "^0.29.1"
"yup": "^0.29.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.25",
@@ -18,24 +18,26 @@ import * as yup from 'yup';
import { EntityPolicy } from '../../types';
import { Entity } from '../Entity';
const DEFAULT_ENTITY_SCHEMA = yup.object({
apiVersion: yup.string().required(),
kind: yup.string().required(),
metadata: yup
.object({
uid: yup.string().notRequired().min(1),
etag: yup.string().notRequired().min(1),
generation: yup.number().notRequired().integer().min(1),
name: yup.string().required(),
namespace: yup.string().notRequired(),
description: yup.string().notRequired(),
labels: yup.object<Record<string, string>>().notRequired(),
annotations: yup.object<Record<string, string>>().notRequired(),
tags: yup.array<string>().notRequired(),
})
.required(),
spec: yup.object({}).notRequired(),
});
const DEFAULT_ENTITY_SCHEMA = yup
.object({
apiVersion: yup.string().required(),
kind: yup.string().required(),
metadata: yup
.object({
uid: yup.string().notRequired().min(1),
etag: yup.string().notRequired().min(1),
generation: yup.number().notRequired().integer().min(1),
name: yup.string().required(),
namespace: yup.string().notRequired(),
description: yup.string().notRequired(),
labels: yup.object<Record<string, string>>().notRequired(),
annotations: yup.object<Record<string, string>>().notRequired(),
tags: yup.array<string>().notRequired(),
})
.required(),
spec: yup.object({}).notRequired(),
})
.required();
/**
* Ensures that the entity spec is valid according to a schema.
@@ -101,4 +101,24 @@ describe('ComponentV1alpha1Policy', () => {
(entity as any).spec.owner = '';
await expect(policy.enforce(entity)).rejects.toThrow(/owner/);
});
it('accepts missing implementsApis', async () => {
delete (entity as any).spec.implementsApis;
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('rejects empty implementsApis', async () => {
(entity as any).spec.implementsApis = [''];
await expect(policy.enforce(entity)).rejects.toThrow(/implementsApis/);
});
it('rejects undefined implementsApis', async () => {
(entity as any).spec.implementsApis = [undefined];
await expect(policy.enforce(entity)).rejects.toThrow(/implementsApis/);
});
it('accepts no implementsApis', async () => {
(entity as any).spec.implementsApis = [];
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
});
@@ -29,7 +29,7 @@ const schema = yup.object<Partial<ComponentEntityV1alpha1>>({
type: yup.string().required().min(1),
lifecycle: yup.string().required().min(1),
owner: yup.string().required().min(1),
implementsApis: yup.array(yup.string()).notRequired(),
implementsApis: yup.array(yup.string().required()).notRequired(),
})
.required(),
});
@@ -90,9 +90,14 @@ describe('GroupV1alpha1Policy', () => {
await expect(policy.enforce(entity)).rejects.toThrow(/ancestor/);
});
it('accepts empty ancestors', async () => {
it('rejects empty ancestors', async () => {
(entity as any).spec.ancestors = [''];
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(policy.enforce(entity)).rejects.toThrow(/ancestor/);
});
it('rejects undefined ancestors', async () => {
(entity as any).spec.ancestors = [undefined];
await expect(policy.enforce(entity)).rejects.toThrow(/ancestor/);
});
it('accepts no ancestors', async () => {
@@ -105,9 +110,14 @@ describe('GroupV1alpha1Policy', () => {
await expect(policy.enforce(entity)).rejects.toThrow(/children/);
});
it('accepts empty children', async () => {
it('rejects empty children', async () => {
(entity as any).spec.children = [''];
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(policy.enforce(entity)).rejects.toThrow(/children/);
});
it('rejects undefined children', async () => {
(entity as any).spec.children = [undefined];
await expect(policy.enforce(entity)).rejects.toThrow(/children/);
});
it('accepts no children', async () => {
@@ -120,9 +130,14 @@ describe('GroupV1alpha1Policy', () => {
await expect(policy.enforce(entity)).rejects.toThrow(/descendants/);
});
it('accepts empty descendants', async () => {
it('rejects empty descendants', async () => {
(entity as any).spec.descendants = [''];
await expect(policy.enforce(entity)).resolves.toBe(entity);
await expect(policy.enforce(entity)).rejects.toThrow(/descendants/);
});
it('rejects undefined descendants', async () => {
(entity as any).spec.descendants = [undefined];
await expect(policy.enforce(entity)).rejects.toThrow(/descendants/);
});
it('accepts no descendants', async () => {
@@ -30,21 +30,23 @@ const schema = yup.object<Partial<GroupEntityV1alpha1>>({
parent: yup.string().notRequired().min(1),
// Use these manual tests because yup .required() requires at least
// one element and there is no simple workaround -_-
ancestors: yup.array(yup.string()).test({
// the cast is there to convince typescript that the array itself is
// required without using .required()
ancestors: yup.array(yup.string().required()).test({
name: 'isDefined',
message: 'ancestors must be defined',
test: v => Boolean(v),
}),
children: yup.array(yup.string()).test({
}) as yup.ArraySchema<string, object>,
children: yup.array(yup.string().required()).test({
name: 'isDefined',
message: 'children must be defined',
test: v => Boolean(v),
}),
descendants: yup.array(yup.string()).test({
}) as yup.ArraySchema<string, object>,
descendants: yup.array(yup.string().required()).test({
name: 'isDefined',
message: 'descendants must be defined',
test: v => Boolean(v),
}),
}) as yup.ArraySchema<string, object>,
})
.required(),
});
@@ -93,6 +93,11 @@ describe('LocationV1alpha1Policy', () => {
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('accepts empty targets', async () => {
(entity as any).spec.targets = [];
await expect(policy.enforce(entity)).resolves.toBe(entity);
});
it('rejects wrong targets', async () => {
(entity as any).spec.targets = 7;
await expect(policy.enforce(entity)).rejects.toThrow(/targets/);
@@ -28,7 +28,7 @@ const schema = yup.object<Partial<LocationEntityV1alpha1>>({
.object({
type: yup.string().required().min(1),
target: yup.string().notRequired().min(1),
targets: yup.array(yup.string()).notRequired(),
targets: yup.array(yup.string().required()).notRequired(),
})
.required(),
});
@@ -35,11 +35,13 @@ const schema = yup.object<Partial<UserEntityV1alpha1>>({
.notRequired(),
// Use this manual test because yup .required() requires at least one
// element and there is no simple workaround -_-
memberOf: yup.array(yup.string()).test({
// the cast is there to convince typescript that the array itself is
// required without using .required()
memberOf: yup.array(yup.string().required()).test({
name: 'isDefined',
message: 'memberOf must be defined',
test: v => Boolean(v),
}),
}) as yup.ArraySchema<string, object>,
})
.required(),
});
@@ -21,9 +21,10 @@ export const locationSpecSchema = yup
.object<LocationSpec>({
type: yup.string().required(),
target: yup.string().required(),
pendingLocation: yup.bool().oneOf([true]),
pendingLocation: yup.bool(),
})
.noUnknown();
.noUnknown()
.required();
export const locationSchema = yup
.object<Location>({
@@ -31,4 +32,5 @@ export const locationSchema = yup
type: yup.string().required(),
target: yup.string().required(),
})
.noUnknown();
.noUnknown()
.required();
+13 -4
View File
@@ -38,7 +38,9 @@
"@rollup/plugin-json": "^4.0.2",
"@rollup/plugin-node-resolve": "^8.1.0",
"@rollup/plugin-yaml": "^2.1.1",
"@spotify/eslint-config": "^7.0.1",
"@spotify/eslint-config-base": "^8.0.0",
"@spotify/eslint-config-react": "^8.0.0",
"@spotify/eslint-config-typescript": "^8.0.0",
"@sucrase/webpack-loader": "^2.0.0",
"@svgr/plugin-jsx": "5.4.x",
"@svgr/plugin-svgo": "5.4.x",
@@ -47,6 +49,8 @@
"@types/start-server-webpack-plugin": "^2.2.0",
"@types/webpack-env": "^1.15.2",
"@types/webpack-node-externals": "^2.5.0",
"@typescript-eslint/eslint-plugin": "^v3.10.1",
"@typescript-eslint/parser": "^v3.10.1",
"bfj": "^7.0.2",
"chalk": "^4.0.0",
"chokidar": "^3.3.1",
@@ -56,9 +60,14 @@
"diff": "^4.0.2",
"esbuild": "^0.7.7",
"eslint": "^7.1.0",
"eslint-config-prettier": "^6.0.0",
"eslint-formatter-friendly": "^7.0.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-jest": "^24.1.0",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-monorepo": "^0.2.1",
"eslint-plugin-react": "^7.12.4",
"eslint-plugin-react-hooks": "^4.0.0",
"fork-ts-checker-webpack-plugin": "^4.0.5",
"fs-extra": "^9.0.0",
"handlebars": "^4.7.3",
@@ -76,7 +85,7 @@
"recursive-readdir": "^2.2.2",
"replace-in-file": "^6.0.0",
"rollup": "2.23.x",
"rollup-plugin-dts": "1.4.11",
"rollup-plugin-dts": "1.4.13",
"rollup-plugin-esbuild": "^2.0.0",
"rollup-plugin-peer-deps-external": "^2.2.2",
"rollup-plugin-postcss": "^3.1.1",
@@ -84,12 +93,12 @@
"rollup-pluginutils": "^2.8.2",
"start-server-webpack-plugin": "^2.2.5",
"style-loader": "^1.2.1",
"sucrase": "^3.14.1",
"sucrase": "^3.16.0",
"tar": "^6.0.1",
"terser-webpack-plugin": "^1.4.3",
"ts-jest": "^26.0.0",
"ts-loader": "^7.0.4",
"typescript": "^3.9.3",
"typescript": "^4.0.3",
"url-loader": "^4.1.0",
"webpack": "^4.41.6",
"webpack-dev-server": "^3.11.0",
@@ -32,7 +32,6 @@ import {
removePluginFromCodeOwners,
} from './removePlugin';
// Some constant variables
const BACKSTAGE = `@backstage`;
const testPluginName = 'yarn-test-package';
const testPluginPackage = `${BACKSTAGE}/plugin-${testPluginName}`;
@@ -68,8 +67,8 @@ const createTestPluginFile = async (
.split('-')
.map(name => capitalize(name))
.join('');
const exportStatement = `export { default as ${pluginNameCapitalized}} from @backstage/plugin-${testPluginName}`;
addExportStatement(testFilePath, exportStatement);
const exportStatement = `export { plugin as ${pluginNameCapitalized}} from @backstage/plugin-${testPluginName}`;
await addExportStatement(testFilePath, exportStatement);
};
const mkTestPluginDir = (testDirPath: string) => {
@@ -78,15 +77,21 @@ const mkTestPluginDir = (testDirPath: string) => {
fse.createFileSync(path.join(testDirPath, `testFile${i}.ts`));
};
beforeAll(() => {
// Create temporary directory for all tests
createTemporaryPluginFolder(tempDir);
});
describe('removePlugin', () => {
beforeAll(() => {
// Create temporary directory for all tests
createTemporaryPluginFolder(tempDir);
});
afterAll(() => {
// Remove temporary directory
fse.removeSync(tempDir);
});
describe('Remove Plugin Dependencies', () => {
const appPath = paths.resolveTargetRoot('packages', 'app');
const githubDir = paths.resolveTargetRoot('.github');
it('removes plugin references from /packages/app/package.json', async () => {
// Set up test
const packageFilePath = path.join(appPath, 'package.json');
@@ -105,7 +110,8 @@ describe('removePlugin', () => {
fse.removeSync(testFilePath);
}
});
it('removes plugin exports from /packages/app/src/packacge.json', async () => {
it('removes plugin exports from /packages/app/src/package.json', async () => {
const testFilePath = path.join(tempDir, 'test.ts');
const pluginsFilePaths = path.join(appPath, 'src', 'plugins.ts');
createTestPluginFile(testFilePath, pluginsFilePaths);
@@ -122,6 +128,7 @@ describe('removePlugin', () => {
fse.removeSync(testFilePath);
}
});
it('removes codeOwners references', async () => {
const testFilePath = path.join(tempDir, 'test');
const codeownersPath = path.join(githubDir, 'CODEOWNERS');
@@ -144,12 +151,14 @@ describe('removePlugin', () => {
}
});
});
describe('Remove files', () => {
const testDirPath = path.join(
paths.resolveTargetRoot(),
'plugins',
testPluginName,
);
describe('Removes Plugin Directory', () => {
it('removes plugin directory from /plugins', async () => {
try {
@@ -162,6 +171,7 @@ describe('removePlugin', () => {
}
});
});
describe('Removes System Link', () => {
it('removes system link from @backstage', async () => {
const scopedDir = paths.resolveTargetRoot('node_modules', '@backstage');
@@ -183,8 +193,3 @@ describe('removePlugin', () => {
});
});
});
afterAll(() => {
// Remove temporary directory
fse.removeSync(tempDir);
});
@@ -20,7 +20,6 @@ import inquirer, { Answers, Question } from 'inquirer';
import { getCodeownersFilePath } from '../../lib/codeowners';
import { paths } from '../../lib/paths';
import { Task } from '../../lib/tasks';
// import os from 'os';
const BACKSTAGE = '@backstage';
+3
View File
@@ -111,6 +111,9 @@ export async function createConfig(
app: {
title: options.config.getString('app.title'),
baseUrl: validBaseUrl.href,
googleAnalyticsTrackingId: options.config.getOptionalString(
'app.googleAnalyticsTrackingId',
),
},
},
}),
+2 -2
View File
@@ -17,7 +17,7 @@
import { isParallelDefault, parseParallel } from './parallel';
describe('parallel', () => {
describe(parseParallel, () => {
describe('parseParallel', () => {
it('coerces "false" string to boolean', () => {
expect(parseParallel('false')).toBeFalsy();
});
@@ -44,7 +44,7 @@ describe('parallel', () => {
});
});
describe(isParallelDefault, () => {
describe('isParallelDefault', () => {
it('returns true if default value', () => {
expect(isParallelDefault(undefined)).toBeTruthy();
expect(isParallelDefault(true)).toBeTruthy();
@@ -4,7 +4,6 @@ import {
InfoCard,
Header,
Page,
pageTheme,
Content,
ContentHeader,
HeaderLabel,
@@ -13,7 +12,7 @@ import {
import ExampleFetchComponent from '../ExampleFetchComponent';
const ExampleComponent: FC<{}> = () => (
<Page theme={pageTheme.tool}>
<Page themeId="tool">
<Header title="Welcome to {{ id }}!" subtitle="Optional subtitle">
<HeaderLabel label="Owner" value="Team X" />
<HeaderLabel label="Lifecycle" value="Alpha" />
@@ -37,5 +36,5 @@ const ExampleComponent: FC<{}> = () => (
</Content>
</Page>
);
export default ExampleComponent;
+2 -2
View File
@@ -33,13 +33,13 @@
"@backstage/config": "^0.1.1-alpha.25",
"fs-extra": "^9.0.0",
"yaml": "^1.9.2",
"yup": "^0.29.1"
"yup": "^0.29.3"
},
"devDependencies": {
"@types/jest": "^26.0.7",
"@types/mock-fs": "^4.10.0",
"@types/node": "^12.0.0",
"@types/yup": "^0.28.2",
"@types/yup": "^0.29.8",
"mock-fs": "^4.13.0"
},
"files": [
+1 -1
View File
@@ -58,7 +58,7 @@ const secretLoaderSchemas = {
};
// The top-level secret schema, which figures out what type of secret it is.
const secretSchema = yup.lazy<object>(value => {
const secretSchema = yup.lazy<object | undefined>(value => {
if (typeof value !== 'object' || value === null) {
return yup.object().required().label('secret');
}
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createApiRef } from '../ApiRef';
import { createApiRef, ApiRef } from '../system';
import { Observable } from '../../types';
export type AlertMessage = {
@@ -38,7 +38,7 @@ export type AlertApi = {
alert$(): Observable<AlertMessage>;
};
export const alertApiRef = createApiRef<AlertApi>({
export const alertApiRef: ApiRef<AlertApi> = createApiRef({
id: 'core.alert',
description: 'Used to report alerts and forward them to the app',
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { createApiRef } from '../ApiRef';
import { ApiRef, createApiRef } from '../system';
import { BackstageTheme } from '@backstage/theme';
import { Observable } from '../../types';
import { SvgIconProps } from '@material-ui/core';
@@ -77,7 +77,7 @@ export type AppThemeApi = {
setActiveThemeId(themeId?: string): void;
};
export const appThemeApiRef = createApiRef<AppThemeApi>({
export const appThemeApiRef: ApiRef<AppThemeApi> = createApiRef({
id: 'core.apptheme',
description: 'API Used to configure the app theme, and enumerate options',
});
@@ -13,13 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createApiRef } from '../ApiRef';
import { ApiRef, createApiRef } from '../system';
import { Config } from '@backstage/config';
// Using interface to make the ConfigApi name show up in docs
export type ConfigApi = Config;
export const configApiRef = createApiRef<ConfigApi>({
export const configApiRef: ApiRef<ConfigApi> = createApiRef({
id: 'core.config',
description: 'Used to access runtime configuration',
});
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createApiRef } from '../ApiRef';
import { ApiRef, createApiRef } from '../system';
/**
* The discovery API is used to provide a mechanism for plugins to
@@ -41,7 +41,7 @@ export type DiscoveryApi = {
getBaseUrl(pluginId: string): Promise<string>;
};
export const discoveryApiRef = createApiRef<DiscoveryApi>({
export const discoveryApiRef: ApiRef<DiscoveryApi> = createApiRef({
id: 'core.discovery',
description: 'Provides service discovery of backend plugins',
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { createApiRef } from '../ApiRef';
import { ApiRef, createApiRef } from '../system';
import { Observable } from '../../types';
/**
@@ -62,7 +62,7 @@ export type ErrorApi = {
error$(): Observable<{ error: Error; context?: ErrorContext }>;
};
export const errorApiRef = createApiRef<ErrorApi>({
export const errorApiRef: ApiRef<ErrorApi> = createApiRef({
id: 'core.error',
description: 'Used to report errors and forward them to the app',
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { createApiRef } from '../ApiRef';
import { ApiRef, createApiRef } from '../system';
import { UserFlags, FeatureFlagsRegistry } from '../../app/FeatureFlags';
import { FeatureFlagName } from '../../plugin';
@@ -57,7 +57,7 @@ export interface FeatureFlagsRegistryItem {
name: FeatureFlagName;
}
export const featureFlagsApiRef = createApiRef<FeatureFlagsApi>({
export const featureFlagsApiRef: ApiRef<FeatureFlagsApi> = createApiRef({
id: 'core.featureflags',
description: 'Used to toggle functionality in features across Backstage',
});
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createApiRef } from '../ApiRef';
import { ApiRef, createApiRef } from '../system';
import { ProfileInfo } from './auth';
/**
@@ -51,7 +51,7 @@ export type IdentityApi = {
signOut(): Promise<void>;
};
export const identityApiRef = createApiRef<IdentityApi>({
export const identityApiRef: ApiRef<IdentityApi> = createApiRef({
id: 'core.identity',
description: 'Provides access to the identity of the signed in user',
});
@@ -16,7 +16,7 @@
import { IconComponent } from '../../icons';
import { Observable } from '../../types';
import { createApiRef } from '../ApiRef';
import { ApiRef, createApiRef } from '../system';
/**
* Information about the auth provider that we're requesting a login towards.
@@ -127,7 +127,7 @@ export type OAuthRequestApi = {
authRequest$(): Observable<PendingAuthRequest[]>;
};
export const oauthRequestApiRef = createApiRef<OAuthRequestApi>({
export const oauthRequestApiRef: ApiRef<OAuthRequestApi> = createApiRef({
id: 'core.oauthrequest',
description: 'An API for implementing unified OAuth flows in Backstage',
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { createApiRef } from '../ApiRef';
import { ApiRef, createApiRef } from '../system';
import { Observable } from '../../types';
import { ErrorApi } from './ErrorApi';
@@ -65,7 +65,7 @@ export interface StorageApi {
observe$<T>(key: string): Observable<StorageValueChange<T>>;
}
export const storageApiRef = createApiRef<StorageApi>({
export const storageApiRef: ApiRef<StorageApi> = createApiRef({
id: 'core.storage',
description: 'Provides the ability to store data which is unique to the user',
});
+18 -18
View File
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import { createApiRef } from '../ApiRef';
import { Observable } from '../..';
import { ApiRef, createApiRef } from '../system';
import { Observable } from '../../types';
/**
* This file contains declarations for common interfaces of auth-related APIs.
@@ -212,13 +212,13 @@ export type SessionApi = {
* Note that the ID token payload is only guaranteed to contain the user's numerical Google ID,
* email and expiration information. Do not rely on any other fields, as they might not be present.
*/
export const googleAuthApiRef = createApiRef<
export const googleAuthApiRef: ApiRef<
OAuthApi &
OpenIdConnectApi &
ProfileInfoApi &
BackstageIdentityApi &
SessionApi
>({
> = createApiRef({
id: 'core.auth.google',
description: 'Provides authentication towards Google APIs and identities',
});
@@ -229,9 +229,9 @@ export const googleAuthApiRef = createApiRef<
* See https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/
* for a full list of supported scopes.
*/
export const githubAuthApiRef = createApiRef<
export const githubAuthApiRef: ApiRef<
OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
>({
> = createApiRef({
id: 'core.auth.github',
description: 'Provides authentication towards GitHub APIs',
});
@@ -242,13 +242,13 @@ export const githubAuthApiRef = createApiRef<
* See https://developer.okta.com/docs/guides/implement-oauth-for-okta/scopes/
* for a full list of supported scopes.
*/
export const oktaAuthApiRef = createApiRef<
export const oktaAuthApiRef: ApiRef<
OAuthApi &
OpenIdConnectApi &
ProfileInfoApi &
BackstageIdentityApi &
SessionApi
>({
> = createApiRef({
id: 'core.auth.okta',
description: 'Provides authentication towards Okta APIs',
});
@@ -259,9 +259,9 @@ export const oktaAuthApiRef = createApiRef<
* See https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#limiting-scopes-of-a-personal-access-token
* for a full list of supported scopes.
*/
export const gitlabAuthApiRef = createApiRef<
export const gitlabAuthApiRef: ApiRef<
OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
>({
> = createApiRef({
id: 'core.auth.gitlab',
description: 'Provides authentication towards GitLab APIs',
});
@@ -272,9 +272,9 @@ export const gitlabAuthApiRef = createApiRef<
* See https://auth0.com/docs/scopes/current/oidc-scopes
* for a full list of supported scopes.
*/
export const auth0AuthApiRef = createApiRef<
export const auth0AuthApiRef: ApiRef<
OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
>({
> = createApiRef({
id: 'core.auth.auth0',
description: 'Provides authentication towards Auth0 APIs',
});
@@ -286,13 +286,13 @@ export const auth0AuthApiRef = createApiRef<
* - https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent
* - https://docs.microsoft.com/en-us/graph/permissions-reference
*/
export const microsoftAuthApiRef = createApiRef<
export const microsoftAuthApiRef: ApiRef<
OAuthApi &
OpenIdConnectApi &
ProfileInfoApi &
BackstageIdentityApi &
SessionApi
>({
> = createApiRef({
id: 'core.auth.microsoft',
description: 'Provides authentication towards Microsoft APIs and identities',
});
@@ -300,13 +300,13 @@ export const microsoftAuthApiRef = createApiRef<
/**
* Provides authentication for custom identity providers.
*/
export const oauth2ApiRef = createApiRef<
export const oauth2ApiRef: ApiRef<
OAuthApi &
OpenIdConnectApi &
ProfileInfoApi &
BackstageIdentityApi &
SessionApi
>({
> = createApiRef({
id: 'core.auth.oauth2',
description: 'Example of how to use oauth2 custom provider',
});
@@ -314,9 +314,9 @@ export const oauth2ApiRef = createApiRef<
/**
* Provides authentication for saml based identity providers
*/
export const samlAuthApiRef = createApiRef<
export const samlAuthApiRef: ApiRef<
ProfileInfoApi & BackstageIdentityApi & SessionApi
>({
> = createApiRef({
id: 'core.auth.saml',
description: 'Example of how to use SAML custom provider',
});
+1 -5
View File
@@ -14,10 +14,6 @@
* limitations under the License.
*/
export { ApiProvider, useApi, useApiHolder } from './ApiProvider';
export { ApiRegistry } from './ApiRegistry';
export * from './ApiRef';
export * from './types';
export * from './helpers';
export * from './system';
export * from './definitions';
export * from './implementations';
@@ -14,8 +14,7 @@
* limitations under the License.
*/
import { ApiRef } from './ApiRef';
import { ApiHolder } from './types';
import { ApiRef, ApiHolder } from './types';
/**
* An ApiHolder that queries multiple other holders from for
@@ -15,12 +15,12 @@
*/
import {
ApiRef,
ApiFactoryHolder,
ApiFactory,
AnyApiRef,
AnyApiFactory,
} from './types';
import { ApiRef } from './ApiRef';
type ApiFactoryScope =
| 'default' // Default factories registered by core and plugins
@@ -16,8 +16,7 @@
import React, { FC, createContext, useContext, ReactNode } from 'react';
import PropTypes from 'prop-types';
import { ApiRef } from './ApiRef';
import { ApiHolder, TypesToApiRefs } from './types';
import { ApiRef, ApiHolder, TypesToApiRefs } from './types';
import { ApiAggregator } from './ApiAggregator';
type ApiProviderProps = {
@@ -14,17 +14,13 @@
* limitations under the License.
*/
import type { ApiRef } from './types';
export type ApiRefConfig = {
id: string;
description: string;
};
export type ApiRef<T> = {
id: string;
description: string;
T: T;
};
class ApiRefImpl<T> implements ApiRef<T> {
constructor(private readonly config: ApiRefConfig) {
const valid = config.id
@@ -14,8 +14,7 @@
* limitations under the License.
*/
import { ApiRef } from './ApiRef';
import { ApiHolder } from './types';
import { ApiRef, ApiHolder } from './types';
type ApiImpl<T = unknown> = readonly [ApiRef<T>, T];
@@ -14,8 +14,8 @@
* limitations under the License.
*/
import { ApiRef } from './ApiRef';
import {
ApiRef,
ApiHolder,
ApiFactoryHolder,
AnyApiRef,
@@ -14,8 +14,7 @@
* limitations under the License.
*/
import { ApiFactory, TypesToApiRefs } from './types';
import { ApiRef } from './ApiRef';
import { ApiRef, ApiFactory, TypesToApiRefs } from './types';
/**
* Used to infer types for a standalone ApiFactory that isn't immediately passed
@@ -0,0 +1,23 @@
/*
* 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.
*/
export { ApiProvider, useApi, useApiHolder } from './ApiProvider';
export { ApiRegistry } from './ApiRegistry';
export { ApiResolver } from './ApiResolver';
export { ApiFactoryRegistry } from './ApiFactoryRegistry';
export { createApiRef } from './ApiRef';
export * from './types';
export * from './helpers';
@@ -14,7 +14,11 @@
* limitations under the License.
*/
import { ApiRef } from './ApiRef';
export type ApiRef<T> = {
id: string;
description: string;
T: T;
};
export type AnyApiRef = ApiRef<unknown>;
+1 -2
View File
@@ -54,8 +54,7 @@ import {
} from '../apis';
import { useAsync } from 'react-use';
import { AppIdentity } from './AppIdentity';
import { ApiFactoryRegistry } from '../apis/ApiFactoryRegistry';
import { ApiResolver } from '../apis/ApiResolver';
import { ApiResolver, ApiFactoryRegistry } from '../apis/system';
type FullAppOptions = {
apis: Iterable<AnyApiFactory>;
+1 -1
View File
@@ -16,7 +16,7 @@
import { ComponentType } from 'react';
import { RouteRef } from '../routing';
import { AnyApiFactory } from '../apis';
import { AnyApiFactory } from '../apis/system';
export type RouteOptions = {
// Whether the route path must match exactly, defaults to true.
@@ -20,10 +20,10 @@ import { wrapInTestApp } from '@backstage/test-utils';
import { CodeSnippet } from './CodeSnippet';
const JAVASCRIPT = `const greeting = "Hello";
const world = "World";
const greet = person => gretting + " " + person + "!";
const JAVASCRIPT = `
const greeting = "Hello";
const world = "World";
const greet = person => gretting + " " + person + "!";
`;
const minProps = {
@@ -48,20 +48,20 @@ describe('<CodeSnippet />', () => {
});
it('renders with line numbers', () => {
const { queryByText } = render(
const { getByText } = render(
wrapInTestApp(<CodeSnippet {...minProps} showLineNumbers />),
);
expect(queryByText(/1/)).toBeInTheDocument();
expect(queryByText(/2/)).toBeInTheDocument();
expect(queryByText(/3/)).toBeInTheDocument();
expect(getByText('1')).toBeInTheDocument();
expect(getByText('2')).toBeInTheDocument();
expect(getByText('3')).toBeInTheDocument();
});
it('copy code using button', async () => {
document.execCommand = jest.fn();
const rendered = render(
const { getByTitle } = render(
wrapInTestApp(<CodeSnippet {...minProps} showCopyCodeButton />),
);
const button = rendered.getByTitle('Text copied to clipboard');
const button = getByTitle('Text copied to clipboard');
fireEvent.click(button);
expect(document.execCommand).toHaveBeenCalled();
});
@@ -0,0 +1,34 @@
/*
* 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 { renderInTestApp } from '@backstage/test-utils';
import { act } from 'react-dom/test-utils';
import { Progress } from './Progress';
describe('<Progress />', () => {
it('renders without exploding', async () => {
jest.useFakeTimers();
const { getByTestId, queryByTestId } = await renderInTestApp(<Progress />);
expect(queryByTestId('progress')).not.toBeInTheDocument();
act(() => {
jest.advanceTimersByTime(250);
});
expect(getByTestId('progress')).toBeInTheDocument();
jest.useRealTimers();
});
});
@@ -28,6 +28,6 @@ export const Progress: FC<LinearProgressProps> = props => {
return isVisible ? (
<LinearProgress {...props} data-testid="progress" />
) : (
<div style={{ display: 'none' }} data-testid="progress" />
<div style={{ display: 'none' }} />
);
};
@@ -0,0 +1,69 @@
/*
* 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 { renderInTestApp } from '@backstage/test-utils';
import {
StatusOK,
StatusWarning,
StatusError,
StatusPending,
StatusRunning,
StatusAborted,
} from './Status';
describe('<StatusOK />', () => {
it('renders without exploding', async () => {
const { getByLabelText } = await renderInTestApp(<StatusOK />);
expect(getByLabelText('Status ok')).toBeInTheDocument();
});
});
describe('<StatusWarning />', () => {
it('renders without exploding', async () => {
const { getByLabelText } = await renderInTestApp(<StatusWarning />);
expect(getByLabelText('Status warning')).toBeInTheDocument();
});
});
describe('<StatusError />', () => {
it('renders without exploding', async () => {
const { getByLabelText } = await renderInTestApp(<StatusError />);
expect(getByLabelText('Status error')).toBeInTheDocument();
});
});
describe('<StatusPending />', () => {
it('renders without exploding', async () => {
const { getByLabelText } = await renderInTestApp(<StatusPending />);
expect(getByLabelText('Status pending')).toBeInTheDocument();
});
});
describe('<StatusRunning />', () => {
it('renders without exploding', async () => {
const { getByLabelText } = await renderInTestApp(<StatusRunning />);
expect(getByLabelText('Status running')).toBeInTheDocument();
});
});
describe('<StatusAborted />', () => {
it('renders without exploding', async () => {
const { getByLabelText } = await renderInTestApp(<StatusAborted />);
expect(getByLabelText('Status aborted')).toBeInTheDocument();
});
});
+17 -3
View File
@@ -65,20 +65,34 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
export const StatusOK: FC<{}> = props => {
const classes = useStyles(props);
return <span className={classNames(classes.status, classes.ok)} {...props} />;
return (
<span
className={classNames(classes.status, classes.ok)}
aria-label="Status ok"
{...props}
/>
);
};
export const StatusWarning: FC<{}> = props => {
const classes = useStyles(props);
return (
<span className={classNames(classes.status, classes.warning)} {...props} />
<span
className={classNames(classes.status, classes.warning)}
aria-label="Status warning"
{...props}
/>
);
};
export const StatusError: FC<{}> = props => {
const classes = useStyles(props);
return (
<span className={classNames(classes.status, classes.error)} {...props} />
<span
className={classNames(classes.status, classes.error)}
aria-label="Status error"
{...props}
/>
);
};
@@ -67,7 +67,7 @@ const columns = [
export const Default = () => (
<div style={containerStyle}>
<InfoCard title="Trends over time">
<InfoCard title="Trends over time" noPadding>
<Table
options={{
search: false,
@@ -16,7 +16,7 @@
import React from 'react';
import { Header } from '.';
import { HeaderLabel } from '../HeaderLabel';
import { Page, pageTheme } from '../Page';
import { Page } from '../Page';
export default {
title: 'Layout/Header',
@@ -32,7 +32,7 @@ const labels = (
);
export const Home = () => (
<Page theme={pageTheme.home}>
<Page themeId="home">
<Header title="Start/Home Page" type="home">
{labels}
</Header>
@@ -46,7 +46,7 @@ export const HomeWithSubtitle = () => (
);
export const Tool = () => (
<Page theme={pageTheme.tool}>
<Page themeId="tool">
<Header title="Stand-alone tool" type="tool">
{labels}
</Header>
@@ -54,7 +54,7 @@ export const Tool = () => (
);
export const Service = () => (
<Page theme={pageTheme.service}>
<Page themeId="service">
<Header title="Service component page" type="service">
{labels}
</Header>
@@ -62,7 +62,7 @@ export const Service = () => (
);
export const Website = () => (
<Page theme={pageTheme.website}>
<Page themeId="website">
<Header title="Website component page" type="website">
{labels}
</Header>
@@ -70,7 +70,7 @@ export const Website = () => (
);
export const Library = () => (
<Page theme={pageTheme.library}>
<Page themeId="library">
<Header title="Library component page" type="library">
{labels}
</Header>
@@ -78,7 +78,7 @@ export const Library = () => (
);
export const App = () => (
<Page theme={pageTheme.app}>
<Page themeId="app">
<Header title="App component page" type="app">
{labels}
</Header>
@@ -86,7 +86,7 @@ export const App = () => (
);
export const Documentation = () => (
<Page theme={pageTheme.documentation}>
<Page themeId="documentation">
<Header title="Documentation component page" type="documentation">
{labels}
</Header>
@@ -94,7 +94,7 @@ export const Documentation = () => (
);
export const Other = () => (
<Page theme={pageTheme.other}>
<Page themeId="other">
<Header title="Other/generic component page" type="other">
{labels}
</Header>
+65 -69
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { ReactNode, CSSProperties, FC, useContext } from 'react';
import React, { ReactNode, CSSProperties, FC } from 'react';
import { Helmet } from 'react-helmet';
import {
Link,
@@ -25,73 +25,70 @@ import {
} from '@material-ui/core';
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
import { BackstageTheme } from '@backstage/theme';
import { PageThemeContext } from '../Page/Page';
const useStyles = makeStyles<BackstageTheme, { backgroundImage: string }>(
theme => ({
header: {
gridArea: 'pageHeader',
padding: theme.spacing(3),
minHeight: 118,
width: '100%',
boxShadow: '0 0 8px 3px rgba(20, 20, 20, 0.3)',
position: 'relative',
zIndex: 100,
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'flex-end',
alignItems: 'center',
backgroundImage: props => props.backgroundImage,
backgroundPosition: 'center',
backgroundSize: 'cover',
},
leftItemsBox: {
flex: '1 1 auto',
},
rightItemsBox: {
flex: '0 1 auto',
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'center',
marginRight: theme.spacing(1),
},
title: {
color: theme.palette.bursts.fontColor,
lineHeight: '1.0em',
wordBreak: 'break-all',
fontSize: 'calc(24px + 6 * ((100vw - 320px) / 680))',
marginBottom: theme.spacing(1),
},
subtitle: {
color: 'rgba(255, 255, 255, 0.8)',
lineHeight: '1.0em',
},
type: {
textTransform: 'uppercase',
fontSize: 11,
opacity: 0.8,
marginBottom: theme.spacing(1),
color: theme.palette.bursts.fontColor,
},
breadcrumb: {
fontSize: 'calc(15px + 1 * ((100vw - 320px) / 680))',
color: theme.palette.bursts.fontColor,
},
breadcrumbType: {
fontSize: 'inherit',
opacity: 0.7,
marginRight: -theme.spacing(0.3),
marginBottom: theme.spacing(0.3),
},
breadcrumbTitle: {
fontSize: 'inherit',
marginLeft: -theme.spacing(0.3),
marginBottom: theme.spacing(0.3),
},
}),
);
const useStyles = makeStyles<BackstageTheme>(theme => ({
header: {
gridArea: 'pageHeader',
padding: theme.spacing(3),
minHeight: 118,
width: '100%',
boxShadow: '0 0 8px 3px rgba(20, 20, 20, 0.3)',
position: 'relative',
zIndex: 100,
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'flex-end',
alignItems: 'center',
backgroundImage: theme.page.backgroundImage,
backgroundPosition: 'center',
backgroundSize: 'cover',
},
leftItemsBox: {
flex: '1 1 auto',
},
rightItemsBox: {
flex: '0 1 auto',
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'center',
marginRight: theme.spacing(1),
},
title: {
color: theme.palette.bursts.fontColor,
lineHeight: '1.0em',
wordBreak: 'break-all',
fontSize: 'calc(24px + 6 * ((100vw - 320px) / 680))',
marginBottom: theme.spacing(1),
},
subtitle: {
color: 'rgba(255, 255, 255, 0.8)',
lineHeight: '1.0em',
},
type: {
textTransform: 'uppercase',
fontSize: 11,
opacity: 0.8,
marginBottom: theme.spacing(1),
color: theme.palette.bursts.fontColor,
},
breadcrumb: {
fontSize: 'calc(15px + 1 * ((100vw - 320px) / 680))',
color: theme.palette.bursts.fontColor,
},
breadcrumbType: {
fontSize: 'inherit',
opacity: 0.7,
marginRight: -theme.spacing(0.3),
marginBottom: theme.spacing(0.3),
},
breadcrumbTitle: {
fontSize: 'inherit',
marginLeft: -theme.spacing(0.3),
marginBottom: theme.spacing(0.3),
},
}));
type HeaderStyles = ReturnType<typeof useStyles>;
@@ -200,8 +197,7 @@ export const Header: FC<Props> = ({
type,
typeLink,
}) => {
const theme = useContext(PageThemeContext);
const classes = useStyles({ backgroundImage: theme.backgroundImage });
const classes = useStyles();
const documentTitle = pageTitleOverride || title;
const pageTitle = title || pageTitleOverride;
const titleTemplate = `${documentTitle} | %s | Backstage`;
@@ -20,8 +20,7 @@ const useStyles = makeStyles(theme => ({
header: {
color: theme.palette.common.white,
padding: theme.spacing(2, 2, 6),
backgroundImage:
'linear-gradient(-137deg, rgb(25, 230, 140) 0%, rgb(29, 127, 110) 100%)',
backgroundImage: 'linear-gradient(-137deg, #4BB8A5 0%, #187656 100%)',
},
content: {
padding: theme.spacing(2),
@@ -21,7 +21,6 @@ import {
HeaderLabel,
ContentHeader,
Content,
pageTheme,
InfoCard,
HeaderTabs,
} from '../';
@@ -196,7 +195,7 @@ export const PluginWithData = () => {
const [selectedTab, setSelectedTab] = useState<number>(2);
return (
<div style={{ border: '1px solid #ddd' }}>
<Page theme={pageTheme.tool}>
<Page themeId="tool">
<ExampleHeader />
<HeaderTabs
selectedIndex={selectedTab}
@@ -218,7 +217,7 @@ export const PluginWithData = () => {
export const PluginWithTable = () => {
return (
<div style={{ border: '1px solid #ddd' }}>
<Page theme={pageTheme.tool}>
<Page themeId="tool">
<ExampleHeader />
<Content>
<ExampleContentHeader />
+11 -8
View File
@@ -15,10 +15,8 @@
*/
import React, { FC } from 'react';
import { PageTheme, pageTheme } from './PageThemeProvider';
import { makeStyles } from '@material-ui/core';
export const PageThemeContext = React.createContext<PageTheme>(pageTheme.home);
import { BackstageTheme } from '@backstage/theme';
import { makeStyles, ThemeProvider } from '@material-ui/core';
const useStyles = makeStyles(() => ({
root: {
@@ -32,14 +30,19 @@ const useStyles = makeStyles(() => ({
}));
type Props = {
theme?: PageTheme;
themeId: string;
};
export const Page: FC<Props> = ({ theme = pageTheme.home, children }) => {
export const Page: FC<Props> = ({ themeId, children }) => {
const classes = useStyles();
return (
<PageThemeContext.Provider value={theme}>
<ThemeProvider
theme={(baseTheme: BackstageTheme) => ({
...baseTheme,
page: baseTheme.getPageTheme({ themeId }),
})}
>
<div className={classes.root}>{children}</div>
</PageThemeContext.Provider>
</ThemeProvider>
);
};
-2
View File
@@ -15,5 +15,3 @@
*/
export { Page } from './Page';
export { pageTheme } from './PageThemeProvider';
export type { PageTheme } from './PageThemeProvider';
@@ -52,7 +52,7 @@ export const SignInPage: FC<Props> = ({
}
return (
<Page>
<Page themeId="home">
<Header title={configApi.getString('app.title')} />
<Content>
{title && <ContentHeader title={title} textAlign={align} />}
@@ -3,7 +3,7 @@ app:
baseUrl: http://localhost:7000
organization:
name: Acme Corporation
name: My Company
backend:
baseUrl: http://localhost:7000
@@ -42,7 +42,7 @@ proxy:
techdocs:
storageUrl: http://localhost:7000/api/techdocs/static/docs
requestUrl: http://localhost:7000/api/techdocs/docs
requestUrl: http://localhost:7000/api/techdocs
generators:
techdocs: 'docker'
@@ -1,6 +1,6 @@
describe('App', () => {
it('should render the catalog', () => {
cy.visit('/');
cy.contains('Backstage Service Catalog');
cy.contains('My Company Service Catalog');
});
});
@@ -48,7 +48,8 @@ export default async function createPlugin({
) as RepoVisibilityOptions;
const githubToken = githubConfig.getString('token');
const githubClient = new Octokit({ auth: githubToken });
const githubHost = githubConfig.getOptionalString('host');
const githubClient = new Octokit({ auth: githubToken, baseUrl: githubHost });
const githubPublisher = new GithubPublisher({
client: githubClient,
token: githubToken,
+1 -1
View File
@@ -31,7 +31,7 @@
"fs-extra": "^9.0.0",
"github-slugger": "^1.3.0",
"ts-node": "^8.6.2",
"typescript": "^3.9.3"
"typescript": "^4.0.3"
},
"devDependencies": {
"@types/fs-extra": "^9.0.1",
-4
View File
@@ -49,10 +49,6 @@ const main = (argv: string[]) => {
process.exit(1);
});
if (!process.argv.slice(2).length) {
program.outputHelp(chalk.yellow);
}
program.parse(argv);
};
+1 -1
View File
@@ -301,7 +301,7 @@ async function testAppServe(pluginName: string, appDir: string) {
try {
const browser = new Browser();
await waitForPageWithText(browser, '/', 'Backstage Service Catalog');
await waitForPageWithText(browser, '/', 'My Company Service Catalog');
await waitForPageWithText(
browser,
`/${pluginName}`,
+1 -5
View File
@@ -28,11 +28,7 @@
"techdocs-cli": "bin/techdocs-cli"
},
"devDependencies": {
"@spotify/eslint-config": "^7.0.0",
"@spotify/prettier-config": "^7.0.0",
"@types/serve-handler": "^6.1.0",
"eslint": "^7.1.0",
"eslint-plugin-import": "^2.22.0"
"@types/serve-handler": "^6.1.0"
},
"files": [
"bin",
+14 -1
View File
@@ -23,6 +23,7 @@ import {
BackstageThemeOptions,
SimpleThemeOptions,
} from './types';
import { pageTheme as defaultPageThemes } from './pageTheme';
const DEFAULT_FONT_FAMILY =
'"Helvetica Neue", Helvetica, Roboto, Arial, sans-serif';
@@ -30,7 +31,16 @@ const DEFAULT_FONT_FAMILY =
export function createThemeOptions(
options: SimpleThemeOptions,
): BackstageThemeOptions {
const { palette, fontFamily = DEFAULT_FONT_FAMILY } = options;
const {
palette,
fontFamily = DEFAULT_FONT_FAMILY,
defaultPageTheme,
pageTheme = defaultPageThemes,
} = options;
if (!pageTheme[defaultPageTheme]) {
throw new Error(`${defaultPageTheme} is not defined in pageTheme.`);
}
return {
palette,
@@ -68,6 +78,9 @@ export function createThemeOptions(
marginBottom: 10,
},
},
page: pageTheme[defaultPageTheme],
getPageTheme: ({ themeId }) =>
pageTheme[themeId] ?? pageTheme[defaultPageTheme],
};
}
+1
View File
@@ -17,3 +17,4 @@
export * from './themes';
export * from './baseTheme';
export * from './types';
export * from './pageTheme';
@@ -13,12 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export type PageTheme = {
colors: string[];
shape: string;
backgroundImage: string;
};
import { PageTheme } from './types';
/*
# How to add a shape
@@ -29,7 +24,6 @@ export type PageTheme = {
with something like https://npm.runkit.com/mini-svg-data-uri
4. Wrap the output in `url("")`
5. Give it a name and paste it into the `shapes` object below.
*/
export const shapes: Record<string, string> = {
wave: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1368' height='400' fill='none'%3e%3cmask id='a' width='1368' height='401' x='0' y='0' maskUnits='userSpaceOnUse'%3e%3cpath fill='url(%23paint0_linear)' d='M437 116C223 116 112 0 112 0h1256v400c-82 0-225-21-282-109-112-175-436-175-649-175z'/%3e%3cpath fill='url(%23paint1_linear)' d='M1368 400V282C891-29 788 40 711 161 608 324 121 372 0 361v39h1368z'/%3e%3cpath fill='url(%23paint2_linear)' d='M1368 244v156H0V94c92-24 198-46 375 0l135 41c176 51 195 109 858 109z'/%3e%3cpath fill='url(%23paint3_linear)' d='M1252 400h116c-14-7-35-14-116-16-663-14-837-128-1013-258l-85-61C98 28 46 8 0 0v400h1252z'/%3e%3c/mask%3e%3cg mask='url(%23a)'%3e%3cpath fill='white' d='M-172-98h1671v601H-172z'/%3e%3c/g%3e%3cdefs%3e%3clinearGradient id='paint0_linear' x1='602' x2='1093.5' y1='-960.5' y2='272' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint1_linear' x1='482' x2='480' y1='1058.5' y2='70.5' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint2_linear' x1='424' x2='446.1' y1='-587.5' y2='274.6' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3clinearGradient id='paint3_linear' x1='587' x2='349' y1='-1120.5' y2='341' gradientUnits='userSpaceOnUse'%3e%3cstop stop-color='white'/%3e%3cstop offset='1' stop-color='white' stop-opacity='0'/%3e%3c/linearGradient%3e%3c/defs%3e%3c/svg%3e")`,
@@ -49,24 +43,24 @@ export const colorVariants: Record<string, string[]> = {
pinkSea: ['#C8077A', '#C2297D'],
};
export const pageTheme: Record<string, PageTheme> = {
home: genTheme(colorVariants.teal, shapes.wave),
documentation: genTheme(colorVariants.pinkSea, shapes.wave2),
tool: genTheme(colorVariants.purpleSky, shapes.round),
service: genTheme(colorVariants.marineBlue, shapes.wave),
website: genTheme(colorVariants.veryBlue, shapes.wave),
library: genTheme(colorVariants.rubyRed, shapes.wave),
other: genTheme(colorVariants.darkGrey, shapes.wave),
app: genTheme(colorVariants.toastyOrange, shapes.wave),
};
// As the background shapes and colors are decorative, we place them onto
// the page as a css background-image instead of an html element of its own.
// Utility to not have to write colors and shapes twice.
function genTheme(colors: string[], shape: string) {
export function genPageTheme(colors: string[], shape: string): PageTheme {
const gradientColors = colors.length === 1 ? [colors[0], colors[0]] : colors;
const gradient = `linear-gradient(90deg, ${gradientColors.join(', ')})`;
const backgroundImage = `${shape}, ${gradient}`;
return { colors, shape, backgroundImage };
}
export const pageTheme: Record<string, PageTheme> = {
home: genPageTheme(colorVariants.teal, shapes.wave),
documentation: genPageTheme(colorVariants.pinkSea, shapes.wave2),
tool: genPageTheme(colorVariants.purpleSky, shapes.round),
service: genPageTheme(colorVariants.marineBlue, shapes.wave),
website: genPageTheme(colorVariants.veryBlue, shapes.wave),
library: genPageTheme(colorVariants.rubyRed, shapes.wave),
other: genPageTheme(colorVariants.darkGrey, shapes.wave),
app: genPageTheme(colorVariants.toastyOrange, shapes.wave),
};
+5
View File
@@ -15,6 +15,7 @@
*/
import { createTheme } from './baseTheme';
import { pageTheme } from './pageTheme';
import { yellow } from '@material-ui/core/colors';
export const lightTheme = createTheme({
@@ -75,6 +76,8 @@ export const lightTheme = createTheme({
indicator: '#9BF0E1',
},
},
defaultPageTheme: 'home',
pageTheme,
});
export const darkTheme = createTheme({
@@ -135,4 +138,6 @@ export const darkTheme = createTheme({
indicator: '#9BF0E1',
},
},
defaultPageTheme: 'home',
pageTheme,
});
+16
View File
@@ -74,12 +74,20 @@ type PaletteAdditions = {
export type BackstagePalette = Palette & PaletteAdditions;
export type BackstagePaletteOptions = PaletteOptions & PaletteAdditions;
export type PageThemeSelector = {
themeId: string;
};
export interface BackstageTheme extends Theme {
palette: BackstagePalette;
page: PageTheme;
getPageTheme: ({ themeId }: PageThemeSelector) => PageTheme;
}
export interface BackstageThemeOptions extends ThemeOptions {
palette: BackstagePaletteOptions;
page: PageTheme;
getPageTheme: ({ themeId }: PageThemeSelector) => PageTheme;
}
/**
@@ -87,5 +95,13 @@ export interface BackstageThemeOptions extends ThemeOptions {
*/
export type SimpleThemeOptions = {
palette: BackstagePaletteOptions;
defaultPageTheme: string;
pageTheme?: Record<string, PageTheme>;
fontFamily?: string;
};
export type PageTheme = {
colors: string[];
shape: string;
backgroundImage: string;
};