diff --git a/plugins/circleci/src/api/index.ts b/plugins/circleci/src/api/index.ts index 34eaae4b35..70068f29ba 100644 --- a/plugins/circleci/src/api/index.ts +++ b/plugins/circleci/src/api/index.ts @@ -14,54 +14,91 @@ * limitations under the License. */ -import { CircleCI, GitType, CircleCIOptions, GitInfo, getBuildSummaries } from 'circleci-api'; +import { CircleCI, GitType, CircleCIOptions } from 'circleci-api'; import { ApiRef } from '@backstage/core'; -//import { default } from '../../../../packages/core/src/components/Status/Status.stories'; -const defaultVcsOptions: GitInfo = { - type: GitType.GITHUB, // default: github - owner: 'CircleCITest3', - repo: 'circleci-test', -} - -const options: Partial = { - // Required for all requests - // token: CIRCLECI_TOKEN, // Set your CircleCi API token - - // Optional - // Anything set here can be overriden when making the request - - // Git information is required for project/build/etc endpoints - vcs: defaultVcsOptions +const defaultOptions: Partial = { + vcs: { + type: GitType.GITHUB, + owner: 'CircleCITest3', + repo: 'circleci-test', + }, }; - -export class CircleCIApi { - api: null | CircleCI = null; - token: string = ''; - constuctor() {} - async authenticate(token: string) { - try { - if (token === '') return Promise.reject(); - this.api = new CircleCI({ ...options, token}); - // await this.api.me(); - this.token = token; - return Promise.resolve(); - } catch (e) { - this.api = null; - return this.cantAuth(); - } - } - async cantAuth() { - return Promise.reject("Can't auth"); - } - async getBuilds({repo, owner}: {repo: string, owner: string}) { - if (!this.api) return this.cantAuth(); - if (owner === '' || repo === '') return Promise.reject(); - return getBuildSummaries(this.token, {vcs: {...defaultVcsOptions, owner, repo}}); - } -} - export const circleCIApiRef = new ApiRef({ id: 'plugin.circleci.service', description: 'Used by the CircleCI plugin to make requests', }); + +export class CircleCIApi { + token: string = ''; + private options: Partial; + + authed: boolean = false; + constructor(options?: Partial) { + this.options = Object.assign(Object.create(null), defaultOptions, options); + } + + setToken(token: string) { + this.token = token; + this.persistToken(); + } + + setVCSOptions(vcs: CircleCIOptions['vcs']) { + this.options.vcs = vcs; + this.persistVCSOptions(); + } + + async persistVCSOptions() { + const key = circleCIApiRef.id; + sessionStorage.setItem(key + '_options', JSON.stringify(this.options.vcs)); + } + + async restorePersistedSettings() { + if (this.authed) return Promise.resolve(); + const key = circleCIApiRef.id; + const persistedToken = sessionStorage.getItem(key); + let persistedVCSOptions: {} | undefined; + try { + persistedVCSOptions = JSON.parse(sessionStorage.getItem(key + '_options') as string); + } catch(e) { + + } + if (persistedToken && persistedVCSOptions) { + this.token = persistedToken; + this.options.vcs = persistedVCSOptions; + return Promise.resolve(); + } + return Promise.reject(); + } + + + + async persistToken() { + if (this.authed) return; + const key = circleCIApiRef.id; + sessionStorage.setItem(key, this.token); + } + + async validateToken() { + if (!this.token || this.token === '') { + return Promise.reject('Wrong token'); + } + + // TODO: switch towards using personal token + await this.api.builds(); + this.authed = true; + return Promise.resolve(); + } + + private get api() { + return new CircleCI({ ...this.options, token: this.token }); + } + + async getBuilds() { + return this.api.builds(); + } + + async getUser() { + return this.api.me(); + } +} diff --git a/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx b/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx index 855fea6243..71d0e0c29b 100644 --- a/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx +++ b/plugins/circleci/src/components/CircleCIFetch/CirleCIFetch.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { FC, useState } from 'react'; +import React, { FC } from 'react'; // import Alert from '@material-ui/lab/Alert'; // import { Progress } from '@backstage/core'; @@ -24,7 +24,6 @@ import { BuildSummary } from 'circleci-api'; import { CITable, CITableBuildInfo } from '../CITable'; import { circleCIApiRef } from 'api'; import { useApi } from '@backstage/core'; -import { ProjectInput } from 'components/ProjectInput/ProjectInput'; // "lifecycle" : "finished", // :queued, :scheduled, :not_run, :not_running, :running or :finished // "outcome" : "failed", // :canceled, :infrastructure_fail, :timedout, :failed, :no_tests or :success @@ -76,22 +75,29 @@ const transform = (buildsData: BuildSummary[]): CITableBuildInfo[] => { export const CircleCIFetch: FC<{}> = () => { - const [vcsOptions, setVcsOptions] = useState({owner: '', repo: ''}); + const [authed, setAuthed] = React.useState(false); const [builds, setBuilds] = React.useState([]); const api = useApi(circleCIApiRef); React.useEffect(() => { - const intervalId = setInterval(() => { - if (!api.api) return; - api.getBuilds(vcsOptions).then(setBuilds); + const intervalId = setInterval(async () => { + if (!authed) { + await api.restorePersistedSettings(); + await api + .validateToken() + .then(() => { + setAuthed(true); + }) + .catch(() => setAuthed(false)); + } + api.getBuilds().then(setBuilds); }, 1500); return () => clearInterval(intervalId); - }, []); + }, [authed]); + if (!authed) return
Not authenticated
; const transformedBuilds = transform(builds || []); return <> - { - setVcsOptions(info); - api.getBuilds(info).then(setBuilds)}}/> - {!api.api ?
Not authenticated
: }; + + {!api.authed ?
Not authenticated
: }; }; diff --git a/plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx b/plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx index 23ac5c0d7d..a37d5e633d 100644 --- a/plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx +++ b/plugins/circleci/src/components/CircleCIPage/CircleCIPage.tsx @@ -15,7 +15,7 @@ */ import React, { FC } from 'react'; -import { Grid } from '@material-ui/core'; +import { Grid, Button } from '@material-ui/core'; import { InfoCard, Header, @@ -27,7 +27,6 @@ import { SupportButton, } from '@backstage/core'; import { CircleCIFetch } from '../CircleCIFetch'; -import { LoginCard } from '../LoginCard'; export const CircleCIPage: FC<{}> = () => { return ( @@ -37,15 +36,13 @@ export const CircleCIPage: FC<{}> = () => { - + + A description of your plugin goes here. - - - - + diff --git a/plugins/circleci/src/components/LoginCard/LoginCard.tsx b/plugins/circleci/src/components/LoginCard/LoginCard.tsx deleted file mode 100644 index 23ff5ee1d1..0000000000 --- a/plugins/circleci/src/components/LoginCard/LoginCard.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import { - Typography, - Button, - TextField, - List, - ListItem, -} from '@material-ui/core'; -import { Person as PersonIcon } from '@material-ui/icons'; -import { InfoCard, useApi } from '@backstage/core'; -import { circleCIApiRef } from 'api'; - -const useSessionStorage = (key: string): [string, (value: string) => void] => { - const [value, setter] = React.useState(sessionStorage.getItem(key) ?? ''); - const setValue = (newValue: string) => { - sessionStorage.setItem(key, newValue); - setter(sessionStorage.getItem(key) ?? ''); - }; - - React.useEffect(() => { - const storageChangeHandle = (e: StorageEvent) => { - if (e.storageArea !== sessionStorage) return; - if (e.key !== key) return; - if (e.newValue !== e.oldValue) { - setter(e.newValue ?? ''); - } - }; - window.addEventListener('storage', storageChangeHandle); - return () => window.removeEventListener('storage', storageChangeHandle); - }, [key, setter]); - - return [value, setValue]; -}; -export const LoginCard = () => { - const [token, setToken] = useSessionStorage(circleCIApiRef.id + '_token'); - - const api = useApi(circleCIApiRef); - - React.useEffect(() => { - if (token && token !== '') { - api.authenticate(token); - } - }, []); - return ( - - - CircleCI Auth - - - - setToken(e.target.value)} - /> - - - - - - - ); -}; diff --git a/plugins/circleci/src/components/LoginCard/index.ts b/plugins/circleci/src/components/LoginCard/index.ts deleted file mode 100644 index 1d3c3bcf3e..0000000000 --- a/plugins/circleci/src/components/LoginCard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './LoginCard'; diff --git a/plugins/circleci/src/components/SettingsPage/SettingsPage.tsx b/plugins/circleci/src/components/SettingsPage/SettingsPage.tsx new file mode 100644 index 0000000000..ea3c8fb3a6 --- /dev/null +++ b/plugins/circleci/src/components/SettingsPage/SettingsPage.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { Button, TextField, List, Grid, ListItem } from '@material-ui/core'; +import { circleCIApiRef } from 'api'; +import { + InfoCard, + useApi, + Header, + Page, + pageTheme, + Content, + ContentHeader, + HeaderLabel, + SupportButton, +} from '@backstage/core'; +import { ProjectInput } from 'components/ProjectInput/ProjectInput'; + +export const SettingsPage = () => { + const [authed, setAuthed] = React.useState(false); + const [token, setToken] = React.useState(''); + + const api = useApi(circleCIApiRef); + + React.useEffect(() => { + api + .restorePersistedSettings() + .then(() => api.validateToken()) + .then(() => setAuthed(true)) + .catch(() => setAuthed(false)); + }, []); + + return ( + +
+ + +
+ + + + A description of your plugin goes here. + + + + + + {authed ? ( + <>Already authed + ) : ( + <> + + setToken(e.target.value)} + /> + + + + + + + )} + + + + api.setVCSOptions(info)}/> + + + +
+ ); +}; diff --git a/plugins/circleci/src/components/SettingsPage/index.ts b/plugins/circleci/src/components/SettingsPage/index.ts new file mode 100644 index 0000000000..f533f5abe0 --- /dev/null +++ b/plugins/circleci/src/components/SettingsPage/index.ts @@ -0,0 +1 @@ +export * from './SettingsPage'; diff --git a/plugins/circleci/src/plugin.ts b/plugins/circleci/src/plugin.ts index 5c1eca9be3..458a7f27bc 100644 --- a/plugins/circleci/src/plugin.ts +++ b/plugins/circleci/src/plugin.ts @@ -15,10 +15,12 @@ */ import { createPlugin } from '@backstage/core'; import { CircleCIPage } from './components/CircleCIPage'; +import { SettingsPage } from './components/SettingsPage'; export const plugin = createPlugin({ id: 'circleci', register({ router }) { router.registerRoute('/circleci', CircleCIPage); + router.registerRoute('/circleci/settings', SettingsPage); }, });