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/serve-static": "^5.0.3",
"@nestjs/typeorm": "^11.0.0", "@nestjs/typeorm": "^11.0.0",
"@noahspan/azure-database": "^3.1.2", "@noahspan/azure-database": "^3.1.2",
"@noahspan/noahspan-modules": "^1.2.8", "@noahspan/noahspan-modules": "^1.2.9",
"@schematics/angular": "^17.3.7", "@schematics/angular": "^17.3.7",
"@types/multer": "^1.4.12", "@types/multer": "^1.4.12",
"better-sqlite3": "^12.2.0", "better-sqlite3": "^12.2.0",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,28 +1,14 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { LogController } from './log.controller'; import { LogController } from './log.controller';
import { LogService } from './log.service'; import { LogService } from './log.service';
// import { AzureTableStorageModule } from '@noahspan/azure-database';
import { LogEntity } from './log.entity'; import { LogEntity } from './log.entity';
import { ConfigModule, ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { FileService } from '../file/file.service'; import { FileService } from '../file/file.service';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { PilotModule } from 'src/pilot/pilot.module'; import { PilotModule } from 'src/pilot/pilot.module';
@Module({ @Module({
imports: [ 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, PilotModule,
TypeOrmModule.forFeature([LogEntity]) TypeOrmModule.forFeature([LogEntity])
], ],

View File

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

View File

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

View File

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

View File

@@ -33,15 +33,15 @@ export class PilotEntity {
@Column() @Column()
userId: string | null; userId: string | null;
@OneToMany(() => LogEntity, (log: LogEntity) => log.pilot, {onDelete: 'CASCADE', onUpdate: 'CASCADE'}) @OneToMany(() => LogEntity, (log: LogEntity) => log.pilot)
logs: LogEntity[]; logs: LogEntity[];
@OneToMany(() => CertificateEntity, (certificate: CertificateEntity) => certificate.pilot, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) @OneToMany(() => CertificateEntity, (certificate: CertificateEntity) => certificate.pilot)
certificates: CertificateEntity[]; certificates: CertificateEntity[];
@OneToMany(() => EndorsementEntity, (endorsement: EndorsementEntity) => endorsement.pilot, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) @OneToMany(() => EndorsementEntity, (endorsement: EndorsementEntity) => endorsement.pilot)
endorsements: EndorsementEntity[]; endorsements: EndorsementEntity[];
@OneToMany(() => MedicalEntity, (medical: MedicalEntity) => medical.pilot, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) @OneToMany(() => MedicalEntity, (medical: MedicalEntity) => medical.pilot)
medical: MedicalEntity[]; 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 { jwtDecode } from 'jwt-decode';
import { Observable, map } from 'rxjs'; import { Observable, map } from 'rxjs';
import { CustomJwtPayload } from 'src/interfaces/customJwtPayload.interface'; import { CustomJwtPayload } from 'src/interfaces/customJwtPayload.interface';
import { PilotEntity } from './pilot.entity'; import { PilotEntity } from './pilot.entity';
import { IS_PUBLIC_KEY } from '@noahspan/noahspan-modules';
import { Reflector } from '@nestjs/core';
export class PilotInterceptor implements NestInterceptor { export class PilotInterceptor implements NestInterceptor {
constructor(private reflector: Reflector) {}
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> { intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest(); const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
const authHeader = req.headers.authorization; context.getHandler(),
const token = authHeader && authHeader.split(' ')[1]; context.getClass(),
const jwtPayload: CustomJwtPayload = jwtDecode(token); ]);
return handler.handle().pipe( return handler.handle().pipe(
map((data: PilotEntity[]) => { map((data: PilotEntity[]) => {
if (data.length && jwtPayload.roles.includes('Flying.Read')) { const req = context.switchToHttp().getRequest();
const pilots = data.map((pilot) => { const limitData = (data) => {
return data.map((pilot: PilotEntity) => {
return { return {
id: pilot.id, id: pilot.id,
name: pilot.name name: pilot.name
}; };
}); })
}
return pilots; if (req.headers.authorization) {
} else if (data.length && jwtPayload.roles.includes('Flying.Write')) { const authHeader = req.headers.authorization;
return data; const token = authHeader && authHeader.split(' ')[1];
} else { const jwtPayload: CustomJwtPayload = jwtDecode(token);
return UnauthorizedException;
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 { Module } from '@nestjs/common';
import { PilotController } from './pilot.controller'; import { PilotController } from './pilot.controller';
import { PilotService } from './pilot.service'; import { PilotService } from './pilot.service';
// import { AzureTableStorageModule } from '@noahspan/azure-database';
import { PilotEntity } from './pilot.entity'; import { PilotEntity } from './pilot.entity';
// import { ConfigModule, ConfigService } from '@nestjs/config';
// import { Log } from 'src/log/log.entity';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
@Module({ @Module({
imports: [ 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]) TypeOrmModule.forFeature([PilotEntity])
], ],
controllers: [PilotController], controllers: [PilotController],

View File

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

View File

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

View File

@@ -31,7 +31,6 @@ export class TrackService {
const logEntity: LogEntity = await this.logService.find(logId); const logEntity: LogEntity = await this.logService.find(logId);
if (logEntity) { if (logEntity) {
console.log(logEntity)
const tracks = await this.trackRepository.find({ const tracks = await this.trackRepository.find({
where: { log: where: { log:
{ {
@@ -40,7 +39,6 @@ export class TrackService {
}, },
}) })
console.log(tracks)
return tracks; return tracks;
} }
} catch (error) { } catch (error) {
@@ -55,7 +53,6 @@ export class TrackService {
if (logEntity) { if (logEntity) {
const url = await this.fileService.uploadFile(file, this.containerName, logId); const url = await this.fileService.uploadFile(file, this.containerName, logId);
console.log(url)
const track = this.trackRepository.create({ const track = this.trackRepository.create({
log: logEntity, log: logEntity,
order: order, order: order,
@@ -67,7 +64,6 @@ export class TrackService {
throw new CustomError('Log not found', 'Not found', 404) throw new CustomError('Log not found', 'Not found', 404)
} }
} catch (error) { } catch (error) {
console.log(error)
throw 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> { async downloadTrackFile(logId: string, fileName: string): Promise<string> {
try { try {
const downloadedFile: string = await this.fileService.downloadFile(this.containerName, logId, fileName); const downloadedFile: string = await this.fileService.downloadFile(this.containerName, logId, fileName);
@@ -99,4 +85,14 @@ export class TrackService {
throw error 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, clientId: import.meta.env.VITE_CLIENT_APP_ID,
homeUrl: import.meta.env.BASE_URL, homeUrl: import.meta.env.BASE_URL,
scopes: ['email', 'openid', 'profile', `api://${import.meta.env.VITE_API_APP_ID}/user_impersonation`], scopes: ['email', 'openid', 'profile', `api://${import.meta.env.VITE_API_APP_ID}/user_impersonation`],
autoLogin: true, autoLogin: false,
postLoginRedirectUrl: '/', postLoginRedirectUrl: '/',
noIframe: true noIframe: true
})); }));

View File

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

View File

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

View File

@@ -20,8 +20,9 @@ const Flights = () => {
if (flights && flights.length > 0) { if (flights && flights.length > 0) {
dispatch({ type: 'SET_FLIGHTS', payload: flights}) dispatch({ type: 'SET_FLIGHTS', payload: flights})
dispatch({ type: 'SET_ALERT', payload: undefined })
} else { } 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]) }, [logs])

View File

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

View File

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

View File

@@ -6,13 +6,7 @@ import { LogbookState } from './LogbookState.interface';
type Action = type Action =
| { type: 'SET_COLUMNS'; payload: ColumnDef<LogbookEntry>[] } | { type: 'SET_COLUMNS'; payload: ColumnDef<LogbookEntry>[] }
| { | { type: 'SET_IS_CONFIRMATION_DIALOG_OPEN'; payload: boolean }
type: 'SET_DELETE';
payload: {
isConfirmationDialogOpen: boolean;
selectedLogId: string | undefined;
};
}
| { type: 'SET_ENTRIES'; payload: LogbookEntry[] } | { type: 'SET_ENTRIES'; payload: LogbookEntry[] }
| { type: 'SET_ALERT'; payload: Alert | undefined } | { type: 'SET_ALERT'; payload: Alert | undefined }
| { type: 'SET_IS_CONFIRMATION_DIALOG_LOADING'; payload: boolean } | { type: 'SET_IS_CONFIRMATION_DIALOG_LOADING'; payload: boolean }
@@ -39,10 +33,10 @@ export const reducer = (
columns: action.payload columns: action.payload
} }
} }
case 'SET_DELETE': { case 'SET_IS_CONFIRMATION_DIALOG_OPEN': {
return { return {
...state, ...state,
isConfirmDialogOpen: action.payload.isConfirmationDialogOpen, isConfirmDialogOpen: action.payload,
}; };
} }
case 'SET_ENTRIES': { 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 { LogbookCardProps } from "./LogbookCardProps.interface";
import ActionMenu from "../actionMenu/ActionMenu"; import TrackMap from "../trackMap/TrackMap";
import LogTrackMaps from "../logTrackMaps/LogTrackMaps";
const LogbookCard = ({ logs, mode, onDelete, onOpenCloseForm }: LogbookCardProps) => { const LogbookCard = ({ logs, mode, onDelete, onOpenCloseForm }: LogbookCardProps) => {
return ( return (
<div> <div>
{logs.map((log) => { {logs.map((log) => {
console.log(log)
const date = new Date(log.date); const date = new Date(log.date);
const formattedDate: string = `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}`; const formattedDate: string = `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}`;
return ( return (
<div> <div>
<Card key={log.id}> <Card className='p-4' key={log.id}>
<CardBody> <CardHeader>
<CardHeader><ActionMenu id={log.id} onDelete={onDelete!} onOpenCloseForm={onOpenCloseForm!} /></CardHeader> <h2 className='font-bold text-2xl'>{formattedDate}</h2>
<CardContent> </CardHeader>
<div> <CardBody>
{mode === 'flights' && log.tracks && log.tracks.length > 0 && {mode === 'flights' && log.tracks && log.tracks.length > 0 &&
<div> <div className='mb-5'>
<LogTrackMaps <TrackMap
logId={log.id} height='400px'
tracks={log.tracks} 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>
</>
}
</div> </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> </CardBody>
</Card> </Card>
</div> </div>

View File

@@ -9,8 +9,10 @@ import { FormMode } from "../../enums/formMode";
import httpClient from "../../httpClient/httpClient"; import httpClient from "../../httpClient/httpClient";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext"; import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext";
import { Key, useState } from "react";
const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => { const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
const [activeTab, setActiveTab] = useState<Key>('time');
const defaultValues = { const defaultValues = {
pilotId: '', pilotId: '',
date: null, date: null,
@@ -34,8 +36,7 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
instrumentApproaches: null, instrumentApproaches: null,
instrumentHolds: null, instrumentHolds: null,
instrumentNavTrack: null, instrumentNavTrack: null,
notes: '', notes: ''
tracks: []
}; };
const methods = useForm(); const methods = useForm();
const logbookContext = useLogbookContext() const logbookContext = useLogbookContext()
@@ -69,9 +70,19 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
} }
}; };
const onSelectedKeyChanged = (key: React.Key) => {
setActiveTab(key)
}
return ( return (
<Drawer <Drawer
closeButton={
<Button isIconOnly>
<FontAwesomeIcon icon={faXmark} />
</Button>
}
isOpen={logbookContext.state.isDrawerOpen} isOpen={logbookContext.state.isDrawerOpen}
onClose={onCancel}
> >
<DrawerContent> <DrawerContent>
<FormProvider {...methods}> <FormProvider {...methods}>
@@ -91,7 +102,13 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
/> />
</div> </div>
)} )}
<Tabs color='default' fullWidth={true} variant='solid'> <Tabs
color='default'
fullWidth={true}
onSelectionChange={onSelectedKeyChanged}
selectedKey={activeTab as string}
variant='solid'
>
<Tab <Tab
key='time' key='time'
title={ title={
@@ -103,47 +120,51 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
> >
<LogForm /> <LogForm />
</Tab> </Tab>
<Tab {logbookContext.state.formMode !== FormMode.ADD &&
key='tracks' <Tab
title={ key='tracks'
<div className="flex items-center space-x-2"> title={
<FontAwesomeIcon icon={faMapLocationDot} /> <div className="flex items-center space-x-2">
<span>Tracks</span> <FontAwesomeIcon icon={faMapLocationDot} />
</div> <span>Tracks</span>
} </div>
> }
<TracksForm /> >
</Tab> <TracksForm />
</Tab>
}
</Tabs> </Tabs>
</DrawerBody> </DrawerBody>
<DrawerFooter> {activeTab !== 'tracks' &&
<div className='grid grid-cols-12 gap-3'> <DrawerFooter>
<div className='col-span-12 justify-self-end self-center'> <div className='grid grid-cols-12 gap-3'>
<Button <div className='col-span-12 justify-self-end self-center'>
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 && (
<Button <Button
className='ml-[10px]' disabled={
color='primary' logbookContext.state.isFormDisabled && logbookContext.state.formMode.toString() !== FormMode.VIEW
disabled={logbookContext.state.isFormDisabled} ? logbookContext.state.isFormDisabled
startContent={<FontAwesomeIcon icon={faSave} />} : false
type="submit" }
startContent={<FontAwesomeIcon icon={faXmark} />}
onPress={onCancel}
> >
Save {logbookContext.state.formMode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
</Button> </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>
</div> </DrawerFooter>
</DrawerFooter> }
</form> </form>
</FormProvider> </FormProvider>
</DrawerContent> </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 { 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 { IPilotFormProps } from './IPilotFormProps';
import { AxiosError, AxiosResponse } from 'axios'; import { AxiosError, AxiosResponse } from 'axios';
import { FormMode } from '../../enums/formMode'; import { FormMode } from '../../enums/formMode';
@@ -18,11 +8,11 @@ import PilotFormCertificates from '../pilotFormCertificates/PilotFormCertificate
import PilotFormEndorsements from '../pilotFormEndorsements/PilotFormEndorsements'; import PilotFormEndorsements from '../pilotFormEndorsements/PilotFormEndorsements';
import PilotFormMedical from '../pilotFormMedical/PilotFormMedical'; import PilotFormMedical from '../pilotFormMedical/PilotFormMedical';
import { useOidc } from '../../auth/oidcConfig'; import { useOidc } from '../../auth/oidcConfig';
import { getOidc } from '../../auth/oidcConfig';
import httpClient from '../../httpClient/httpClient'; 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 { 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> = ({ const PilotForm: React.FC<IPilotFormProps> = ({
pilotId, pilotId,
@@ -31,7 +21,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
onOpenClose onOpenClose
}: IPilotFormProps) => { }: IPilotFormProps) => {
const [peoplePickerValue, setPeoplePickerValue] = useState<string>(''); const [peoplePickerValue, setPeoplePickerValue] = useState<string>('');
const [peoplePickerResults, setPeoplePickerResults] = useState<any[]>([]); const [peoplePickerResults, setPeoplePickerResults] = useState<Person[]>([]);
const [isPeoplePickerLoading, setIsPeoplePickerLoading] = const [isPeoplePickerLoading, setIsPeoplePickerLoading] =
useState<boolean>(false); useState<boolean>(false);
const [selectedPerson, setSelectedPerson] = useState<Person>({ const [selectedPerson, setSelectedPerson] = useState<Person>({
@@ -47,6 +37,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
postalCode: '', postalCode: '',
email: '', email: '',
phone: '', phone: '',
userId: ''
}; };
const methods = useForm({ const methods = useForm({
defaultValues: defaultValues defaultValues: defaultValues
@@ -63,15 +54,10 @@ const PilotForm: React.FC<IPilotFormProps> = ({
try { try {
if (value !== '') { if (value !== '') {
const searchString: string = value; const searchString: string = value;
const oidc = await getOidc();
const response: AxiosResponse = await httpClient.get( const response: AxiosResponse = await httpClient.get(
`api/msgraph/search?search=${searchString}`, { `api/msgraph/search?search=${searchString}`
headers: {
Authorization: oidc.isUserLoggedIn ? `Bearer ${(await oidc.getTokens()).accessToken}` : ''
}
}
); );
console.log(response)
setPeoplePickerResults(response.data); setPeoplePickerResults(response.data);
} else { } else {
setPeoplePickerResults([]); setPeoplePickerResults([]);
@@ -83,9 +69,12 @@ const PilotForm: React.FC<IPilotFormProps> = ({
} }
}; };
const onPersonSelected = (person: Person) => { const onPersonSelected = (userPrincipalName: string) => {
methods.setValue('name', person.displayName!.toString()); const person: Person | undefined = peoplePickerResults.find((person) => person.userPrincipalName === userPrincipalName as string);
setPeoplePickerValue(person.displayName!);
methods.setValue('name', person?.displayName!);
methods.setValue('userId', person?.userPrincipalName!);
setPeoplePickerValue(person?.displayName!);
setPeoplePickerResults([]) setPeoplePickerResults([])
}; };
@@ -134,9 +123,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
); );
const pilot = response.data; const pilot = response.data;
setSelectedPerson({ setPeoplePickerValue(pilot.name);
displayName: pilot.name
});
methods.reset(pilot); methods.reset(pilot);
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@@ -175,15 +162,19 @@ const PilotForm: React.FC<IPilotFormProps> = ({
<h6>Name *</h6> <h6>Name *</h6>
</div> </div>
<div className='col-span-9'> <div className='col-span-9'>
<PeoplePicker <Autocomplete
disabled={isDisabled} inputValue={peoplePickerValue}
// loading={isPeoplePickerLoading} isLoading={isPeoplePickerLoading}
onInputChanged={onPeoplePickerSearch} items={peoplePickerResults}
onPersonSelected={onPersonSelected} onInputChange={onPeoplePickerSearch}
people={peoplePickerResults} onSelectionChange={(key: Key | null) => onPersonSelected(key as string)}
value={peoplePickerValue} >
width='w-full' {peoplePickerResults.map((person: Person) => (
/> <AutocompleteItem key={person.userPrincipalName}>
{person.displayName}
</AutocompleteItem>
))}
</Autocomplete>
</div> </div>
{isUserLoggedIn && {isUserLoggedIn &&
<> <>
@@ -244,19 +235,14 @@ const PilotForm: React.FC<IPilotFormProps> = ({
control={methods.control} control={methods.control}
rules={{ required: 'A state must be selected' }} rules={{ required: 'A state must be selected' }}
render={({ field: { onChange, value } }) => ( render={({ field: { onChange, value } }) => (
<StateSelect <Select
disabled={isDisabled}
// error={methods.formState.errors.state ? true : false}
// helperText={
// methods.formState.errors.state
// ? methods.formState.errors.state.message?.toString()
// : undefined
// }
onChange={onChange} onChange={onChange}
value={value} selectedKeys={[value]}
width='w-full' >
data-testid="pilot-form-state-dropdown" {states.map((state) => (
/> <SelectItem key={state.value}>{state.label}</SelectItem>
))}
</Select>
)} )}
/> />
</div> </div>
@@ -369,7 +355,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
? isDisabled ? isDisabled
: false : false
} }
startContent={<Icon iconName={IconName.XMARK} />} startContent={<FontAwesomeIcon icon={faXmark} />}
onPress={onCancel} onPress={onCancel}
data-testid="pilot-cancel-button" data-testid="pilot-cancel-button"
> >
@@ -379,7 +365,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
<Button <Button
color='primary' color='primary'
disabled={isDisabled} disabled={isDisabled}
startContent={<Icon iconName={IconName.SAVE} />} startContent={<FontAwesomeIcon icon={faSave} />}
type="submit" type="submit"
data-testid="pilot-save-button" 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 PilotForm from '../pilotForm/PilotForm';
import { import { AxiosError, AxiosResponse } from 'axios';
// Alert,
// Button,
// ColumnDef,
// Dropdown,
Icon,
IconButton,
IconName,
// Table,
} from '@noahspan/noahspan-components';
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import { FormMode } from '../../enums/formMode'; import { FormMode } from '../../enums/formMode';
import { Pilot } from './Pilot.interface';
import { initialState, reducer } from './reducer'; import { initialState, reducer } from './reducer';
import ActionMenu from '../actionMenu/ActionMenu';
import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog'; import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog';
import PilotCard from '../pilotCard/PilotCard'; import PilotCard from '../pilotCard/PilotCard';
import { getOidc, useOidc } from '../../auth/oidcConfig';
import { useUserRole } from '../../hooks/userRole/UseUserRole'; import { useUserRole } from '../../hooks/userRole/UseUserRole';
import { UserRole } from '../../enums/userRole'; import { UserRole } from '../../enums/userRole';
import httpClient from '../../httpClient/httpClient' import httpClient from '../../httpClient/httpClient'
@@ -25,36 +12,31 @@ import { ScreenSize } from '../../enums/screenSize';
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints'; import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
import { Alert, Button, Dropdown, DropdownItem, DropdownSection, Table, TableHeader, TableBody, TableColumn, TableRow, TableCell, DropdownTrigger, DropdownMenu } from '@heroui/react' import { Alert, Button, Dropdown, DropdownItem, DropdownSection, Table, TableHeader, TableBody, TableColumn, TableRow, TableCell, DropdownTrigger, DropdownMenu } from '@heroui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 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 Pilots: React.FC<unknown> = () => {
const [state, dispatch] = useReducer(reducer, initialState); const [state, dispatch] = useReducer(reducer, initialState);
// const { httpClient } = useHttpClient();
const { userRole } = useUserRole(); const { userRole } = useUserRole();
const { screenSize } = useBreakpoints() const { screenSize } = useBreakpoints()
const getPilots = async () => { const getPilots = async () => {
try { try {
// const oidc = await getOidc();
let response: AxiosResponse; let response: AxiosResponse;
// if (oidc.isUserLoggedIn) {
// const { accessToken } = await oidc.getTokens();
console.log('blah')
response = await httpClient.get(
`api/pilots`
);
console.log(response); response = await httpClient.get(
if (response.data.length > 0) { `api/pilots`
dispatch({ type: 'SET_PILOTS', payload: response.data }); );
if (state.alert) { if (response.data.length > 0) {
dispatch({ type: 'SET_ALERT', payload: undefined }) dispatch({ type: 'SET_PILOTS', payload: response.data });
}
} else { if (state.alert) {
dispatch({ type: 'SET_ALERT', payload: { severity: 'default', message: 'No pilots found.' }}) 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) { } catch (error) {
const axiosError = error as AxiosError; const axiosError = error as AxiosError;
@@ -68,6 +50,7 @@ const Pilots: React.FC<unknown> = () => {
}; };
const onOpenClosePilotForm = async (mode: FormMode, pilotId?: string) => { const onOpenClosePilotForm = async (mode: FormMode, pilotId?: string) => {
console.log(pilotId)
switch (mode) { switch (mode) {
case FormMode.ADD: case FormMode.ADD:
case FormMode.EDIT: case FormMode.EDIT:
@@ -96,7 +79,7 @@ const Pilots: React.FC<unknown> = () => {
} }
}; };
const onDeleteEntry = (pilotId: string) => { const onDeletePilot = (pilotId: string) => {
dispatch({ dispatch({
type: 'SET_DELETE', type: 'SET_DELETE',
payload: { isConfirmDialogOpen: true, selectedPilotId: pilotId } payload: { isConfirmDialogOpen: true, selectedPilotId: pilotId }
@@ -107,7 +90,7 @@ const Pilots: React.FC<unknown> = () => {
try { try {
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: true }); 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({ dispatch({
type: 'SET_DELETE', 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 renderCell = (pilot: any, columnKey: any) => {
const cellValue = pilot[columnKey] const cellValue = pilot[columnKey]
console.log(cellValue)
switch (columnKey) { switch (columnKey) {
case 'actions': { case 'actions': {
return ( return (
@@ -191,14 +143,14 @@ const Pilots: React.FC<unknown> = () => {
<DropdownSection showDivider> <DropdownSection showDivider>
<DropdownItem <DropdownItem
key='edit' key='edit'
onPress={() => onOpenClosePilotForm(FormMode.EDIT)} onPress={() => onOpenClosePilotForm(FormMode.EDIT, pilot.id)}
startContent={<FontAwesomeIcon icon={faPen} />} startContent={<FontAwesomeIcon icon={faPen} />}
> >
Edit Edit
</DropdownItem> </DropdownItem>
<DropdownItem <DropdownItem
key='view' key='view'
onPress={() => onOpenClosePilotForm(FormMode.VIEW)} onPress={() => onOpenClosePilotForm(FormMode.VIEW, pilot.id)}
startContent={<FontAwesomeIcon icon={faEye} />} startContent={<FontAwesomeIcon icon={faEye} />}
> >
View View
@@ -207,6 +159,7 @@ const Pilots: React.FC<unknown> = () => {
<DropdownSection> <DropdownSection>
<DropdownItem <DropdownItem
key='Delete' key='Delete'
onPress={() => onDeletePilot(pilot.id)}
startContent={<FontAwesomeIcon icon={faTrash} />} startContent={<FontAwesomeIcon icon={faTrash} />}
> >
Delete Delete
@@ -238,8 +191,8 @@ const Pilots: React.FC<unknown> = () => {
{userRole === UserRole.WRITE && {userRole === UserRole.WRITE &&
<Button <Button
color='primary' color='primary'
onClick={() => onOpenClosePilotForm(FormMode.ADD)} onPress={() => onOpenClosePilotForm(FormMode.ADD)}
startContent={<Icon iconName={IconName.PLUS} />} startContent={<FontAwesomeIcon icon={faPlus} />}
data-testid="pilot-add-button" data-testid="pilot-add-button"
> >
Add Pilot Add Pilot
@@ -247,7 +200,7 @@ const Pilots: React.FC<unknown> = () => {
} }
</div> </div>
{!state.isLoading && state.alert && ( {!state.isLoading && state.alert && (
<div> <div className='col-span-12'>
<Alert <Alert
onClose={() => onClose={() =>
dispatch({ type: 'SET_ALERT', payload: undefined }) dispatch({ type: 'SET_ALERT', payload: undefined })
@@ -259,7 +212,6 @@ const Pilots: React.FC<unknown> = () => {
)} )}
<div className='col-span-12'> <div className='col-span-12'>
{state.pilots.length > 0 && screenSize !== ScreenSize.SM && {state.pilots.length > 0 && screenSize !== ScreenSize.SM &&
// <Table columns={columns} data={state.pilots} />
<Table> <Table>
<TableHeader columns={columns}> <TableHeader columns={columns}>
{(column) => ( {(column) => (
@@ -285,7 +237,7 @@ const Pilots: React.FC<unknown> = () => {
</Table> </Table>
} }
{state.pilots.length > 0 && screenSize === ScreenSize.SM && {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>
</div> </div>
@@ -299,7 +251,7 @@ const Pilots: React.FC<unknown> = () => {
)} )}
{state.isConfirmDialogOpen && ( {state.isConfirmDialogOpen && (
<ConfirmationDialog <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} isLoading={state.isConfirmDialogLoading}
isOpen={state.isConfirmDialogOpen} isOpen={state.isConfirmDialogOpen}
onCancel={onConfirmationDialogCancel} onCancel={onConfirmationDialogCancel}

View File

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

View File

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

View File

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

View File

@@ -1,31 +1,44 @@
import { useEffect, useReducer } from "react"; import { useEffect, useReducer, useRef } from "react";
// import { Button, Drawer, Icon, IconButton, IconName, Input, Loading } from "@noahspan/noahspan-components"; import { AxiosError, AxiosResponse } from "axios";
import { AxiosError, AxiosInstance, AxiosResponse } from "axios";
import { TracksFormProps } from "./TracksFormProps.interface"; import { TracksFormProps } from "./TracksFormProps.interface";
import { FormMode } from "../../enums/formMode"; import { FormMode } from "../../enums/formMode";
import { LogbookEntry } from "../logbook/LogbookEntry.interface"; import { LogbookEntry } from "../logbook/LogbookEntry.interface";
import ConfirmationDialog from "../confirmationDialog/ConfirmationDialog"; import ConfirmationDialog from "../confirmationDialog/ConfirmationDialog";
import { initialState, reducer } from "./reducer"; import { initialState, reducer } from "./reducer";
import LogTrackMaps from "../logTrackMaps/LogTrackMaps"; import TrackMap from "../trackMap/TrackMap";
import httpClient from "../../httpClient/httpClient"; 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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUpload, faTrash, faXmark } from '@fortawesome/free-solid-svg-icons'; import { faUpload, faTrash, faXmark } from '@fortawesome/free-solid-svg-icons';
import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext"; import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext";
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
const TracksForm = () => { const TracksForm = () => {
const [state, dispatch] = useReducer(reducer, initialState) const [state, dispatch] = useReducer(reducer, initialState)
const logbookContext = useLogbookContext(); 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>) => { const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
console.log('blah')
try { try {
dispatch({ type: 'SET_IS_LOADING', payload: true}) dispatch({ type: 'SET_IS_LOADING', payload: true})
const file = event.target.files![0] const file = event.target.files![0]
const formData = new FormData(); const formData = new FormData();
const order = state.tracks.length + 1
formData.append('file', file); formData.append('file', file);
@@ -33,9 +46,10 @@ const TracksForm = () => {
config.headers["Content-Type"] = 'multipart/form-data' config.headers["Content-Type"] = 'multipart/form-data'
return config 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 uploadUrl = uploadResponse.data.url;
// const tracks: string[] = log.tracks ? JSON.parse(log.tracks!) : []; // const tracks: string[] = log.tracks ? JSON.parse(log.tracks!) : [];
@@ -53,49 +67,77 @@ const TracksForm = () => {
} }
} }
// const onDeleteTrack = async (index: number) => { const onDeleteTrack = async (id: string, filename: string, index: number) => {
// dispatch({ type: 'SET_ON_DELETE_TRACK', payload: { isConfirmDialogOpen: true, selectedTrack: { index: index }}}) 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(() => { 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) { if (logbookContext.state.selectedLogId) {
getTracks(); getTracks();
} }
}, [logbookContext.state.selectedLogId]) }, [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 ( return (
<div className='grid grid-cols-12 gap-3'> <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 && {state.tracks.length > 0 && state.tracks.map((track, index) => {
<> const filename: string = track.url.substring(track.url.lastIndexOf('/') + 1);
<div className='col-span-10'>
<Input type='text' /> return (
</div> <>
<div className='col-span-2'> <div className='col-span-10'>
<Button isIconOnly onPress={() => console.log('delete')}><FontAwesomeIcon icon={faTrash} /></Button> <Input isDisabled={state.isDisabled} key={index} type='text' value={filename}/>
</div> </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'> <div className='col-span-12'>
<Button <Button
as='label' as='label'
disabled={state.isLoading ? true : false} color='primary'
isDisabled={state.isDisabled}
fullWidth={true} fullWidth={true}
startContent={<FontAwesomeIcon icon={faUpload} />} startContent={<FontAwesomeIcon icon={faUpload} />}
> >
@@ -103,6 +145,16 @@ const TracksForm = () => {
<input hidden onChange={handleFileUpload} type='file' /> <input hidden onChange={handleFileUpload} type='file' />
</Button> </Button>
</div> </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> </div>
); );
@@ -136,31 +188,7 @@ const TracksForm = () => {
// dispatch({ type: 'SET_ON_DELETE_TRACK', payload: { isConfirmDialogOpen: true, selectedTrack: { fileName: fileName, index: index }}}) // 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(() => { // useEffect(() => {
// const updateTracks = async () => { // 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> // </div>
// ) // )
// } // }

View File

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

View File

@@ -3,13 +3,15 @@ import { TracksFormState } from "./TracksFormState.interface";
type Action = type Action =
| { type: 'SET_IS_CONFIRM_DIALOG_OPEN'; payload: boolean } | { type: 'SET_IS_CONFIRM_DIALOG_OPEN'; payload: boolean }
| { type: 'SET_IS_CONFORM_DIALOG_LOADING'; payload: boolean } | { type: 'SET_IS_CONFORM_DIALOG_LOADING'; payload: boolean }
| { type: 'SET_IS_DISABLED'; payload: boolean }
| { type: 'SET_IS_LOADING'; 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 }[] }; | { type: 'SET_TRACKS'; payload: { id: string; order: number; url: string }[] };
export const initialState: TracksFormState = { export const initialState: TracksFormState = {
isConfirmDialogOpen: false, isConfirmDialogOpen: false,
isConfirmDialogLoading: false, isConfirmDialogLoading: false,
isDisabled: false,
isLoading: false, isLoading: false,
selectedTrack: undefined, selectedTrack: undefined,
tracks: [] tracks: []
@@ -29,6 +31,12 @@ export const reducer = (state: TracksFormState, action: Action): TracksFormState
isConfirmDialogLoading: action.payload isConfirmDialogLoading: action.payload
} }
} }
case 'SET_IS_DISABLED': {
return {
...state,
isDisabled: action.payload
}
}
case 'SET_IS_LOADING': { case 'SET_IS_LOADING': {
return { return {
...state, ...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( const response: AxiosResponse = await httpClient.get(
`/api/pilots` `/api/pilots`
); );
console.log(response)
setPilots(response.data); setPilots(response.data);
} catch (error) { } catch (error) {
return error; return error;

View File

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

View File

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

Binary file not shown.

View File

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

View File

@@ -2,6 +2,10 @@ output "app_name" {
value = local.app_name[var.environment] value = local.app_name[var.environment]
} }
output "container_app_environment_name" {
value = local.container_app_environment_name[var.environment]
}
output "container_image" { output "container_image" {
value = local.container_image[var.environment] 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" { module "app" {
source = "./app" source = "./app"
environment = var.environment environment = var.environment
} }
module "container_app_environment" {
source = "./container_app_environment"
environment = var.environment
}
module "storage" { module "storage" {
source = "./storage" source = "./storage"
environment = var.environment environment = var.environment

View File

@@ -1,15 +1,7 @@
output "api" {
value = module.api
}
output "app" { output "app" {
value = module.app value = module.app
} }
output "container_app_environment" {
value = module.container_app_environment
}
output "storage" { output "storage" {
value = module.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" { data "azurerm_client_config" "current" {}
source = "github.com/noahspannbauer/noahspan-terraform/modules/storage"
resource_group_name = var.RESOURCE_GROUP_NAME data "azurerm_resource_group" "resource_group" {
storage_account_name = module.environment.storage.account_name name = var.RESOURCE_GROUP_NAME
storage_containers = module.environment.storage.containers
storage_shares = module.environment.storage.shares
storage_tables = module.environment.storage.tables
} }
module "container_app_environment" { data "azurerm_container_app_environment" "container_app_environment" {
source = "github.com/noahspannbauer/noahspan-terraform/modules/container_app_environment" name = module.environment.app.container_app_environment_name
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"
resource_group_name = var.RESOURCE_GROUP_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" { variable "CLIENT_ID" {
type = string type = string
@@ -15,18 +8,6 @@ variable "CLIENT_SECRET" {
sensitive = true sensitive = true
} }
variable "DB_PATH" {
type = string
}
variable "DB_SYNC" {
type = string
}
variable "DNS_ZONE_RESOURCE_GROUP" {
type = string
}
variable "DOCKER_IO_PASSWORD" { variable "DOCKER_IO_PASSWORD" {
type = string type = string
sensitive = true sensitive = true
@@ -36,10 +17,6 @@ variable "DOCKER_IO_USERNAME" {
type = string type = string
} }
variable "DOMAIN_NAME" {
type = string
}
variable "RESOURCE_GROUP_NAME" { variable "RESOURCE_GROUP_NAME" {
type = string type = string
} }

28
package-lock.json generated
View File

@@ -51,7 +51,7 @@
"@nestjs/serve-static": "^5.0.3", "@nestjs/serve-static": "^5.0.3",
"@nestjs/typeorm": "^11.0.0", "@nestjs/typeorm": "^11.0.0",
"@noahspan/azure-database": "^3.1.2", "@noahspan/azure-database": "^3.1.2",
"@noahspan/noahspan-modules": "^1.2.8", "@noahspan/noahspan-modules": "^1.2.9",
"@schematics/angular": "^17.3.7", "@schematics/angular": "^17.3.7",
"@types/multer": "^1.4.12", "@types/multer": "^1.4.12",
"better-sqlite3": "^12.2.0", "better-sqlite3": "^12.2.0",
@@ -5148,6 +5148,25 @@
"@types/node": "*" "@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": { "node_modules/@nestjs/passport": {
"version": "11.0.5", "version": "11.0.5",
"resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz", "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz",
@@ -5752,9 +5771,9 @@
} }
}, },
"node_modules/@noahspan/noahspan-modules": { "node_modules/@noahspan/noahspan-modules": {
"version": "1.2.8", "version": "1.2.9",
"resolved": "https://registry.npmjs.org/@noahspan/noahspan-modules/-/noahspan-modules-1.2.8.tgz", "resolved": "https://registry.npmjs.org/@noahspan/noahspan-modules/-/noahspan-modules-1.2.9.tgz",
"integrity": "sha512-Zx4wtMAiRj2D132ng4DhlKt/O4kXj1NzCjxwVAXL1tinSsX9xHxjecY/GmYs9j/iMdFwAb5SpnyN5YeLX/THFw==", "integrity": "sha512-YUkFpI7UvvCUA3cAkSsWJZS3telrlVraCgXY1TrSoimPcT48fO4HKNqKboSJAeVq03rK6mV+BGimF9ZXZjrJzg==",
"dependencies": { "dependencies": {
"@azure/identity": "^4.2.0", "@azure/identity": "^4.2.0",
"@azure/msal-node": "^2.9.2", "@azure/msal-node": "^2.9.2",
@@ -5762,6 +5781,7 @@
"@nestjs/common": "^11.0.11", "@nestjs/common": "^11.0.11",
"@nestjs/core": "^11.0.11", "@nestjs/core": "^11.0.11",
"@nestjs/jwt": "^11.0.0", "@nestjs/jwt": "^11.0.0",
"@nestjs/mapped-types": "*",
"@nestjs/passport": "^11.0.5", "@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.11", "@nestjs/platform-express": "^11.0.11",
"axios": "^1.7.2", "axios": "^1.7.2",