Feature/33 api fails when no pilots or logbook entries (#35)

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* refactoring

* refactoring

* refactoring

* refactoring

* adding test environment

* adding test environment
This commit was merged in pull request #35.
This commit is contained in:
2025-02-02 23:24:48 +00:00
committed by GitHub
parent 2199c6d016
commit 468837c985
97 changed files with 13360 additions and 27219 deletions

View File

@@ -7,8 +7,14 @@ import { AxiosInstance, AxiosResponse } from 'axios';
import { useHttpClient } from './hooks/httpClient/UseHttpClient';
import { useFeatureFlag } from './hooks/featureFlag/UseFeatureFlag';
import SiteNav from './components/siteNav/SiteNav';
import { IPublicClientApplication } from '@azure/msal-browser';
import { MsalProvider } from '@azure/msal-react';
const App: React.FC<unknown> = () => {
interface AppProps {
pca: IPublicClientApplication
}
const App: React.FC<AppProps> = ({ pca }: AppProps) => {
const httpClient: AxiosInstance = useHttpClient();
const appContext = useAppContext();
@@ -36,7 +42,7 @@ const App: React.FC<unknown> = () => {
// }, []);
return (
<div>
<MsalProvider instance={pca}>
<SiteNav />
<Routes>
{/* {useFeatureFlag('flying-pilots')?.enabled && ( */}
@@ -44,8 +50,8 @@ const App: React.FC<unknown> = () => {
{/* )} */}
<Route path="/" element={<Logbook />} />
</Routes>
</div>
</MsalProvider>
);
};
export default App;
export default App;

View File

@@ -0,0 +1,70 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { LogLevel } from '@azure/msal-browser';
/**
* Configuration object to be passed to MSAL instance on creation.
* For a full list of MSAL.js configuration parameters, visit:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md
*/
export const msalConfig = {
auth: {
clientId: import.meta.env.VITE_CLIENT_ID, // This is the ONLY mandatory field that you need to supply.
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}`, // Replace the placeholder with your tenant subdomain
redirectUri: import.meta.env.VITE_REDIRECT_URL, // Points to window.location.origin. You must register this URI on Microsoft Entra admin center/App Registration.
postLogoutRedirectUri: '/', // Indicates the page to navigate after logout.
navigateToLoginRequestUrl: false, // If "true", will navigate back to the original request location before processing the auth code response.
},
cache: {
cacheLocation: 'sessionStorage', // Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO between tabs.
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;
}
},
},
},
};
/**
* Scopes you add here will be prompted for user consent during sign-in.
* By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request.
* For more information about OIDC scopes, visit:
* https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
*/
export const loginRequest = {
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`],
};
/**
* An optional silentRequest object can be used to achieve silent SSO
* between applications by providing a "login_hint" property.
*/
// export const silentRequest = {
// scopes: ["openid", "profile"],
// loginHint: "example@domain.net"
// };

View File

@@ -1,6 +1,6 @@
import { FormMode } from '../../enums/formMode';
export interface ILogbookEntryFormProps {
export interface ILogFormProps {
entryId?: string;
isDrawerOpen: boolean;
mode: FormMode;

View File

@@ -1,4 +1,4 @@
export interface ILogbookEntryFormState {
export interface ILogFormState {
error: string | undefined;
isDisabled: boolean;
isLoading: boolean;

View File

@@ -17,7 +17,7 @@ import {
XmarkIcon
} from '@noahspan/noahspan-components';
import { useForm, Controller, FormProvider } from 'react-hook-form';
import { ILogbookEntryFormProps } from './ILogbookEntryFormProps';
import { ILogFormProps } from './ILogFormProps';
import { initialState, reducer } from './reducer';
import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
@@ -26,7 +26,7 @@ import { useIsAuthenticated } from '@azure/msal-react';
import { FormMode } from '../../enums/formMode';
import { usePilots } from '../../hooks/pilots/UsePilots';
const LogbookEntryForm: React.FC<ILogbookEntryFormProps> = ({
const LogForm: React.FC<ILogFormProps> = ({
entryId,
isDrawerOpen,
mode,
@@ -37,8 +37,6 @@ const LogbookEntryForm: React.FC<ILogbookEntryFormProps> = ({
const { getAccessToken } = useAccessToken();
const isAuthenticated = useIsAuthenticated();
const defaultValues = {
partitionKey: '',
rowKey: '',
pilotId: '',
pilotName: '',
date: null,
@@ -80,13 +78,13 @@ const LogbookEntryForm: React.FC<ILogbookEntryFormProps> = ({
const accessToken: string = await getAccessToken();
if (!entryId) {
await httpClient.post(`api/logbook`, data, {
await httpClient.post(`api/logs`, data, {
headers: {
Authorization: accessToken
}
});
} else {
await httpClient.put(`api/logbook/${entryId}`, data, {
await httpClient.put(`api/logs/${entryId}`, data, {
headers: {
Authorization: accessToken
}
@@ -120,7 +118,7 @@ const LogbookEntryForm: React.FC<ILogbookEntryFormProps> = ({
? { headers: { Authorization: await getAccessToken() } }
: {};
const response: AxiosResponse = await httpClient.get(
`api/logbook/${entryId}`,
`api/logs/${entryId}`,
config
);
const entry = response.data;
@@ -939,4 +937,4 @@ const LogbookEntryForm: React.FC<ILogbookEntryFormProps> = ({
);
};
export default LogbookEntryForm;
export default LogForm;

View File

@@ -1,4 +1,4 @@
import { ILogbookEntryFormState } from './ILogbookEntryFormState';
import { ILogFormState } from './ILogFormState';
type Action =
| { type: 'SET_ERROR'; payload: string | undefined }
@@ -7,7 +7,7 @@ type Action =
| { type: 'SET_PILOT_OPTIONS'; payload: { label: string; value: string }[] }
| { type: 'SET_SELECTED_ENTRY_PILOT_NAME'; payload: string };
export const initialState: ILogbookEntryFormState = {
export const initialState: ILogFormState = {
error: undefined,
isDisabled: false,
isLoading: true,
@@ -16,9 +16,9 @@ export const initialState: ILogbookEntryFormState = {
};
export const reducer = (
state: ILogbookEntryFormState,
state: ILogFormState,
action: Action
): ILogbookEntryFormState => {
): ILogFormState => {
switch (action.type) {
case 'SET_ERROR': {
return {

View File

@@ -1,5 +1,5 @@
import { useEffect, useReducer, useState } from 'react';
import LogbookEntryForm from '../logbookEntryForm/LogbookEntryForm';
import { useEffect, useReducer } from 'react';
import LogForm from '../logForm/LogForm';
import {
Alert,
Box,
@@ -35,11 +35,8 @@ const Logbook: React.FC<unknown> = () => {
const config = isAuthenticated
? { headers: { Authorization: `${token}` } }
: {};
const response: AxiosResponse = await httpClient.get(
`api/logbook`,
config
);
const response: AxiosResponse = await httpClient.get(`api/logs`, config);
console.log(response)
dispatch({ type: 'SET_ENTRIES', payload: response.data });
} catch (error) {
const axiosError = error as AxiosError;
@@ -98,7 +95,7 @@ const Logbook: React.FC<unknown> = () => {
? { headers: { Authorization: `${token}` } }
: {};
await httpClient.delete(`api/logbook/${state.selectedEntryId}`, config);
await httpClient.delete(`api/logs/${state.selectedEntryId}`, config);
dispatch({
type: 'SET_DELETE',
@@ -418,20 +415,24 @@ const Logbook: React.FC<unknown> = () => {
</>
)}
</Grid>
<LogbookEntryForm
entryId={state.selectedEntryId}
isDrawerOpen={state.isFormOpen}
mode={state.formMode}
onOpenClose={(mode) => onOpenCloseEntryForm(mode)}
/>
<ConfirmationDialog
contentText="Are you sure you want to delete the logbook entry?"
isLoading={state.isConfirmDialogLoading}
isOpen={state.isConfirmDialogOpen}
onCancel={onConfirmationDialogCancel}
onConfirm={onConfirmationDialogConfirm}
title="Confirm Delete"
/>
{state.isFormOpen && (
<LogForm
entryId={state.selectedEntryId}
isDrawerOpen={state.isFormOpen}
mode={state.formMode}
onOpenClose={(mode) => onOpenCloseEntryForm(mode)}
/>
)}
{state.isConfirmDialogOpen && (
<ConfirmationDialog
contentText="Are you sure you want to delete the logbook entry?"
isLoading={state.isConfirmDialogLoading}
isOpen={state.isConfirmDialogOpen}
onCancel={onConfirmationDialogCancel}
onConfirm={onConfirmationDialogConfirm}
title="Confirm Delete"
/>
)}
</Box>
);
};

View File

@@ -13,7 +13,7 @@ import {
XmarkIcon
} from '@noahspan/noahspan-components';
import { IPilotFormProps } from './IPilotFormProps';
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
import { useIsAuthenticated } from '@azure/msal-react';
@@ -38,8 +38,8 @@ const PilotForm: React.FC<IPilotFormProps> = ({
const { getAccessToken } = useAccessToken();
const isAuthenticated = useIsAuthenticated();
const defaultValues = {
partitionKey: '',
rowKey: '',
partitionKey: 'pilot',
rowKey: 'noah@noahspannbauer.com',
id: '',
name: '',
address: '',
@@ -53,6 +53,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
defaultValues: defaultValues
});
const [isDisabled, setIsDisabled] = useState<boolean>(false);
const [isError, setIsError] = useState<boolean>(false);
const onPeoplePickerSearch = async (
_event: React.SyntheticEvent,
@@ -105,24 +106,19 @@ const PilotForm: React.FC<IPilotFormProps> = ({
setIsLoading(true);
const accessToken: string = await getAccessToken();
const response: AxiosResponse = await httpClient.post(
`api/pilots`,
data,
{
headers: {
Authorization: accessToken
}
await httpClient.post(`api/pilots`, data, {
headers: {
Authorization: accessToken
}
);
});
if (response) console.log(response);
onOpenClose(FormMode.CANCEL);
} catch (error) {
if (axios.isAxiosError(error)) {
const errResp = error.response;
const axiosError = error as AxiosError;
const responseData = axiosError.response?.data as any;
console.log(errResp?.data.message);
} else {
}
console.log(responseData.message);
} finally {
setIsLoading(false);
}

View File

@@ -39,7 +39,25 @@ const Pilots: React.FC<unknown> = () => {
const [pilotFormMode, setPilotFormMode] = useState<FormMode>(FormMode.CANCEL);
const [selectedPilotId, setSelectedPilotId] = useState<string | undefined>();
const [pilots, setPilots] = useState<Pilot[]>([]);
const onOpenClosePilotForm = (mode: FormMode, pilotId?: string) => {
const getPilots = async (): Promise<Pilot[]> => {
try {
const config = isAuthenticated
? { headers: { Authorization: await getAccessToken() } }
: {};
const response: AxiosResponse = await httpClient.get(
`api/pilots`,
config
);
const pilots: Pilot[] = response.data;
return pilots;
} catch (error) {
throw new Error('broken');
}
};
const onOpenClosePilotForm = async (mode: FormMode, pilotId?: string) => {
switch (mode) {
case FormMode.ADD:
case FormMode.EDIT:
@@ -49,6 +67,9 @@ const Pilots: React.FC<unknown> = () => {
setIsDrawerOpen(true);
break;
case FormMode.CANCEL:
const pilots = await getPilots();
setPilots(pilots);
setPilotFormMode(mode);
setSelectedPilotId(undefined);
setIsDrawerOpen(false);
@@ -73,6 +94,7 @@ const Pilots: React.FC<unknown> = () => {
const onCloseActionMenu = () => {
setAnchorElAction(null);
};
return (
<div>
<IconButton onClick={onOpenActionMenu}>
@@ -124,23 +146,17 @@ const Pilots: React.FC<unknown> = () => {
];
useEffect(() => {
const getPilots = async () => {
const loadPilots = async () => {
try {
const config = isAuthenticated
? { headers: { Authorization: await getAccessToken() } }
: {};
const response: AxiosResponse = await httpClient.get(
`api/pilots`,
config
);
console.log(response.data);
setPilots(response.data);
const pilots = await getPilots();
setPilots(pilots);
} catch (error) {
console.log(error);
}
};
getPilots();
loadPilots();
}, []);
return (

View File

@@ -43,8 +43,8 @@ const SiteNav: React.FC<unknown> = () => {
url: '/pilots'
}
];
const handleSignIn = async () => {
await instance.loginRedirect({
const handleSignIn = () => {
instance.loginRedirect({
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`]
});
};
@@ -52,6 +52,7 @@ const SiteNav: React.FC<unknown> = () => {
instance.logoutRedirect();
};
const getUserProfile = async (accessToken: string): Promise<User> => {
console.log(accessToken)
try {
const response: AxiosResponse = await httpClient.get(`api/userProfile`, {
headers: {
@@ -66,6 +67,7 @@ const SiteNav: React.FC<unknown> = () => {
}
};
const getUserPhoto = async (accessToken: string): Promise<string> => {
console.log(accessToken)
try {
const response: AxiosResponse = await httpClient.get(`api/userPhoto`, {
headers: {
@@ -98,40 +100,44 @@ const SiteNav: React.FC<unknown> = () => {
);
};
useEffect(() => {
const callback = instance.addEventCallback(
async (message: EventMessage) => {
if (message.eventType === EventType.LOGIN_SUCCESS) {
try {
setLoading(true);
// useEffect(() => {
// const callback = instance.addEventCallback(
// async (message: EventMessage) => {
// if (message.eventType === EventType.LOGIN_SUCCESS) {
// try {
// setLoading(true);
const eventPayload: EventPayloadExtended =
message.payload as EventPayloadExtended;
const userProfile: User = await getUserProfile(
eventPayload.accessToken
);
const userPhoto = await getUserPhoto(eventPayload.accessToken);
// 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);
}
}
}
);
// 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: {} });
}
};
}, []);
// instance.handleRedirectPromise().then((response) => {
// console.log(response)
// })
// return () => {
// if (callback) {
// instance.removeEventCallback(callback);
// appContext.dispatch({ type: 'SET_USER_PROFILE', payload: {} });
// }
// };
// }, []);
useEffect(() => {
const setUserProfile = async () => {
@@ -176,4 +182,4 @@ const SiteNav: React.FC<unknown> = () => {
);
};
export default SiteNav;
export default SiteNav;

View File

@@ -5,6 +5,6 @@ export const useFeatureFlag = (featureFlagKey: string) => {
const featureFlag = appContext.state.featureFlags.find(
(featureFlag) => featureFlag.key === featureFlagKey
);
console.log(featureFlag)
return featureFlag;
};

12
app/src/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@@ -5,27 +5,34 @@ import AppContextProvider from './context/appContext/AppContextProvider.tsx';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import '@noahspan/noahspan-components/noahspan-components.css';
import { PublicClientApplication } from '@azure/msal-browser';
import { AuthenticationResult, EventMessage, EventType, PublicClientApplication } from '@azure/msal-browser';
import { MsalProvider } from '@azure/msal-react';
import { msalConfig } from './auth/msalConfig';
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
}
});
const msalInstance: PublicClientApplication = new PublicClientApplication(msalConfig);
msalInstance.initialize().then(() => {
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
msalInstance.setActiveAccount(accounts[0]);
}
msalInstance.addEventCallback((event: EventMessage) => {
if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {
const payload = event.payload as AuthenticationResult;
const account = payload.account;
msalInstance.setActiveAccount(account);
}
});
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<MsalProvider instance={msalInstance}>
<AppContextProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</AppContextProvider>
</MsalProvider>
<AppContextProvider>
<BrowserRouter>
<App pca={msalInstance} />
</BrowserRouter>
</AppContextProvider>
</React.StrictMode>
);
});
})