src/pages/github/github.tsx (240 lines of code) (raw):

import { EuiCallOut, EuiCode, EuiFieldPassword, EuiLink, EuiPageTemplate, EuiSteps, EuiText, EuiTextColor, EuiSpacer, } from '@elastic/eui'; import { FC, useCallback, useEffect, useState } from 'react'; import type { EuiStepsProps } from '@elastic/eui'; import { clearGitHubService, GITHUB_TOKEN, TokenValidated, validateToken } from '../../common'; import { Machine, assign, DoneInvokeEvent, State } from 'xstate'; import { useMachine } from '@xstate/react'; import { useLocation } from 'react-router-dom'; interface Context { name?: string; error?: string; token: string; } interface StateSchema { states: { idle: {}; fetching: {}; success: {}; error: { states: { invalidScope: {}; validationError: {}; }; }; }; } type Events = { type: 'FETCH'; token: string }; const stateMachine = Machine<Context, StateSchema, Events>( { initial: 'idle', context: { token: '', name: undefined, error: undefined, }, states: { idle: { on: { FETCH: [ { target: 'fetching', cond: (context, event) => !!event.token }, { target: 'idle' }, ], }, }, fetching: { entry: assign({ token: (context, event) => event.token, }), invoke: { src: (context, event) => validateToken(event.token), onDone: [ { target: 'error.invalidScope', cond: (context, event: DoneInvokeEvent<TokenValidated>) => !event.data.scopes.includes('repo'), }, { target: 'success', actions: [ 'storeToken', assign({ name: (context, event: DoneInvokeEvent<TokenValidated>) => event.data.name, }), ], }, ], onError: [ { target: 'error.validationError', actions: assign({ error: (context, event) => event.data.message, }), }, ], }, }, error: { entry: ['clearToken'], on: { FETCH: [ { target: 'fetching', cond: (context, event) => !!event.token }, { target: 'idle', actions: ['clearToken'] }, ], }, states: { invalidScope: {}, validationError: {}, }, }, success: { entry: ['storeToken'], on: { FETCH: [ { target: 'fetching', cond: (context, event) => !!event.token }, { target: 'idle', actions: ['clearToken'] }, ], }, }, }, }, { actions: { storeToken: (context) => { localStorage.setItem(GITHUB_TOKEN, context.token); clearGitHubService(); }, clearToken: () => { localStorage.removeItem(GITHUB_TOKEN); }, }, } ); function mapStateToIcon( state: State<Context, Events> ): 'incomplete' | 'complete' | 'danger' | 'loading' { if (state.matches('success')) { return 'complete'; } if (state.matches('fetching')) { return 'loading'; } if (state.matches('error')) { return 'danger'; } return 'incomplete'; } function mapStateToTitle(state: State<Context, Events>): string { if (state.matches('fetching')) { return 'Checking your token'; } if (state.matches('success')) { return 'Setup complete'; } if (state.matches('error')) { return 'Failure validating your token'; } return 'Waiting for your token'; } export const GitHubSettings: FC = () => { const [token, setToken] = useState(localStorage.getItem(GITHUB_TOKEN) ?? ''); const [current, send] = useMachine(stateMachine); const location = useLocation(); const onChangeToken = useCallback( (ev: React.ChangeEvent<HTMLInputElement>) => { const token = ev.target.value; setToken(token); send({ type: 'FETCH', token }); }, [send] ); useEffect(() => { if (token) { send({ type: 'FETCH', token }); } }, [send, token]); const steps: EuiStepsProps['steps'] = [ { title: 'Create a GitHub token', children: ( <EuiText> <p> Go to{' '} <EuiLink href="https://github.com/settings/tokens" target="_blank"> GitHub </EuiLink>{' '} and click <em>Generate new token</em>. The token must have the{' '} <EuiCode>public_repo</EuiCode> permission for public repos and the full{' '} <EuiCode>repo</EuiCode> scope for private repos. </p> <p> Enable SSO for this token after creating it by clicking <em>Configure SSO</em> in the token list behind the generated token and then <em>Authorize</em>. </p> </EuiText> ), }, { title: 'Enter your token', children: ( <EuiFieldPassword disabled={current.matches('fetching')} value={token} onChange={onChangeToken} /> ), }, { title: mapStateToTitle(current), status: mapStateToIcon(current), children: ( <EuiText> {current.matches('success') && <p>Hi {current.context.name} 👋 You are all setup.</p>} {current.matches('error.validationError') && ( <EuiTextColor color="danger">{current.context.error}</EuiTextColor> )} {current.matches('error.invalidScope') && ( <EuiTextColor color="danger"> Your token is missing the <EuiCode>repo</EuiCode> permission. </EuiTextColor> )} </EuiText> ), }, ]; return ( <EuiPageTemplate pageHeader={{ pageTitle: 'GitHub Settings' }}> {location.state && ( <> <EuiCallOut color="danger"> {location.state.statusCode === 403 && ( <> Your token is not authorized to access the Elastic organization. Please make sure you follow the steps outlined under (1) below to authorize it for Single sign on. </> )} {location.state.statusCode !== 403 && ( <> Your token got rejected from GitHub. Most often this means it expired, in which case you should regenerate it on{' '} <a href="https://github.com/settings/tokens" target="_blank" rel="noreferrer"> GitHub </a> . </> )} </EuiCallOut> <EuiSpacer /> </> )} <EuiSteps steps={steps} /> </EuiPageTemplate> ); };