feat: settings page

This commit is contained in:
Ivan Shmidt
2020-05-04 15:48:07 +02:00
parent 00efbfe87e
commit a6dcc9b2e3
8 changed files with 160 additions and 117 deletions
+57 -33
View File
@@ -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';
+2
View File
@@ -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);
},
});