diff --git a/ui/package-lock.json b/ui/package-lock.json index 2a858b9..28c08d2 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -52,6 +52,15 @@ "@types/react": "*" } }, + "@types/enzyme-adapter-react-16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.3.tgz", + "integrity": "sha512-9eRLBsC/Djkys05BdTWgav8v6fSCjyzjNuLwG2sfa2b2g/VAN10luP0zB0VwtOWFQ0LGjIboJJvIsVdU5gqRmg==", + "dev": true, + "requires": { + "@types/enzyme": "*" + } + }, "@types/history": { "version": "4.7.2", "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.2.tgz", @@ -105,6 +114,15 @@ "@types/react": "*" } }, + "@types/react-test-renderer": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-16.0.3.tgz", + "integrity": "sha512-NWOAxVQeJxpXuNKgw83Hah0nquiw1nUexM9qY/Hk3a+XhZwgMtaa6GLA9E1TKMT75Odb3/KE/jiBO4enTuEJjQ==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@webassemblyjs/ast": { "version": "1.7.11", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.11.tgz", diff --git a/ui/package.json b/ui/package.json index 2b180e2..f6c794b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -20,10 +20,12 @@ }, "devDependencies": { "@types/enzyme": "^3.1.15", + "@types/enzyme-adapter-react-16": "^1.0.3", "@types/jest": "^23.3.13", "@types/reach__router": "^1.2.3", "@types/react": "^16.7.20", "@types/react-dom": "^16.0.11", + "@types/react-test-renderer": "^16.0.3", "awesome-typescript-loader": "^5.2.1", "css-loader": "^1.0.1", "dotenv": "^6.2.0", diff --git a/ui/src/components/App.tsx b/ui/src/components/App.tsx index e405465..339acaa 100644 --- a/ui/src/components/App.tsx +++ b/ui/src/components/App.tsx @@ -1,7 +1,8 @@ import { Router } from '@reach/router' -import * as Msal from 'msal' +import { UserAgentApplication } from 'msal' import * as React from 'react' +import './../styles/App.css' import { AuthProvider } from './AuthContext' import { AuthResponseBar } from './AuthResponseBar' import { DefaultComponent } from './DefaultComponent' @@ -10,8 +11,6 @@ import { Navbar } from './Navbar' import { Person } from './Person' import { Title } from './Title' -import './../styles/App.css' - // Webpack will automatically replace this variable during build time const appConfig = { clientID: '', // defaulted to '' when no OAuth client id is passed in @@ -19,7 +18,7 @@ const appConfig = { // initialize the UserAgentApplication globally so popup and iframe can run in the background // short circuit userAgentApp. If clientID is null so is userAgentApp -const userAgentApp = appConfig.clientID && new Msal.UserAgentApplication(appConfig.clientID, null, null) +const userAgentApp = appConfig.clientID && new UserAgentApplication(appConfig.clientID, null, () => null) // webpack replaces this variable on build time let basepath = process.env.WEBPACK_PROP_UI_BASEPATH || '' // default to empty string for testing @@ -45,18 +44,28 @@ export class App extends React.Component { console.warn('AAD Client ID has not been configured. If you are currently in production mode, see the \'deploy\' documentation for details on how to fix this.') this.setState({ accessToken: '' }) } - } else { // normal or 'production' auth flow + } else if (userAgentApp === '') { // normal or 'production' auth flow + alert('userAgentApp not initialized') + } else { let accessToken = null try { if (this.state.accessToken !== null) { // log out - await userAgentApp.logout() + if (userAgentApp) { + await userAgentApp.logout() + } else { + alert('userAgentApp not initialized') + } } else { // log in const graphScopes = [appConfig.clientID] - await userAgentApp.loginPopup(graphScopes) - accessToken = await userAgentApp.acquireTokenSilent(graphScopes, - 'https://login.microsoftonline.com/microsoft.onmicrosoft.com') + if (userAgentApp) { + await userAgentApp.loginPopup(graphScopes) + accessToken = await userAgentApp.acquireTokenSilent(graphScopes, + 'https://login.microsoftonline.com/microsoft.onmicrosoft.com') + } else { + alert('userAgentApp not initialized') + } } this.setState({ accessToken }) } catch (err) { @@ -75,12 +84,7 @@ export class App extends React.Component { // by linking the accessToken to the app state we can be certain the // context will always update and propogate the value to subscribed nodes return ( - +
diff --git a/ui/src/components/AuthContext.tsx b/ui/src/components/AuthContext.tsx index 13afe7b..741bcee 100644 --- a/ui/src/components/AuthContext.tsx +++ b/ui/src/components/AuthContext.tsx @@ -1,19 +1,14 @@ import { createContext } from 'react' export interface IAuthContextValues { - accessToken: string, - authResponse: string, - handleAuth: () => void, - setAuthResponse: (msg?: string) => void, + accessToken?: string, + authResponse?: string, + handleAuth?: () => any, + setAuthResponse?: (msg?: string) => any, } // Default values and enforce use of interface -const defaultAuthContextValue: IAuthContextValues = { - accessToken: null, - authResponse: null, - handleAuth: () => null, - setAuthResponse: () => null, -} +const defaultAuthContextValue: IAuthContextValues = {} export const AuthContext = createContext(defaultAuthContextValue) // exporting the Provider and Consumer components for more specific imports diff --git a/ui/src/components/Navbar.tsx b/ui/src/components/Navbar.tsx index 773acde..d8b5999 100644 --- a/ui/src/components/Navbar.tsx +++ b/ui/src/components/Navbar.tsx @@ -1,11 +1,10 @@ import { Link } from '@reach/router' import * as React from 'react' -import { AuthButton } from './AuthButton' - import './../styles/Navbar.css' +import { AuthButton } from './AuthButton' -const isActive = ({ isCurrent }) => { +const isActive = ({ isCurrent }: {isCurrent: boolean}) => { return { className: isCurrent ? 'nav-link active' : 'nav-link', } diff --git a/ui/src/components/PageForm.tsx b/ui/src/components/PageForm.tsx index 28f7c54..337651f 100644 --- a/ui/src/components/PageForm.tsx +++ b/ui/src/components/PageForm.tsx @@ -3,8 +3,8 @@ import * as React from 'react' export interface IPageFormProps { inputTitle: string, inputPlaceholder: string, - onInputChange: (event) => void, - onSubmitClick: (event) => void, + onInputChange: (event: React.ChangeEvent) => any, + onSubmitClick: (event: React.MouseEvent) => any, } export function PageForm(props: IPageFormProps) { diff --git a/ui/src/components/Person.tsx b/ui/src/components/Person.tsx index ce8d4d3..524a0eb 100644 --- a/ui/src/components/Person.tsx +++ b/ui/src/components/Person.tsx @@ -5,7 +5,7 @@ import { PageForm } from './PageForm' export interface IPersonProps { path: string } -export interface IPersonState { loading: boolean, personId: string, result: object } +export interface IPersonState { loading: boolean, personId?: string, result?: object } export interface IPersonResponse extends Response { _embedded?: { persons: Array<{ nconst: string }> }, @@ -18,10 +18,8 @@ export interface IPersonResult { nconst: string } export class Person extends React.Component { public static contextType = AuthContext - public state = { + public state: IPersonState = { loading: false, - personId: null, - result: null, } public render() { @@ -47,12 +45,11 @@ export class Person extends React.Component { ) } - private handleNameInputChange = (event) => this.setState({ + private handleNameInputChange = (event: React.ChangeEvent) => this.setState({ personId: event.target.value, - result: null, }) - private handleFormSubmit = async (event) => { + private handleFormSubmit = async (event: React.MouseEvent) => { event.preventDefault() this.setState({ loading: true }) @@ -75,11 +72,11 @@ export class Person extends React.Component { const response = await fetch(endpoint, options) const resOut: IPersonResponse = await response.json() - if (resOut.hasOwnProperty('error')) { + if (resOut.error) { throw new Error(`Error: ${resOut.error}, ${resOut.error_description}`) } else if (id) { this.setState({ result: resOut }) - } else { + } else if (resOut._embedded) { const persons = resOut._embedded.persons const result: IPersonResult = persons[Math.floor(Math.random() * persons.length)] this.setState({ result, personId: result.nconst }) diff --git a/ui/src/components/PrivateRoute.tsx b/ui/src/components/PrivateRoute.tsx index 08f725b..29d5ece 100644 --- a/ui/src/components/PrivateRoute.tsx +++ b/ui/src/components/PrivateRoute.tsx @@ -14,7 +14,7 @@ export class PrivateRoute extends React.Component { } } - public componentDidUpdate(prevProps, prevState, snapshot) { + public componentDidUpdate(prevProps: IPrivateRouteProps) { if (this.context.accessToken === null && this.props.path !== prevProps.path) { this.context.setAuthResponse(`Please log in to access: ${this.props.path}`) } else if (this.context.authResponse !== null && this.context.accessToken !== null) { diff --git a/ui/src/components/Title.tsx b/ui/src/components/Title.tsx index 1644621..c535a06 100644 --- a/ui/src/components/Title.tsx +++ b/ui/src/components/Title.tsx @@ -5,7 +5,7 @@ import { PageForm } from './PageForm' export interface ITitleProps { path: string } -export interface ITitleState { loading: boolean, titleId: string, result: object } +export interface ITitleState { loading: boolean, titleId?: string, result?: object } export interface ITitleResponse extends Response { _embedded?: { titles: Array<{ tconst: string }> }, @@ -18,10 +18,8 @@ export interface ITitleResult { tconst: string } export class Title extends React.Component { public static contextType = AuthContext - public state = { + public state: ITitleState = { loading: false, - result: null, - titleId: null, } public render() { @@ -47,12 +45,13 @@ export class Title extends React.Component { ) } - private handleNameInputChange = (event) => this.setState({ - result: null, - titleId: event.target.value, - }) + private handleNameInputChange = (event: React.ChangeEvent) => { + this.setState({ + titleId: event.target.value, + }) + } - private handleFormSubmit = async (event) => { + private handleFormSubmit = async (event: React.MouseEvent) => { event.preventDefault() this.setState({ loading: true }) @@ -79,10 +78,13 @@ export class Title extends React.Component { throw new Error(`Error: ${resOut.error}, ${resOut.error_description}`) } else if (id) { this.setState({ result: resOut }) - } else { + } else if (resOut._embedded) { const titles = resOut._embedded.titles const result: ITitleResult = titles[Math.floor(Math.random() * titles.length)] this.setState({ result, titleId: result.tconst }) + } else { + throw new Error('Response does not match expected format. _embedded not found\n' + + 'Received: ${JSON.stringify(resOut)}') } } catch (err) { this.setState({ result: { error: err.message } }) diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 95651e9..f0e4403 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -3,18 +3,12 @@ "outDir": "./dist/", "module": "commonjs", "target": "es5", + "strict": true, "jsx": "react", - "lib": [ "es2015", "dom" ], + "lib": ["es2015", "dom"], "allowSyntheticDefaultImports": true, - "typeRoots": [ - "./node_modules/@types", - "./types" - ] + "typeRoots": ["./node_modules/@types", "./types"] }, - "exclude": [ - "./node_modules/**/*" - ], - "include": [ - "./src/**/*" - ] -} \ No newline at end of file + "exclude": ["./node_modules/**/*"], + "include": ["./src/**/*"] +} diff --git a/ui/tslint.json b/ui/tslint.json index be91fcb..7aa4568 100644 --- a/ui/tslint.json +++ b/ui/tslint.json @@ -5,6 +5,7 @@ ], "jsRules": {}, "rules": { + "no-console": [false], "quotemark": [true, "single"], "semicolon": [true, "never"], "indent": [true, "spaces", 4], @@ -16,4 +17,4 @@ ] }, "rulesDirectory": [] -} \ No newline at end of file +}