Feature/21 pilots view (#23)

* adding pilot details view

* adding pilot details view

* pilot view

* pilot view
This commit was merged in pull request #23.
This commit is contained in:
2024-10-03 20:43:11 -05:00
committed by GitHub
parent 071e211525
commit 11e00b5b14
15 changed files with 570 additions and 320 deletions

View File

@@ -30,7 +30,7 @@
"@nestjs/core": "^10.0.0", "@nestjs/core": "^10.0.0",
"@nestjs/passport": "^10.0.3", "@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0", "@nestjs/platform-express": "^10.0.0",
"@noahspan/noahspan-modules": "^0.3.9", "@noahspan/noahspan-modules": "^0.4.0",
"@schematics/angular": "^17.3.7", "@schematics/angular": "^17.3.7",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",

View File

@@ -74,7 +74,7 @@ export class AppController {
} }
@Get('userProfile') @Get('userProfile')
async getUserProfile(@Headers() headers: any) { async getUserProfile(@Headers() headers: any): Promise<any> {
try { try {
const graphToken: string = await this.msGraphService.getMsGraphAuth( const graphToken: string = await this.msGraphService.getMsGraphAuth(
headers.authorization.replace('Bearer ', ''), headers.authorization.replace('Bearer ', ''),
@@ -94,12 +94,12 @@ export class AppController {
async searchUsers( async searchUsers(
@Headers() headers: any, @Headers() headers: any,
@Query('search') search: any @Query('search') search: any
): Promise<Person[]> { ): Promise<any> {
try { try {
const accessToken: string = headers.authorization.replace('Bearer ', ''); const accessToken: string = headers.authorization.replace('Bearer ', '');
const personSearchResults: Person[] = const personSearchResults: any[] =
await this.appService.getPersonSearchResults(accessToken, search); await this.appService.getPersonSearchResults(accessToken, search);
console.log(personSearchResults);
return personSearchResults; return personSearchResults;
} catch (error) { } catch (error) {
return error; return error;

View File

@@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { MsGraphService, MsGraphClient } from '@noahspan/noahspan-modules'; import { MsGraphService, MsGraphClient } from '@noahspan/noahspan-modules';
import { Person } from '@microsoft/microsoft-graph-types';
@Injectable() @Injectable()
export class AppService { export class AppService {
@@ -13,7 +12,7 @@ export class AppService {
async getPersonSearchResults( async getPersonSearchResults(
accessToken: string, accessToken: string,
search: string search: string
): Promise<Person[]> { ): Promise<any[]> {
try { try {
const graphToken: string = await this.msGraphService.getMsGraphAuth( const graphToken: string = await this.msGraphService.getMsGraphAuth(
accessToken, accessToken,
@@ -22,12 +21,16 @@ export class AppService {
const client: MsGraphClient = const client: MsGraphClient =
await this.msGraphService.getMsGraphClientDelegated(graphToken); await this.msGraphService.getMsGraphClientDelegated(graphToken);
const results: any = await client const results: any = await client
.api(`me/people/?$search=${search}`) .api('users')
.header('ConsistencyLevel', 'eventual')
.search(`"displayName:${search}"`)
.orderby('displayName')
.select(['displayName', 'userPrincipalName'])
.get(); .get();
let personResults: Person[]; let personResults: any[];
if (results.value) { if (results.value) {
personResults = results.value.filter((result: Person) => { personResults = results.value.filter((result: any) => {
if (result.userPrincipalName !== null) { if (result.userPrincipalName !== null) {
return result; return result;
} }

View File

@@ -11,9 +11,43 @@ export class PilotInfoService {
constructor(private readonly tableService: TableService) {} constructor(private readonly tableService: TableService) {}
// async find(rowKey: string): Promise<PilotInfo> { async find(pilotId: string): Promise<PilotInfoEntity> {
// return await this.pilotInfoRepository.find(this.partitionKey, rowKey); try {
// } const client: TableClient =
await this.tableService.getTableClient('Pilots');
const entities = await client.listEntities({
queryOptions: {
filter: odata`PartitionKey eq 'pilot' and RowKey eq '${pilotId}'`
}
});
let pilot: PilotInfoEntity;
console.log(entities);
for await (const entity of entities) {
pilot = {
partitionKey: entity.partitionKey,
rowKey: entity.rowKey,
id: entity.id.toString(),
name: entity.name.toString(),
address: entity.address.toString(),
city: entity.city.toString(),
state: entity.state.toString(),
postalCode: entity.postalCode.toString(),
email: entity.email.toString(),
phone: entity.phone.toString()
};
}
return pilot;
} catch (error) {
const restError: RestError = error as RestError;
throw new CustomError(
restError.details['odataError']['message']['value'],
restError.details['odataError']['code'],
restError.statusCode
);
}
}
async findAll(): Promise<PilotInfoEntity[]> { async findAll(): Promise<PilotInfoEntity[]> {
try { try {

View File

@@ -0,0 +1,38 @@
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
import { Observable, map } from 'rxjs';
export class PilotInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return handler.handle().pipe(
map((data) => {
if (data.length) {
const pilots = data.map((pilot) => {
return {
partitionKey: pilot.partitionKey,
rowKey: pilot.rowKey,
id: pilot.id,
name: pilot.name
};
});
return pilots;
} else {
return {
partitionKey: data.partitionKey,
rowKey: data.rowKey,
id: data.id,
name: data.name
};
}
})
);
}
return handler.handle().pipe(map((data) => data));
}
}

View File

@@ -1,15 +1,46 @@
import { Body, Controller, Get, HttpException, Post } from '@nestjs/common'; import {
Body,
Controller,
Get,
HttpException,
Param,
Post,
UseInterceptors
} from '@nestjs/common';
import { PilotInfoService } from './info/pilot-info.service'; import { PilotInfoService } from './info/pilot-info.service';
import { PilotInfoDto } from './info/pilot-info.dto'; import { PilotInfoDto } from './info/pilot-info.dto';
import { TableInsertEntityHeaders } from '@azure/data-tables'; import { TableInsertEntityHeaders } from '@azure/data-tables';
import { CustomError } from '../customError/CustomError'; import { CustomError } from '../customError/CustomError';
import { PilotInfoEntity } from './info/pilot-info.entity'; import { PilotInfoEntity } from './info/pilot-info.entity';
import { PilotInterceptor } from 'src/pilot/interceptors/pilot.interceptor';
import { Public } from '@noahspan/noahspan-modules';
@Controller('pilots') @Controller('pilots')
export class PilotController { export class PilotController {
constructor(private readonly pilotInfoService: PilotInfoService) {} constructor(private readonly pilotInfoService: PilotInfoService) {}
@Get(':pilotId')
@Public()
@UseInterceptors(PilotInterceptor)
async find(@Param() params: any): Promise<PilotInfoEntity> {
try {
const pilot: PilotInfoEntity = await this.pilotInfoService.find(
params.pilotId
);
return pilot;
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode, {
cause: customError.name
});
}
}
@Get() @Get()
@Public()
@UseInterceptors(PilotInterceptor)
async findAll(): Promise<PilotInfoEntity[]> { async findAll(): Promise<PilotInfoEntity[]> {
try { try {
const pilots: PilotInfoEntity[] = await this.pilotInfoService.findAll(); const pilots: PilotInfoEntity[] = await this.pilotInfoService.findAll();

View File

@@ -16,7 +16,7 @@
"@fortawesome/free-regular-svg-icons": "^6.5.2", "@fortawesome/free-regular-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/react-fontawesome": "^0.2.2",
"@noahspan/noahspan-components": "^0.6.8", "@noahspan/noahspan-components": "^0.7.0",
"axios": "^1.7.2", "axios": "^1.7.2",
"framer-motion": "^11.1.7", "framer-motion": "^11.1.7",
"react": "^18.2.0", "react": "^18.2.0",

View File

@@ -1,5 +1,8 @@
import { PilotFormMode } from './PilotForm';
export interface IPilotFormProps { export interface IPilotFormProps {
pilotId?: string;
isDrawerOpen: boolean; isDrawerOpen: boolean;
onOpenCloseDrawer: () => void; mode: PilotFormMode;
onOpenClose: (mode: PilotFormMode) => void;
pilotId?: string;
} }

View File

@@ -1,49 +1,60 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { import { useForm, Controller, FormProvider } from 'react-hook-form';
useForm,
Controller,
FormProvider,
FieldValues
} from 'react-hook-form';
import { import {
Button, Button,
DatePicker,
Drawer, Drawer,
DrawerBody, DrawerBody,
DrawerHeader, DrawerHeader,
DrawerFooter, DrawerFooter,
Input, Input,
Option,
PeoplePicker, PeoplePicker,
SaveIcon, SaveIcon,
Select,
StateSelect, StateSelect,
Typography, Typography,
XmarkIcon XmarkIcon
} from '@noahspan/noahspan-components'; } from '@noahspan/noahspan-components';
import { IPilotFormProps } from './IPilotFormProps'; import { IPilotFormProps } from './IPilotFormProps';
import PilotFormCertificates from '../pilotFormCertificates/PilotFormCertificates'; import axios, { AxiosInstance, AxiosResponse } from 'axios';
import PilotFormEndorsements from '../pilotFormEndorsements/PilotFormEndorsements';
import { Person } from '@microsoft/microsoft-graph-types';
import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient'; import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken'; import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
import { IPilotFormCertificates } from '../pilotFormCertificates/IPilotFormCertificates'; import { useIsAuthenticated } from '@azure/msal-react';
import { IPilotFormEndorsements } from '../pilotFormEndorsements/IPilotFormEndorsements';
import { EventMessage, EventPayload, EventType } from '@azure/msal-browser'; export enum PilotFormMode {
ADD = 'ADD',
EDIT = 'EDIT',
VIEW = 'VIEW',
CANCEL = 'CANCEL'
}
const PilotForm: React.FC<IPilotFormProps> = ({ const PilotForm: React.FC<IPilotFormProps> = ({
pilotId, pilotId,
isDrawerOpen, isDrawerOpen,
onOpenCloseDrawer mode,
onOpenClose
}: IPilotFormProps) => { }: IPilotFormProps) => {
const httpClient: AxiosInstance = useHttpClient(); const httpClient: AxiosInstance = useHttpClient();
const [peoplePickerResults, setPeoplePickerResults] = useState<Person[]>([]); const [peoplePickerResults, setPeoplePickerResults] = useState<any[]>([]);
const [isPeoplePickerLoading, setIsPeoplePickerLoading] = const [isPeoplePickerLoading, setIsPeoplePickerLoading] =
useState<boolean>(false); useState<boolean>(false);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const { getAccessToken } = useAccessToken(); const { getAccessToken } = useAccessToken();
const methods = useForm(); const isAuthenticated = useIsAuthenticated();
const defaultValues = {
partitionKey: '',
rowKey: '',
id: '',
name: '',
address: '',
city: '',
state: '',
postalCode: '',
email: '',
phone: ''
};
const methods = useForm({
defaultValues: defaultValues
});
const [isDisabled, setIsDisabled] = useState<boolean>(false);
const handlePeoplePickerOnClick = ( const handlePeoplePickerOnClick = (
event: React.MouseEvent<HTMLDivElement> event: React.MouseEvent<HTMLDivElement>
@@ -51,7 +62,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
const divElement: HTMLDivElement = event.target as HTMLDivElement; const divElement: HTMLDivElement = event.target as HTMLDivElement;
methods.setValue('id', divElement.id); methods.setValue('id', divElement.id);
methods.setValue('name', divElement.textContent); methods.setValue('name', divElement.textContent!);
setPeoplePickerResults([]); setPeoplePickerResults([]);
}; };
@@ -82,6 +93,11 @@ const PilotForm: React.FC<IPilotFormProps> = ({
} }
}; };
const onCancel = () => {
methods.reset(defaultValues);
onOpenClose(PilotFormMode.CANCEL);
};
const onSubmit = async (data: unknown) => { const onSubmit = async (data: unknown) => {
try { try {
setIsLoading(true); setIsLoading(true);
@@ -111,8 +127,38 @@ const PilotForm: React.FC<IPilotFormProps> = ({
}; };
useEffect(() => { useEffect(() => {
console.log(methods.formState.errors); if (mode === PilotFormMode.VIEW) {
}, [methods.formState.errors]); setIsDisabled(true);
}
}, [mode]);
useEffect(() => {
const getPilot = async () => {
try {
setIsLoading(true);
const config = isAuthenticated
? { headers: { Authorization: await getAccessToken() } }
: {};
const response: AxiosResponse = await httpClient.get(
`api/pilots/${pilotId}`,
config
);
const pilot = response.data;
console.log(pilot);
// methods.setValue('blah', pilot.value)
methods.reset(pilot);
console.log(methods.getValues());
} catch (error) {
} finally {
setIsLoading(false);
}
};
if (pilotId) {
getPilot();
}
}, [pilotId]);
return ( return (
<Drawer <Drawer
@@ -122,7 +168,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
data-testid="pilot-drawer" data-testid="pilot-drawer"
> >
<FormProvider {...methods}> <FormProvider {...methods}>
<DrawerHeader text="Add Pilot" onClose={onOpenCloseDrawer} /> <DrawerHeader text="Add Pilot" onClose={onCancel} />
<form onSubmit={methods.handleSubmit(onSubmit)}> <form onSubmit={methods.handleSubmit(onSubmit)}>
<DrawerBody> <DrawerBody>
<div className="grid grid-cols-4 gap-4"> <div className="grid grid-cols-4 gap-4">
@@ -134,11 +180,11 @@ const PilotForm: React.FC<IPilotFormProps> = ({
name="name" name="name"
control={methods.control} control={methods.control}
rules={{ required: 'A name must be selected' }} rules={{ required: 'A name must be selected' }}
render={({ field: { disabled, value } }) => ( render={({ field: { value } }) => (
<PeoplePicker <PeoplePicker
results={peoplePickerResults} results={peoplePickerResults}
inputProps={{ inputProps={{
disabled: disabled, disabled: isDisabled,
labelProps: { labelProps: {
className: 'before:content-none after:content-none' className: 'before:content-none after:content-none'
}, },
@@ -159,178 +205,209 @@ const PilotForm: React.FC<IPilotFormProps> = ({
)} )}
/> />
</div> </div>
<div className="col-span-1"> {isAuthenticated && (
<Typography variant="h6">Address *</Typography> <>
</div> <div className="col-span-1">
<div className="col-span-3"> <Typography variant="h6">Address *</Typography>
<Controller </div>
name="address" <div className="col-span-3">
control={methods.control} <Controller
rules={{ required: 'An address is required' }} name="address"
render={({ field: { disabled, onChange, value } }) => ( control={methods.control}
<Input rules={{ required: 'An address is required' }}
className="!border-t-blue-gray-200 focus:!border-t-gray-900" render={({ field: { onChange, value } }) => (
disabled={disabled} <Input
labelProps={{ className="!border-t-blue-gray-200 focus:!border-t-gray-900"
className: 'before:content-none after:content-none' disabled={isDisabled}
labelProps={{
className: 'before:content-none after:content-none'
}}
error={
methods.formState.errors.address ? true : false
}
helperText={
methods.formState.errors.address
? methods.formState.errors.address.message?.toString()
: undefined
}
onChange={onChange}
value={value}
data-testid="pilot-form-address-input"
/>
)}
/>
</div>
</>
)}
{isAuthenticated && (
<>
<div className="col-span-1">
<Typography variant="h6">City *</Typography>
</div>
<div className="col-span-3">
<Controller
name="city"
control={methods.control}
rules={{ required: 'A city is required' }}
render={({ field: { onChange, value } }) => (
<Input
disabled={isDisabled}
labelProps={{
className: 'before:content-none after:content-none'
}}
error={methods.formState.errors.city ? true : false}
helperText={
methods.formState.errors.city
? methods.formState.errors.city.message?.toString()
: undefined
}
onChange={onChange}
value={value}
data-testid="pilot-form-city-input"
/>
)}
/>
</div>
</>
)}
{isAuthenticated && (
<>
<div className="col-span-1">
<Typography variant="h6">State *</Typography>
</div>
<div className="col-span-3">
<Controller
name="state"
control={methods.control}
rules={{ required: 'A state must be selected' }}
render={({ field: { onChange, value } }) => (
<StateSelect
disabled={isDisabled}
labelProps={{
className: 'before:content-none after:content-none'
}}
error={methods.formState.errors.state ? true : false}
helperText={
methods.formState.errors.state
? methods.formState.errors.state.message?.toString()
: undefined
}
onChange={onChange}
value={value}
variant="outlined"
data-testid="pilot-form-state-dropdown"
/>
)}
/>
</div>
</>
)}
{isAuthenticated && (
<>
<div className="col-span-1">
<Typography variant="h6">Postal Code *</Typography>
</div>
<div className="col-span-3">
<Controller
name="postalCode"
control={methods.control}
rules={{ required: 'A postal code is required' }}
render={({ field: { onChange, value } }) => (
<Input
disabled={isDisabled}
labelProps={{
className: 'before:content-none after:content-none'
}}
error={
methods.formState.errors.postalCode ? true : false
}
helperText={
methods.formState.errors.postalCode
? methods.formState.errors.postalCode.message?.toString()
: undefined
}
onChange={onChange}
value={value}
data-testid="pilot-form-postal-code-input"
/>
)}
/>
</div>
</>
)}
{isAuthenticated && (
<>
<div className="col-span-1">
<Typography variant="h6">Email</Typography>
</div>
<div className="col-span-3">
<Controller
name="email"
control={methods.control}
rules={{
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address'
}
}} }}
error={methods.formState.errors.address ? true : false} render={({ field: { onChange, value } }) => (
helperText={ <Input
methods.formState.errors.address disabled={isDisabled}
? methods.formState.errors.address.message?.toString() labelProps={{
: undefined className: 'before:content-none after:content-none'
} }}
onChange={onChange} error={methods.formState.errors.email ? true : false}
value={value} helperText={
data-testid="pilot-form-address-input" methods.formState.errors.email
? methods.formState.errors.email.message?.toString()
: undefined
}
onChange={onChange}
value={value}
data-testid="pilot-form-email-input"
/>
)}
/> />
)} </div>
/> </>
</div> )}
<div className="col-span-1"> {isAuthenticated && (
<Typography variant="h6">City *</Typography> <>
</div> <div className="col-span-1">
<div className="col-span-3"> <Typography variant="h6">Phone Number</Typography>
<Controller </div>
name="city" <div className="col-span-3">
control={methods.control} <Controller
rules={{ required: 'A city is required' }} name="phone"
render={({ field: { disabled, onChange, value } }) => ( control={methods.control}
<Input rules={{
disabled={disabled} pattern: {
labelProps={{ value: /^[0-9]{3}[-\s\.][0-9]{3}[-\s\.][0-9]{4}$/i,
className: 'before:content-none after:content-none' message: 'Enter phone number as 123-456-7890'
}
}} }}
error={methods.formState.errors.city ? true : false} render={({ field: { onChange, value } }) => (
helperText={ <Input
methods.formState.errors.city disabled={isDisabled}
? methods.formState.errors.city.message?.toString() labelProps={{
: undefined className: 'before:content-none after:content-none'
} }}
onChange={onChange} error={methods.formState.errors.phone ? true : false}
value={value} helperText={
data-testid="pilot-form-city-input" methods.formState.errors.phone
? methods.formState.errors.phone.message?.toString()
: undefined
}
onChange={onChange}
value={value}
data-testid="pilot-form-phone-input"
/>
)}
/> />
)} </div>
/> </>
</div> )}
<div className="col-span-1"> {/* {pilotId && (
<Typography variant="h6">State *</Typography>
</div>
<div className="col-span-3">
<Controller
name="state"
control={methods.control}
rules={{ required: 'A state must be selected' }}
render={({ field: { disabled, onChange, value } }) => (
<StateSelect
disabled={disabled}
error={methods.formState.errors.state ? true : false}
helperText={
methods.formState.errors.state
? methods.formState.errors.state.message?.toString()
: undefined
}
onChange={onChange}
value={value}
variant="outlined"
data-testid="pilot-form-state-dropdown"
/>
)}
/>
</div>
<div className="col-span-1">
<Typography variant="h6">Postal Code *</Typography>
</div>
<div className="col-span-3">
<Controller
name="postalCode"
control={methods.control}
rules={{ required: 'A postal code is required' }}
render={({ field: { disabled, onChange, value } }) => (
<Input
disabled={disabled}
labelProps={{
className: 'before:content-none after:content-none'
}}
error={methods.formState.errors.postalCode ? true : false}
helperText={
methods.formState.errors.postalCode
? methods.formState.errors.postalCode.message?.toString()
: undefined
}
onChange={onChange}
value={value}
data-testid="pilot-form-postal-code-input"
/>
)}
/>
</div>
<div className="col-span-1">
<Typography variant="h6">Email</Typography>
</div>
<div className="col-span-3">
<Controller
name="email"
control={methods.control}
rules={{
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address'
}
}}
render={({ field: { disabled, onChange, value } }) => (
<Input
disabled={disabled}
labelProps={{
className: 'before:content-none after:content-none'
}}
error={methods.formState.errors.email ? true : false}
helperText={
methods.formState.errors.email
? methods.formState.errors.email.message?.toString()
: undefined
}
onChange={onChange}
value={value}
data-testid="pilot-form-email-input"
/>
)}
/>
</div>
<div className="col-span-1">
<Typography variant="h6">Phone Number</Typography>
</div>
<div className="col-span-3">
<Controller
name="phone"
control={methods.control}
rules={{
pattern: {
value: /^[0-9]{3}[-\s\.][0-9]{3}[-\s\.][0-9]{4}$/i,
message: 'Enter phone number as 123-456-7890'
}
}}
render={({ field: { disabled, onChange, value } }) => (
<Input
disabled={disabled}
labelProps={{
className: 'before:content-none after:content-none'
}}
error={methods.formState.errors.phone ? true : false}
helperText={
methods.formState.errors.phone
? methods.formState.errors.phone.message?.toString()
: undefined
}
onChange={onChange}
value={value}
data-testid="pilot-form-phone-input"
/>
)}
/>
</div>
{pilotId && (
<> <>
<div className="col-span-1"> <div className="col-span-1">
<Typography variant="h6">Last Review</Typography> <Typography variant="h6">Last Review</Typography>
@@ -354,8 +431,8 @@ const PilotForm: React.FC<IPilotFormProps> = ({
/> />
</div> </div>
</> </>
)} )} */}
{pilotId && ( {/* {pilotId && (
<> <>
<div className="col-span-4"> <div className="col-span-4">
<Typography variant="h5">Medical</Typography> <Typography variant="h5">Medical</Typography>
@@ -434,35 +511,41 @@ const PilotForm: React.FC<IPilotFormProps> = ({
</div> </div>
<PilotFormEndorsements endorsements={[]} /> <PilotFormEndorsements endorsements={[]} />
</> </>
)} )} */}
</div> </div>
</DrawerBody> </DrawerBody>
<DrawerFooter> <DrawerFooter>
<div className="flex gap-2 justify-end justify-self-center pt-4"> <>
<div> {mode !== PilotFormMode.VIEW && (
<Button <div className="flex gap-2 justify-end justify-self-center pt-4">
className="flex items-center gap-3" <div>
variant="outlined" <Button
onClick={onOpenCloseDrawer} className="flex items-center gap-3"
data-testid="pilot-cancel-button" disabled={isDisabled}
> variant="outlined"
<XmarkIcon size="lg" /> onClick={onCancel}
Cancel data-testid="pilot-cancel-button"
</Button> >
</div> <XmarkIcon size="lg" />
<div> Cancel
<Button </Button>
className="flex items-center gap-3" </div>
loading={isLoading} <div>
variant="filled" <Button
type="submit" className="flex items-center gap-3"
data-testid="pilot-save-button" disabled={isDisabled}
> loading={isLoading}
<SaveIcon size="lg" /> variant="filled"
Save type="submit"
</Button> data-testid="pilot-save-button"
</div> >
</div> <SaveIcon size="lg" />
Save
</Button>
</div>
</div>
)}
</>
</DrawerFooter> </DrawerFooter>
</form> </form>
</FormProvider> </FormProvider>

View File

@@ -19,14 +19,34 @@ import {
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient'; import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
import { AxiosInstance, AxiosResponse } from 'axios'; import { AxiosInstance, AxiosResponse } from 'axios';
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken'; import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
import { useIsAuthenticated } from '@azure/msal-react';
import { PilotFormMode } from '../pilotForm/PilotForm';
const Pilots: React.FC<unknown> = () => { const Pilots: React.FC<unknown> = () => {
const httpClient: AxiosInstance = useHttpClient(); const httpClient: AxiosInstance = useHttpClient();
const isAuthenticated = useIsAuthenticated();
const { getAccessToken } = useAccessToken(); const { getAccessToken } = useAccessToken();
const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [pilotFormMode, setPilotFormMode] = useState<PilotFormMode>(
PilotFormMode.CANCEL
);
const [selectedPilotId, setSelectedPilotId] = useState<string | undefined>();
const [pilots, setPilots] = useState<Pilot[]>([]); const [pilots, setPilots] = useState<Pilot[]>([]);
const onOpenCloseDrawer = () => { const onOpenClosePilotForm = (mode: PilotFormMode, pilotId?: string) => {
setIsDrawerOpen(!isDrawerOpen); switch (mode) {
case PilotFormMode.ADD:
case PilotFormMode.EDIT:
case PilotFormMode.VIEW:
setPilotFormMode(mode);
setSelectedPilotId(pilotId);
setIsDrawerOpen(true);
break;
case PilotFormMode.CANCEL:
setPilotFormMode(mode);
setSelectedPilotId(undefined);
setIsDrawerOpen(false);
break;
}
}; };
type Pilot = { type Pilot = {
@@ -40,54 +60,66 @@ const Pilots: React.FC<unknown> = () => {
{ {
accessorKey: 'name', accessorKey: 'name',
header: 'Name' header: 'Name'
},
{
id: 'actions',
header: 'Actions',
cellProps: {
className: 'text-end'
},
cell: (info: any) => {
const pilotId = info.row.original.rowKey;
return (
<Menu placement="bottom-end">
<MenuHandler>
<div>
<IconButton variant="text">
<EllipsisVerticalIcon size="xl" />
</IconButton>
</div>
</MenuHandler>
<MenuList>
<MenuItem
className="flex gap-3"
onClick={() =>
onOpenClosePilotForm(PilotFormMode.EDIT, pilotId)
}
>
<PenIcon size="lg" />
Edit
</MenuItem>
<MenuItem
className="flex gap-3"
onClick={() =>
onOpenClosePilotForm(PilotFormMode.VIEW, pilotId)
}
>
<EyeIcon size="lg" />
View
</MenuItem>
<hr className="my-3" />
<MenuItem className="flex gap-3">
<TrashIcon size="lg" />
Delete
</MenuItem>
</MenuList>
</Menu>
);
},
enableSorting: false
} }
// {
// id: 'actions',
// header: 'Actions',
// cellProps: {
// className: 'text-center'
// },
// cell: () => {
// return (
// <Menu placement="bottom-end">
// <MenuHandler>
// <div>
// <IconButton variant="text">
// <EllipsisVerticalIcon size="xl" />
// </IconButton>
// </div>
// </MenuHandler>
// <MenuList>
// <MenuItem className="flex gap-3">
// <PenIcon size="lg" />
// Edit
// </MenuItem>
// <MenuItem className="flex gap-3">
// <EyeIcon size="lg" />
// View
// </MenuItem>
// <hr className="my-3" />
// <MenuItem className="flex gap-3">
// <TrashIcon size="lg" />
// Delete
// </MenuItem>
// </MenuList>
// </Menu>
// );
// },
// enableSorting: false
// }
]; ];
useEffect(() => { useEffect(() => {
const getPilots = async () => { const getPilots = async () => {
try { try {
const accessToken: string = await getAccessToken(); const config = isAuthenticated
const response: AxiosResponse = await httpClient.get(`api/pilots`, { ? { headers: { Authorization: await getAccessToken() } }
headers: { : {};
Authorization: accessToken const response: AxiosResponse = await httpClient.get(
} `api/pilots`,
}); config
);
console.log(response.data); console.log(response.data);
setPilots(response.data); setPilots(response.data);
} catch (error) { } catch (error) {
@@ -108,7 +140,7 @@ const Pilots: React.FC<unknown> = () => {
<Button <Button
className="flex justify-center gap-3" className="flex justify-center gap-3"
variant="filled" variant="filled"
onClick={onOpenCloseDrawer} onClick={() => onOpenClosePilotForm(PilotFormMode.ADD)}
data-testid="pilot-add-button" data-testid="pilot-add-button"
> >
<PlusIcon size="lg" /> <PlusIcon size="lg" />
@@ -119,7 +151,9 @@ const Pilots: React.FC<unknown> = () => {
</div> </div>
<PilotForm <PilotForm
isDrawerOpen={isDrawerOpen} isDrawerOpen={isDrawerOpen}
onOpenCloseDrawer={onOpenCloseDrawer} mode={pilotFormMode}
onOpenClose={(mode) => onOpenClosePilotForm(mode)}
pilotId={selectedPilotId}
/> />
</> </>
); );

View File

@@ -3,6 +3,7 @@ import { ISiteNavProps } from './ISiteNavProps';
// import { Link as ReactRouterLink } from 'react-router-dom'; // import { Link as ReactRouterLink } from 'react-router-dom';
import { useAppContext } from '../../hooks/appContext/UseAppContext'; import { useAppContext } from '../../hooks/appContext/UseAppContext';
import { import {
Avatar,
Button, Button,
Menu, Menu,
MenuHandler, MenuHandler,
@@ -160,24 +161,21 @@ const SiteNav: React.FC<unknown> = () => {
{!loading && isAuthenticated && ( {!loading && isAuthenticated && (
<Menu placement="bottom-end"> <Menu placement="bottom-end">
<MenuHandler> <MenuHandler>
<> <div>
{/* {userPhoto && */} {userPhoto && (
{/* <img className='rounded-full' src={userPhoto} /> */} <img
{/* } */} className="rounded-full cursor-pointer"
{/* {!userPhoto && */} height="40"
<div className="rounded-full text-white text-center pt-2 bg-black h-[40px] w-[40px]"> width="40"
NS src={userPhoto}
</div> />
)}
{/* <div className="flex gap-2"> {!userPhoto && (
<div className='flex-none'> <div className="rounded-full cursor-pointer text-white text-center pt-2 bg-black h-[40px] w-[40px]">
NS
</div> </div>
<Button className='flex-1' variant="text" size="sm"> )}
{appContext.state.userProfile.displayName} </div>
</Button>
</div> */}
</>
</MenuHandler> </MenuHandler>
<MenuList> <MenuList>
<MenuItem onClick={handleSignOut}> <MenuItem onClick={handleSignOut}>

4
infrastructure/dns.tf Normal file
View File

@@ -0,0 +1,4 @@
data "azurerm_dns_zone" "dns_zone" {
name = var.DOMAIN_NAME
resource_group_name = data.azurerm_resource_group.resource_group.name
}

View File

@@ -1,9 +1,31 @@
module "static_web_app" { # module "static_web_app" {
source = "github.com/noahspannbauer/noahspan-root/infrastructure/modules/static_web_app" # source = "github.com/noahspannbauer/noahspan-root/infrastructure/modules/static_web_app"
region = var.REGION # region = var.REGION
resource_group_name = data.azurerm_resource_group.resource_group.name # resource_group_name = data.azurerm_resource_group.resource_group.name
static_web_app_name = var.STATIC_WEB_APP_NAME # static_web_app_name = var.STATIC_WEB_APP_NAME
custom_domain_name_count = var.CUSTOM_DOMAIN_NAME_COUNT # custom_domain_name_count = var.CUSTOM_DOMAIN_NAME_COUNT
domain_name = var.DOMAIN_NAME # domain_name = var.DOMAIN_NAME
subdomain_name = var.SUBDOMAIN_NAME # subdomain_name = var.SUBDOMAIN_NAME
# }
resource "azurerm_static_web_app" "static_web_app" {
name = var.STATIC_WEB_APP_NAME
resource_group_name = data.azurerm_resource_group.resource_group.name
location = var.REGION
}
resource "azurerm_dns_cname_record" "dns_cname_record" {
name = var.SUBDOMAIN_NAME
zone_name = data.azurerm_dns_zone.dns_zone.name
resource_group_name = data.azurerm_resource_group.resource_group.name
ttl = 14400
record = azurerm_static_web_app.static_web_app.default_host_name
}
resource "azurerm_static_web_app_custom_domain" "static_web_app_custom_domain" {
static_web_app_id = azurerm_static_web_app.static_web_app.id
domain_name = "${var.SUBDOMAIN_NAME}.${var.DOMAIN_NAME}"
validation_type = "cname-delegation"
depends_on = [ azurerm_dns_cname_record.dns_cname_record ]
} }

View File

@@ -1,8 +1,8 @@
output "api_key" { output "api_key" {
value = module.static_web_app.api_key value = azurerm_static_web_app.static_web_app.api_key
sensitive = true sensitive = true
} }
output "default_host_name" { output "default_host_name" {
value = module.static_web_app.default_host_name value = azurerm_static_web_app.static_web_app.default_host_name
} }

16
package-lock.json generated
View File

@@ -40,7 +40,7 @@
"@nestjs/core": "^10.0.0", "@nestjs/core": "^10.0.0",
"@nestjs/passport": "^10.0.3", "@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0", "@nestjs/platform-express": "^10.0.0",
"@noahspan/noahspan-modules": "^0.3.9", "@noahspan/noahspan-modules": "^0.4.0",
"@schematics/angular": "^17.3.7", "@schematics/angular": "^17.3.7",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
@@ -76,7 +76,7 @@
"@fortawesome/free-regular-svg-icons": "^6.5.2", "@fortawesome/free-regular-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/react-fontawesome": "^0.2.2",
"@noahspan/noahspan-components": "^0.6.8", "@noahspan/noahspan-components": "^0.7.0",
"axios": "^1.7.2", "axios": "^1.7.2",
"framer-motion": "^11.1.7", "framer-motion": "^11.1.7",
"react": "^18.2.0", "react": "^18.2.0",
@@ -3397,9 +3397,9 @@
"link": true "link": true
}, },
"node_modules/@noahspan/noahspan-components": { "node_modules/@noahspan/noahspan-components": {
"version": "0.6.8", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/@noahspan/noahspan-components/-/noahspan-components-0.6.8.tgz", "resolved": "https://registry.npmjs.org/@noahspan/noahspan-components/-/noahspan-components-0.7.0.tgz",
"integrity": "sha512-X4ftKtH9/ICyOCRKUnPMAh/u27Bi391edSKvIwWOxxawyIEZj58whjB0RfgcHV0O/Ap84lGt7YYctZTTzJHBEQ==", "integrity": "sha512-UJ0UBEwjt3IJTUhdNh/sUMFuuruJl6zn8pzy2JOZoMBfRmrX/6Dzz+u+3F0Q7RAitgV/XhJskSYrJBripenGzw==",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-brands-svg-icons": "^6.5.2", "@fortawesome/free-brands-svg-icons": "^6.5.2",
@@ -3431,9 +3431,9 @@
} }
}, },
"node_modules/@noahspan/noahspan-modules": { "node_modules/@noahspan/noahspan-modules": {
"version": "0.3.9", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/@noahspan/noahspan-modules/-/noahspan-modules-0.3.9.tgz", "resolved": "https://registry.npmjs.org/@noahspan/noahspan-modules/-/noahspan-modules-0.4.0.tgz",
"integrity": "sha512-gRW+DiXQP+/0vHvEBi5yijQwwUnP+z5YxlSU7Q/bzm7YdUv5ecbZrz4is+BzH1S6Ctds9DeoQKtvb9xbAKwg8w==", "integrity": "sha512-oFOFL6AiEVlNmURAxDdnKg6xNZVZhQOPNeDtwWwbgYf2CQAHOapiBpKwHDAWT7DVpoBEurvmDEXeCHGl8mhGUw==",
"dependencies": { "dependencies": {
"@azure/app-configuration": "^1.6.0", "@azure/app-configuration": "^1.6.0",
"@azure/data-tables": "^13.2.2", "@azure/data-tables": "^13.2.2",