fix(gcp-projects): some postponed cleanup of the plugin code (#2523)

This commit is contained in:
Fredrik Adelöw
2020-09-21 08:43:51 +02:00
committed by GitHub
parent c5d4458962
commit c742a38ddb
8 changed files with 171 additions and 208 deletions
-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);
},
});