diff --git a/microsite/pages/en/index.js b/microsite/pages/en/index.js
index 37298ae8e2..6e649aa574 100644
--- a/microsite/pages/en/index.js
+++ b/microsite/pages/en/index.js
@@ -471,8 +471,8 @@ class Index extends React.Component {
Cloud Native Computing Foundation
{' '}
sandbox project
+
-
diff --git a/microsite/static/css/custom.css b/microsite/static/css/custom.css
index 00a102ba00..79b8fd9de4 100644
--- a/microsite/static/css/custom.css
+++ b/microsite/static/css/custom.css
@@ -1072,6 +1072,7 @@ code {
}
.cncf-block {
+ padding-top: 40px;
text-align: center;
}
@@ -1080,4 +1081,5 @@ code {
width: 100%;
height: 100px;
margin-bottom: 40px;
+ margin-top: 20px;
}
diff --git a/package.json b/package.json
index c64b7f6951..2622f1e322 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
"docgen": "lerna run docgen",
"docker-build:app": "yarn workspace example-app build && docker build . -t spotify/backstage",
"docker-build": "yarn tsc && yarn workspace example-backend build-image",
- "create-plugin": "backstage-cli create-plugin",
+ "create-plugin": "backstage-cli create-plugin --scope backstage --no-private",
"remove-plugin": "backstage-cli remove-plugin",
"release": "if [ \"$(git symbolic-ref --short HEAD)\" = master ]; then echo \"don't try to release master\"; exit 1; else lerna version --no-push --force-publish; fi",
"prettier:check": "prettier --check .",
diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts
index 8f75728544..37c026ecae 100644
--- a/packages/cli/src/commands/create-plugin/createPlugin.ts
+++ b/packages/cli/src/commands/create-plugin/createPlugin.ts
@@ -21,6 +21,7 @@ import inquirer, { Answers, Question } from 'inquirer';
import { exec as execCb } from 'child_process';
import { resolve as resolvePath } from 'path';
import os from 'os';
+import { Command } from 'commander';
import {
parseOwnerIds,
addCodeownersEntry,
@@ -32,12 +33,12 @@ import { version as backstageVersion } from '../../lib/version';
const exec = promisify(execCb);
-async function checkExists(rootDir: string, id: string) {
- await Task.forItem('checking', id, async () => {
- const destination = resolvePath(rootDir, 'plugins', id);
-
+async function checkExists(destination: string) {
+ await Task.forItem('checking', destination, async () => {
if (await fs.pathExists(destination)) {
- const existing = chalk.cyan(destination.replace(`${rootDir}/`, ''));
+ const existing = chalk.cyan(
+ destination.replace(`${paths.targetRoot}/`, ''),
+ );
throw new Error(
`A plugin with the same name already exists: ${existing}\nPlease try again with a different plugin ID`,
);
@@ -86,10 +87,9 @@ export const addExportStatement = async (
export async function addPluginDependencyToApp(
rootDir: string,
- pluginName: string,
+ pluginPackage: string,
versionStr: string,
) {
- const pluginPackage = `@backstage/plugin-${pluginName}`;
const packageFilePath = 'packages/app/package.json';
const packageFile = resolvePath(rootDir, packageFilePath);
@@ -116,8 +116,11 @@ export async function addPluginDependencyToApp(
});
}
-export async function addPluginToApp(rootDir: string, pluginName: string) {
- const pluginPackage = `@backstage/plugin-${pluginName}`;
+export async function addPluginToApp(
+ rootDir: string,
+ pluginName: string,
+ pluginPackage: string,
+) {
const pluginNameCapitalized = pluginName
.split('-')
.map(name => capitalize(name))
@@ -175,7 +178,7 @@ export async function movePlugin(
});
}
-export default async ({ backend }: { backend: boolean }) => {
+export default async (cmd: Command) => {
const codeownersPath = await getCodeownersFilePath(paths.targetRoot);
const questions: Question[] = [
@@ -221,22 +224,33 @@ export default async ({ backend }: { backend: boolean }) => {
}
const answers: Answers = await inquirer.prompt(questions);
-
+ const name = cmd.scope
+ ? `@${cmd.scope.replace(/^@/, '')}/plugin-${answers.id}`
+ : `plugin-${answers.id}`;
+ const npmRegistry = cmd.npmRegistry && cmd.scope ? cmd.npmRegistry : '';
+ const privatePackage = cmd.private === false ? false : true;
+ const isMonoRepo = await fs.pathExists(paths.resolveTargetRoot('lerna.json'));
const appPackage = paths.resolveTargetRoot('packages/app');
const templateDir = paths.resolveOwn(
- backend ? 'templates/default-backend-plugin' : 'templates/default-plugin',
+ cmd.backend
+ ? 'templates/default-backend-plugin'
+ : 'templates/default-plugin',
);
const tempDir = resolvePath(os.tmpdir(), answers.id);
- const pluginDir = paths.resolveTargetRoot('plugins', answers.id);
+ const pluginDir = isMonoRepo
+ ? paths.resolveTargetRoot('plugins', answers.id)
+ : paths.resolveTargetRoot(answers.id);
const ownerIds = parseOwnerIds(answers.owner);
- const { version } = await fs.readJson(paths.resolveTargetRoot('lerna.json'));
+ const { version } = isMonoRepo
+ ? await fs.readJson(paths.resolveTargetRoot('lerna.json'))
+ : { version: '0.1.0' };
Task.log();
Task.log('Creating the plugin...');
try {
Task.section('Checking if the plugin ID is available');
- await checkExists(paths.targetRoot, answers.id);
+ await checkExists(pluginDir);
Task.section('Creating a temporary plugin directory');
await createTemporaryPluginFolder(tempDir);
@@ -247,6 +261,9 @@ export default async ({ backend }: { backend: boolean }) => {
...answers,
version,
backstageVersion,
+ name,
+ privatePackage,
+ npmRegistry,
});
Task.section('Moving to final location');
@@ -257,10 +274,10 @@ export default async ({ backend }: { backend: boolean }) => {
if ((await fs.pathExists(appPackage)) && !backend) {
Task.section('Adding plugin as dependency in app');
- await addPluginDependencyToApp(paths.targetRoot, answers.id, version);
+ await addPluginDependencyToApp(paths.targetRoot, name, version);
Task.section('Import plugin in app');
- await addPluginToApp(paths.targetRoot, answers.id);
+ await addPluginToApp(paths.targetRoot, answers.id, name);
}
if (ownerIds && ownerIds.length) {
@@ -272,11 +289,7 @@ export default async ({ backend }: { backend: boolean }) => {
}
Task.log();
- Task.log(
- `🥇 Successfully created ${chalk.cyan(
- `@backstage/plugin-${answers.id}`,
- )}`,
- );
+ Task.log(`🥇 Successfully created ${chalk.cyan(`${name}`)}`);
Task.log();
Task.exit();
} catch (error) {
diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts
index a0b898db75..fea250cd55 100644
--- a/packages/cli/src/commands/index.ts
+++ b/packages/cli/src/commands/index.ts
@@ -66,6 +66,9 @@ export function registerCommands(program: CommanderStatic) {
'Create plugin with the backend dependencies as default',
)
.description('Creates a new plugin in the current repository')
+ .option('--scope ', 'NPM scope')
+ .option('--npm-registry ', 'NPM registry URL')
+ .option('--no-private', 'Public NPM Package')
.action(
lazy(() => import('./create-plugin/createPlugin').then(m => m.default)),
);
diff --git a/packages/cli/src/commands/plugin/diff.ts b/packages/cli/src/commands/plugin/diff.ts
index 99af093f2c..f5dfa670b7 100644
--- a/packages/cli/src/commands/plugin/diff.ts
+++ b/packages/cli/src/commands/plugin/diff.ts
@@ -30,6 +30,9 @@ import { version as backstageVersion } from '../../lib/version';
export type PluginData = {
id: string;
name: string;
+ privatePackage: string;
+ version: string;
+ npmRegistry: string;
};
const fileHandlers = [
@@ -62,11 +65,8 @@ export default async (cmd: Command) => {
promptFunc = yesPromptFunc;
}
- const { version } = await fs.readJson(paths.resolveTargetRoot('lerna.json'));
-
const data = await readPluginData();
const templateFiles = await diffTemplateFiles('default-plugin', {
- version,
backstageVersion,
...data,
});
@@ -77,9 +77,19 @@ export default async (cmd: Command) => {
// Reads templating data from the existing plugin
async function readPluginData(): Promise {
let name: string;
+ let privatePackage: string;
+ let version: string;
+ let npmRegistry: string;
try {
const pkg = require(paths.resolveTarget('package.json'));
name = pkg.name;
+ privatePackage = pkg.private;
+ version = pkg.version;
+ const scope = name.split('/')[0];
+ if (`${scope}:registry` in pkg.publishConfig) {
+ const registryURL = pkg.publishConfig[`${scope}:registry`];
+ npmRegistry = `"${scope}:registry" : "${registryURL}"`;
+ } else npmRegistry = '';
} catch (error) {
throw new Error(`Failed to read target package, ${error}`);
}
@@ -96,5 +106,5 @@ async function readPluginData(): Promise {
const id = pluginIdMatch[1];
- return { id, name };
+ return { id, name, privatePackage, version, npmRegistry };
}
diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs
index fd2c57f87d..2ecdbcef01 100644
--- a/packages/cli/templates/default-backend-plugin/package.json.hbs
+++ b/packages/cli/templates/default-backend-plugin/package.json.hbs
@@ -1,41 +1,44 @@
{
- "name": "@backstage/plugin-{{id}}-backend",
+ "name": "{{name}}",
"version": "{{version}}",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
- "private": true,
+ {{#if privatePackage}} "private": {{privatePackage}},
+ {{/if}}
"publishConfig": {
- "access": "public",
- "main": "dist/index.cjs.js",
- "types": "dist/index.d.ts"
- },
- "scripts": {
- "start": "backstage-cli backend:dev",
- "build": "backstage-cli backend:build",
- "lint": "backstage-cli lint",
- "test": "backstage-cli test",
- "prepack": "backstage-cli prepack",
- "postpack": "backstage-cli postpack",
- "clean": "backstage-cli clean"
- },
- "dependencies": {
- "@backstage/backend-common": "^{{version}}",
- "@backstage/config": "^{{version}}",
- "@types/express": "^4.17.6",
- "express": "^4.17.1",
- "express-promise-router": "^3.0.3",
- "winston": "^3.2.1",
- "node-fetch": "^2.6.0",
- "yn": "^4.0.0"
- },
- "devDependencies": {
- "@backstage/cli": "^{{version}}",
- "@types/supertest": "^2.0.8",
- "supertest": "^4.0.2",
- "msw": "^0.19.5"
- },
- "files": [
- "dist"
- ]
-}
+ {{#if npmRegistry}} "registry": "{{npmRegistry}}",
+ {{/if}}
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "types": "dist/index.d.ts"
+ },
+ "scripts": {
+ "start": "backstage-cli backend:dev",
+ "build": "backstage-cli backend:build",
+ "lint": "backstage-cli lint",
+ "test": "backstage-cli test",
+ "prepack": "backstage-cli prepack",
+ "postpack": "backstage-cli postpack",
+ "clean": "backstage-cli clean"
+ },
+ "dependencies": {
+ "@backstage/backend-common": "^{{version}}",
+ "@backstage/config": "^{{version}}",
+ "@types/express": "^4.17.6",
+ "express": "^4.17.1",
+ "express-promise-router": "^3.0.3",
+ "winston": "^3.2.1",
+ "node-fetch": "^2.6.0",
+ "yn": "^4.0.0"
+ },
+ "devDependencies": {
+ "@backstage/cli": "^{{version}}",
+ "@types/supertest": "^2.0.8",
+ "supertest": "^4.0.2",
+ "msw": "^0.19.5"
+ },
+ "files": [
+ "dist"
+ ]
+ }
diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs
index 8adfb0d7bc..7597d3dc28 100644
--- a/packages/cli/templates/default-plugin/package.json.hbs
+++ b/packages/cli/templates/default-plugin/package.json.hbs
@@ -1,11 +1,14 @@
{
- "name": "@backstage/plugin-{{id}}",
+ "name": "{{name}}",
"version": "{{version}}",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
- "private": true,
+{{#if privatePackage}} "private": {{privatePackage}},
+{{/if}}
"publishConfig": {
+{{#if npmRegistry}} "registry": "{{npmRegistry}}",
+{{/if}}
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
diff --git a/packages/core/package.json b/packages/core/package.json
index 3ed730e0f3..440f9f05b0 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -30,7 +30,7 @@
},
"dependencies": {
"@backstage/config": "^0.1.1-alpha.22",
- "@backstage/core-api": "^0.1.1-alpha.22",
+ "@backstage/core-api": "0.1.1-alpha.22",
"@backstage/theme": "^0.1.1-alpha.22",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs
index b95b5bbc5b..19e3891551 100644
--- a/packages/create-app/templates/default-app/package.json.hbs
+++ b/packages/create-app/templates/default-app/package.json.hbs
@@ -16,7 +16,7 @@
"test:all": "lerna run test -- --coverage",
"lint": "lerna run lint --since origin/master --",
"lint:all": "lerna run lint --",
- "create-plugin": "backstage-cli create-plugin",
+ "create-plugin": "backstage-cli create-plugin --scope backstage --no-private",
"remove-plugin": "backstage-cli remove-plugin"
},
"workspaces": {
diff --git a/packages/e2e-test/src/e2e-test.ts b/packages/e2e-test/src/e2e-test.ts
index 146a3ec644..969a77cb2a 100644
--- a/packages/e2e-test/src/e2e-test.ts
+++ b/packages/e2e-test/src/e2e-test.ts
@@ -81,7 +81,11 @@ async function buildDistWorkspace(workspaceName: string, rootDir: string) {
const path = paths.resolveOwnRoot(pkgJsonPath);
const pkgTemplate = await fs.readFile(path, 'utf8');
const { dependencies = {}, devDependencies = {} } = JSON.parse(
- handlebars.compile(pkgTemplate)({ version: '0.0.0' }),
+ handlebars.compile(pkgTemplate)({
+ version: '0.0.0',
+ privatePackage: true,
+ scopeName: '@backstage',
+ }),
);
Array()
diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx
index db490cf542..ea99753c14 100644
--- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx
+++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx
@@ -46,6 +46,9 @@ const useStyles = makeStyles(theme => ({
gridTemplateColumns: '250px 1fr',
gridColumnGap: theme.spacing(2),
},
+ buttonSpacing: {
+ marginLeft: theme.spacing(2),
+ },
}));
const CatalogPageContents = () => {
@@ -56,6 +59,7 @@ const CatalogPageContents = () => {
reload,
matchingEntities,
availableTags,
+ isCatalogEmpty,
} = useFilteredEntities();
const configApi = useApi(configApiRef);
const catalogApi = useApi(catalogApiRef);
@@ -141,6 +145,9 @@ const CatalogPageContents = () => {
[isStarredEntity, userId, orgName],
);
+ const showAddExampleEntities =
+ configApi.has('catalog.exampleEntityLocations') && isCatalogEmpty;
+
return (
{
>
Create Component
+ {showAddExampleEntities && (
+
+ )}
All your software catalog entities
@@ -174,7 +191,6 @@ const CatalogPageContents = () => {
entities={matchingEntities}
loading={loading}
error={error}
- onAddMockData={addMockData}
/>
diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx
index d1eef2a005..46d6010f2f 100644
--- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx
+++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx
@@ -46,7 +46,6 @@ describe('CatalogTable component', () => {
entities={[]}
loading={false}
error={{ code: 'error' }}
- onAddMockData={() => {}}
/>,
),
);
@@ -63,7 +62,6 @@ describe('CatalogTable component', () => {
titlePreamble="Owned"
entities={entities}
loading={false}
- onAddMockData={() => {}}
/>,
),
);
diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx
index cd4df13775..f5885145b3 100644
--- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx
+++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx
@@ -16,11 +16,10 @@
import { Entity, LocationSpec } from '@backstage/catalog-model';
import { Table, TableColumn, TableProps } from '@backstage/core';
import { Chip, Link } from '@material-ui/core';
-import Add from '@material-ui/icons/Add';
import Edit from '@material-ui/icons/Edit';
import GitHub from '@material-ui/icons/GitHub';
import { Alert } from '@material-ui/lab';
-import React, { Dispatch } from 'react';
+import React from 'react';
import { generatePath, Link as RouterLink } from 'react-router-dom';
import { findLocationForEntityMeta } from '../../data/utils';
import { useStarredEntities } from '../../hooks/useStarredEntites';
@@ -87,7 +86,6 @@ type CatalogTableProps = {
titlePreamble: string;
loading: boolean;
error?: any;
- onAddMockData: Dispatch;
};
export const CatalogTable = ({
@@ -95,7 +93,6 @@ export const CatalogTable = ({
loading,
error,
titlePreamble,
- onAddMockData,
}: CatalogTableProps) => {
const { isStarredEntity, toggleStarredEntity } = useStarredEntities();
@@ -151,13 +148,6 @@ export const CatalogTable = ({
onClick: () => toggleStarredEntity(rowData),
};
},
- {
- icon: () => ,
- tooltip: 'Add example components',
- isFreeAction: true,
- onClick: onAddMockData,
- hidden: !(entities && entities.length === 0),
- },
];
return (
diff --git a/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx b/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx
index 94bc428a45..ceecc17953 100644
--- a/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx
+++ b/plugins/catalog/src/filter/EntityFilterGroupsProvider.tsx
@@ -47,7 +47,7 @@ export const EntityFilterGroupsProvider = ({
function useProvideEntityFilters(): FilterGroupsContext {
const catalogApi = useApi(catalogApiRef);
const [{ value: entities, error }, doReload] = useAsyncFn(() =>
- catalogApi.getEntities(),
+ catalogApi.getEntities({ kind: 'Component' }),
);
const filterGroups = useRef<{
@@ -62,6 +62,7 @@ function useProvideEntityFilters(): FilterGroupsContext {
}>({});
const [matchingEntities, setMatchingEntities] = useState([]);
const [availableTags, setAvailableTags] = useState([]);
+ const [isCatalogEmpty, setCatalogEmpty] = useState(false);
useEffect(() => {
doReload();
@@ -86,6 +87,7 @@ function useProvideEntityFilters(): FilterGroupsContext {
),
);
setAvailableTags(collectTags(entities));
+ setCatalogEmpty(entities !== undefined && entities.length === 0);
}, [entities, error]);
const register = useCallback(
@@ -143,6 +145,7 @@ function useProvideEntityFilters(): FilterGroupsContext {
filterGroupStates,
matchingEntities,
availableTags,
+ isCatalogEmpty,
};
}
diff --git a/plugins/catalog/src/filter/context.ts b/plugins/catalog/src/filter/context.ts
index 838ec8d532..c025480fa6 100644
--- a/plugins/catalog/src/filter/context.ts
+++ b/plugins/catalog/src/filter/context.ts
@@ -33,6 +33,7 @@ export type FilterGroupsContext = {
filterGroupStates: { [filterGroupId: string]: FilterGroupStates };
matchingEntities: Entity[];
availableTags: string[];
+ isCatalogEmpty: boolean;
};
/**
diff --git a/plugins/catalog/src/filter/useFilteredEntities.ts b/plugins/catalog/src/filter/useFilteredEntities.ts
index 138a7547c0..2d7dcfd89d 100644
--- a/plugins/catalog/src/filter/useFilteredEntities.ts
+++ b/plugins/catalog/src/filter/useFilteredEntities.ts
@@ -31,6 +31,7 @@ export function useFilteredEntities() {
error: context.error,
matchingEntities: context.matchingEntities,
availableTags: context.availableTags,
+ isCatalogEmpty: context.isCatalogEmpty,
reload: context.reload,
};
}
diff --git a/plugins/gcp-projects/src/api/GCPClient.ts b/plugins/gcp-projects/src/api/GCPClient.ts
deleted file mode 100644
index dc4fe21132..0000000000
--- a/plugins/gcp-projects/src/api/GCPClient.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * Copyright 2020 Spotify AB
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { GCPApi } from './GCPApi';
-import { Project, Operation, Status } from './types';
-
-const BaseURL =
- 'https://content-cloudresourcemanager.googleapis.com/v1/projects';
-
-export class GCPClient implements GCPApi {
- async listProjects({ token }: { token: string }): Promise {
- const response = await fetch(BaseURL, {
- headers: new Headers({
- Accept: '*/*',
- Authorization: `Bearer ${token}`,
- }),
- });
-
- if (!response.ok) {
- return [
- {
- name: 'Error',
- projectNumber: 'Response status is not OK',
- projectId: 'Error',
- lifecycleState: 'error',
- createTime: 'Error',
- },
- ];
- }
-
- const data = await response.json();
-
- return data.projects;
- }
-
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- async getProject(
- projectId: string,
- token: Promise,
- ): Promise {
- const url = `${BaseURL}/${projectId}`;
- const response = await fetch(url, {
- headers: new Headers({
- Authorization: `Bearer ${await token}`,
- }),
- });
-
- const dataBlank: Project = {
- name: 'Error',
- projectNumber: `Response status is ${response.status}`,
- projectId: 'Error',
- lifecycleState: 'error',
- createTime: 'Error',
- };
-
- if (!response.ok) {
- return dataBlank;
- }
-
- const data = await response.json();
-
- const newData: Project = data;
-
- return newData;
- }
-
- async createProject(
- projectName: string,
- projectId: string,
- token: string,
- ): Promise {
- const status: Status = {
- code: 0,
- message: '',
- details: [],
- };
-
- const op: Operation = {
- name: '',
- metadata: '',
- done: true,
- error: status,
- response: '',
- };
-
- const newProject: Project = {
- name: projectName,
- projectId: projectId,
- };
-
- const body = JSON.stringify(newProject);
-
- const response = await fetch(BaseURL, {
- headers: new Headers({
- Accept: '*/*',
- Authorization: `Bearer ${token}`,
- }),
- body: body,
- method: 'POST',
- });
-
- if (!response.ok) {
- status.code = response.status;
- return op;
- }
-
- const data = await response.json();
-
- return data;
- }
-}
diff --git a/plugins/gcp-projects/src/api/GCPApi.ts b/plugins/gcp-projects/src/api/GcpApi.ts
similarity index 69%
rename from plugins/gcp-projects/src/api/GCPApi.ts
rename to plugins/gcp-projects/src/api/GcpApi.ts
index 95b686815e..24ad1a93cc 100644
--- a/plugins/gcp-projects/src/api/GCPApi.ts
+++ b/plugins/gcp-projects/src/api/GcpApi.ts
@@ -17,18 +17,16 @@
import { createApiRef } from '@backstage/core';
import { Project, Operation } from './types';
-export const GCPApiRef = createApiRef({
+export const gcpApiRef = createApiRef({
id: 'plugin.gcpprojects.service',
description: 'Used by the GCP Projects plugin to make requests',
});
-export type GCPApi = {
- listProjects: ({ token }: { token: string }) => Promise;
- getProject: (projectId: string, token: Promise) => Promise;
- createProject: (
- projectName: string,
- projectId: string,
- owner: string,
- token: string,
- ) => Promise;
+export type GcpApi = {
+ listProjects(): Promise;
+ getProject(projectId: string): Promise;
+ createProject(options: {
+ projectId: string;
+ projectName: string;
+ }): Promise;
};
diff --git a/plugins/gcp-projects/src/api/GcpClient.ts b/plugins/gcp-projects/src/api/GcpClient.ts
new file mode 100644
index 0000000000..00bb859219
--- /dev/null
+++ b/plugins/gcp-projects/src/api/GcpClient.ts
@@ -0,0 +1,98 @@
+/*
+ * 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 { OAuthApi } from '@backstage/core';
+import { GcpApi } from './GcpApi';
+import { Operation, Project } from './types';
+
+const BASE_URL =
+ 'https://content-cloudresourcemanager.googleapis.com/v1/projects';
+
+export class GcpClient implements GcpApi {
+ constructor(private readonly googleAuthApi: OAuthApi) {}
+
+ async listProjects(): Promise {
+ const response = await fetch(BASE_URL, {
+ headers: {
+ Accept: '*/*',
+ Authorization: `Bearer ${await this.getToken()}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `List request failed to ${BASE_URL} with ${response.status} ${response.statusText}`,
+ );
+ }
+
+ const { projects } = await response.json();
+ return projects;
+ }
+
+ async getProject(projectId: string): Promise {
+ const url = `${BASE_URL}/${projectId}`;
+ const response = await fetch(url, {
+ headers: {
+ Authorization: `Bearer ${await this.getToken()}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `Get request failed to ${url} with ${response.status} ${response.statusText}`,
+ );
+ }
+
+ return await response.json();
+ }
+
+ async createProject(options: {
+ projectId: string;
+ projectName: string;
+ }): Promise {
+ const newProject: Project = {
+ name: options.projectName,
+ projectId: options.projectId,
+ };
+
+ const response = await fetch(BASE_URL, {
+ method: 'POST',
+ headers: {
+ Accept: '*/*',
+ Authorization: `Bearer ${await this.getToken()}`,
+ },
+ body: JSON.stringify(newProject),
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `Create request failed to ${BASE_URL} with ${response.status} ${response.statusText}`,
+ );
+ }
+
+ return await response.json();
+ }
+
+ async getToken(): Promise {
+ // NOTE(freben): There's a .read-only variant of this scope that we could
+ // use for readonly operations, but that means we would ask the user for a
+ // second auth during creation and I decided to keep the wider scope for
+ // all ops for now
+ return this.googleAuthApi.getAccessToken(
+ 'https://www.googleapis.com/auth/cloud-platform',
+ );
+ }
+}
diff --git a/plugins/gcp-projects/src/api/index.ts b/plugins/gcp-projects/src/api/index.ts
index 51f617f19c..8e842c8e55 100644
--- a/plugins/gcp-projects/src/api/index.ts
+++ b/plugins/gcp-projects/src/api/index.ts
@@ -14,6 +14,6 @@
* limitations under the License.
*/
-export * from './GCPApi';
-export * from './GCPClient';
+export * from './GcpApi';
+export * from './GcpClient';
export * from './types';
diff --git a/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx b/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx
index 0149f8ac92..f03149bc1a 100644
--- a/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx
+++ b/plugins/gcp-projects/src/components/NewProjectPage/NewProjectPage.tsx
@@ -14,24 +14,23 @@
* limitations under the License.
*/
-import React, { FC, useState } from 'react';
-import { Grid, Button, TextField } from '@material-ui/core';
-
import {
- InfoCard,
Content,
ContentHeader,
+ Header,
+ HeaderLabel,
+ InfoCard,
+ Page,
+ pageTheme,
SimpleStepper,
SimpleStepperStep,
StructuredMetadataTable,
- HeaderLabel,
- Page,
- Header,
- pageTheme,
SupportButton,
} from '@backstage/core';
+import { Button, Grid, TextField } from '@material-ui/core';
+import React, { useState } from 'react';
-export const Project: FC<{}> = () => {
+export const Project = () => {
const [projectName, setProjectName] = useState('');
const [projectId, setProjectId] = useState('');
const [disabled, setDisabled] = useState(true);
diff --git a/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx
index 41d0b11019..3dfb4e886b 100644
--- a/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx
+++ b/plugins/gcp-projects/src/components/ProjectDetailsPage/ProjectDetailsPage.tsx
@@ -14,6 +14,17 @@
* limitations under the License.
*/
+import {
+ Content,
+ ContentHeader,
+ Header,
+ HeaderLabel,
+ Page,
+ pageTheme,
+ SupportButton,
+ useApi,
+ WarningPanel,
+} from '@backstage/core';
import {
Button,
ButtonGroup,
@@ -27,20 +38,9 @@ import {
Theme,
Typography,
} from '@material-ui/core';
-import {
- useApi,
- googleAuthApiRef,
- HeaderLabel,
- Page,
- Header,
- pageTheme,
- SupportButton,
- Content,
- ContentHeader,
-} from '@backstage/core';
import React from 'react';
import { useAsync } from 'react-use';
-import { GCPApiRef } from '../../api';
+import { gcpApiRef } from '../../api';
const useStyles = makeStyles(theme => ({
root: {
@@ -56,34 +56,27 @@ const useStyles = makeStyles(theme => ({
}));
const DetailsPage = () => {
- const api = useApi(GCPApiRef);
- const googleApi = useApi(googleAuthApiRef);
- const token = googleApi.getAccessToken(
- 'https://www.googleapis.com/auth/cloud-platform.read-only',
- );
-
+ const api = useApi(gcpApiRef);
const classes = useStyles();
- const status = useAsync(
- () =>
+
+ const { loading, error, value: details } = useAsync(
+ async () =>
api.getProject(
decodeURIComponent(location.search.split('projectId=')[1]),
- token,
),
[location.search],
);
- if (status.loading) {
+ if (loading) {
return ;
- } else if (status.error) {
+ } else if (error) {
return (
-
- Failed to load build, {status.error.message}
-
+
+ {error.toString()}
+
);
}
- const details = status.value;
-
return (
diff --git a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx
index 7e09a307c9..542d03d5fd 100644
--- a/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx
+++ b/plugins/gcp-projects/src/components/ProjectListPage/ProjectListPage.tsx
@@ -17,18 +17,19 @@
// NEEDS WORK
import {
- Link,
- useApi,
- googleAuthApiRef,
- HeaderLabel,
- Page,
- Header,
- pageTheme,
- SupportButton,
Content,
ContentHeader,
+ Header,
+ HeaderLabel,
+ Link,
+ Page,
+ pageTheme,
+ SupportButton,
+ useApi,
+ WarningPanel,
} from '@backstage/core';
import {
+ Button,
LinearProgress,
Paper,
Table,
@@ -38,11 +39,10 @@ import {
TableRow,
Tooltip,
Typography,
- Button,
} from '@material-ui/core';
import React from 'react';
import { useAsync } from 'react-use';
-import { GCPApiRef, Project } from '../../api';
+import { gcpApiRef, Project } from '../../api';
const LongText = ({ text, max }: { text: string; max: number }) => {
if (text.length < max) {
@@ -63,27 +63,17 @@ const labels = (
);
const PageContents = () => {
- const api = useApi(GCPApiRef);
- const googleApi = useApi(googleAuthApiRef);
+ const api = useApi(gcpApiRef);
- const { loading, error, value } = useAsync(async () => {
- const token = await googleApi.getAccessToken(
- 'https://www.googleapis.com/auth/cloud-platform.read-only',
- );
-
- const projects = api.listProjects({ token });
- return projects;
- });
+ const { loading, error, value } = useAsync(() => api.listProjects());
if (loading) {
return ;
- }
-
- if (error) {
+ } else if (error) {
return (
-
- {error.message}{' '}
-
+
+ {error.toString()}
+
);
}
diff --git a/plugins/gcp-projects/src/plugin.ts b/plugins/gcp-projects/src/plugin.ts
index 41aacde597..2baf3bce7d 100644
--- a/plugins/gcp-projects/src/plugin.ts
+++ b/plugins/gcp-projects/src/plugin.ts
@@ -15,34 +15,43 @@
*/
import {
+ createApiFactory,
createPlugin,
createRouteRef,
- createApiFactory,
+ googleAuthApiRef,
} from '@backstage/core';
-import { ProjectListPage } from './components/ProjectListPage';
-import { ProjectDetailsPage } from './components/ProjectDetailsPage';
+import { gcpApiRef, GcpClient } from './api';
import { NewProjectPage } from './components/NewProjectPage';
-import { GCPApiRef, GCPClient } from './api';
+import { ProjectDetailsPage } from './components/ProjectDetailsPage';
+import { ProjectListPage } from './components/ProjectListPage';
export const rootRouteRef = createRouteRef({
path: '/gcp-projects',
title: 'GCP Projects',
});
-export const ProjectRouteRef = createRouteRef({
+export const projectRouteRef = createRouteRef({
path: '/gcp-projects/project',
title: 'GCP Project Page',
});
-export const NewProjectRouteRef = createRouteRef({
+export const newProjectRouteRef = createRouteRef({
path: '/gcp-projects/new',
title: 'GCP Project Page',
});
export const plugin = createPlugin({
id: 'gcp-projects',
- apis: [createApiFactory(GCPApiRef, new GCPClient())],
+ apis: [
+ createApiFactory({
+ api: gcpApiRef,
+ deps: { googleAuthApi: googleAuthApiRef },
+ factory({ googleAuthApi }) {
+ return new GcpClient(googleAuthApi);
+ },
+ }),
+ ],
register({ router }) {
router.addRoute(rootRouteRef, ProjectListPage);
- router.addRoute(ProjectRouteRef, ProjectDetailsPage);
- router.addRoute(NewProjectRouteRef, NewProjectPage);
+ router.addRoute(projectRouteRef, ProjectDetailsPage);
+ router.addRoute(newProjectRouteRef, NewProjectPage);
},
});