updating terraform

This commit is contained in:
2025-11-20 19:08:24 -06:00
parent 280b222d9a
commit 2a596b29f7
56 changed files with 1008 additions and 933 deletions

View File

@@ -35,7 +35,7 @@
"@nestjs/serve-static": "^5.0.3",
"@nestjs/typeorm": "^11.0.0",
"@noahspan/azure-database": "^3.1.2",
"@noahspan/noahspan-modules": "^1.2.8",
"@noahspan/noahspan-modules": "^1.2.9",
"@schematics/angular": "^17.3.7",
"@types/multer": "^1.4.12",
"better-sqlite3": "^12.2.0",

View File

@@ -15,7 +15,7 @@ export class CertificateEntity {
@Column()
issueDate: Date
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.certificates)
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.certificates, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
@JoinColumn({ name: 'pilotId' })
pilot: PilotEntity;
}

View File

@@ -12,7 +12,7 @@ export class EndorsementEntity {
@Column()
issueDate: Date;
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.endorsements)
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.endorsements, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
@JoinColumn({ name: 'pilotId' })
pilot: PilotEntity
}

View File

@@ -58,11 +58,32 @@ import { ConfigService } from '@nestjs/config';
return downloaded
}
async deleteFile(containerName: string, rowKey:string, fileName: string): Promise<void> {
async deleteFile(containerName: string, logId: string, fileName: string): Promise<void> {
this.containerName = containerName;
const blockBlobClient = await this.getBlobClient(`${rowKey}/${fileName}`);
const blockBlobClient = await this.getBlobClient(`${logId}/${fileName}`);
await blockBlobClient.deleteIfExists();
}
async deleteFolder(containerName: string, logId: string): Promise<void> {
const blobService = await this.getBlobServiceInstance();
this.containerName = containerName;
const containerClient = blobService.getContainerClient(containerName);
const blobsToDelete = []
for await (const blob of containerClient.listBlobsFlat({ prefix: logId })) {
blobsToDelete.push(blob.name)
}
for (const blobName of blobsToDelete) {
const blockBlobClient = containerClient.getBlockBlobClient(blobName);
await blockBlobClient.delete();
}
return;
}
}

View File

