Merge branch 'master' of github.com:spotify/backstage into feat/backend-plugin

* 'master' of github.com:spotify/backstage: (21 commits)
  core/package.json: pin core-api to same version as core
  [microsite] tweaking CNCF logo styling (#2529)
  feat(catalog): show only components
  fix(gcp-projects): some postponed cleanup of the plugin code (#2523)
  feat: move add example components to a more visible space and change behavior
  use version from lerna.json or fallback to 0.1.0
  remove scope from registry
  revert templatingTask
  revert templatingTask test
  update templatingTask test
  fix plugin template verification
  fix plugin:diff
  remove console.log
  fix plugin:diff
  un-escape var
  keep addPluginDependencyToApp argument consistent
  detect monorepo
  fix registryURL
  rename packageName
  improvement
  ...
This commit is contained in:
blam
2020-09-21 16:04:33 +02:00
25 changed files with 301 additions and 291 deletions
+1 -1
View File
@@ -471,8 +471,8 @@ class Index extends React.Component {
Cloud Native Computing Foundation
</a>{' '}
sandbox project
<div className="cncf-logo" />
</Block.SmallTitle>
<div className="cncf-logo" />
</Block.Container>
</Block>
</main>
+2
View File
@@ -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;
}
+1 -1
View File
@@ -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 .",
@@ -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) {
+3
View File
@@ -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 <scope>', 'NPM scope')
.option('--npm-registry <URL>', 'NPM registry URL')
.option('--no-private', 'Public NPM Package')
.action(
lazy(() => import('./create-plugin/createPlugin').then(m => m.default)),
);
+14 -4
View File
@@ -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<PluginData> {
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<PluginData> {
const id = pluginIdMatch[1];
return { id, name };
return { id, name, privatePackage, version, npmRegistry };
}
@@ -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"
]
}
@@ -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"
+1 -1
View File
@@ -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",
@@ -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": {
+5 -1
View File
@@ -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<string>()
@@ -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 (
<CatalogLayout>
<CatalogTabs
@@ -158,6 +165,16 @@ const CatalogPageContents = () => {
>
Create Component
</Button>
{showAddExampleEntities && (
<Button
className={styles.buttonSpacing}
variant="outlined"
color="primary"
onClick={addMockData}
>
Add example components
</Button>
)}
<SupportButton>All your software catalog entities</SupportButton>
</ContentHeader>
<div className={styles.contentWrapper}>
@@ -174,7 +191,6 @@ const CatalogPageContents = () => {
entities={matchingEntities}
loading={loading}
error={error}
onAddMockData={addMockData}
/>
</div>
</Content>
@@ -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={() => {}}
/>,
),
);
@@ -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<void>;
};
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: () => <Add />,
tooltip: 'Add example components',
isFreeAction: true,
onClick: onAddMockData,
hidden: !(entities && entities.length === 0),
},
];
return (
@@ -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<Entity[]>([]);
const [availableTags, setAvailableTags] = useState<string[]>([]);
const [isCatalogEmpty, setCatalogEmpty] = useState<boolean>(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,
};
}
+1
View File
@@ -33,6 +33,7 @@ export type FilterGroupsContext = {
filterGroupStates: { [filterGroupId: string]: FilterGroupStates };
matchingEntities: Entity[];
availableTags: string[];
isCatalogEmpty: boolean;
};
/**
@@ -31,6 +31,7 @@ export function useFilteredEntities() {
error: context.error,
matchingEntities: context.matchingEntities,
availableTags: context.availableTags,
isCatalogEmpty: context.isCatalogEmpty,
reload: context.reload,
};
}
-124
View File
@@ -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<Project[]> {
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<string>,
): Promise<Project> {
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<Operation> {
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;
}
}
@@ -17,18 +17,16 @@
import { createApiRef } from '@backstage/core';
import { Project, Operation } from './types';
export const GCPApiRef = createApiRef<GCPApi>({
export const gcpApiRef = createApiRef<GcpApi>({
id: 'plugin.gcpprojects.service',
description: 'Used by the GCP Projects plugin to make requests',
});
export type GCPApi = {
listProjects: ({ token }: { token: string }) => Promise<Project[]>;
getProject: (projectId: string, token: Promise<string>) => Promise<Project>;
createProject: (
projectName: string,
projectId: string,
owner: string,
token: string,
) => Promise<Operation>;
export type GcpApi = {
listProjects(): Promise<Project[]>;
getProject(projectId: string): Promise<Project>;
createProject(options: {
projectId: string;
projectName: string;
}): Promise<Operation>;
};
+98
View File
@@ -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<Project[]> {
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<Project> {
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<Operation> {
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<string> {
// 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',
);
}
}
+2 -2
View File
@@ -14,6 +14,6 @@
* limitations under the License.
*/
export * from './GCPApi';
export * from './GCPClient';
export * from './GcpApi';
export * from './GcpClient';
export * from './types';
@@ -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);
@@ -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>(theme => ({
root: {
@@ -56,34 +56,27 @@ const useStyles = makeStyles<Theme>(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 <LinearProgress />;
} else if (status.error) {
} else if (error) {
return (
<Typography variant="h6" color="error">
Failed to load build, {status.error.message}
</Typography>
<WarningPanel title="Failed to load project">
{error.toString()}
</WarningPanel>
);
}
const details = status.value;
return (
<Table component={Paper} className={classes.table}>
<Table>
@@ -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 <LinearProgress />;
}
if (error) {
} else if (error) {
return (
<Typography variant="h2" color="error">
{error.message}{' '}
</Typography>
<WarningPanel title="Failed to load projects">
{error.toString()}
</WarningPanel>
);
}
+18 -9
View File
@@ -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);
},
});