feat: settings page
This commit is contained in:
@@ -17,45 +17,69 @@
|
||||
import { CircleCI, GitType, CircleCIOptions } from 'circleci-api';
|
||||
import { ApiRef } from '@backstage/core';
|
||||
|
||||
const options: Partial<CircleCIOptions> = {
|
||||
// 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
|
||||
const defaultOptions: Partial<CircleCIOptions> = {
|
||||
vcs: {
|
||||
type: GitType.GITHUB, // default: github
|
||||
type: GitType.GITHUB,
|
||||
owner: 'CircleCITest3',
|
||||
repo: 'circleci-test',
|
||||
},
|
||||
};
|
||||
|
||||
export class CircleCIApi {
|
||||
api: null | CircleCI = null;
|
||||
constuctor() {}
|
||||
async authenticate(token: string) {
|
||||
try {
|
||||
if (token === '') return Promise.reject();
|
||||
this.api = new CircleCI({ ...options, token });
|
||||
// await this.api.me();
|
||||
return Promise.resolve();
|
||||
} catch (e) {
|
||||
this.api = null;
|
||||
return this.cantAuth();
|
||||
}
|
||||
}
|
||||
async cantAuth() {
|
||||
return Promise.reject("Can't auth");
|
||||
}
|
||||
async getBuilds() {
|
||||
if (!this.api) return this.cantAuth();
|
||||
return this.api.builds();
|
||||
}
|
||||
}
|
||||
|
||||
export const circleCIApiRef = new ApiRef<CircleCIApi>({
|
||||
id: 'plugin.circleci.service',
|
||||
description: 'Used by the CircleCI plugin to make requests',
|
||||
});
|
||||
|
||||
export class CircleCIApi {
|
||||
token: string = '';
|
||||
private options: Partial<CircleCIOptions>;
|
||||
|
||||
authed: boolean = false;
|
||||
constructor(options?: Partial<CircleCIOptions>) {
|
||||
this.options = Object.assign(Object.create(null), defaultOptions, options);
|
||||
}
|
||||
|
||||
setToken(token: string) {
|
||||
this.token = token;
|
||||
this.persistToken();
|
||||
}
|
||||
|
||||
async restorePersistedToken() {
|
||||
if (this.authed) return Promise.resolve();
|
||||
const key = circleCIApiRef.id;
|
||||
const persistedToken = sessionStorage.getItem(key);
|
||||
if (persistedToken) {
|
||||
this.token = persistedToken;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,18 +74,27 @@ const transform = (buildsData: BuildSummary[]): CITableBuildInfo[] => {
|
||||
};
|
||||
|
||||
export const CircleCIFetch: FC<{}> = () => {
|
||||
const [authed, setAuthed] = React.useState(false);
|
||||
const [builds, setBuilds] = React.useState<BuildSummary[]>([]);
|
||||
const api = useApi(circleCIApiRef);
|
||||
|
||||
React.useEffect(() => {
|
||||
const intervalId = setInterval(() => {
|
||||
if (!api.api) return;
|
||||
const intervalId = setInterval(async () => {
|
||||
if (!authed) {
|
||||
await api.restorePersistedToken();
|
||||
await api
|
||||
.validateToken()
|
||||
.then(() => {
|
||||
setAuthed(true);
|
||||
})
|
||||
.catch(() => setAuthed(false));
|
||||
}
|
||||
api.getBuilds().then(setBuilds);
|
||||
}, 1500);
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
}, [authed]);
|
||||
|
||||
if (!api.api) return <div>Not authenticated</div>;
|
||||
if (!authed) return <div>Not authenticated</div>;
|
||||
const transformedBuilds = transform(builds || []);
|
||||
return <CITable builds={transformedBuilds} />;
|
||||
};
|
||||
|
||||
@@ -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<{}> = () => {
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentHeader title="Plugin title">
|
||||
<ContentHeader title="Circle CI">
|
||||
<Button href="/circleci/settings">Settings</Button>
|
||||
<SupportButton>A description of your plugin goes here.</SupportButton>
|
||||
</ContentHeader>
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<LoginCard />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<InfoCard title="CI/CD">
|
||||
<InfoCard title="Pipelines">
|
||||
<CircleCIFetch />
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
|
||||
@@ -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);
|
||||
const api = useApi(circleCIApiRef);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (token && token !== '') {
|
||||
api.authenticate(token);
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<InfoCard>
|
||||
<Typography variant="h6">
|
||||
<PersonIcon /> CircleCI Auth
|
||||
</Typography>
|
||||
<List>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="circleci-token"
|
||||
type="password"
|
||||
label="Token"
|
||||
value={token}
|
||||
onChange={e => setToken(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<Button
|
||||
data-testid="github-auth-button"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => api.authenticate(token)}
|
||||
>
|
||||
Authenticate
|
||||
</Button>
|
||||
</ListItem>
|
||||
</List>
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './LoginCard';
|
||||
@@ -0,0 +1,83 @@
|
||||
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';
|
||||
|
||||
export const SettingsPage = () => {
|
||||
const [authed, setAuthed] = React.useState(false);
|
||||
const [token, setToken] = React.useState('');
|
||||
const api = useApi(circleCIApiRef);
|
||||
|
||||
React.useEffect(() => {
|
||||
api
|
||||
.restorePersistedToken()
|
||||
.then(() => api.validateToken())
|
||||
.then(() => setAuthed(true))
|
||||
.catch(() => setAuthed(false));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Page theme={pageTheme.tool}>
|
||||
<Header title="Circle CI" subtitle="Settings">
|
||||
<HeaderLabel label="Owner" value="Team X" />
|
||||
<HeaderLabel label="Lifecycle" value="Alpha" />
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentHeader title="Settings">
|
||||
<Button href="/circleci">Back</Button>
|
||||
<SupportButton>A description of your plugin goes here.</SupportButton>
|
||||
</ContentHeader>
|
||||
<Grid container spacing={3} direction="column">
|
||||
<Grid item>
|
||||
<InfoCard title="Authentication">
|
||||
<List>
|
||||
{authed ? (
|
||||
<>Already authed</>
|
||||
) : (
|
||||
<>
|
||||
<ListItem>
|
||||
<TextField
|
||||
name="circleci-token"
|
||||
type="password"
|
||||
label="Token"
|
||||
value={token}
|
||||
onChange={e => setToken(e.target.value)}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem>
|
||||
<Button
|
||||
data-testid="github-auth-button"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={async () => {
|
||||
api.setToken(token);
|
||||
api
|
||||
.validateToken()
|
||||
.then(() => setAuthed(true))
|
||||
.catch(() => setAuthed(false));
|
||||
}}
|
||||
>
|
||||
Authenticate
|
||||
</Button>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
</List>
|
||||
</InfoCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Content>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './SettingsPage';
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user