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); }, });