Fix validation file
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/camelcase */
|
||||
|
||||
import { resolve as resolvePath } from 'path';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -111,6 +111,9 @@ export async function createConfig(
|
||||
app: {
|
||||
title: options.config.getString('app.title'),
|
||||
baseUrl: validBaseUrl.href,
|
||||
googleAnalyticsTrackingId: options.config.getOptionalString(
|
||||
'app.googleAnalyticsTrackingId',
|
||||
),
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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();
|
||||
|
||||
+2
-3
@@ -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;
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
+1
-2
@@ -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
|
||||
+1
-1
@@ -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
|
||||
+1
-2
@@ -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
|
||||
+1
-2
@@ -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];
|
||||
|
||||
+1
-1
@@ -14,8 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ApiRef } from './ApiRef';
|
||||
import {
|
||||
ApiRef,
|
||||
ApiHolder,
|
||||
ApiFactoryHolder,
|
||||
AnyApiRef,
|
||||
+1
-2
@@ -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>;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -49,10 +49,6 @@ const main = (argv: string[]) => {
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp(chalk.yellow);
|
||||
}
|
||||
|
||||
program.parse(argv);
|
||||
};
|
||||
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,3 +17,4 @@
|
||||
export * from './themes';
|
||||
export * from './baseTheme';
|
||||
export * from './types';
|
||||
export * from './pageTheme';
|
||||
|
||||
+13
-19
@@ -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),
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user