@@ -14,14 +14,16 @@ import { LogDto } from './log.dto';
import { LogEntity } from './log.entity';
import { LogService } from './log.service';
import { CustomError } from '../error/customError';
import { AuthGuard } from '@noahspan/noahspan-modules';
import { AuthGuard, Public } from '@noahspan/noahspan-modules';
import { LogInterceptor } from './log.interceptor';
import { FileService } from '../file/file.service';
import { DeleteResult, InsertResult, UpdateResult } from 'typeorm';
import { Reflector } from '@nestjs/core';
const reflector = new Reflector();
@Controller('logs')
@UseInterceptors(new LogInterceptor())
// @UseGuards(AuthGuard)
@UseInterceptors(new LogInterceptor(reflector))
export class LogController {
constructor(
private readonly fileService: FileService,
@@ -30,6 +32,7 @@ export class LogController {
@Get(':id')
@Public()
async find(
@Param('id') id: string,
): Promise<LogEntity> {
@@ -44,6 +47,7 @@ export class LogController {
}
@Get()
@Public()
async findAll(): Promise<LogEntity[]> {
try {
return await this.logService.findAll();
@@ -56,6 +60,7 @@ export class LogController {
@Post()
@UseGuards(AuthGuard)
async create(@Body() logDto: LogDto): Promise<InsertResult> {
try {
return await this.logService.create(logDto);
@@ -67,6 +72,7 @@ export class LogController {
}
@Put(':id')
@UseGuards(AuthGuard)
async update(
@Param('id') id: string,
@Body() logDto: LogDto
@@ -83,12 +89,14 @@ export class LogController {
}
@Delete(':id')
@UseGuards(AuthGuard)
async delete(
@Param('id') id: string,
): Promise<DeleteResult> {
try {
return await this.logService.delete(id);
} catch (error) {
console.log(error)
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);

View File

@@ -76,10 +76,10 @@ export class LogEntity {
@Column({ nullable: true })
notes: string | null;
@OneToMany(() => TrackEntity, (track: TrackEntity) => track.log, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
@OneToMany(() => TrackEntity, (track: TrackEntity) => track.log)
tracks: TrackEntity[]
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.logs)
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.logs, {onDelete: 'CASCADE', onUpdate: 'CASCADE'})
@JoinColumn({ name: 'pilotId' })
pilot: PilotEntity;
}

View File

@@ -3,18 +3,23 @@ import { Observable, map } from 'rxjs';
import { LogEntity } from './log.entity';
import { jwtDecode } from 'jwt-decode';
import { CustomJwtPayload } from 'src/interfaces/customJwtPayload.interface';
import { IS_PUBLIC_KEY } from '@noahspan/noahspan-modules';
import { Reflector } from '@nestjs/core';
export class LogInterceptor implements NestInterceptor {
constructor(private reflector: Reflector) {}
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];
const jwtPayload: CustomJwtPayload = jwtDecode(token);
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
return handler.handle().pipe(
map((data: LogEntity[]) => {
if (data.length > 0 && jwtPayload.roles.includes('Flying.Read')) {
const logs = data.map((log: LogEntity) => {
const req = context.switchToHttp().getRequest();
const limitData = (data) => {
return data.map((log: LogEntity) => {
return {
id: log.id,
pilot: {
@@ -28,10 +33,25 @@ export class LogInterceptor implements NestInterceptor {
tracks: log.tracks,
};
});
}
if (req.headers.authorization) {
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];
const jwtPayload: CustomJwtPayload = jwtDecode(token);
if (jwtPayload.roles.includes('Flying.Read')) {
const logs = limitData(data);
return logs;
} else {
return data;
}
} else if (!req.headers.authorization && isPublic) {
const publicData = limitData(data)
const logs = publicData.slice(0, 5)
return logs;
} else {
return data;
}
})
);

View File

@@ -1,28 +1,14 @@
import { Module } from '@nestjs/common';
import { LogController } from './log.controller';
import { LogService } from './log.service';
// import { AzureTableStorageModule } from '@noahspan/azure-database';
import { LogEntity } from './log.entity';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ConfigService } from '@nestjs/config';
import { FileService } from '../file/file.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PilotModule } from 'src/pilot/pilot.module';
@Module({
imports: [
// AzureTableStorageModule.forRootAsync({
// imports: [ConfigModule],
// useFactory: async (configService: ConfigService) => {
// return {
// connectionString: configService.get<string>('azureStorageConnectionString')
// };
// },
// inject: [ConfigService]
// }),
// AzureTableStorageModule.forFeature(Log, {
// createTableIfNotExists: false,
// table: 'logs'
// }),
PilotModule,
TypeOrmModule.forFeature([LogEntity])
],

View File

@@ -6,19 +6,20 @@ import { LogDto } from './log.dto';
import { PilotService } from 'src/pilot/pilot.service';
import { PilotEntity } from 'src/pilot/pilot.entity';
import { CustomError } from 'src/error/customError';
import { TrackService } from 'src/track/track.service';
import { FileService } from 'src/file/file.service';
@Injectable()
export class LogService {
constructor(
@InjectRepository(LogEntity) private readonly logRepository: Repository<LogEntity>,
private readonly pilotService: PilotService
private readonly fileService: FileService,
private readonly pilotService: PilotService,
) {}
async find(id: string): Promise<LogEntity> {
const logEntity: LogEntity = await this.logRepository.findOne({
where: { id: id },
// relations: ['pilot', 'tracks']
relations: ['pilot', 'tracks']
});
return logEntity;
@@ -55,6 +56,8 @@ export class LogService {
}
async delete(id: string): Promise<DeleteResult> {
await this.fileService.deleteFolder('tracks', id);
return await this.logRepository.delete({ id });
}
}

View File

@@ -12,7 +12,7 @@ export class MedicalEntity {
@Column()
expirationDate: Date;
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.medical)
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.medical, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
@JoinColumn({ name: 'pilotId' })
pilot: PilotEntity;
}

View File

@@ -11,19 +11,21 @@ import {
UseInterceptors,
} from '@nestjs/common';
import { PilotDto } from './pilot.dto';
import { PilotEntity } from './pilot.entity';
import { PilotService } from './pilot.service';
import { CustomError } from '../error/customError';
import { PilotInterceptor } from './pilot.interceptor';
import { AuthGuard } from '@noahspan/noahspan-modules';
import { AuthGuard, Public } from '@noahspan/noahspan-modules';
import { Reflector } from '@nestjs/core';
const reflector = new Reflector();
@Controller('pilots')
@UseInterceptors(new PilotInterceptor())
@UseGuards(AuthGuard)
@UseInterceptors(new PilotInterceptor(reflector))
export class PilotController {
constructor(private readonly pilotService: PilotService) {}
@Get(':id')
@Public()
async find(@Param('id') id: string) {
try {
return await this.pilotService.find(id);
@@ -35,28 +37,33 @@ export class PilotController {
}
@Get()
@Public()
async findAll() {
try {
return await this.pilotService.findAll();
} catch (error) {
const customError = error as CustomError;
console.log(error)
throw new HttpException(customError.message, customError.statusCode);
}
}
@Post()
@UseGuards(AuthGuard)
async create(@Body() pilotDto: PilotDto) {
try {
return await this.pilotService.create(pilotDto);
} catch (error) {
const customError = error as CustomError;
console.log(error)
// const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
// throw new HttpException(customError.message, customError.statusCode);
}
}
@Put(':id')
@UseGuards(AuthGuard)
async update(
@Param('id') id: string,
@Body() pilotDto: PilotDto
@@ -71,6 +78,7 @@ export class PilotController {
}
@Delete(':id')
@UseGuards(AuthGuard)
async delete(
@Param('id') id: string,
) {

View File

@@ -33,15 +33,15 @@ export class PilotEntity {
@Column()
userId: string | null;
@OneToMany(() => LogEntity, (log: LogEntity) => log.pilot, {onDelete: 'CASCADE', onUpdate: 'CASCADE'})
@OneToMany(() => LogEntity, (log: LogEntity) => log.pilot)
logs: LogEntity[];
@OneToMany(() => CertificateEntity, (certificate: CertificateEntity) => certificate.pilot, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
@OneToMany(() => CertificateEntity, (certificate: CertificateEntity) => certificate.pilot)
certificates: CertificateEntity[];
@OneToMany(() => EndorsementEntity, (endorsement: EndorsementEntity) => endorsement.pilot, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
@OneToMany(() => EndorsementEntity, (endorsement: EndorsementEntity) => endorsement.pilot)
endorsements: EndorsementEntity[];
@OneToMany(() => MedicalEntity, (medical: MedicalEntity) => medical.pilot, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
@OneToMany(() => MedicalEntity, (medical: MedicalEntity) => medical.pilot)
medical: MedicalEntity[];
}

View File

@@ -1,31 +1,49 @@
import { CallHandler, ExecutionContext, NestInterceptor, UnauthorizedException } from '@nestjs/common';
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
import { jwtDecode } from 'jwt-decode';
import { Observable, map } from 'rxjs';
import { CustomJwtPayload } from 'src/interfaces/customJwtPayload.interface';
import { PilotEntity } from './pilot.entity';
import { IS_PUBLIC_KEY } from '@noahspan/noahspan-modules';
import { Reflector } from '@nestjs/core';
export class PilotInterceptor implements NestInterceptor {
constructor(private reflector: Reflector) {}
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];
const jwtPayload: CustomJwtPayload = jwtDecode(token);
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
return handler.handle().pipe(
map((data: PilotEntity[]) => {
if (data.length && jwtPayload.roles.includes('Flying.Read')) {
const pilots = data.map((pilot) => {
const req = context.switchToHttp().getRequest();
const limitData = (data) => {
return data.map((pilot: PilotEntity) => {
return {
id: pilot.id,
name: pilot.name
};
});
})
}
return pilots;
} else if (data.length && jwtPayload.roles.includes('Flying.Write')) {
return data;
} else {
return UnauthorizedException;
if (req.headers.authorization) {
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];
const jwtPayload: CustomJwtPayload = jwtDecode(token);
if (jwtPayload.roles.includes('Flying.Read')) {
const pilots = limitData(data)
return pilots;
} else {
return data;
}
} else if (!req.headers.authorization && isPublic) {
const publicData = limitData(data);
const logs = publicData.slice(0,5)
return logs;
}
})
);

View File

@@ -1,40 +1,11 @@
import { Module } from '@nestjs/common';
import { PilotController } from './pilot.controller';
import { PilotService } from './pilot.service';
// import { AzureTableStorageModule } from '@noahspan/azure-database';
import { PilotEntity } from './pilot.entity';
// import { ConfigModule, ConfigService } from '@nestjs/config';
// import { Log } from 'src/log/log.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
// AzureTableStorageModule.forRootAsync({
// imports: [ConfigModule],
// useFactory: async (configService: ConfigService) => {
// return {
// connectionString: configService.get<string>('azureStorageConnectionString')
// };
// },
// inject: [ConfigService]
// }),
// AzureTableStorageModule.forFeature(Log, {
// createTableIfNotExists: false,
// table: 'logs'
// }),
// AzureTableStorageModule.forRootAsync({
// imports: [ConfigModule],
// useFactory: async (configService: ConfigService) => {
// return {
// connectionString: configService.get<string>('azureStorageConnectionString')
// };
// },
// inject: [ConfigService]
// }),
// AzureTableStorageModule.forFeature(PilotEntity, {
// createTableIfNotExists: false,
// table: 'pilots'
// }),
TypeOrmModule.forFeature([PilotEntity])
],
controllers: [PilotController],

View File

@@ -1,12 +1,10 @@
import {
Body,
Controller,
Delete,
Get,
HttpException,
Param,
Post,
Put,
Query,
UploadedFile,
UseGuards,
@@ -18,8 +16,7 @@ import { FileInterceptor } from '@nestjs/platform-express';
import { FileService } from '../file/file.service';
import { TrackService } from './track.service';
import { TrackEntity } from './track.entity';
import { TrackDto } from './track.dto';
import { DeleteResult, InsertResult, UpdateResult } from 'typeorm';
import { DeleteResult, InsertResult } from 'typeorm';
@Controller('tracks')
export class TrackController {
@@ -28,22 +25,10 @@ export class TrackController {
private readonly trackService: TrackService
) {}
// @Get('id')
// async find(@Param('id') id: string): Promise<TrackEntity> {
// try {
// return await this.trackService.find(id);
// } catch (error) {
// const customError = error as CustomError;
// throw new HttpException(customError.message, customError.statusCode);
// }
// }
@UseGuards(AuthGuard)
@Get(':logId')
async findAll(@Param('logId') logId: string): Promise<TrackEntity[]> {
try {
console.log('logId: ' + logId)
return await this.trackService.findAll(logId);
} catch (error) {
const customError = error as CustomError;
@@ -66,11 +51,10 @@ export class TrackController {
}
@UseGuards(AuthGuard)
@Delete(':id')
async delete(@Param('id') id: string, @Query('fileName') fileName: string, @Query('logId') logId: string): Promise<DeleteResult> {
@Delete(':id/:filename/:logId')
async delete(@Param('id') id: string, @Query('fileName') filename: string, @Query('logId') logId: string): Promise<DeleteResult> {
try {
return await this.trackService.delete(id, logId, fileName);
return await this.trackService.delete(id, logId, filename);
} catch (error) {
const customError = error as CustomError;

View File

@@ -12,7 +12,7 @@ export class TrackEntity {
@Column()
order: number;
@ManyToOne(() => LogEntity, (log: LogEntity) => log.tracks)
@ManyToOne(() => LogEntity, (log: LogEntity) => log.tracks, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
@JoinColumn({ name: 'logId' })
log: LogEntity;
}

View File

@@ -31,7 +31,6 @@ export class TrackService {
const logEntity: LogEntity = await this.logService.find(logId);
if (logEntity) {
console.log(logEntity)
const tracks = await this.trackRepository.find({
where: { log:
{
@@ -40,7 +39,6 @@ export class TrackService {
},
})
console.log(tracks)
return tracks;
}
} catch (error) {
@@ -55,7 +53,6 @@ export class TrackService {
if (logEntity) {
const url = await this.fileService.uploadFile(file, this.containerName, logId);
console.log(url)
const track = this.trackRepository.create({
log: logEntity,
order: order,
@@ -67,7 +64,6 @@ export class TrackService {
throw new CustomError('Log not found', 'Not found', 404)
}
} catch (error) {
console.log(error)
throw error;
}
}
@@ -80,16 +76,6 @@ export class TrackService {
}
}
async delete(id: string, logId: string, fileName: string): Promise<DeleteResult> {
try {
await this.fileService.deleteFile(this.containerName, logId, fileName);
return await this.trackRepository.delete({ id });
} catch (error) {
throw error
}
}
async downloadTrackFile(logId: string, fileName: string): Promise<string> {
try {
const downloadedFile: string = await this.fileService.downloadFile(this.containerName, logId, fileName);
@@ -99,4 +85,14 @@ export class TrackService {
throw error
}
}
async delete(id: string, logId: string, fileName: string): Promise<DeleteResult> {
try {
await this.fileService.deleteFile(this.containerName, logId, fileName);
return await this.trackRepository.delete({ id });
} catch (error) {
throw error
}
}
}

View File

@@ -5,7 +5,7 @@ export const { OidcProvider, useOidc, getOidc } = createReactOidc(async () => ({
clientId: import.meta.env.VITE_CLIENT_APP_ID,
homeUrl: import.meta.env.BASE_URL,
scopes: ['email', 'openid', 'profile', `api://${import.meta.env.VITE_API_APP_ID}/user_impersonation`],
autoLogin: true,
autoLogin: false,
postLoginRedirectUrl: '/',
noIframe: true
}));

View File

@@ -1,13 +1,16 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
Icon,
IconName,
Loading
} from '@noahspan/noahspan-components';
import { IDialogConfirmationProps } from './IConfirmationDialogProps';
// import {
// Button,
// Dialog,
// DialogActions,
// DialogContent,
// Icon,
// IconName,
// Loading
// } from '@noahspan/noahspan-components';
import { Button, Modal, ModalBody, ModalContent, ModalHeader, ModalFooter, Spinner } from '@heroui/react'
import { DialogConfirmationProps } from './ConfirmationDialogProps.interface';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCircleCheck, faXmark } from '@fortawesome/free-solid-svg-icons';
const ConfirmationDialog = ({
contentText,
@@ -16,28 +19,33 @@ const ConfirmationDialog = ({
onCancel,
onConfirm,
title
}: IDialogConfirmationProps) => {
}: DialogConfirmationProps) => {
return (
<Dialog
open={isOpen}
<Modal
isDismissable={false}
isKeyboardDismissDisabled={true}
isOpen={isOpen}
>
<h3>{title}</h3>
<DialogContent>
{!isLoading && <div>{contentText}</div>}
{isLoading && <Loading size='xl' />}
</DialogContent>
<DialogActions>
<Button onClick={onCancel} startContent={<Icon iconName={IconName.XMARK} />}>
No
</Button>
<Button
onClick={onConfirm}
startContent={<Icon iconName={IconName.CIRCLE_CHECK} />}
>
Yes
</Button>
</DialogActions>
</Dialog>
<ModalContent>
<ModalHeader>{title}</ModalHeader>
<ModalBody>
{!isLoading && <div>{contentText}</div>}
{isLoading && <Spinner size='lg' />}
</ModalBody>
<ModalFooter>
<Button onPress={onCancel} startContent={<FontAwesomeIcon icon={faXmark} />}>
No
</Button>
<Button
color='primary'
onPress={onConfirm}
startContent={<FontAwesomeIcon icon={faCircleCheck} />}
>
Yes
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};

View File

@@ -1,4 +1,4 @@
export interface IDialogConfirmationProps {
export interface DialogConfirmationProps {
contentText: string;
isLoading: boolean;
isOpen: boolean;

View File

@@ -20,8 +20,9 @@ const Flights = () => {
if (flights && flights.length > 0) {
dispatch({ type: 'SET_FLIGHTS', payload: flights})
dispatch({ type: 'SET_ALERT', payload: undefined })
} else {
dispatch({ type: 'SET_ALERT', payload: { severity: 'default', message: 'There are no flights' }})
dispatch({ type: 'SET_ALERT', payload: { severity: 'default', message: 'No flights found' }})
}
}, [logs])

View File

@@ -64,10 +64,6 @@ const LogForm = () => {
}
}, [pilots]);
useEffect(() => {
console.log(state.isDisabled)
}, [state.isDisabled])
return (
<div className='grid grid-cols-12 gap-3'>
<div className={`col-span-4 self-center text-small after:content-['*'] after:text-danger after:ms-0.5`}>

View File

@@ -132,17 +132,11 @@ const Logbook: React.FC<unknown> = () => {
>
View
</DropdownItem>
<DropdownItem
key='tracks'
onPress={() => onOpenCloseDrawer(FormMode.EDIT, info.row.original.id)}
startContent={<FontAwesomeIcon icon={faMapLocationDot} />}
>
Tracks
</DropdownItem>
</DropdownSection>
<DropdownSection>
<DropdownItem
key='Delete'
onPress={() => onDeleteLog(info.row.original.id)}
startContent={<FontAwesomeIcon icon={faTrash} />}
>
Delete
@@ -393,7 +387,6 @@ const Logbook: React.FC<unknown> = () => {
};
const onOpenCloseDrawer= (mode: FormMode, logId?: string) => {
console.log(logId)
switch (mode) {
case FormMode.ADD:
case FormMode.EDIT:
@@ -423,10 +416,8 @@ const Logbook: React.FC<unknown> = () => {
};
const onDeleteLog = (logId: string) => {
dispatch({
type: 'SET_DELETE',
payload: { isConfirmationDialogOpen: true, selectedLogId: logId }
});
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_OPEN', payload: true });
logbookContext.dispatch({ type: 'SET_SELECTED_LOG_ID', payload: logId })
};
const onConfirmationDialogConfirm = async () => {
@@ -436,9 +427,10 @@ const Logbook: React.FC<unknown> = () => {
await httpClient.delete(`api/logs/${logbookContext.state.selectedLogId}`);
dispatch({
type: 'SET_DELETE',
payload: { isConfirmationDialogOpen: false, selectedLogId: undefined }
type: 'SET_IS_CONFIRMATION_DIALOG_OPEN',
payload: false
});
logbookContext.dispatch({ type: 'SET_SELECTED_LOG_ID', payload: '' })
await getLogbookEntries();
} catch (error) {
const axiosError = error as AxiosError;
@@ -454,8 +446,8 @@ const Logbook: React.FC<unknown> = () => {
const onConfirmationDialogCancel = () => {
dispatch({
type: 'SET_DELETE',
payload: { isConfirmationDialogOpen: false, selectedLogId: undefined }
type: 'SET_IS_CONFIRMATION_DIALOG_OPEN',
payload: false
});
};
@@ -523,7 +515,7 @@ const Logbook: React.FC<unknown> = () => {
}
</div>
{!state.isLoading && state.alert && (
<div>
<div className='col-span-12 mb-5'>
<Alert
onClose={() =>
dispatch({ type: 'SET_ALERT', payload: undefined })

View File

@@ -34,7 +34,6 @@ const date: ColumnDef<LogbookEntry> = {
header: 'Date',
cell: (info: CellContext<LogbookEntry, unknown>) => {
const date = new Date(info.getValue() as string);
console.log(date)
const formattedDate = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`
return formattedDate;

View File

@@ -6,13 +6,7 @@ import { LogbookState } from './LogbookState.interface';
type Action =
| { type: 'SET_COLUMNS'; payload: ColumnDef<LogbookEntry>[] }
| {
type: 'SET_DELETE';
payload: {
isConfirmationDialogOpen: boolean;
selectedLogId: string | undefined;
};
}
| { type: 'SET_IS_CONFIRMATION_DIALOG_OPEN'; payload: boolean }
| { type: 'SET_ENTRIES'; payload: LogbookEntry[] }
| { type: 'SET_ALERT'; payload: Alert | undefined }
| { type: 'SET_IS_CONFIRMATION_DIALOG_LOADING'; payload: boolean }
@@ -39,10 +33,10 @@ export const reducer = (
columns: action.payload
}
}
case 'SET_DELETE': {
case 'SET_IS_CONFIRMATION_DIALOG_OPEN': {
return {
...state,
isConfirmDialogOpen: action.payload.isConfirmationDialogOpen,
isConfirmDialogOpen: action.payload,
};
}
case 'SET_ENTRIES': {

View File

@@ -1,86 +1,72 @@
import { Card, CardBody, CardContent, CardHeader } from "@noahspan/noahspan-components";
import { Accordion, AccordionItem, Card, CardBody, CardHeader } from '@heroui/react'
import { LogbookCardProps } from "./LogbookCardProps.interface";
import ActionMenu from "../actionMenu/ActionMenu";
import LogTrackMaps from "../logTrackMaps/LogTrackMaps";
import TrackMap from "../trackMap/TrackMap";
const LogbookCard = ({ logs, mode, onDelete, onOpenCloseForm }: LogbookCardProps) => {
return (
<div>
{logs.map((log) => {
console.log(log)
const date = new Date(log.date);
const formattedDate: string = `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}`;
return (
<div>
<Card key={log.id}>
<CardBody>
<CardHeader><ActionMenu id={log.id} onDelete={onDelete!} onOpenCloseForm={onOpenCloseForm!} /></CardHeader>
<CardContent>
<div>
{mode === 'flights' && log.tracks && log.tracks.length > 0 &&
<div>
<LogTrackMaps
logId={log.id}
tracks={log.tracks}
/>
</div>
}
<div>
<span>Aircraft Make and Model</span>
</div>
<div>
<span>{log.aircraftMakeModel}</span>
</div>
<div>
<span>Route From</span>
</div>
<div>
<span>{log.routeFrom}</span>
</div>
<div>
<span>Route To</span>
</div>
<div>
<span>{log.routeTo}</span>
</div>
<div>
<span>Duration Of Flight</span>
</div>
<div>
<span>{log.durationOfFlight}</span>
</div>
{mode === 'logbook' && log.tracks && log.tracks.length > 0 &&
<>
<div>
<span>Tracks</span>
</div>
{log.tracks.map((track: { id: string; order: number; url: string}) => {
const trackSplit = track.url.split('/')
const filename = trackSplit[trackSplit.length - 1];
return (
<div>
<span>{filename}</span>
</div>
)
})}
</>
}
{log.notes &&
<>
<div>
<span>Notes</span>
</div>
<div>
<span>{log.notes}</span>
</div>
</>
}
<Card className='p-4' key={log.id}>
<CardHeader>
<h2 className='font-bold text-2xl'>{formattedDate}</h2>
</CardHeader>
<CardBody>
{mode === 'flights' && log.tracks && log.tracks.length > 0 &&
<div className='mb-5'>
<TrackMap
height='400px'
logId={log.id}
tracks={log.tracks}
/>
</div>
</CardContent>
}
<Accordion variant='bordered'>
<AccordionItem key='1' title='Details'>
<div className='grid grid-cols-12 gap-3 mr-[30%] ml-[30%] mt-4 mb-4'>
<div className='col-span-6 font-bold'>
<span>Aircraft Make and Model</span>
</div>
<div className='col-span-6'>
<span>{log.aircraftMakeModel}</span>
</div>
<div className='col-span-6 font-bold'>
<span>Route From</span>
</div>
<div className='col-span-6'>
<span>{log.routeFrom}</span>
</div>
<div className='col-span-6 font-bold'>
<span>Route To</span>
</div>
<div className='col-span-6'>
<span>{log.routeTo}</span>
</div>
<div className='col-span-6 font-bold'>
<span>Duration Of Flight</span>
</div>
<div className='col-span-6'>
<span>{log.durationOfFlight}</span>
</div>
{log.notes &&
<>
<div className='col-span-6 font-bold'>
<span>Notes</span>
</div>
<div className='col-span-6'>
<span>{log.notes}</span>
</div>
</>
}
</div>
</AccordionItem>
</Accordion>
</CardBody>
</Card>
</div>

View File

@@ -9,8 +9,10 @@ import { FormMode } from "../../enums/formMode";
import httpClient from "../../httpClient/httpClient";
import { AxiosError } from "axios";
import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext";
import { Key, useState } from "react";
const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
const [activeTab, setActiveTab] = useState<Key>('time');
const defaultValues = {
pilotId: '',
date: null,
@@ -34,8 +36,7 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
instrumentApproaches: null,
instrumentHolds: null,
instrumentNavTrack: null,
notes: '',
tracks: []
notes: ''
};
const methods = useForm();
const logbookContext = useLogbookContext()
@@ -69,9 +70,19 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
}
};
const onSelectedKeyChanged = (key: React.Key) => {
setActiveTab(key)
}
return (
<Drawer
closeButton={
<Button isIconOnly>
<FontAwesomeIcon icon={faXmark} />
</Button>
}
isOpen={logbookContext.state.isDrawerOpen}
onClose={onCancel}
>
<DrawerContent>
<FormProvider {...methods}>
@@ -91,7 +102,13 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
/>
</div>
)}
<Tabs color='default' fullWidth={true} variant='solid'>
<Tabs
color='default'
fullWidth={true}
onSelectionChange={onSelectedKeyChanged}
selectedKey={activeTab as string}
variant='solid'
>
<Tab
key='time'
title={
@@ -103,47 +120,51 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
>
<LogForm />
</Tab>
<Tab
key='tracks'
title={
<div className="flex items-center space-x-2">
<FontAwesomeIcon icon={faMapLocationDot} />
<span>Tracks</span>
</div>
}
>
<TracksForm />
</Tab>
{logbookContext.state.formMode !== FormMode.ADD &&
<Tab
key='tracks'
title={
<div className="flex items-center space-x-2">
<FontAwesomeIcon icon={faMapLocationDot} />
<span>Tracks</span>
</div>
}
>
<TracksForm />
</Tab>
}
</Tabs>
</DrawerBody>
<DrawerFooter>
<div className='grid grid-cols-12 gap-3'>
<div className='col-span-12 justify-self-end self-center'>
<Button
disabled={
logbookContext.state.isFormDisabled && logbookContext.state.formMode.toString() !== FormMode.VIEW
? logbookContext.state.isFormDisabled
: false
}
startContent={<FontAwesomeIcon icon={faXmark} />}
onPress={onCancel}
>
{logbookContext.state.formMode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
</Button>
{logbookContext.state.formMode.toString() !== FormMode.VIEW && (
{activeTab !== 'tracks' &&
<DrawerFooter>
<div className='grid grid-cols-12 gap-3'>
<div className='col-span-12 justify-self-end self-center'>
<Button
className='ml-[10px]'
color='primary'
disabled={logbookContext.state.isFormDisabled}
startContent={<FontAwesomeIcon icon={faSave} />}
type="submit"
disabled={
logbookContext.state.isFormDisabled && logbookContext.state.formMode.toString() !== FormMode.VIEW
? logbookContext.state.isFormDisabled
: false
}
startContent={<FontAwesomeIcon icon={faXmark} />}
onPress={onCancel}
>
Save
{logbookContext.state.formMode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
</Button>
)}
{logbookContext.state.formMode.toString() !== FormMode.VIEW && (
<Button
className='ml-[10px]'
color='primary'
disabled={logbookContext.state.isFormDisabled}
startContent={<FontAwesomeIcon icon={faSave} />}
type="submit"
>
Save
</Button>
)}
</div>
</div>
</div>
</DrawerFooter>
</DrawerFooter>
}
</form>
</FormProvider>
</DrawerContent>

View File

@@ -1,15 +1,5 @@
import { useEffect, useState } from 'react';
import { Key, useEffect, useState } from 'react';
import { useForm, Controller, FormProvider } from 'react-hook-form';
import {
// Button,
// Drawer,
Icon,
IconButton,
IconName,
// Input,
PeoplePicker,
StateSelect
} from '@noahspan/noahspan-components';
import { IPilotFormProps } from './IPilotFormProps';
import { AxiosError, AxiosResponse } from 'axios';
import { FormMode } from '../../enums/formMode';
@@ -18,11 +8,11 @@ import PilotFormCertificates from '../pilotFormCertificates/PilotFormCertificate
import PilotFormEndorsements from '../pilotFormEndorsements/PilotFormEndorsements';
import PilotFormMedical from '../pilotFormMedical/PilotFormMedical';
import { useOidc } from '../../auth/oidcConfig';
import { getOidc } from '../../auth/oidcConfig';
import httpClient from '../../httpClient/httpClient';
import { Button, Drawer, DrawerHeader, DrawerContent, DrawerBody, DrawerFooter, Input } from '@heroui/react'
import { Button, Drawer, DrawerHeader, DrawerContent, DrawerBody, DrawerFooter, Input, Autocomplete, AutocompleteItem, SelectItem, Select } from '@heroui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faXmark } from '@fortawesome/free-solid-svg-icons'
import { faSave, faXmark } from '@fortawesome/free-solid-svg-icons';
import { states } from './states';
const PilotForm: React.FC<IPilotFormProps> = ({
pilotId,
@@ -31,7 +21,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
onOpenClose
}: IPilotFormProps) => {
const [peoplePickerValue, setPeoplePickerValue] = useState<string>('');
const [peoplePickerResults, setPeoplePickerResults] = useState<any[]>([]);
const [peoplePickerResults, setPeoplePickerResults] = useState<Person[]>([]);
const [isPeoplePickerLoading, setIsPeoplePickerLoading] =
useState<boolean>(false);
const [selectedPerson, setSelectedPerson] = useState<Person>({
@@ -47,6 +37,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
postalCode: '',
email: '',
phone: '',
userId: ''
};
const methods = useForm({
defaultValues: defaultValues
@@ -63,15 +54,10 @@ const PilotForm: React.FC<IPilotFormProps> = ({
try {
if (value !== '') {
const searchString: string = value;
const oidc = await getOidc();
const response: AxiosResponse = await httpClient.get(
`api/msgraph/search?search=${searchString}`, {
headers: {
Authorization: oidc.isUserLoggedIn ? `Bearer ${(await oidc.getTokens()).accessToken}` : ''
}
}
`api/msgraph/search?search=${searchString}`
);
console.log(response)
setPeoplePickerResults(response.data);
} else {
setPeoplePickerResults([]);
@@ -83,9 +69,12 @@ const PilotForm: React.FC<IPilotFormProps> = ({
}
};
const onPersonSelected = (person: Person) => {
methods.setValue('name', person.displayName!.toString());
setPeoplePickerValue(person.displayName!);
const onPersonSelected = (userPrincipalName: string) => {
const person: Person | undefined = peoplePickerResults.find((person) => person.userPrincipalName === userPrincipalName as string);
methods.setValue('name', person?.displayName!);
methods.setValue('userId', person?.userPrincipalName!);
setPeoplePickerValue(person?.displayName!);
setPeoplePickerResults([])
};
@@ -134,9 +123,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
);
const pilot = response.data;
setSelectedPerson({
displayName: pilot.name
});
setPeoplePickerValue(pilot.name);
methods.reset(pilot);
} catch (error) {
console.log(error);
@@ -175,15 +162,19 @@ const PilotForm: React.FC<IPilotFormProps> = ({
<h6>Name *</h6>
</div>
<div className='col-span-9'>
<PeoplePicker
disabled={isDisabled}
// loading={isPeoplePickerLoading}
onInputChanged={onPeoplePickerSearch}
onPersonSelected={onPersonSelected}
people={peoplePickerResults}
value={peoplePickerValue}
width='w-full'
/>
<Autocomplete
inputValue={peoplePickerValue}
isLoading={isPeoplePickerLoading}
items={peoplePickerResults}
onInputChange={onPeoplePickerSearch}
onSelectionChange={(key: Key | null) => onPersonSelected(key as string)}
>
{peoplePickerResults.map((person: Person) => (
<AutocompleteItem key={person.userPrincipalName}>
{person.displayName}
</AutocompleteItem>
))}
</Autocomplete>
</div>
{isUserLoggedIn &&
<>
@@ -244,19 +235,14 @@ const PilotForm: React.FC<IPilotFormProps> = ({
control={methods.control}
rules={{ required: 'A state must be selected' }}
render={({ field: { onChange, value } }) => (
<StateSelect
disabled={isDisabled}
// error={methods.formState.errors.state ? true : false}
// helperText={
// methods.formState.errors.state
// ? methods.formState.errors.state.message?.toString()
// : undefined
// }
<Select
onChange={onChange}
value={value}
width='w-full'
data-testid="pilot-form-state-dropdown"
/>
selectedKeys={[value]}
>
{states.map((state) => (
<SelectItem key={state.value}>{state.label}</SelectItem>
))}
</Select>
)}
/>
</div>
@@ -369,7 +355,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
? isDisabled
: false
}
startContent={<Icon iconName={IconName.XMARK} />}
startContent={<FontAwesomeIcon icon={faXmark} />}
onPress={onCancel}
data-testid="pilot-cancel-button"
>
@@ -379,7 +365,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
<Button
color='primary'
disabled={isDisabled}
startContent={<Icon iconName={IconName.SAVE} />}
startContent={<FontAwesomeIcon icon={faSave} />}
type="submit"
data-testid="pilot-save-button"
>

View File

@@ -0,0 +1,198 @@
export const states: { label: string; value: string }[] = [
{
label: 'Alabama',
value: 'Alabama'
},
{
label: 'Alaska',
value: 'Alaska'
},
{
label: 'Arizona',
value: 'Arizona'
},
{
label: 'Arkansas',
value: 'Arkansas'
},
{
label: 'California',
value: 'California'
},
{
label: 'Colorado',
value: 'Colorado'
},
{
label: 'Connecticut',
value: 'Connecticut'
},
{
label: 'Delaware',
value: 'Deleware'
},
{
label: 'Florida',
value: 'Florida'
},
{
label: 'Georgia',
value: 'Georgia'
},
{
label: 'Hawaii',
value: 'Hawaii'
},
{
label: 'Idaho',
value: 'Idaho'
},
{
label: 'Illinois',
value: 'Illinois'
},
{
label: 'Indiana',
value: 'Indiana'
},
{
label: 'Kansas',
value: 'Kansas'
},
{
label: 'Kentucky',
value: 'Kentucky'
},
{
label: 'Louisiana',
value: 'Louisiana'
},
{
label: 'Maine',
value: 'Maine'
},
{
label: 'Maryland',
value: 'Maryland'
},
{
label: 'Massachusetts',
value: 'Massachusetts'
},
{
label: 'Michigan',
value: 'Michigan'
},
{
label: 'Minnesota',
value: 'Minnesota'
},
{
label: 'Mississippi',
value: 'Mississippi'
},
{
label: 'Missouri',
value: 'Missouri'
},
{
label: 'Montana',
value: 'Montana'
},
{
label: 'Nebraska',
value: 'Nebraska'
},
{
label: 'Nevada',
value: 'Nevada'
},
{
label: 'New Hampshire',
value: 'New Hampshire'
},
{
label: 'New Jersey',
value: 'New Jersey'
},
{
label: 'New Mexico',
value: 'New Mexico'
},
{
label: 'New York',
value: 'New York'
},
{
label: 'North Carolina',
value: 'North Carolina'
},
{
label: 'North Dakota',
value: 'North Dakota'
},
{
label: 'Ohio',
value: 'Ohio'
},
{
label: 'Oklahoma',
value: 'Oklahoma'
},
{
label: 'Oregon',
value: 'Oregon'
},
{
label: 'Pennsylvania',
value: 'Pennsylvania'
},
{
label: 'Rhode Island',
value: 'Rhode Island'
},
{
label: 'South Carolina',
value: 'South Carolina'
},
{
label: 'South Dakota',
value: 'South Dakota'
},
{
label: 'Tennessee',
value: 'Tennessee'
},
{
label: 'Texas',
value: 'Texas'
},
{
label: 'Utah',
value: 'Utah'
},
{
label: 'Vermont',
value: 'Vermont'
},
{
label: 'Virginia',
value: 'Virginia'
},
{
label: 'Washington',
value: 'Washington'
},
{
label: 'West Virginia',
value: 'West Virginia'
},
{
label: 'Wisconsin',
value: 'Wisconsin'
},
{
label: 'Wyoming',
value: 'Wyoming'
}
];

View File

@@ -1,23 +1,10 @@
import { useEffect, useReducer, useState } from 'react';
import { useEffect, useReducer } from 'react';
import PilotForm from '../pilotForm/PilotForm';
import {
// Alert,
// Button,
// ColumnDef,
// Dropdown,
Icon,
IconButton,
IconName,
// Table,
} from '@noahspan/noahspan-components';
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import { AxiosError, AxiosResponse } from 'axios';
import { FormMode } from '../../enums/formMode';
import { Pilot } from './Pilot.interface';
import { initialState, reducer } from './reducer';
import ActionMenu from '../actionMenu/ActionMenu';
import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog';
import PilotCard from '../pilotCard/PilotCard';
import { getOidc, useOidc } from '../../auth/oidcConfig';
import { useUserRole } from '../../hooks/userRole/UseUserRole';
import { UserRole } from '../../enums/userRole';
import httpClient from '../../httpClient/httpClient'
@@ -25,36 +12,31 @@ import { ScreenSize } from '../../enums/screenSize';
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
import { Alert, Button, Dropdown, DropdownItem, DropdownSection, Table, TableHeader, TableBody, TableColumn, TableRow, TableCell, DropdownTrigger, DropdownMenu } from '@heroui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faEllipsisVertical, faPen, faEye, faTrash } from '@fortawesome/free-solid-svg-icons'
import { faEllipsisVertical, faPen, faEye, faTrash, faPlus } from '@fortawesome/free-solid-svg-icons'
const Pilots: React.FC<unknown> = () => {
const [state, dispatch] = useReducer(reducer, initialState);
// const { httpClient } = useHttpClient();
const { userRole } = useUserRole();
const { screenSize } = useBreakpoints()
const getPilots = async () => {
try {
// const oidc = await getOidc();
let response: AxiosResponse;
// if (oidc.isUserLoggedIn) {
// const { accessToken } = await oidc.getTokens();
console.log('blah')
response = await httpClient.get(
`api/pilots`
);
console.log(response);
if (response.data.length > 0) {
dispatch({ type: 'SET_PILOTS', payload: response.data });
response = await httpClient.get(
`api/pilots`
);
if (state.alert) {
dispatch({ type: 'SET_ALERT', payload: undefined })
}
} else {
dispatch({ type: 'SET_ALERT', payload: { severity: 'default', message: 'No pilots found.' }})
if (response.data.length > 0) {
dispatch({ type: 'SET_PILOTS', payload: response.data });
if (state.alert) {
dispatch({ type: 'SET_ALERT', payload: undefined })
}
// }
} else {
dispatch({ type: 'SET_ALERT', payload: { severity: 'default', message: 'No pilots found.' }})
dispatch({ type: 'SET_PILOTS', payload: [] });
}
} catch (error) {
const axiosError = error as AxiosError;
@@ -68,6 +50,7 @@ const Pilots: React.FC<unknown> = () => {
};
const onOpenClosePilotForm = async (mode: FormMode, pilotId?: string) => {
console.log(pilotId)
switch (mode) {
case FormMode.ADD:
case FormMode.EDIT:
@@ -96,7 +79,7 @@ const Pilots: React.FC<unknown> = () => {
}
};
const onDeleteEntry = (pilotId: string) => {
const onDeletePilot = (pilotId: string) => {
dispatch({
type: 'SET_DELETE',
payload: { isConfirmDialogOpen: true, selectedPilotId: pilotId }
@@ -107,7 +90,7 @@ const Pilots: React.FC<unknown> = () => {
try {
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: true });
await httpClient.delete(`api/pilots/pilot/${state.selectedPilotId}`);
await httpClient.delete(`api/pilots/${state.selectedPilotId}`);
dispatch({
type: 'SET_DELETE',
@@ -144,40 +127,9 @@ const Pilots: React.FC<unknown> = () => {
}
]
// const columns: ColumnDef<Pilot>[] = [
// {
// accessorKey: 'name',
// header: 'Name'
// },
// {
// header: 'Actions',
// cell: (info: any) => {
// return (
// <Dropdown
// options={[
// 'Edit',
// 'View',
// 'Delete'
// ]}
// onOptionSelected={() => console.log(info.row.original.id)}
// >
// <IconButton>
// <Icon className='text-xl' iconName={IconName.ELLIPSIS_VERTICAL} />
// </IconButton>
// </Dropdown>
// )
// // return <ActionMenu
// // id={info.row.original.id}
// // onDelete={onDeleteEntry}
// // onOpenCloseForm={onOpenClosePilotForm}
// // />
// }
// }
// ];
const renderCell = (pilot: any, columnKey: any) => {
const cellValue = pilot[columnKey]
console.log(cellValue)
switch (columnKey) {
case 'actions': {
return (
@@ -191,14 +143,14 @@ const Pilots: React.FC<unknown> = () => {
<DropdownSection showDivider>
<DropdownItem
key='edit'
onPress={() => onOpenClosePilotForm(FormMode.EDIT)}
onPress={() => onOpenClosePilotForm(FormMode.EDIT, pilot.id)}
startContent={<FontAwesomeIcon icon={faPen} />}
>
Edit
</DropdownItem>
<DropdownItem
key='view'
onPress={() => onOpenClosePilotForm(FormMode.VIEW)}
onPress={() => onOpenClosePilotForm(FormMode.VIEW, pilot.id)}
startContent={<FontAwesomeIcon icon={faEye} />}
>
View
@@ -207,6 +159,7 @@ const Pilots: React.FC<unknown> = () => {
<DropdownSection>
<DropdownItem
key='Delete'
onPress={() => onDeletePilot(pilot.id)}
startContent={<FontAwesomeIcon icon={faTrash} />}
>
Delete
@@ -238,8 +191,8 @@ const Pilots: React.FC<unknown> = () => {
{userRole === UserRole.WRITE &&
<Button
color='primary'
onClick={() => onOpenClosePilotForm(FormMode.ADD)}
startContent={<Icon iconName={IconName.PLUS} />}
onPress={() => onOpenClosePilotForm(FormMode.ADD)}
startContent={<FontAwesomeIcon icon={faPlus} />}
data-testid="pilot-add-button"
>
Add Pilot
@@ -247,7 +200,7 @@ const Pilots: React.FC<unknown> = () => {
}
</div>
{!state.isLoading && state.alert && (
<div>
<div className='col-span-12'>
<Alert
onClose={() =>
dispatch({ type: 'SET_ALERT', payload: undefined })
@@ -259,7 +212,6 @@ const Pilots: React.FC<unknown> = () => {
)}
<div className='col-span-12'>
{state.pilots.length > 0 && screenSize !== ScreenSize.SM &&
// <Table columns={columns} data={state.pilots} />
<Table>
<TableHeader columns={columns}>
{(column) => (
@@ -285,7 +237,7 @@ const Pilots: React.FC<unknown> = () => {
</Table>
}
{state.pilots.length > 0 && screenSize === ScreenSize.SM &&
<PilotCard pilots={state.pilots} onDelete={onDeleteEntry} onOpenCloseForm={onOpenClosePilotForm} />
<PilotCard pilots={state.pilots} onDelete={onDeletePilot} onOpenCloseForm={onOpenClosePilotForm} />
}
</div>
</div>
@@ -299,7 +251,7 @@ const Pilots: React.FC<unknown> = () => {
)}
{state.isConfirmDialogOpen && (
<ConfirmationDialog
contentText="Are you sure you want to delete the pilot entry? Deleting a pilot will delete the pilot and delete all logbook entries for the pilot."
contentText="Are you sure you want to delete the pilot entry? Deleting a pilot will delete the pilot and delete all of the pilot's logbook entries."
isLoading={state.isConfirmDialogLoading}
isOpen={state.isConfirmDialogOpen}
onCancel={onConfirmationDialogCancel}

View File

@@ -1,60 +1,36 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAppContext } from '../../hooks/appContext/UseAppContext';
import { AxiosResponse } from 'axios';
import { User } from '@microsoft/microsoft-graph-types';
import { getOidc, useOidc } from '../../auth/oidcConfig';
import { Button, Link, Navbar, NavbarBrand, NavbarContent, NavbarItem, NavbarMenuToggle, NavbarMenu, NavbarMenuItem } from '@heroui/react';
import { useOidc } from '../../auth/oidcConfig';
import { Avatar, Button, Link, Navbar, NavbarBrand, NavbarContent, NavbarItem, DropdownTrigger, DropdownMenu, DropdownItem, Dropdown } from '@heroui/react';
import httpClient from '../../httpClient/httpClient'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faPlane } from '@fortawesome/free-solid-svg-icons'
import { faPlane, faSignIn, faSignOut } from '@fortawesome/free-solid-svg-icons'
import { useLocation } from 'react-router-dom';
const SiteNav = () => {
const [loading, setLoading] = useState<boolean>(false);
const [userPhoto, setUserPhoto] = useState<string>();
const [pages, setPages] = useState<{ name: string; path: string; }[]>([]);
const appContext = useAppContext();
const { isUserLoggedIn, login, logout } = useOidc()
const navigate = useNavigate();
const { isUserLoggedIn, logout, login } = useOidc()
const { pathname } = useLocation()
const getPages = () => {
const pages = [
{
name: 'Flights',
path: '/'
},
{
name: 'Logbook',
path: '/logbook'
},
{
name: 'Pilots',
path: '/pilots'
}
];
setPages(pages)
};
const handleSignIn = () => {
// auth.signinRedirect();
// auth.signinRedirect({
// scope: `api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`
// })
login();
// console.log(auth.user?.access_token)
};
const handleSignOut = () => {
// auth.signoutRedirect();
logout()
};
const pages = [
{
name: 'Flights',
path: '/'
},
{
name: 'Logbook',
path: '/logbook'
},
{
name: 'Pilots',
path: '/pilots'
}
];
const getUserProfile = async (): Promise<User> => {
try {
const response: AxiosResponse = await httpClient.get(`api/user/profile`, {
// headers: {
// Authorization: `Bearer ${accessToken}`
// }
});
const response: AxiosResponse = await httpClient.get(`api/msgraph/profile`);
const userProfile: User = response.data;
return userProfile;
@@ -64,10 +40,7 @@ const SiteNav = () => {
};
const getUserPhoto = async (): Promise<string> => {
try {
const response: AxiosResponse = await httpClient.get(`api/user/photo`, {
// headers: {
// Authorization: accessToken
// },
const response: AxiosResponse = await httpClient.get(`api/msgraph/photo`, {
responseType: 'arraybuffer'
});
const arrayBufferView = new Uint8Array(response.data);
@@ -79,82 +52,49 @@ const SiteNav = () => {
throw new Error();
}
};
const handlePageClick = (url: string) => {
navigate(url);
};
// const Settings = () => {
// return (
// <div>
// <Icon iconName={IconName.SIGN_OUT} />
// <span>
// Sign Out
// </span>
// </div>
// );
// };
useEffect(() => {
const setUserProfile = async () => {
try {
setLoading(true);
const userProfile = await getUserProfile();
const userPhoto = await getUserPhoto();
// const userProfile = await getUserProfile();
// const userPhoto = await getUserPhoto();
setUserPhoto(userPhoto);
// setUserPhoto(userPhoto);
// appContext.dispatch({
// type: 'SET_USER_PROFILE',
// payload: userProfile
// });
const oidc = await getOidc();
if (oidc.isUserLoggedIn) {
console.log((await oidc.getTokens()).accessToken)
}
appContext.dispatch({
type: 'SET_USER_PROFILE',
payload: userProfile
});
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
}
};
if (
isUserLoggedIn &&
Object.keys(appContext.state.userProfile).length === 0
) {
console.log(isUserLoggedIn)
console.log()
setUserProfile();
getPages();
}
}, [isUserLoggedIn]);
useEffect(() => {
getPages();
}, [])
useEffect(() => {
console.log(pathname)
}, [pathname])
return (
<Navbar isBordered maxWidth='full' position='static'>
<NavbarBrand>
<img
height={35}
width={35}
src='noahspan-logo.png'
style={{ marginRight: '5px' }}
/>
<FontAwesomeIcon icon={faPlane} size='2x' />
</NavbarBrand>
<NavbarContent>
<NavbarBrand>
<img
height={35}
width={35}
src='noahspan-logo.png'
style={{ marginRight: '5px' }}
/>
<FontAwesomeIcon icon={faPlane} size='2x' />
</NavbarBrand>
</NavbarContent>
<NavbarContent justify='center'>
{pages.length > 0 && pages.map((page) => {
{pages.length > 0 && pages.map((page, index) => {
return (
<NavbarItem isActive={pathname === page.path ? true : false}>
<NavbarItem isActive={pathname === page.path ? true : false} key={index}>
<Link color={pathname === page.path ? 'primary' : 'foreground'} href={page.path}>
{page.name}
</Link>
@@ -163,9 +103,27 @@ const SiteNav = () => {
})}
</NavbarContent>
<NavbarContent justify='end'>
<Button color='default' onClick={handleSignIn} variant='flat'>
Sign In
</Button>
{!isUserLoggedIn &&
<Button
color='default'
onPress={() => login()}
startContent={<FontAwesomeIcon icon={faSignIn} />}
>
Sign In
</Button>
}
{isUserLoggedIn &&
<Dropdown>
<DropdownTrigger>
<Avatar name={appContext.state.userProfile.displayName?.toString()} src={userPhoto}></Avatar>
</DropdownTrigger>
<DropdownMenu>
<DropdownItem key='signout' onPress={() => logout({redirectTo: 'specific url', url: '/'})} startContent={<FontAwesomeIcon icon={faSignOut} />}>
Sign Out
</DropdownItem>
</DropdownMenu>
</Dropdown>
}
</NavbarContent>
</Navbar>
);

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { LogTrackMapsProps } from './LogTrackMapsProps.interface';
import { Suspense, useEffect, useState } from 'react';
import { TrackMapProps } from './TrackMapProps.interface';
import { AxiosInstance, AxiosResponse } from 'axios';
import { useAuth } from 'react-oidc-context'
import { MapContainer, TileLayer } from 'react-leaflet';
@@ -7,16 +7,15 @@ import ReactLeafletKml from 'react-leaflet-kml';
import 'swiper/css';
import 'swiper/css/pagination';
import 'swiper/css';
import './LogTrackMaps.css';
import './TrackMap.css';
import 'leaflet/dist/leaflet.css';
import httpClient from '../../httpClient/httpClient'
const LogTrackMaps = ({ logId, tracks }: LogTrackMapsProps) => {
const TrackMap = ({ height, logId, tracks }: TrackMapProps) => {
const [kmls, setKmls] = useState<any[]>([])
const auth = useAuth()
const auth = useAuth();
useEffect(() => {
console.log(tracks)
const getTracks = async () => {
const convertedTracks: any[] = []
@@ -24,12 +23,7 @@ const LogTrackMaps = ({ logId, tracks }: LogTrackMapsProps) => {
const trackUrlSplit = track.url.split('/')
const filename = trackUrlSplit[trackUrlSplit.length - 1];
const response: AxiosResponse = await httpClient.get(
`api/tracks/${logId}/${filename}`,
{
headers: {
Authorization: auth.user?.access_token
}
}
`api/tracks/${logId}/${filename}`
);
const kml = new DOMParser().parseFromString(response.data, 'text/xml')
@@ -40,24 +34,26 @@ const LogTrackMaps = ({ logId, tracks }: LogTrackMapsProps) => {
}
getTracks();
}, [tracks])
}, [])
return (
<MapContainer
center={[45.14489, -93.21019]}
scrollWheelZoom={false}
style={{ height: '500px', width: '100%' }}
style={{ height: height, width: '100%' }}
zoom={8}
>
<TileLayer
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{kmls.length > 0 && kmls.map((kml) => (
<ReactLeafletKml kml={kml} />
))}
<Suspense>
<TileLayer
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{kmls.length > 0 && kmls.map((kml) => (
<ReactLeafletKml kml={kml} />
))}
</Suspense>
</MapContainer>
);
}
export default LogTrackMaps;
export default TrackMap;

View File

@@ -1,4 +1,5 @@
export interface LogTrackMapsProps {
export interface TrackMapProps {
height: string;
logId: string;
tracks: {id: string; order: number; url: string}[];
}

View File

@@ -1,31 +1,44 @@
import { useEffect, useReducer } from "react";
// import { Button, Drawer, Icon, IconButton, IconName, Input, Loading } from "@noahspan/noahspan-components";
import { AxiosError, AxiosInstance, AxiosResponse } from "axios";
import { useEffect, useReducer, useRef } from "react";
import { AxiosError, AxiosResponse } from "axios";
import { TracksFormProps } from "./TracksFormProps.interface";
import { FormMode } from "../../enums/formMode";
import { LogbookEntry } from "../logbook/LogbookEntry.interface";
import ConfirmationDialog from "../confirmationDialog/ConfirmationDialog";
import { initialState, reducer } from "./reducer";
import LogTrackMaps from "../logTrackMaps/LogTrackMaps";
import TrackMap from "../trackMap/TrackMap";
import httpClient from "../../httpClient/httpClient";
import { Button, Drawer, DrawerContent, DrawerBody, DrawerHeader, Input, Spinner, DrawerFooter } from '@heroui/react';
import { Button, Input, Spinner } from '@heroui/react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUpload, faTrash, faXmark } from '@fortawesome/free-solid-svg-icons';
import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext";
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
const TracksForm = () => {
const [state, dispatch] = useReducer(reducer, initialState)
const logbookContext = useLogbookContext();
const getTracks = async () => {
try {
const response: AxiosResponse = await httpClient.get(
`api/tracks/${logbookContext.state.selectedLogId}`
)
const tracks = response.data;
dispatch({ type: 'SET_TRACKS', payload: tracks })
} catch (error) {
const axiosError = error as AxiosError;
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: { severity: 'danger', message: axiosError.message }})
}
}
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
console.log('blah')
try {
dispatch({ type: 'SET_IS_LOADING', payload: true})
const file = event.target.files![0]
const formData = new FormData();
const order = state.tracks.length + 1
formData.append('file', file);
@@ -33,9 +46,10 @@ const TracksForm = () => {
config.headers["Content-Type"] = 'multipart/form-data'
return config
})
});
const uploadResponse: AxiosResponse = await httpClient.post(`api/tracks/${logbookContext.state.selectedLogId}/1`, formData);
await httpClient.post(`api/tracks/${logbookContext.state.selectedLogId}/${order}`, formData);
await getTracks();
// const uploadUrl = uploadResponse.data.url;
// const tracks: string[] = log.tracks ? JSON.parse(log.tracks!) : [];
@@ -53,49 +67,77 @@ const TracksForm = () => {
}
}
// const onDeleteTrack = async (index: number) => {
// dispatch({ type: 'SET_ON_DELETE_TRACK', payload: { isConfirmDialogOpen: true, selectedTrack: { index: index }}})
// }
const onDeleteTrack = async (id: string, filename: string, index: number) => {
dispatch({ type: 'SET_ON_DELETE_TRACK', payload: { isConfirmDialogOpen: true, selectedTrack: { id: id, filename: filename, index: index }}})
}
const onConfirmDialogConfirm = async () => {
try {
await httpClient.delete(`api/tracks/${state.selectedTrack!.id}/${state.selectedTrack!.filename}/${logbookContext.state.selectedLogId}`);
await getTracks();
// tracks.splice(state.selectedTrack!.index, 1);
// log.tracks = JSON.stringify(tracks);
// await httpClient.put(`api/logs/log/${selectedRowKey}`, log, config);
// const updatedLog = await getLog();
// dispatch({ type: 'SET_TRACKS', payload: JSON.parse(updatedLog.tracks!) })
dispatch({ type: 'SET_IS_CONFIRM_DIALOG_OPEN', payload: false })
} catch (error) {
console.log(error);
}
}
const onConfirmDialogCancel = async () => {
dispatch({ type: 'SET_IS_CONFIRM_DIALOG_OPEN', payload: false })
}
useEffect(() => {
const getTracks = async () => {
try {
const response: AxiosResponse = await httpClient.get(
`api/tracks/${logbookContext.state.selectedLogId}`
)
const tracks = response.data;
dispatch({ type: 'SET_TRACKS', payload: tracks })
} catch (error) {
const axiosError = error as AxiosError;
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: { severity: 'danger', message: axiosError.message }})
}
}
if (logbookContext.state.selectedLogId) {
getTracks();
}
}, [logbookContext.state.selectedLogId])
useEffect(() => {
console.log(logbookContext.state.formMode)
if (logbookContext.state.formMode === FormMode.VIEW) {
dispatch({ type: 'SET_IS_DISABLED', payload: true });
}
}, [logbookContext.state.formMode]);
useEffect(() => {
console.log(state.isDisabled)
}, [state.isDisabled])
return (
<div className='grid grid-cols-12 gap-3'>
{state.tracks.length > 0 &&
<div className="col-span-12">
<TrackMap height='400px' logId={logbookContext.state.selectedLogId!} tracks={state.tracks} />
</div>
}
<>
{state.tracks.length > 0 &&
<>
<div className='col-span-10'>
<Input type='text' />
</div>
<div className='col-span-2'>
<Button isIconOnly onPress={() => console.log('delete')}><FontAwesomeIcon icon={faTrash} /></Button>
</div>
</>
}
{state.tracks.length > 0 && state.tracks.map((track, index) => {
const filename: string = track.url.substring(track.url.lastIndexOf('/') + 1);
return (
<>
<div className='col-span-10'>
<Input isDisabled={state.isDisabled} key={index} type='text' value={filename}/>
</div>
<div className='col-span-2'>
<Button isDisabled={state.isDisabled} key={index} isIconOnly onPress={() => onDeleteTrack(track.id, filename, index)}><FontAwesomeIcon icon={faTrash} /></Button>
</div>
</>
)
})}
<div className='col-span-12'>
<Button
as='label'
disabled={state.isLoading ? true : false}
color='primary'
isDisabled={state.isDisabled}
fullWidth={true}
startContent={<FontAwesomeIcon icon={faUpload} />}
>
@@ -103,6 +145,16 @@ const TracksForm = () => {
<input hidden onChange={handleFileUpload} type='file' />
</Button>
</div>
{state.isConfirmDialogOpen && (
<ConfirmationDialog
contentText="Are you sure you want to delete this track?"
isLoading={state.isConfirmDialogLoading}
isOpen={state.isConfirmDialogOpen}
onCancel={onConfirmDialogCancel}
onConfirm={onConfirmDialogConfirm}
title="Confirm Delete"
/>
)}
</>
</div>
);
@@ -136,31 +188,7 @@ const TracksForm = () => {
// dispatch({ type: 'SET_ON_DELETE_TRACK', payload: { isConfirmDialogOpen: true, selectedTrack: { fileName: fileName, index: index }}})
// }
// const onConfirmDialogConfirm = async () => {
// try {
// const config = await getConfig();
// await httpClient.delete(`api/logs/log/${selectedRowKey}/track?fileName=${state.selectedTrack!.fileName}`, config);
// const log = await getLog();
// const tracks: string[] = JSON.parse(log.tracks!);
// tracks.splice(state.selectedTrack!.index, 1);
// log.tracks = JSON.stringify(tracks);
// await httpClient.put(`api/logs/log/${selectedRowKey}`, log, config);
// const updatedLog = await getLog();
// dispatch({ type: 'SET_TRACKS', payload: JSON.parse(updatedLog.tracks!) })
// dispatch({ type: 'SET_IS_CONFIRM_DIALOG_OPEN', payload: false })
// } catch (error) {
// console.log(error);
// }
// }
// const onConfirmDialogCancel = async () => {
// dispatch({ type: 'SET_IS_CONFIRM_DIALOG_OPEN', payload: false })
// }
// useEffect(() => {
// const updateTracks = async () => {
@@ -204,19 +232,8 @@ const TracksForm = () => {
// })}
// </>
// }
{/* {logbookContext.state.formMode === FormMode.VIEW &&
<LogTrackMaps logId={logbookContext.state.selectedLogId!} tracks={state.tracks} />
} */}
{/* {state.isConfirmDialogOpen && (
<ConfirmationDialog
contentText="Are you sure you want to delete this track?"
isLoading={state.isConfirmDialogLoading}
isOpen={state.isConfirmDialogOpen}
onCancel={onConfirmDialogCancel}
onConfirm={onConfirmDialogConfirm}
title="Confirm Delete"
/>
)} */}
// </div>
// )
// }

View File

@@ -1,9 +1,11 @@
export interface TracksFormState {
isConfirmDialogOpen: boolean;
isConfirmDialogLoading: boolean;
isDisabled: boolean;
isLoading: boolean;
selectedTrack: {
fileName: string,
id: string,
filename: string,
index: number
} | undefined;
tracks: { id: string; order: number; url: string; }[];

View File

@@ -3,13 +3,15 @@ import { TracksFormState } from "./TracksFormState.interface";
type Action =
| { type: 'SET_IS_CONFIRM_DIALOG_OPEN'; payload: boolean }
| { type: 'SET_IS_CONFORM_DIALOG_LOADING'; payload: boolean }
| { type: 'SET_IS_DISABLED'; payload: boolean }
| { type: 'SET_IS_LOADING'; payload: boolean }
| { type: 'SET_ON_DELETE_TRACK'; payload: { isConfirmDialogOpen: boolean, selectedTrack: { fileName: string, index: number } }}
| { type: 'SET_ON_DELETE_TRACK'; payload: { isConfirmDialogOpen: boolean, selectedTrack: { id: string, filename: string, index: number } }}
| { type: 'SET_TRACKS'; payload: { id: string; order: number; url: string }[] };
export const initialState: TracksFormState = {
isConfirmDialogOpen: false,
isConfirmDialogLoading: false,
isDisabled: false,
isLoading: false,
selectedTrack: undefined,
tracks: []
@@ -29,6 +31,12 @@ export const reducer = (state: TracksFormState, action: Action): TracksFormState
isConfirmDialogLoading: action.payload
}
}
case 'SET_IS_DISABLED': {
return {
...state,
isDisabled: action.payload
}
}
case 'SET_IS_LOADING': {
return {
...state,

View File

@@ -1,10 +0,0 @@
import { useAppContext } from '../appContext/UseAppContext';
export const useFeatureFlag = (featureFlagKey: string) => {
const appContext = useAppContext();
const featureFlag = appContext.state.featureFlags.find(
(featureFlag) => featureFlag.key === featureFlagKey
);
console.log(featureFlag)
return featureFlag;
};

View File

@@ -23,7 +23,7 @@ export const usePilots = () => {
const response: AxiosResponse = await httpClient.get(
`/api/pilots`
);
console.log(response)
setPilots(response.data);
} catch (error) {
return error;

View File

@@ -7,15 +7,13 @@ import { BrowserRouter } from 'react-router-dom';
import { OidcProvider } from './auth/oidcConfig.ts';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<OidcProvider>
<AppContextProvider>
<LogbookContextProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</LogbookContextProvider>
</AppContextProvider>
</OidcProvider>
</React.StrictMode>
<OidcProvider>
<AppContextProvider>
<LogbookContextProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</LogbookContextProvider>
</AppContextProvider>
</OidcProvider>
);

View File

@@ -5,6 +5,8 @@
@custom-variant dark (&:is(.dark *));
@plugin "@tailwindcss/typography";
/* body {
@apply bg-base-300;
} */
@theme inline {
--color-primary: #000000;
}

Binary file not shown.

View File

@@ -1,7 +1,12 @@
locals {
app_name = {
test = "flying-app-test"
prod = "flying-app-prod"
test = "flying-test"
prod = "flying-prod"
}
container_app_environment_name = {
test = "noahspan-test"
prod = "noahspan-prod"
}
container_image = {

View File

@@ -2,6 +2,10 @@ output "app_name" {
value = local.app_name[var.environment]
}
output "container_app_environment_name" {
value = local.container_app_environment_name[var.environment]
}
output "container_image" {
value = local.container_image[var.environment]
}

View File

@@ -1,11 +0,0 @@
locals {
name = {
test = "flying-test"
prod = "flying-prod"
}
log_analytics_workspace_name = {
test = "flying-log-analytics-workspace-test"
prod = "flying-log-analytics-workspace-prod"
}
}

View File

@@ -1,7 +0,0 @@
output "name" {
value = local.name[var.environment]
}
output "log_analytics_workspace_name" {
value = local.log_analytics_workspace_name[var.environment]
}

View File

@@ -1 +0,0 @@
variable "environment" {}

View File

@@ -1,18 +1,8 @@
module "api" {
source = "./api"
environment = var.environment
}
module "app" {
source = "./app"
environment = var.environment
}
module "container_app_environment" {
source = "./container_app_environment"
environment = var.environment
}
module "storage" {
source = "./storage"
environment = var.environment

View File

@@ -1,15 +1,7 @@
output "api" {
value = module.api
}
output "app" {
value = module.app
}
output "container_app_environment" {
value = module.container_app_environment
}
output "storage" {
value = module.storage
}

View File

@@ -0,0 +1,165 @@
resource "azurerm_container_app" "container_app" {
name = module.environment.app.app_name
container_app_environment_id = data.azurerm_container_app_environment.container_app_environment.id
resource_group_name = data.azurerm_resource_group.resource_group.name
revision_mode = "Single"
template {
min_replicas = module.environment.app.template_min_replicas
max_replicas = module.environment.app.template_max_replicas
init_container {
args = ["restore", "-if-db-not-exists", "-if-replica-exists", "/var/lib/data/flying.db"]
cpu = 0.25
image = "litestream/litestream:0.5.2"
memory = "0.5Gi"
name = "restore"
volume_mounts {
name = "data"
path = "/var/lib/data"
}
volume_mounts {
name = "backup"
path = "/mnt/data"
sub_path = "data"
}
volume_mounts {
name = "backup"
path = "/etc"
sub_path = "litestream"
}
}
container {
args = ["replicate"]
cpu = 0.25
image = "litestream/litestream:0.5.2"
memory = "0.5Gi"
name = "replicate"
volume_mounts {
name = "data"
path = "/var/lib/data"
}
volume_mounts {
name = "backup"
path = "/mnt/data"
sub_path = "data"
}
volume_mounts {
name = "backup"
path = "/etc"
sub_path = "litestream"
}
}
container {
cpu = 0.25
image = module.environment.app.container_image
memory = "0.5Gi"
name = module.environment.app.container_name
env {
name = "AZURE_STORAGE_CONNECTION_STRING"
secret_name = "azure-storage-connection-string"
}
env {
name = "CLIENT_ID"
secret_name = "client-id"
}
env {
name = "CLIENT_SECRET"
secret_name = "client-secret"
}
env {
name = "TENANT_ID"
value = var.TENANT_ID
}
env {
name = "DB_PATH"
value = "/var/lib/data/flying.db"
}
env {
name = "DB_SYNC"
value = "false"
}
startup_probe {
failure_count_threshold = 10
initial_delay = 1
interval_seconds = 2
path = "/api/health"
port = 3000
transport = "HTTP"
}
volume_mounts {
name = "data"
path = "/var/lib/data"
}
}
volume {
name = "backup"
storage_name = azurerm_container_app_environment_storage.container_app_environment_storage_backup.name
storage_type = "AzureFile"
}
volume {
name = "data"
storage_type = "EmptyDir"
}
}
ingress {
allow_insecure_connections = false
external_enabled = module.environment.app.ingress_external_enabled
target_port = module.environment.app.ingress_target_port
transport = module.environment.app.ingress_transport
traffic_weight {
latest_revision = true
percentage = module.environment.app.traffic_weight_percentage
}
}
registry {
server = "docker.io"
username = var.DOCKER_IO_USERNAME
password_secret_name = "docker-io-password"
}
secret {
name = "azure-storage-connection-string"
value = azurerm_storage_account.storage_account.primary_connection_string
}
secret {
name = "client-id"
value = var.CLIENT_ID
}
secret {
name = "client-secret"
value = var.CLIENT_SECRET
}
secret {
name = "docker-io-password"
value = var.DOCKER_IO_PASSWORD
}
lifecycle {
ignore_changes = [ template[0].container[0].image, template[0].container[0].image, template[0].init_container[0].image, registry[0].server ]
}
}

View File

@@ -0,0 +1,8 @@
resource "azurerm_container_app_environment_storage" "container_app_environment_storage_backup" {
name = "${module.environment.app.app_name}-backup"
container_app_environment_id = data.azurerm_container_app_environment.container_app_environment.id
account_name = azurerm_storage_account.storage_account.name
share_name = azurerm_storage_share.backup_storage_share.name
access_key = azurerm_storage_account.storage_account.primary_access_key
access_mode = "ReadOnly"
}

View File

@@ -1,216 +1,10 @@
module "storage" {
source = "github.com/noahspannbauer/noahspan-terraform/modules/storage"
resource_group_name = var.RESOURCE_GROUP_NAME
storage_account_name = module.environment.storage.account_name
storage_containers = module.environment.storage.containers
storage_shares = module.environment.storage.shares
storage_tables = module.environment.storage.tables
data "azurerm_client_config" "current" {}
data "azurerm_resource_group" "resource_group" {
name = var.RESOURCE_GROUP_NAME
}
module "container_app_environment" {
source = "github.com/noahspannbauer/noahspan-terraform/modules/container_app_environment"
container_app_environment_name = module.environment.container_app_environment.name
log_analytics_workspace_name = module.environment.container_app_environment.log_analytics_workspace_name
log_analytics_workspace_retention_in_days = 30
log_analytics_workspace_sku = "PerGB2018"
data "azurerm_container_app_environment" "container_app_environment" {
name = module.environment.app.container_app_environment_name
resource_group_name = var.RESOURCE_GROUP_NAME
}
module "container_app_environment_storage" {
source = "github.com/noahspannbauer/noahspan-terraform/modules/container_app_environment_storage"
name = "${module.container_app_environment.name}-files"
container_app_environment_id = module.container_app_environment.id
account_name = module.storage.name
share_name = module.environment.storage.shares[0]
access_key = module.storage.primary_access_key
access_mode = "ReadWrite"
}
module "api" {
source = "github.com/noahspannbauer/noahspan-terraform/modules/container_app"
containers = [
{
cpu = 0.25
envs = [
{
name = "AZURE_STORAGE_CONNECTION_STRING"
secret_name = "azure-storage-connection-string"
},
{
name = "CLIENT_ID"
secret_name = "client-id"
},
{
name = "CLIENT_SECRET"
secret_name = "client-secret"
},
{
name = "TENANT_ID"
secret_name = "tenant-id"
},
{
name = "DB_PATH"
secret_name = "db-path"
},
{
name = "DB_SYNC"
secret_name = "db-sync"
}
]
image = module.environment.api.container_image
memory = "0.5Gi"
name = module.environment.api.container_name
startup_probe = [
{
failure_count_threshold = 10
initial_delay = 1
interval_seconds = 2
path = "/api/health"
port = 3000
transport = "HTTP"
}
]
volume_mounts = [
{
name = "${module.container_app_environment.name}-emptydir"
path = "/var/lib/data"
}
]
},
{
args = ["replicate"]
cpu = 0.25
image = "docker.io/litestream/litestream:0.3.13"
memory = "0.5Gi"
name = "replicate"
volume_mounts = [
{
name = "${module.container_app_environment.name}-emptydir"
path = "/var/lib/data"
},
{
name = "${module.container_app_environment.name}-fileshare"
path = "/mnt/data"
sub_path = "data"
},
{
name = "${module.container_app_environment.name}-fileshare"
path = "/etc"
sub_path = "litestream"
}
]
}
]
container_app_name = module.environment.api.app_name
container_app_environment_id = module.container_app_environment.id
custom_domain_count = module.environment.api.custom_domain_count
init_containers = [
{
args = ["restore", "-if-db-not-exists", "-if-replica-exists", "/var/lib/data/flying.db"]
cpu = 0.25
image = "docker.io/litestream/litestream:0.3.13"
memory = "0.5Gi"
name = "restore"
volume_mounts = [
{
name = "${module.container_app_environment.name}-emptydir"
path = "/var/lib/data"
},
{
name = "${module.container_app_environment.name}-fileshare"
path = "/mnt/data"
sub_path = "data"
},
{
name = "${module.container_app_environment.name}-fileshare"
path = "/etc"
sub_path = "litestream"
}
]
}
]
ingress_external_enabled = module.environment.api.ingress_external_enabled
ingress_target_port = module.environment.api.ingress_target_port
ingress_transport = module.environment.api.ingress_transport
registry_password = var.DOCKER_IO_PASSWORD
registry_username = var.DOCKER_IO_USERNAME
registry_password_secret_name = "docker-io-password"
registry_server_name = "docker.io"
resource_group_name = var.RESOURCE_GROUP_NAME
secrets = [
{
name = "azure-storage-connection-string"
value = module.storage.primary_connection_string
},
{
name = "docker-io-password"
value = var.DOCKER_IO_PASSWORD
},
{
name = "client-id"
value = var.CLIENT_ID
},
{
name = "client-secret"
value = var.CLIENT_SECRET
},
{
name = "tenant-id"
value = var.TENANT_ID
},
{
name = "db-path"
value = var.DB_PATH
},
{
name = "db-sync"
value = var.DB_SYNC
}
]
storage_account_primary_connection_string = module.storage.primary_connection_string
traffic_weight_percentage = module.environment.api.traffic_weight_percentage
template_min_replicas = module.environment.api.template_min_replicas
template_max_replicas = module.environment.api.template_max_replicas
volume = [
{
name = "${module.container_app_environment.name}-fileshare"
storage_name = module.container_app_environment_storage.name
storage_type = "AzureFile"
},
{
name = "${module.container_app_environment.name}-emptydir"
storage_type = "EmptyDir"
}
]
}
module "app" {
source = "github.com/noahspannbauer/noahspan-terraform/modules/container_app"
containers = [
{
cpu = 0.25
image = module.environment.app.container_image
memory = "0.5Gi"
name = module.environment.app.container_name
}
]
container_app_name = module.environment.app.app_name
container_app_environment_id = module.container_app_environment.id
custom_domain_count = module.environment.app.custom_domain_count
ingress_external_enabled = module.environment.app.ingress_external_enabled
ingress_target_port = module.environment.app.ingress_target_port
registry_password = var.DOCKER_IO_PASSWORD
registry_username = var.DOCKER_IO_USERNAME
registry_password_secret_name = "docker-io-password"
registry_server_name = "docker.io"
resource_group_name = var.RESOURCE_GROUP_NAME
secrets = [
{
name = "docker-io-password"
value = var.DOCKER_IO_PASSWORD
}
]
traffic_weight_percentage = module.environment.app.traffic_weight_percentage
template_max_replicas = module.environment.app.template_max_replicas
template_min_replicas = module.environment.app.template_min_replicas
}

19
infrastructure/storage.tf Normal file
View File

@@ -0,0 +1,19 @@
resource "azurerm_storage_account" "storage_account" {
name = module.environment.storage.account_name
resource_group_name = data.azurerm_resource_group.resource_group.name
location = data.azurerm_resource_group.resource_group.location
account_tier = "Standard"
account_replication_type = "LRS"
}
resource "azurerm_storage_container" "storage_container" {
name = "tracks"
storage_account_id = azurerm_storage_account.storage_account.id
container_access_type = "private"
}
resource "azurerm_storage_share" "backup_storage_share" {
name = "${module.environment.app.app_name}-backup-storage-share"
quota = 50
storage_account_name = azurerm_storage_account.storage_account.name
}

View File

@@ -1,10 +1,3 @@
variable "API_SUBDOMAIN_NAME" {
type = string
}
variable "APP_SUBDOMAIN_NAME" {
type = string
}
variable "CLIENT_ID" {
type = string
@@ -15,18 +8,6 @@ variable "CLIENT_SECRET" {
sensitive = true
}
variable "DB_PATH" {
type = string
}
variable "DB_SYNC" {
type = string
}
variable "DNS_ZONE_RESOURCE_GROUP" {
type = string
}
variable "DOCKER_IO_PASSWORD" {
type = string
sensitive = true
@@ -36,10 +17,6 @@ variable "DOCKER_IO_USERNAME" {
type = string
}
variable "DOMAIN_NAME" {
type = string
}
variable "RESOURCE_GROUP_NAME" {
type = string
}

28
package-lock.json generated
View File

@@ -51,7 +51,7 @@
"@nestjs/serve-static": "^5.0.3",
"@nestjs/typeorm": "^11.0.0",
"@noahspan/azure-database": "^3.1.2",
"@noahspan/noahspan-modules": "^1.2.8",
"@noahspan/noahspan-modules": "^1.2.9",
"@schematics/angular": "^17.3.7",
"@types/multer": "^1.4.12",
"better-sqlite3": "^12.2.0",
@@ -5148,6 +5148,25 @@
"@types/node": "*"
}
},
"node_modules/@nestjs/mapped-types": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz",
"integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==",
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"class-transformer": "^0.4.0 || ^0.5.0",
"class-validator": "^0.13.0 || ^0.14.0",
"reflect-metadata": "^0.1.12 || ^0.2.0"
},
"peerDependenciesMeta": {
"class-transformer": {
"optional": true
},
"class-validator": {
"optional": true
}
}
},
"node_modules/@nestjs/passport": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz",
@@ -5752,9 +5771,9 @@
}
},
"node_modules/@noahspan/noahspan-modules": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@noahspan/noahspan-modules/-/noahspan-modules-1.2.8.tgz",
"integrity": "sha512-Zx4wtMAiRj2D132ng4DhlKt/O4kXj1NzCjxwVAXL1tinSsX9xHxjecY/GmYs9j/iMdFwAb5SpnyN5YeLX/THFw==",
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/@noahspan/noahspan-modules/-/noahspan-modules-1.2.9.tgz",
"integrity": "sha512-YUkFpI7UvvCUA3cAkSsWJZS3telrlVraCgXY1TrSoimPcT48fO4HKNqKboSJAeVq03rK6mV+BGimF9ZXZjrJzg==",
"dependencies": {
"@azure/identity": "^4.2.0",
"@azure/msal-node": "^2.9.2",
@@ -5762,6 +5781,7 @@
"@nestjs/common": "^11.0.11",
"@nestjs/core": "^11.0.11",
"@nestjs/jwt": "^11.0.0",
"@nestjs/mapped-types": "*",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.11",
"axios": "^1.7.2",