Feature/33 api fails when no pilots or logbook entries #35
11
.github/workflows/app.yaml
vendored
11
.github/workflows/app.yaml
vendored
@@ -13,6 +13,11 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.calling_workflow }}
|
||||
env:
|
||||
VITE_API_URL: ${{ secrets.VITE_API_URL }}
|
||||
VITE_CLIENT_ID: ${{ secrets.VITE_CLIENT_ID }}
|
||||
VITE_TENANT_ID: ${{ secrets.VITE_TENANT_ID }}
|
||||
VITE_REDIRECT_URL: ${{ secrets.VITE_REDIRECT_URL }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -32,6 +37,7 @@ jobs:
|
||||
- name: Build
|
||||
working-directory: app
|
||||
run: |
|
||||
printenv
|
||||
npm run build
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
@@ -50,11 +56,6 @@ jobs:
|
||||
tags: noahspan/flying-app:${{ inputs.version_number }}
|
||||
context: .
|
||||
target: app
|
||||
build-args: |
|
||||
VITE_API_URL=${{ secrets.VITE_API_URL }}
|
||||
VITE_CLIENT_ID=${{ secrets.VITE_CLIENT_ID }}
|
||||
VITE_TENANT_ID=${{ secrets.VITE_TENANT_ID }}
|
||||
VITE_REDIRECT_URL=${{ secrets.VITE_REDIRECT_URL }}
|
||||
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -5,13 +5,8 @@ COPY ./api/dist ./api/dist
|
||||
COPY ./api/node_modules ./api/node_modules
|
||||
EXPOSE 3000
|
||||
CMD ["node", "/api/dist/main.js"]
|
||||
# CMD ["tail", "-f", "/dev/null"]
|
||||
|
||||
FROM base AS app
|
||||
ARG VITE_API_URL
|
||||
ARG VITE_CLIENT_ID
|
||||
ARG VITE_TENANT_ID
|
||||
ARG VITE_REDIRECT_URL
|
||||
RUN npm i -g serve
|
||||
COPY ./app/dist ./app/dist
|
||||
EXPOSE 8080
|
||||
|
||||
218
app/src/App.tsx
218
app/src/App.tsx
@@ -1,79 +1,179 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import Pilots from './components/pilots/Pilots';
|
||||
import Logbook from './components/logbook/Logbook';
|
||||
import { useAppContext } from './hooks/appContext/UseAppContext';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ISiteNavProps } from './ISiteNavProps';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppContext } from '../../hooks/appContext/UseAppContext';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Navbar,
|
||||
PlaneIcon,
|
||||
SignOutIcon,
|
||||
Spinner,
|
||||
Typography
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { useIsAuthenticated, useMsal } from '@azure/msal-react';
|
||||
import { InteractionStatus } from '@azure/msal-browser';
|
||||
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { useHttpClient } from './hooks/httpClient/UseHttpClient';
|
||||
import { useFeatureFlag } from './hooks/featureFlag/UseFeatureFlag';
|
||||
import SiteNav from './components/siteNav/SiteNav';
|
||||
import { useMsal } from '@azure/msal-react';
|
||||
import { Button } from '@noahspan/noahspan-components';
|
||||
import { User } from '@microsoft/microsoft-graph-types';
|
||||
import { EventMessage, EventPayload, EventType } from '@azure/msal-browser';
|
||||
|
||||
const App: React.FC<unknown> = () => {
|
||||
type EventPayloadExtended = EventPayload & { accessToken: string };
|
||||
|
||||
const SiteNav: React.FC<unknown> = () => {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [userPhoto, setUserPhoto] = useState<string>();
|
||||
const httpClient: AxiosInstance = useHttpClient();
|
||||
const appContext = useAppContext();
|
||||
const { instance } = useMsal();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const { getAccessToken } = useAccessToken();
|
||||
const { inProgress, instance } = useMsal();
|
||||
const navigate = useNavigate();
|
||||
const pages = [
|
||||
{
|
||||
name: 'Logbook',
|
||||
url: '/'
|
||||
},
|
||||
{
|
||||
name: 'Pilots',
|
||||
url: '/pilots'
|
||||
}
|
||||
];
|
||||
const handleSignIn = async () => {
|
||||
await instance.loginRedirect({
|
||||
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`]
|
||||
});
|
||||
};
|
||||
const handleSignOut = () => {
|
||||
instance.logoutRedirect();
|
||||
};
|
||||
const getUserProfile = async (accessToken: string): Promise<User> => {
|
||||
try {
|
||||
const response: AxiosResponse = await httpClient.get(`api/userProfile`, {
|
||||
headers: {
|
||||
Authorization: accessToken
|
||||
}
|
||||
});
|
||||
const userProfile: User = response.data;
|
||||
|
||||
// const handleSignInRedirect = () => {
|
||||
// instance
|
||||
// .loginRedirect({
|
||||
// scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`]
|
||||
// })
|
||||
// .catch((error) => console.log(error));
|
||||
// };
|
||||
return userProfile;
|
||||
} catch (error) {
|
||||
throw new Error();
|
||||
}
|
||||
};
|
||||
const getUserPhoto = async (accessToken: string): Promise<string> => {
|
||||
try {
|
||||
const response: AxiosResponse = await httpClient.get(`api/userPhoto`, {
|
||||
headers: {
|
||||
Authorization: accessToken
|
||||
},
|
||||
responseType: 'arraybuffer'
|
||||
});
|
||||
const arrayBufferView = new Uint8Array(response.data);
|
||||
const blob = new Blob([arrayBufferView], { type: 'image/png' });
|
||||
const imageUrl = window.URL.createObjectURL(blob);
|
||||
|
||||
const handleSignInRedirect = () => {
|
||||
// instance.acquireTokenSilent({
|
||||
// scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`]
|
||||
// })
|
||||
instance.loginPopup({
|
||||
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`],
|
||||
prompt: 'create'
|
||||
})
|
||||
}
|
||||
return imageUrl;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new Error();
|
||||
}
|
||||
};
|
||||
const handlePageClick = (url: string) => {
|
||||
navigate(url);
|
||||
};
|
||||
|
||||
const Settings = () => {
|
||||
return (
|
||||
<MenuItem onClick={handleSignOut}>
|
||||
<SignOutIcon />
|
||||
<Typography sx={{ marginLeft: '10px', textAlign: 'center' }}>
|
||||
Sign Out
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getFeatureFlags = async () => {
|
||||
try {
|
||||
const featureFlags: { key: string; enabled: boolean }[] = []
|
||||
const response: AxiosResponse = await httpClient.get(
|
||||
'api/featureFlags'
|
||||
);
|
||||
|
||||
for (const featureFlag of response.data) {
|
||||
featureFlags.push({
|
||||
key: featureFlag.rowKey,
|
||||
enabled: featureFlag.active === 'true' ? true : false
|
||||
})
|
||||
}
|
||||
const callback = instance.addEventCallback(
|
||||
async (message: EventMessage) => {
|
||||
if (message.eventType === EventType.LOGIN_SUCCESS) {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
if (featureFlags.length > 0) {
|
||||
appContext.dispatch({
|
||||
type: 'SET_FEATURE_FLAGS',
|
||||
payload: featureFlags
|
||||
});
|
||||
const eventPayload: EventPayloadExtended =
|
||||
message.payload as EventPayloadExtended;
|
||||
const userProfile: User = await getUserProfile(
|
||||
eventPayload.accessToken
|
||||
);
|
||||
const userPhoto = await getUserPhoto(eventPayload.accessToken);
|
||||
|
||||
appContext.dispatch({
|
||||
type: 'SET_USER_PROFILE',
|
||||
payload: userProfile
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
if (callback) {
|
||||
instance.removeEventCallback(callback);
|
||||
appContext.dispatch({ type: 'SET_USER_PROFILE', payload: {} });
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const setUserProfile = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const accessToken: string = await getAccessToken();
|
||||
const userProfile = await getUserProfile(accessToken);
|
||||
const userPhoto = await getUserPhoto(accessToken);
|
||||
|
||||
setUserPhoto(userPhoto);
|
||||
|
||||
appContext.dispatch({
|
||||
type: 'SET_USER_PROFILE',
|
||||
payload: userProfile
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
getFeatureFlags();
|
||||
}, []);
|
||||
if (
|
||||
isAuthenticated &&
|
||||
Object.keys(appContext.state.userProfile).length === 0
|
||||
) {
|
||||
setUserProfile();
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={handleSignInRedirect}>Sign On</Button>
|
||||
{/* <SiteNav /> */}
|
||||
<Routes>
|
||||
{useFeatureFlag('pilots')?.enabled && (
|
||||
<Route path="/pilots" element={<Pilots />} />
|
||||
)}
|
||||
<Route path="/" element={<Logbook />} />
|
||||
</Routes>
|
||||
</div>
|
||||
<Navbar
|
||||
handlePageClick={handlePageClick}
|
||||
handleSignIn={handleSignIn}
|
||||
isAuthenticated={isAuthenticated}
|
||||
logo={<PlaneIcon size="2x" />}
|
||||
pages={pages}
|
||||
settings={<Settings />}
|
||||
userPhoto={userPhoto}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
export default SiteNav;
|
||||
@@ -5,55 +5,27 @@ import AppContextProvider from './context/appContext/AppContextProvider.tsx';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import './index.css';
|
||||
import '@noahspan/noahspan-components/noahspan-components.css';
|
||||
import { LogLevel, PublicClientApplication } from '@azure/msal-browser';
|
||||
import { PublicClientApplication } from '@azure/msal-browser';
|
||||
import { MsalProvider } from '@azure/msal-react';
|
||||
|
||||
const msalConfig = {
|
||||
auth: {
|
||||
clientId: import.meta.env.VITE_CLIENT_ID,
|
||||
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}`,
|
||||
redirectUri: import.meta.env.VITE_REDIRECT_URL,
|
||||
},
|
||||
// cache: {
|
||||
// cacheLocation: "sessionStorage", // This configures where your cache will be stored
|
||||
// storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
|
||||
// },
|
||||
// system: {
|
||||
// loggerOptions: {
|
||||
// loggerCallback: (level: any, message: any, containsPii: any) => {
|
||||
// if (containsPii) {
|
||||
// return;
|
||||
// }
|
||||
// switch (level) {
|
||||
// case LogLevel.Error:
|
||||
// console.error(message);
|
||||
// return;
|
||||
// case LogLevel.Info:
|
||||
// console.info(message);
|
||||
// return;
|
||||
// case LogLevel.Verbose:
|
||||
// console.debug(message);
|
||||
// return;
|
||||
// case LogLevel.Warning:
|
||||
// console.warn(message);
|
||||
// return;
|
||||
// default:
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
};
|
||||
console.log(msalConfig)
|
||||
const msalInstance = new PublicClientApplication(msalConfig)
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<MsalProvider instance={msalInstance}>
|
||||
<AppContextProvider>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</AppContextProvider>
|
||||
</MsalProvider>
|
||||
);
|
||||
const msalInstance: PublicClientApplication = new PublicClientApplication({
|
||||
auth: {
|
||||
clientId: import.meta.env.VITE_CLIENT_ID,
|
||||
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}`,
|
||||
redirectUri: import.meta.env.VITE_REDIRECT_URL
|
||||
}
|
||||
});
|
||||
|
||||
msalInstance.initialize().then(() => {
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<MsalProvider instance={msalInstance}>
|
||||
<AppContextProvider>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</AppContextProvider>
|
||||
</MsalProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user