96 switch from azure table storage to sqlite #97
@@ -35,7 +35,7 @@
|
||||
"@nestjs/serve-static": "^5.0.3",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"@noahspan/azure-database": "^3.1.2",
|
||||
"@noahspan/noahspan-modules": "^1.2.8",
|
||||
"@noahspan/noahspan-modules": "^1.2.9",
|
||||
"@schematics/angular": "^17.3.7",
|
||||
"@types/multer": "^1.4.12",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
|
||||
@@ -15,7 +15,7 @@ export class CertificateEntity {
|
||||
@Column()
|
||||
issueDate: Date
|
||||
|
||||
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.certificates)
|
||||
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.certificates, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
@JoinColumn({ name: 'pilotId' })
|
||||
pilot: PilotEntity;
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export class EndorsementEntity {
|
||||
@Column()
|
||||
issueDate: Date;
|
||||
|
||||
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.endorsements)
|
||||
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.endorsements, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
@JoinColumn({ name: 'pilotId' })
|
||||
pilot: PilotEntity
|
||||
}
|
||||
@@ -58,11 +58,32 @@ import { ConfigService } from '@nestjs/config';
|
||||
return downloaded
|
||||
}
|
||||
|
||||
async deleteFile(containerName: string, rowKey:string, fileName: string): Promise<void> {
|
||||
async deleteFile(containerName: string, logId: string, fileName: string): Promise<void> {
|
||||
this.containerName = containerName;
|
||||
|
||||
const blockBlobClient = await this.getBlobClient(`${rowKey}/${fileName}`);
|
||||
const blockBlobClient = await this.getBlobClient(`${logId}/${fileName}`);
|
||||
|
||||
await blockBlobClient.deleteIfExists();
|
||||
}
|
||||
|
||||
async deleteFolder(containerName: string, logId: string): Promise<void> {
|
||||
const blobService = await this.getBlobServiceInstance();
|
||||
|
||||
this.containerName = containerName;
|
||||
|
||||
const containerClient = blobService.getContainerClient(containerName);
|
||||
const blobsToDelete = []
|
||||
|
||||
for await (const blob of containerClient.listBlobsFlat({ prefix: logId })) {
|
||||
blobsToDelete.push(blob.name)
|
||||
}
|
||||
|
||||
for (const blobName of blobsToDelete) {
|
||||
const blockBlobClient = containerClient.getBlockBlobClient(blobName);
|
||||
|
||||
await blockBlobClient.delete();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,16 @@ import { LogDto } from './log.dto';
|
||||
import { LogEntity } from './log.entity';
|
||||
import { LogService } from './log.service';
|
||||
import { CustomError } from '../error/customError';
|
||||
import { AuthGuard } from '@noahspan/noahspan-modules';
|
||||
import { AuthGuard, Public } from '@noahspan/noahspan-modules';
|
||||
import { LogInterceptor } from './log.interceptor';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { DeleteResult, InsertResult, UpdateResult } from 'typeorm';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
const reflector = new Reflector();
|
||||
|
||||
@Controller('logs')
|
||||
@UseInterceptors(new LogInterceptor())
|
||||
// @UseGuards(AuthGuard)
|
||||
@UseInterceptors(new LogInterceptor(reflector))
|
||||
export class LogController {
|
||||
constructor(
|
||||
private readonly fileService: FileService,
|
||||
@@ -30,6 +32,7 @@ export class LogController {
|
||||
|
||||
|
||||
@Get(':id')
|
||||
@Public()
|
||||
async find(
|
||||
@Param('id') id: string,
|
||||
): Promise<LogEntity> {
|
||||
@@ -44,6 +47,7 @@ export class LogController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Public()
|
||||
async findAll(): Promise<LogEntity[]> {
|
||||
try {
|
||||
return await this.logService.findAll();
|
||||
@@ -56,6 +60,7 @@ export class LogController {
|
||||
|
||||
|
||||
@Post()
|
||||
@UseGuards(AuthGuard)
|
||||
async create(@Body() logDto: LogDto): Promise<InsertResult> {
|
||||
try {
|
||||
return await this.logService.create(logDto);
|
||||
@@ -67,6 +72,7 @@ export class LogController {
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@UseGuards(AuthGuard)
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() logDto: LogDto
|
||||
@@ -83,12 +89,14 @@ export class LogController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(AuthGuard)
|
||||
async delete(
|
||||
@Param('id') id: string,
|
||||
): Promise<DeleteResult> {
|
||||
try {
|
||||
return await this.logService.delete(id);
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
|
||||
@@ -76,10 +76,10 @@ export class LogEntity {
|
||||
@Column({ nullable: true })
|
||||
notes: string | null;
|
||||
|
||||
@OneToMany(() => TrackEntity, (track: TrackEntity) => track.log, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
@OneToMany(() => TrackEntity, (track: TrackEntity) => track.log)
|
||||
tracks: TrackEntity[]
|
||||
|
||||
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.logs)
|
||||
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.logs, {onDelete: 'CASCADE', onUpdate: 'CASCADE'})
|
||||
@JoinColumn({ name: 'pilotId' })
|
||||
pilot: PilotEntity;
|
||||
}
|
||||
@@ -3,18 +3,23 @@ import { Observable, map } from 'rxjs';
|
||||
import { LogEntity } from './log.entity';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { CustomJwtPayload } from 'src/interfaces/customJwtPayload.interface';
|
||||
import { IS_PUBLIC_KEY } from '@noahspan/noahspan-modules';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
export class LogInterceptor implements NestInterceptor {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
const jwtPayload: CustomJwtPayload = jwtDecode(token);
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
return handler.handle().pipe(
|
||||
map((data: LogEntity[]) => {
|
||||
if (data.length > 0 && jwtPayload.roles.includes('Flying.Read')) {
|
||||
const logs = data.map((log: LogEntity) => {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const limitData = (data) => {
|
||||
return data.map((log: LogEntity) => {
|
||||
return {
|
||||
id: log.id,
|
||||
pilot: {
|
||||
@@ -28,11 +33,26 @@ export class LogInterceptor implements NestInterceptor {
|
||||
tracks: log.tracks,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (req.headers.authorization) {
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
const jwtPayload: CustomJwtPayload = jwtDecode(token);
|
||||
|
||||
if (jwtPayload.roles.includes('Flying.Read')) {
|
||||
const logs = limitData(data);
|
||||
|
||||
return logs;
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
} else if (!req.headers.authorization && isPublic) {
|
||||
const publicData = limitData(data)
|
||||
const logs = publicData.slice(0, 5)
|
||||
|
||||
return logs;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { LogController } from './log.controller';
|
||||
import { LogService } from './log.service';
|
||||
// import { AzureTableStorageModule } from '@noahspan/azure-database';
|
||||
import { LogEntity } from './log.entity';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { PilotModule } from 'src/pilot/pilot.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// AzureTableStorageModule.forRootAsync({
|
||||
// imports: [ConfigModule],
|
||||
// useFactory: async (configService: ConfigService) => {
|
||||
// return {
|
||||
// connectionString: configService.get<string>('azureStorageConnectionString')
|
||||
// };
|
||||
// },
|
||||
// inject: [ConfigService]
|
||||
// }),
|
||||
// AzureTableStorageModule.forFeature(Log, {
|
||||
// createTableIfNotExists: false,
|
||||
// table: 'logs'
|
||||
// }),
|
||||
PilotModule,
|
||||
TypeOrmModule.forFeature([LogEntity])
|
||||
],
|
||||
|
||||
@@ -6,19 +6,20 @@ import { LogDto } from './log.dto';
|
||||
import { PilotService } from 'src/pilot/pilot.service';
|
||||
import { PilotEntity } from 'src/pilot/pilot.entity';
|
||||
import { CustomError } from 'src/error/customError';
|
||||
import { TrackService } from 'src/track/track.service';
|
||||
import { FileService } from 'src/file/file.service';
|
||||
|
||||
@Injectable()
|
||||
export class LogService {
|
||||
constructor(
|
||||
@InjectRepository(LogEntity) private readonly logRepository: Repository<LogEntity>,
|
||||
private readonly pilotService: PilotService
|
||||
private readonly fileService: FileService,
|
||||
private readonly pilotService: PilotService,
|
||||
) {}
|
||||
|
||||
async find(id: string): Promise<LogEntity> {
|
||||
const logEntity: LogEntity = await this.logRepository.findOne({
|
||||
where: { id: id },
|
||||
// relations: ['pilot', 'tracks']
|
||||
relations: ['pilot', 'tracks']
|
||||
});
|
||||
|
||||
return logEntity;
|
||||
@@ -55,6 +56,8 @@ export class LogService {
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<DeleteResult> {
|
||||
await this.fileService.deleteFolder('tracks', id);
|
||||
|
||||
return await this.logRepository.delete({ id });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export class MedicalEntity {
|
||||
@Column()
|
||||
expirationDate: Date;
|
||||
|
||||
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.medical)
|
||||
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.medical, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
@JoinColumn({ name: 'pilotId' })
|
||||
pilot: PilotEntity;
|
||||
}
|
||||
@@ -11,19 +11,21 @@ import {
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { PilotDto } from './pilot.dto';
|
||||
import { PilotEntity } from './pilot.entity';
|
||||
import { PilotService } from './pilot.service';
|
||||
import { CustomError } from '../error/customError';
|
||||
import { PilotInterceptor } from './pilot.interceptor';
|
||||
import { AuthGuard } from '@noahspan/noahspan-modules';
|
||||
import { AuthGuard, Public } from '@noahspan/noahspan-modules';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
const reflector = new Reflector();
|
||||
|
||||
@Controller('pilots')
|
||||
@UseInterceptors(new PilotInterceptor())
|
||||
@UseGuards(AuthGuard)
|
||||
@UseInterceptors(new PilotInterceptor(reflector))
|
||||
export class PilotController {
|
||||
constructor(private readonly pilotService: PilotService) {}
|
||||
|
||||
@Get(':id')
|
||||
@Public()
|
||||
async find(@Param('id') id: string) {
|
||||
try {
|
||||
return await this.pilotService.find(id);
|
||||
@@ -35,28 +37,33 @@ export class PilotController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Public()
|
||||
async findAll() {
|
||||
try {
|
||||
return await this.pilotService.findAll();
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
console.log(error)
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UseGuards(AuthGuard)
|
||||
async create(@Body() pilotDto: PilotDto) {
|
||||
try {
|
||||
|
||||
return await this.pilotService.create(pilotDto);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
console.log(error)
|
||||
// const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
// throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@UseGuards(AuthGuard)
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() pilotDto: PilotDto
|
||||
@@ -71,6 +78,7 @@ export class PilotController {
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(AuthGuard)
|
||||
async delete(
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
|
||||
@@ -33,15 +33,15 @@ export class PilotEntity {
|
||||
@Column()
|
||||
userId: string | null;
|
||||
|
||||
@OneToMany(() => LogEntity, (log: LogEntity) => log.pilot, {onDelete: 'CASCADE', onUpdate: 'CASCADE'})
|
||||
@OneToMany(() => LogEntity, (log: LogEntity) => log.pilot)
|
||||
logs: LogEntity[];
|
||||
|
||||
@OneToMany(() => CertificateEntity, (certificate: CertificateEntity) => certificate.pilot, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
@OneToMany(() => CertificateEntity, (certificate: CertificateEntity) => certificate.pilot)
|
||||
certificates: CertificateEntity[];
|
||||
|
||||
@OneToMany(() => EndorsementEntity, (endorsement: EndorsementEntity) => endorsement.pilot, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
@OneToMany(() => EndorsementEntity, (endorsement: EndorsementEntity) => endorsement.pilot)
|
||||
endorsements: EndorsementEntity[];
|
||||
|
||||
@OneToMany(() => MedicalEntity, (medical: MedicalEntity) => medical.pilot, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
@OneToMany(() => MedicalEntity, (medical: MedicalEntity) => medical.pilot)
|
||||
medical: MedicalEntity[];
|
||||
}
|
||||
|
||||
@@ -1,31 +1,49 @@
|
||||
import { CallHandler, ExecutionContext, NestInterceptor, UnauthorizedException } from '@nestjs/common';
|
||||
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { Observable, map } from 'rxjs';
|
||||
import { CustomJwtPayload } from 'src/interfaces/customJwtPayload.interface';
|
||||
import { PilotEntity } from './pilot.entity';
|
||||
import { IS_PUBLIC_KEY } from '@noahspan/noahspan-modules';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
export class PilotInterceptor implements NestInterceptor {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
const jwtPayload: CustomJwtPayload = jwtDecode(token);
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
return handler.handle().pipe(
|
||||
map((data: PilotEntity[]) => {
|
||||
if (data.length && jwtPayload.roles.includes('Flying.Read')) {
|
||||
const pilots = data.map((pilot) => {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const limitData = (data) => {
|
||||
return data.map((pilot: PilotEntity) => {
|
||||
return {
|
||||
id: pilot.id,
|
||||
name: pilot.name
|
||||
};
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
if (req.headers.authorization) {
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
const jwtPayload: CustomJwtPayload = jwtDecode(token);
|
||||
|
||||
if (jwtPayload.roles.includes('Flying.Read')) {
|
||||
const pilots = limitData(data)
|
||||
|
||||
return pilots;
|
||||
} else if (data.length && jwtPayload.roles.includes('Flying.Write')) {
|
||||
return data;
|
||||
} else {
|
||||
return UnauthorizedException;
|
||||
return data;
|
||||
}
|
||||
} else if (!req.headers.authorization && isPublic) {
|
||||
const publicData = limitData(data);
|
||||
const logs = publicData.slice(0,5)
|
||||
|
||||
return logs;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,40 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PilotController } from './pilot.controller';
|
||||
import { PilotService } from './pilot.service';
|
||||
// import { AzureTableStorageModule } from '@noahspan/azure-database';
|
||||
import { PilotEntity } from './pilot.entity';
|
||||
// import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
// import { Log } from 'src/log/log.entity';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// AzureTableStorageModule.forRootAsync({
|
||||
// imports: [ConfigModule],
|
||||
// useFactory: async (configService: ConfigService) => {
|
||||
// return {
|
||||
// connectionString: configService.get<string>('azureStorageConnectionString')
|
||||
// };
|
||||
// },
|
||||
// inject: [ConfigService]
|
||||
// }),
|
||||
// AzureTableStorageModule.forFeature(Log, {
|
||||
// createTableIfNotExists: false,
|
||||
// table: 'logs'
|
||||
// }),
|
||||
// AzureTableStorageModule.forRootAsync({
|
||||
// imports: [ConfigModule],
|
||||
// useFactory: async (configService: ConfigService) => {
|
||||
// return {
|
||||
// connectionString: configService.get<string>('azureStorageConnectionString')
|
||||
// };
|
||||
// },
|
||||
// inject: [ConfigService]
|
||||
// }),
|
||||
// AzureTableStorageModule.forFeature(PilotEntity, {
|
||||
// createTableIfNotExists: false,
|
||||
// table: 'pilots'
|
||||
// }),
|
||||
TypeOrmModule.forFeature([PilotEntity])
|
||||
],
|
||||
controllers: [PilotController],
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpException,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
@@ -18,8 +16,7 @@ import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { TrackService } from './track.service';
|
||||
import { TrackEntity } from './track.entity';
|
||||
import { TrackDto } from './track.dto';
|
||||
import { DeleteResult, InsertResult, UpdateResult } from 'typeorm';
|
||||
import { DeleteResult, InsertResult } from 'typeorm';
|
||||
|
||||
@Controller('tracks')
|
||||
export class TrackController {
|
||||
@@ -28,22 +25,10 @@ export class TrackController {
|
||||
private readonly trackService: TrackService
|
||||
) {}
|
||||
|
||||
// @Get('id')
|
||||
// async find(@Param('id') id: string): Promise<TrackEntity> {
|
||||
// try {
|
||||
// return await this.trackService.find(id);
|
||||
// } catch (error) {
|
||||
// const customError = error as CustomError;
|
||||
|
||||
// throw new HttpException(customError.message, customError.statusCode);
|
||||
// }
|
||||
// }
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Get(':logId')
|
||||
async findAll(@Param('logId') logId: string): Promise<TrackEntity[]> {
|
||||
try {
|
||||
console.log('logId: ' + logId)
|
||||
return await this.trackService.findAll(logId);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
@@ -66,11 +51,10 @@ export class TrackController {
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Delete(':id')
|
||||
async delete(@Param('id') id: string, @Query('fileName') fileName: string, @Query('logId') logId: string): Promise<DeleteResult> {
|
||||
@Delete(':id/:filename/:logId')
|
||||
async delete(@Param('id') id: string, @Query('fileName') filename: string, @Query('logId') logId: string): Promise<DeleteResult> {
|
||||
try {
|
||||
|
||||
return await this.trackService.delete(id, logId, fileName);
|
||||
return await this.trackService.delete(id, logId, filename);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export class TrackEntity {
|
||||
@Column()
|
||||
order: number;
|
||||
|
||||
@ManyToOne(() => LogEntity, (log: LogEntity) => log.tracks)
|
||||
@ManyToOne(() => LogEntity, (log: LogEntity) => log.tracks, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
@JoinColumn({ name: 'logId' })
|
||||
log: LogEntity;
|
||||
}
|
||||
@@ -31,7 +31,6 @@ export class TrackService {
|
||||
const logEntity: LogEntity = await this.logService.find(logId);
|
||||
|
||||
if (logEntity) {
|
||||
console.log(logEntity)
|
||||
const tracks = await this.trackRepository.find({
|
||||
where: { log:
|
||||
{
|
||||
@@ -40,7 +39,6 @@ export class TrackService {
|
||||
},
|
||||
})
|
||||
|
||||
console.log(tracks)
|
||||
return tracks;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -55,7 +53,6 @@ export class TrackService {
|
||||
|
||||
if (logEntity) {
|
||||
const url = await this.fileService.uploadFile(file, this.containerName, logId);
|
||||
console.log(url)
|
||||
const track = this.trackRepository.create({
|
||||
log: logEntity,
|
||||
order: order,
|
||||
@@ -67,7 +64,6 @@ export class TrackService {
|
||||
throw new CustomError('Log not found', 'Not found', 404)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -80,16 +76,6 @@ export class TrackService {
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: string, logId: string, fileName: string): Promise<DeleteResult> {
|
||||
try {
|
||||
await this.fileService.deleteFile(this.containerName, logId, fileName);
|
||||
|
||||
return await this.trackRepository.delete({ id });
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async downloadTrackFile(logId: string, fileName: string): Promise<string> {
|
||||
try {
|
||||
const downloadedFile: string = await this.fileService.downloadFile(this.containerName, logId, fileName);
|
||||
@@ -99,4 +85,14 @@ export class TrackService {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: string, logId: string, fileName: string): Promise<DeleteResult> {
|
||||
try {
|
||||
await this.fileService.deleteFile(this.containerName, logId, fileName);
|
||||
|
||||
return await this.trackRepository.delete({ id });
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ export const { OidcProvider, useOidc, getOidc } = createReactOidc(async () => ({
|
||||
clientId: import.meta.env.VITE_CLIENT_APP_ID,
|
||||
homeUrl: import.meta.env.BASE_URL,
|
||||
scopes: ['email', 'openid', 'profile', `api://${import.meta.env.VITE_API_APP_ID}/user_impersonation`],
|
||||
autoLogin: true,
|
||||
autoLogin: false,
|
||||
postLoginRedirectUrl: '/',
|
||||
noIframe: true
|
||||
}));
|
||||
@@ -1,13 +1,16 @@
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
Icon,
|
||||
IconName,
|
||||
Loading
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { IDialogConfirmationProps } from './IConfirmationDialogProps';
|
||||
// import {
|
||||
// Button,
|
||||
// Dialog,
|
||||
// DialogActions,
|
||||
// DialogContent,
|
||||
// Icon,
|
||||
// IconName,
|
||||
// Loading
|
||||
// } from '@noahspan/noahspan-components';
|
||||
import { Button, Modal, ModalBody, ModalContent, ModalHeader, ModalFooter, Spinner } from '@heroui/react'
|
||||
import { DialogConfirmationProps } from './ConfirmationDialogProps.interface';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCircleCheck, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
const ConfirmationDialog = ({
|
||||
contentText,
|
||||
@@ -16,28 +19,33 @@ const ConfirmationDialog = ({
|
||||
onCancel,
|
||||
onConfirm,
|
||||
title
|
||||
}: IDialogConfirmationProps) => {
|
||||
}: DialogConfirmationProps) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
<Modal
|
||||
isDismissable={false}
|
||||
isKeyboardDismissDisabled={true}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<h3>{title}</h3>
|
||||
<DialogContent>
|
||||
<ModalContent>
|
||||
<ModalHeader>{title}</ModalHeader>
|
||||
<ModalBody>
|
||||
{!isLoading && <div>{contentText}</div>}
|
||||
{isLoading && <Loading size='xl' />}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel} startContent={<Icon iconName={IconName.XMARK} />}>
|
||||
{isLoading && <Spinner size='lg' />}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button onPress={onCancel} startContent={<FontAwesomeIcon icon={faXmark} />}>
|
||||
No
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
startContent={<Icon iconName={IconName.CIRCLE_CHECK} />}
|
||||
color='primary'
|
||||
onPress={onConfirm}
|
||||
startContent={<FontAwesomeIcon icon={faCircleCheck} />}
|
||||
>
|
||||
Yes
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface IDialogConfirmationProps {
|
||||
export interface DialogConfirmationProps {
|
||||
contentText: string;
|
||||
isLoading: boolean;
|
||||
isOpen: boolean;
|
||||
@@ -20,8 +20,9 @@ const Flights = () => {
|
||||
|
||||
if (flights && flights.length > 0) {
|
||||
dispatch({ type: 'SET_FLIGHTS', payload: flights})
|
||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||
} else {
|
||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'default', message: 'There are no flights' }})
|
||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'default', message: 'No flights found' }})
|
||||
}
|
||||
}, [logs])
|
||||
|
||||
|
||||
@@ -64,10 +64,6 @@ const LogForm = () => {
|
||||
}
|
||||
}, [pilots]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(state.isDisabled)
|
||||
}, [state.isDisabled])
|
||||
|
||||
return (
|
||||
<div className='grid grid-cols-12 gap-3'>
|
||||
<div className={`col-span-4 self-center text-small after:content-['*'] after:text-danger after:ms-0.5`}>
|
||||
|
||||
@@ -132,17 +132,11 @@ const Logbook: React.FC<unknown> = () => {
|
||||
>
|
||||
View
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key='tracks'
|
||||
onPress={() => onOpenCloseDrawer(FormMode.EDIT, info.row.original.id)}
|
||||
startContent={<FontAwesomeIcon icon={faMapLocationDot} />}
|
||||
>
|
||||
Tracks
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
<DropdownSection>
|
||||
<DropdownItem
|
||||
key='Delete'
|
||||
onPress={() => onDeleteLog(info.row.original.id)}
|
||||
startContent={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Delete
|
||||
@@ -393,7 +387,6 @@ const Logbook: React.FC<unknown> = () => {
|
||||
};
|
||||
|
||||
const onOpenCloseDrawer= (mode: FormMode, logId?: string) => {
|
||||
console.log(logId)
|
||||
switch (mode) {
|
||||
case FormMode.ADD:
|
||||
case FormMode.EDIT:
|
||||
@@ -423,10 +416,8 @@ const Logbook: React.FC<unknown> = () => {
|
||||
};
|
||||
|
||||
const onDeleteLog = (logId: string) => {
|
||||
dispatch({
|
||||
type: 'SET_DELETE',
|
||||
payload: { isConfirmationDialogOpen: true, selectedLogId: logId }
|
||||
});
|
||||
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_OPEN', payload: true });
|
||||
logbookContext.dispatch({ type: 'SET_SELECTED_LOG_ID', payload: logId })
|
||||
};
|
||||
|
||||
const onConfirmationDialogConfirm = async () => {
|
||||
@@ -436,9 +427,10 @@ const Logbook: React.FC<unknown> = () => {
|
||||
await httpClient.delete(`api/logs/${logbookContext.state.selectedLogId}`);
|
||||
|
||||
dispatch({
|
||||
type: 'SET_DELETE',
|
||||
payload: { isConfirmationDialogOpen: false, selectedLogId: undefined }
|
||||
type: 'SET_IS_CONFIRMATION_DIALOG_OPEN',
|
||||
payload: false
|
||||
});
|
||||
logbookContext.dispatch({ type: 'SET_SELECTED_LOG_ID', payload: '' })
|
||||
await getLogbookEntries();
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError;
|
||||
@@ -454,8 +446,8 @@ const Logbook: React.FC<unknown> = () => {
|
||||
|
||||
const onConfirmationDialogCancel = () => {
|
||||
dispatch({
|
||||
type: 'SET_DELETE',
|
||||
payload: { isConfirmationDialogOpen: false, selectedLogId: undefined }
|
||||
type: 'SET_IS_CONFIRMATION_DIALOG_OPEN',
|
||||
payload: false
|
||||
});
|
||||
};
|
||||
|
||||
@@ -523,7 +515,7 @@ const Logbook: React.FC<unknown> = () => {
|
||||
}
|
||||
</div>
|
||||
{!state.isLoading && state.alert && (
|
||||
<div>
|
||||
<div className='col-span-12 mb-5'>
|
||||
<Alert
|
||||
onClose={() =>
|
||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||
|
||||
@@ -34,7 +34,6 @@ const date: ColumnDef<LogbookEntry> = {
|
||||
header: 'Date',
|
||||
cell: (info: CellContext<LogbookEntry, unknown>) => {
|
||||
const date = new Date(info.getValue() as string);
|
||||
console.log(date)
|
||||
const formattedDate = `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`
|
||||
|
||||
return formattedDate;
|
||||
|
||||
@@ -6,13 +6,7 @@ import { LogbookState } from './LogbookState.interface';
|
||||
|
||||
type Action =
|
||||
| { type: 'SET_COLUMNS'; payload: ColumnDef<LogbookEntry>[] }
|
||||
| {
|
||||
type: 'SET_DELETE';
|
||||
payload: {
|
||||
isConfirmationDialogOpen: boolean;
|
||||
selectedLogId: string | undefined;
|
||||
};
|
||||
}
|
||||
| { type: 'SET_IS_CONFIRMATION_DIALOG_OPEN'; payload: boolean }
|
||||
| { type: 'SET_ENTRIES'; payload: LogbookEntry[] }
|
||||
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
||||
| { type: 'SET_IS_CONFIRMATION_DIALOG_LOADING'; payload: boolean }
|
||||
@@ -39,10 +33,10 @@ export const reducer = (
|
||||
columns: action.payload
|
||||
}
|
||||
}
|
||||
case 'SET_DELETE': {
|
||||
case 'SET_IS_CONFIRMATION_DIALOG_OPEN': {
|
||||
return {
|
||||
...state,
|
||||
isConfirmDialogOpen: action.payload.isConfirmationDialogOpen,
|
||||
isConfirmDialogOpen: action.payload,
|
||||
};
|
||||
}
|
||||
case 'SET_ENTRIES': {
|
||||
|
||||
@@ -1,86 +1,72 @@
|
||||
import { Card, CardBody, CardContent, CardHeader } from "@noahspan/noahspan-components";
|
||||
import { Accordion, AccordionItem, Card, CardBody, CardHeader } from '@heroui/react'
|
||||
import { LogbookCardProps } from "./LogbookCardProps.interface";
|
||||
import ActionMenu from "../actionMenu/ActionMenu";
|
||||
import LogTrackMaps from "../logTrackMaps/LogTrackMaps";
|
||||
|
||||
import TrackMap from "../trackMap/TrackMap";
|
||||
|
||||
const LogbookCard = ({ logs, mode, onDelete, onOpenCloseForm }: LogbookCardProps) => {
|
||||
return (
|
||||
<div>
|
||||
{logs.map((log) => {
|
||||
console.log(log)
|
||||
const date = new Date(log.date);
|
||||
const formattedDate: string = `${date.getMonth()}/${date.getDate()}/${date.getFullYear()}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card key={log.id}>
|
||||
<Card className='p-4' key={log.id}>
|
||||
<CardHeader>
|
||||
<h2 className='font-bold text-2xl'>{formattedDate}</h2>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<CardHeader><ActionMenu id={log.id} onDelete={onDelete!} onOpenCloseForm={onOpenCloseForm!} /></CardHeader>
|
||||
<CardContent>
|
||||
<div>
|
||||
{mode === 'flights' && log.tracks && log.tracks.length > 0 &&
|
||||
<div>
|
||||
<LogTrackMaps
|
||||
<div className='mb-5'>
|
||||
<TrackMap
|
||||
height='400px'
|
||||
logId={log.id}
|
||||
tracks={log.tracks}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
<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>
|
||||
<div className='col-span-6'>
|
||||
<span>{log.aircraftMakeModel}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className='col-span-6 font-bold'>
|
||||
<span>Route From</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className='col-span-6'>
|
||||
<span>{log.routeFrom}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className='col-span-6 font-bold'>
|
||||
<span>Route To</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className='col-span-6'>
|
||||
<span>{log.routeTo}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className='col-span-6 font-bold'>
|
||||
<span>Duration Of Flight</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className='col-span-6'>
|
||||
<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>
|
||||
<div className='col-span-6 font-bold'>
|
||||
<span>Notes</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className='col-span-6'>
|
||||
<span>{log.notes}</span>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
</CardContent>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -9,8 +9,10 @@ import { FormMode } from "../../enums/formMode";
|
||||
import httpClient from "../../httpClient/httpClient";
|
||||
import { AxiosError } from "axios";
|
||||
import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext";
|
||||
import { Key, useState } from "react";
|
||||
|
||||
const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||
const [activeTab, setActiveTab] = useState<Key>('time');
|
||||
const defaultValues = {
|
||||
pilotId: '',
|
||||
date: null,
|
||||
@@ -34,8 +36,7 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: '',
|
||||
tracks: []
|
||||
notes: ''
|
||||
};
|
||||
const methods = useForm();
|
||||
const logbookContext = useLogbookContext()
|
||||
@@ -69,9 +70,19 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectedKeyChanged = (key: React.Key) => {
|
||||
setActiveTab(key)
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
closeButton={
|
||||
<Button isIconOnly>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</Button>
|
||||
}
|
||||
isOpen={logbookContext.state.isDrawerOpen}
|
||||
onClose={onCancel}
|
||||
>
|
||||
<DrawerContent>
|
||||
<FormProvider {...methods}>
|
||||
@@ -91,7 +102,13 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Tabs color='default' fullWidth={true} variant='solid'>
|
||||
<Tabs
|
||||
color='default'
|
||||
fullWidth={true}
|
||||
onSelectionChange={onSelectedKeyChanged}
|
||||
selectedKey={activeTab as string}
|
||||
variant='solid'
|
||||
>
|
||||
<Tab
|
||||
key='time'
|
||||
title={
|
||||
@@ -103,6 +120,7 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||
>
|
||||
<LogForm />
|
||||
</Tab>
|
||||
{logbookContext.state.formMode !== FormMode.ADD &&
|
||||
<Tab
|
||||
key='tracks'
|
||||
title={
|
||||
@@ -114,8 +132,10 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||
>
|
||||
<TracksForm />
|
||||
</Tab>
|
||||
}
|
||||
</Tabs>
|
||||
</DrawerBody>
|
||||
{activeTab !== 'tracks' &&
|
||||
<DrawerFooter>
|
||||
<div className='grid grid-cols-12 gap-3'>
|
||||
<div className='col-span-12 justify-self-end self-center'>
|
||||
@@ -144,6 +164,7 @@ const LogbookDrawer = ({ onOpenClose }: LogbookDrawerProps) => {
|
||||
</div>
|
||||
</div>
|
||||
</DrawerFooter>
|
||||
}
|
||||
</form>
|
||||
</FormProvider>
|
||||
</DrawerContent>
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Key, useEffect, useState } from 'react';
|
||||
import { useForm, Controller, FormProvider } from 'react-hook-form';
|
||||
import {
|
||||
// Button,
|
||||
// Drawer,
|
||||
Icon,
|
||||
IconButton,
|
||||
IconName,
|
||||
// Input,
|
||||
PeoplePicker,
|
||||
StateSelect
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { IPilotFormProps } from './IPilotFormProps';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
@@ -18,11 +8,11 @@ import PilotFormCertificates from '../pilotFormCertificates/PilotFormCertificate
|
||||
import PilotFormEndorsements from '../pilotFormEndorsements/PilotFormEndorsements';
|
||||
import PilotFormMedical from '../pilotFormMedical/PilotFormMedical';
|
||||
import { useOidc } from '../../auth/oidcConfig';
|
||||
import { getOidc } from '../../auth/oidcConfig';
|
||||
import httpClient from '../../httpClient/httpClient';
|
||||
import { Button, Drawer, DrawerHeader, DrawerContent, DrawerBody, DrawerFooter, Input } from '@heroui/react'
|
||||
import { Button, Drawer, DrawerHeader, DrawerContent, DrawerBody, DrawerFooter, Input, Autocomplete, AutocompleteItem, SelectItem, Select } from '@heroui/react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faXmark } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faSave, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { states } from './states';
|
||||
|
||||
const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
pilotId,
|
||||
@@ -31,7 +21,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
onOpenClose
|
||||
}: IPilotFormProps) => {
|
||||
const [peoplePickerValue, setPeoplePickerValue] = useState<string>('');
|
||||
const [peoplePickerResults, setPeoplePickerResults] = useState<any[]>([]);
|
||||
const [peoplePickerResults, setPeoplePickerResults] = useState<Person[]>([]);
|
||||
const [isPeoplePickerLoading, setIsPeoplePickerLoading] =
|
||||
useState<boolean>(false);
|
||||
const [selectedPerson, setSelectedPerson] = useState<Person>({
|
||||
@@ -47,6 +37,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
postalCode: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
userId: ''
|
||||
};
|
||||
const methods = useForm({
|
||||
defaultValues: defaultValues
|
||||
@@ -63,15 +54,10 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
try {
|
||||
if (value !== '') {
|
||||
const searchString: string = value;
|
||||
const oidc = await getOidc();
|
||||
const response: AxiosResponse = await httpClient.get(
|
||||
`api/msgraph/search?search=${searchString}`, {
|
||||
headers: {
|
||||
Authorization: oidc.isUserLoggedIn ? `Bearer ${(await oidc.getTokens()).accessToken}` : ''
|
||||
}
|
||||
}
|
||||
`api/msgraph/search?search=${searchString}`
|
||||
);
|
||||
console.log(response)
|
||||
|
||||
setPeoplePickerResults(response.data);
|
||||
} else {
|
||||
setPeoplePickerResults([]);
|
||||
@@ -83,9 +69,12 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const onPersonSelected = (person: Person) => {
|
||||
methods.setValue('name', person.displayName!.toString());
|
||||
setPeoplePickerValue(person.displayName!);
|
||||
const onPersonSelected = (userPrincipalName: string) => {
|
||||
const person: Person | undefined = peoplePickerResults.find((person) => person.userPrincipalName === userPrincipalName as string);
|
||||
|
||||
methods.setValue('name', person?.displayName!);
|
||||
methods.setValue('userId', person?.userPrincipalName!);
|
||||
setPeoplePickerValue(person?.displayName!);
|
||||
setPeoplePickerResults([])
|
||||
};
|
||||
|
||||
@@ -134,9 +123,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
);
|
||||
const pilot = response.data;
|
||||
|
||||
setSelectedPerson({
|
||||
displayName: pilot.name
|
||||
});
|
||||
setPeoplePickerValue(pilot.name);
|
||||
methods.reset(pilot);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@@ -175,15 +162,19 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
<h6>Name *</h6>
|
||||
</div>
|
||||
<div className='col-span-9'>
|
||||
<PeoplePicker
|
||||
disabled={isDisabled}
|
||||
// loading={isPeoplePickerLoading}
|
||||
onInputChanged={onPeoplePickerSearch}
|
||||
onPersonSelected={onPersonSelected}
|
||||
people={peoplePickerResults}
|
||||
value={peoplePickerValue}
|
||||
width='w-full'
|
||||
/>
|
||||
<Autocomplete
|
||||
inputValue={peoplePickerValue}
|
||||
isLoading={isPeoplePickerLoading}
|
||||
items={peoplePickerResults}
|
||||
onInputChange={onPeoplePickerSearch}
|
||||
onSelectionChange={(key: Key | null) => onPersonSelected(key as string)}
|
||||
>
|
||||
{peoplePickerResults.map((person: Person) => (
|
||||
<AutocompleteItem key={person.userPrincipalName}>
|
||||
{person.displayName}
|
||||
</AutocompleteItem>
|
||||
))}
|
||||
</Autocomplete>
|
||||
</div>
|
||||
{isUserLoggedIn &&
|
||||
<>
|
||||
@@ -244,19 +235,14 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
control={methods.control}
|
||||
rules={{ required: 'A state must be selected' }}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<StateSelect
|
||||
disabled={isDisabled}
|
||||
// error={methods.formState.errors.state ? true : false}
|
||||
// helperText={
|
||||
// methods.formState.errors.state
|
||||
// ? methods.formState.errors.state.message?.toString()
|
||||
// : undefined
|
||||
// }
|
||||
<Select
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
width='w-full'
|
||||
data-testid="pilot-form-state-dropdown"
|
||||
/>
|
||||
selectedKeys={[value]}
|
||||
>
|
||||
{states.map((state) => (
|
||||
<SelectItem key={state.value}>{state.label}</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -369,7 +355,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
? isDisabled
|
||||
: false
|
||||
}
|
||||
startContent={<Icon iconName={IconName.XMARK} />}
|
||||
startContent={<FontAwesomeIcon icon={faXmark} />}
|
||||
onPress={onCancel}
|
||||
data-testid="pilot-cancel-button"
|
||||
>
|
||||
@@ -379,7 +365,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
<Button
|
||||
color='primary'
|
||||
disabled={isDisabled}
|
||||
startContent={<Icon iconName={IconName.SAVE} />}
|
||||
startContent={<FontAwesomeIcon icon={faSave} />}
|
||||
type="submit"
|
||||
data-testid="pilot-save-button"
|
||||
>
|
||||
|
||||
198
client/src/components/pilotForm/states.ts
Normal file
198
client/src/components/pilotForm/states.ts
Normal 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'
|
||||
}
|
||||
];
|
||||
@@ -1,23 +1,10 @@
|
||||
import { useEffect, useReducer, useState } from 'react';
|
||||
import { useEffect, useReducer } from 'react';
|
||||
import PilotForm from '../pilotForm/PilotForm';
|
||||
import {
|
||||
// Alert,
|
||||
// Button,
|
||||
// ColumnDef,
|
||||
// Dropdown,
|
||||
Icon,
|
||||
IconButton,
|
||||
IconName,
|
||||
// Table,
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
import { Pilot } from './Pilot.interface';
|
||||
import { initialState, reducer } from './reducer';
|
||||
import ActionMenu from '../actionMenu/ActionMenu';
|
||||
import ConfirmationDialog from '../confirmationDialog/ConfirmationDialog';
|
||||
import PilotCard from '../pilotCard/PilotCard';
|
||||
import { getOidc, useOidc } from '../../auth/oidcConfig';
|
||||
import { useUserRole } from '../../hooks/userRole/UseUserRole';
|
||||
import { UserRole } from '../../enums/userRole';
|
||||
import httpClient from '../../httpClient/httpClient'
|
||||
@@ -25,26 +12,21 @@ import { ScreenSize } from '../../enums/screenSize';
|
||||
import { useBreakpoints } from '../../hooks/useBreakpoints/UseBreakpoints';
|
||||
import { Alert, Button, Dropdown, DropdownItem, DropdownSection, Table, TableHeader, TableBody, TableColumn, TableRow, TableCell, DropdownTrigger, DropdownMenu } from '@heroui/react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faEllipsisVertical, faPen, faEye, faTrash } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faEllipsisVertical, faPen, faEye, faTrash, faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
const Pilots: React.FC<unknown> = () => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
// const { httpClient } = useHttpClient();
|
||||
const { userRole } = useUserRole();
|
||||
const { screenSize } = useBreakpoints()
|
||||
|
||||
const getPilots = async () => {
|
||||
try {
|
||||
// const oidc = await getOidc();
|
||||
let response: AxiosResponse;
|
||||
// if (oidc.isUserLoggedIn) {
|
||||
// const { accessToken } = await oidc.getTokens();
|
||||
console.log('blah')
|
||||
|
||||
response = await httpClient.get(
|
||||
`api/pilots`
|
||||
);
|
||||
|
||||
console.log(response);
|
||||
if (response.data.length > 0) {
|
||||
dispatch({ type: 'SET_PILOTS', payload: response.data });
|
||||
|
||||
@@ -53,8 +35,8 @@ const Pilots: React.FC<unknown> = () => {
|
||||
}
|
||||
} else {
|
||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'default', message: 'No pilots found.' }})
|
||||
dispatch({ type: 'SET_PILOTS', payload: [] });
|
||||
}
|
||||
// }
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError;
|
||||
|
||||
@@ -68,6 +50,7 @@ const Pilots: React.FC<unknown> = () => {
|
||||
};
|
||||
|
||||
const onOpenClosePilotForm = async (mode: FormMode, pilotId?: string) => {
|
||||
console.log(pilotId)
|
||||
switch (mode) {
|
||||
case FormMode.ADD:
|
||||
case FormMode.EDIT:
|
||||
@@ -96,7 +79,7 @@ const Pilots: React.FC<unknown> = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteEntry = (pilotId: string) => {
|
||||
const onDeletePilot = (pilotId: string) => {
|
||||
dispatch({
|
||||
type: 'SET_DELETE',
|
||||
payload: { isConfirmDialogOpen: true, selectedPilotId: pilotId }
|
||||
@@ -107,7 +90,7 @@ const Pilots: React.FC<unknown> = () => {
|
||||
try {
|
||||
dispatch({ type: 'SET_IS_CONFIRMATION_DIALOG_LOADING', payload: true });
|
||||
|
||||
await httpClient.delete(`api/pilots/pilot/${state.selectedPilotId}`);
|
||||
await httpClient.delete(`api/pilots/${state.selectedPilotId}`);
|
||||
|
||||
dispatch({
|
||||
type: 'SET_DELETE',
|
||||
@@ -144,40 +127,9 @@ const Pilots: React.FC<unknown> = () => {
|
||||
}
|
||||
]
|
||||
|
||||
// const columns: ColumnDef<Pilot>[] = [
|
||||
// {
|
||||
// accessorKey: 'name',
|
||||
// header: 'Name'
|
||||
// },
|
||||
// {
|
||||
// header: 'Actions',
|
||||
// cell: (info: any) => {
|
||||
// return (
|
||||
// <Dropdown
|
||||
// options={[
|
||||
// 'Edit',
|
||||
// 'View',
|
||||
// 'Delete'
|
||||
// ]}
|
||||
// onOptionSelected={() => console.log(info.row.original.id)}
|
||||
// >
|
||||
// <IconButton>
|
||||
// <Icon className='text-xl' iconName={IconName.ELLIPSIS_VERTICAL} />
|
||||
// </IconButton>
|
||||
// </Dropdown>
|
||||
// )
|
||||
// // return <ActionMenu
|
||||
// // id={info.row.original.id}
|
||||
// // onDelete={onDeleteEntry}
|
||||
// // onOpenCloseForm={onOpenClosePilotForm}
|
||||
// // />
|
||||
// }
|
||||
// }
|
||||
// ];
|
||||
|
||||
const renderCell = (pilot: any, columnKey: any) => {
|
||||
const cellValue = pilot[columnKey]
|
||||
console.log(cellValue)
|
||||
|
||||
switch (columnKey) {
|
||||
case 'actions': {
|
||||
return (
|
||||
@@ -191,14 +143,14 @@ const Pilots: React.FC<unknown> = () => {
|
||||
<DropdownSection showDivider>
|
||||
<DropdownItem
|
||||
key='edit'
|
||||
onPress={() => onOpenClosePilotForm(FormMode.EDIT)}
|
||||
onPress={() => onOpenClosePilotForm(FormMode.EDIT, pilot.id)}
|
||||
startContent={<FontAwesomeIcon icon={faPen} />}
|
||||
>
|
||||
Edit
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key='view'
|
||||
onPress={() => onOpenClosePilotForm(FormMode.VIEW)}
|
||||
onPress={() => onOpenClosePilotForm(FormMode.VIEW, pilot.id)}
|
||||
startContent={<FontAwesomeIcon icon={faEye} />}
|
||||
>
|
||||
View
|
||||
@@ -207,6 +159,7 @@ const Pilots: React.FC<unknown> = () => {
|
||||
<DropdownSection>
|
||||
<DropdownItem
|
||||
key='Delete'
|
||||
onPress={() => onDeletePilot(pilot.id)}
|
||||
startContent={<FontAwesomeIcon icon={faTrash} />}
|
||||
>
|
||||
Delete
|
||||
@@ -238,8 +191,8 @@ const Pilots: React.FC<unknown> = () => {
|
||||
{userRole === UserRole.WRITE &&
|
||||
<Button
|
||||
color='primary'
|
||||
onClick={() => onOpenClosePilotForm(FormMode.ADD)}
|
||||
startContent={<Icon iconName={IconName.PLUS} />}
|
||||
onPress={() => onOpenClosePilotForm(FormMode.ADD)}
|
||||
startContent={<FontAwesomeIcon icon={faPlus} />}
|
||||
data-testid="pilot-add-button"
|
||||
>
|
||||
Add Pilot
|
||||
@@ -247,7 +200,7 @@ const Pilots: React.FC<unknown> = () => {
|
||||
}
|
||||
</div>
|
||||
{!state.isLoading && state.alert && (
|
||||
<div>
|
||||
<div className='col-span-12'>
|
||||
<Alert
|
||||
onClose={() =>
|
||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||
@@ -259,7 +212,6 @@ const Pilots: React.FC<unknown> = () => {
|
||||
)}
|
||||
<div className='col-span-12'>
|
||||
{state.pilots.length > 0 && screenSize !== ScreenSize.SM &&
|
||||
// <Table columns={columns} data={state.pilots} />
|
||||
<Table>
|
||||
<TableHeader columns={columns}>
|
||||
{(column) => (
|
||||
@@ -285,7 +237,7 @@ const Pilots: React.FC<unknown> = () => {
|
||||
</Table>
|
||||
}
|
||||
{state.pilots.length > 0 && screenSize === ScreenSize.SM &&
|
||||
<PilotCard pilots={state.pilots} onDelete={onDeleteEntry} onOpenCloseForm={onOpenClosePilotForm} />
|
||||
<PilotCard pilots={state.pilots} onDelete={onDeletePilot} onOpenCloseForm={onOpenClosePilotForm} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -299,7 +251,7 @@ const Pilots: React.FC<unknown> = () => {
|
||||
)}
|
||||
{state.isConfirmDialogOpen && (
|
||||
<ConfirmationDialog
|
||||
contentText="Are you sure you want to delete the pilot entry? Deleting a pilot will delete the pilot and delete all logbook entries for the pilot."
|
||||
contentText="Are you sure you want to delete the pilot entry? Deleting a pilot will delete the pilot and delete all of the pilot's logbook entries."
|
||||
isLoading={state.isConfirmDialogLoading}
|
||||
isOpen={state.isConfirmDialogOpen}
|
||||
onCancel={onConfirmationDialogCancel}
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppContext } from '../../hooks/appContext/UseAppContext';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { User } from '@microsoft/microsoft-graph-types';
|
||||
import { getOidc, useOidc } from '../../auth/oidcConfig';
|
||||
import { Button, Link, Navbar, NavbarBrand, NavbarContent, NavbarItem, NavbarMenuToggle, NavbarMenu, NavbarMenuItem } from '@heroui/react';
|
||||
import { useOidc } from '../../auth/oidcConfig';
|
||||
import { Avatar, Button, Link, Navbar, NavbarBrand, NavbarContent, NavbarItem, DropdownTrigger, DropdownMenu, DropdownItem, Dropdown } from '@heroui/react';
|
||||
import httpClient from '../../httpClient/httpClient'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faPlane } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faPlane, faSignIn, faSignOut } from '@fortawesome/free-solid-svg-icons'
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
const SiteNav = () => {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [userPhoto, setUserPhoto] = useState<string>();
|
||||
const [pages, setPages] = useState<{ name: string; path: string; }[]>([]);
|
||||
const appContext = useAppContext();
|
||||
const { isUserLoggedIn, login, logout } = useOidc()
|
||||
const navigate = useNavigate();
|
||||
const { isUserLoggedIn, logout, login } = useOidc()
|
||||
const { pathname } = useLocation()
|
||||
const getPages = () => {
|
||||
const pages = [
|
||||
{
|
||||
name: 'Flights',
|
||||
@@ -33,28 +28,9 @@ const SiteNav = () => {
|
||||
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> => {
|
||||
try {
|
||||
const response: AxiosResponse = await httpClient.get(`api/user/profile`, {
|
||||
// headers: {
|
||||
// Authorization: `Bearer ${accessToken}`
|
||||
// }
|
||||
});
|
||||
const response: AxiosResponse = await httpClient.get(`api/msgraph/profile`);
|
||||
const userProfile: User = response.data;
|
||||
|
||||
return userProfile;
|
||||
@@ -64,10 +40,7 @@ const SiteNav = () => {
|
||||
};
|
||||
const getUserPhoto = async (): Promise<string> => {
|
||||
try {
|
||||
const response: AxiosResponse = await httpClient.get(`api/user/photo`, {
|
||||
// headers: {
|
||||
// Authorization: accessToken
|
||||
// },
|
||||
const response: AxiosResponse = await httpClient.get(`api/msgraph/photo`, {
|
||||
responseType: 'arraybuffer'
|
||||
});
|
||||
const arrayBufferView = new Uint8Array(response.data);
|
||||
@@ -79,45 +52,21 @@ const SiteNav = () => {
|
||||
throw new Error();
|
||||
}
|
||||
};
|
||||
const handlePageClick = (url: string) => {
|
||||
navigate(url);
|
||||
};
|
||||
|
||||
// const Settings = () => {
|
||||
// return (
|
||||
// <div>
|
||||
// <Icon iconName={IconName.SIGN_OUT} />
|
||||
// <span>
|
||||
// Sign Out
|
||||
// </span>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
useEffect(() => {
|
||||
const setUserProfile = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const userProfile = await getUserProfile();
|
||||
const userPhoto = await getUserPhoto();
|
||||
|
||||
// const userProfile = await getUserProfile();
|
||||
// const userPhoto = await getUserPhoto();
|
||||
|
||||
// setUserPhoto(userPhoto);
|
||||
|
||||
// appContext.dispatch({
|
||||
// type: 'SET_USER_PROFILE',
|
||||
// payload: userProfile
|
||||
// });
|
||||
const oidc = await getOidc();
|
||||
|
||||
if (oidc.isUserLoggedIn) {
|
||||
console.log((await oidc.getTokens()).accessToken)
|
||||
}
|
||||
setUserPhoto(userPhoto);
|
||||
|
||||
appContext.dispatch({
|
||||
type: 'SET_USER_PROFILE',
|
||||
payload: userProfile
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -125,23 +74,13 @@ const SiteNav = () => {
|
||||
isUserLoggedIn &&
|
||||
Object.keys(appContext.state.userProfile).length === 0
|
||||
) {
|
||||
console.log(isUserLoggedIn)
|
||||
console.log()
|
||||
setUserProfile();
|
||||
getPages();
|
||||
}
|
||||
}, [isUserLoggedIn]);
|
||||
|
||||
useEffect(() => {
|
||||
getPages();
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(pathname)
|
||||
}, [pathname])
|
||||
|
||||
return (
|
||||
<Navbar isBordered maxWidth='full' position='static'>
|
||||
<NavbarContent>
|
||||
<NavbarBrand>
|
||||
<img
|
||||
height={35}
|
||||
@@ -151,10 +90,11 @@ const SiteNav = () => {
|
||||
/>
|
||||
<FontAwesomeIcon icon={faPlane} size='2x' />
|
||||
</NavbarBrand>
|
||||
</NavbarContent>
|
||||
<NavbarContent justify='center'>
|
||||
{pages.length > 0 && pages.map((page) => {
|
||||
{pages.length > 0 && pages.map((page, index) => {
|
||||
return (
|
||||
<NavbarItem isActive={pathname === page.path ? true : false}>
|
||||
<NavbarItem isActive={pathname === page.path ? true : false} key={index}>
|
||||
<Link color={pathname === page.path ? 'primary' : 'foreground'} href={page.path}>
|
||||
{page.name}
|
||||
</Link>
|
||||
@@ -163,9 +103,27 @@ const SiteNav = () => {
|
||||
})}
|
||||
</NavbarContent>
|
||||
<NavbarContent justify='end'>
|
||||
<Button color='default' onClick={handleSignIn} variant='flat'>
|
||||
{!isUserLoggedIn &&
|
||||
<Button
|
||||
color='default'
|
||||
onPress={() => login()}
|
||||
startContent={<FontAwesomeIcon icon={faSignIn} />}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
}
|
||||
{isUserLoggedIn &&
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Avatar name={appContext.state.userProfile.displayName?.toString()} src={userPhoto}></Avatar>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu>
|
||||
<DropdownItem key='signout' onPress={() => logout({redirectTo: 'specific url', url: '/'})} startContent={<FontAwesomeIcon icon={faSignOut} />}>
|
||||
Sign Out
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
}
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
);
|
||||
|
||||
0
client/src/components/stateSelect/StateSelect.tsx
Normal file
0
client/src/components/stateSelect/StateSelect.tsx
Normal file
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { LogTrackMapsProps } from './LogTrackMapsProps.interface';
|
||||
import { Suspense, useEffect, useState } from 'react';
|
||||
import { TrackMapProps } from './TrackMapProps.interface';
|
||||
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { useAuth } from 'react-oidc-context'
|
||||
import { MapContainer, TileLayer } from 'react-leaflet';
|
||||
@@ -7,16 +7,15 @@ import ReactLeafletKml from 'react-leaflet-kml';
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/pagination';
|
||||
import 'swiper/css';
|
||||
import './LogTrackMaps.css';
|
||||
import './TrackMap.css';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import httpClient from '../../httpClient/httpClient'
|
||||
|
||||
const LogTrackMaps = ({ logId, tracks }: LogTrackMapsProps) => {
|
||||
const TrackMap = ({ height, logId, tracks }: TrackMapProps) => {
|
||||
const [kmls, setKmls] = useState<any[]>([])
|
||||
const auth = useAuth()
|
||||
const auth = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
console.log(tracks)
|
||||
const getTracks = async () => {
|
||||
const convertedTracks: any[] = []
|
||||
|
||||
@@ -24,12 +23,7 @@ const LogTrackMaps = ({ logId, tracks }: LogTrackMapsProps) => {
|
||||
const trackUrlSplit = track.url.split('/')
|
||||
const filename = trackUrlSplit[trackUrlSplit.length - 1];
|
||||
const response: AxiosResponse = await httpClient.get(
|
||||
`api/tracks/${logId}/${filename}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: auth.user?.access_token
|
||||
}
|
||||
}
|
||||
`api/tracks/${logId}/${filename}`
|
||||
);
|
||||
const kml = new DOMParser().parseFromString(response.data, 'text/xml')
|
||||
|
||||
@@ -40,15 +34,16 @@ const LogTrackMaps = ({ logId, tracks }: LogTrackMapsProps) => {
|
||||
}
|
||||
|
||||
getTracks();
|
||||
}, [tracks])
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<MapContainer
|
||||
center={[45.14489, -93.21019]}
|
||||
scrollWheelZoom={false}
|
||||
style={{ height: '500px', width: '100%' }}
|
||||
style={{ height: height, width: '100%' }}
|
||||
zoom={8}
|
||||
>
|
||||
<Suspense>
|
||||
<TileLayer
|
||||
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
@@ -56,8 +51,9 @@ const LogTrackMaps = ({ logId, tracks }: LogTrackMapsProps) => {
|
||||
{kmls.length > 0 && kmls.map((kml) => (
|
||||
<ReactLeafletKml kml={kml} />
|
||||
))}
|
||||
</Suspense>
|
||||
</MapContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogTrackMaps;
|
||||
export default TrackMap;
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface LogTrackMapsProps {
|
||||
export interface TrackMapProps {
|
||||
height: string;
|
||||
logId: string;
|
||||
tracks: {id: string; order: number; url: string}[];
|
||||
}
|
||||
@@ -1,31 +1,44 @@
|
||||
import { useEffect, useReducer } from "react";
|
||||
// import { Button, Drawer, Icon, IconButton, IconName, Input, Loading } from "@noahspan/noahspan-components";
|
||||
import { AxiosError, AxiosInstance, AxiosResponse } from "axios";
|
||||
import { useEffect, useReducer, useRef } from "react";
|
||||
import { AxiosError, AxiosResponse } from "axios";
|
||||
import { TracksFormProps } from "./TracksFormProps.interface";
|
||||
import { FormMode } from "../../enums/formMode";
|
||||
import { LogbookEntry } from "../logbook/LogbookEntry.interface";
|
||||
import ConfirmationDialog from "../confirmationDialog/ConfirmationDialog";
|
||||
import { initialState, reducer } from "./reducer";
|
||||
import LogTrackMaps from "../logTrackMaps/LogTrackMaps";
|
||||
import TrackMap from "../trackMap/TrackMap";
|
||||
import httpClient from "../../httpClient/httpClient";
|
||||
import { Button, Drawer, DrawerContent, DrawerBody, DrawerHeader, Input, Spinner, DrawerFooter } from '@heroui/react';
|
||||
import { Button, Input, Spinner } from '@heroui/react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faUpload, faTrash, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useLogbookContext } from "../../hooks/logbookContext/UseLogbookContext";
|
||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||
|
||||
|
||||
const TracksForm = () => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState)
|
||||
const logbookContext = useLogbookContext();
|
||||
|
||||
const getTracks = async () => {
|
||||
try {
|
||||
const response: AxiosResponse = await httpClient.get(
|
||||
`api/tracks/${logbookContext.state.selectedLogId}`
|
||||
)
|
||||
|
||||
const tracks = response.data;
|
||||
|
||||
dispatch({ type: 'SET_TRACKS', payload: tracks })
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError;
|
||||
|
||||
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: { severity: 'danger', message: axiosError.message }})
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
console.log('blah')
|
||||
try {
|
||||
dispatch({ type: 'SET_IS_LOADING', payload: true})
|
||||
|
||||
const file = event.target.files![0]
|
||||
const formData = new FormData();
|
||||
const order = state.tracks.length + 1
|
||||
|
||||
formData.append('file', file);
|
||||
|
||||
@@ -33,9 +46,10 @@ const TracksForm = () => {
|
||||
config.headers["Content-Type"] = 'multipart/form-data'
|
||||
|
||||
return config
|
||||
})
|
||||
});
|
||||
|
||||
const uploadResponse: AxiosResponse = await httpClient.post(`api/tracks/${logbookContext.state.selectedLogId}/1`, formData);
|
||||
await httpClient.post(`api/tracks/${logbookContext.state.selectedLogId}/${order}`, formData);
|
||||
await getTracks();
|
||||
// const uploadUrl = uploadResponse.data.url;
|
||||
// const tracks: string[] = log.tracks ? JSON.parse(log.tracks!) : [];
|
||||
|
||||
@@ -53,49 +67,77 @@ const TracksForm = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// const onDeleteTrack = async (index: number) => {
|
||||
// dispatch({ type: 'SET_ON_DELETE_TRACK', payload: { isConfirmDialogOpen: true, selectedTrack: { index: index }}})
|
||||
// }
|
||||
const onDeleteTrack = async (id: string, filename: string, index: number) => {
|
||||
dispatch({ type: 'SET_ON_DELETE_TRACK', payload: { isConfirmDialogOpen: true, selectedTrack: { id: id, filename: filename, index: index }}})
|
||||
}
|
||||
|
||||
const onConfirmDialogConfirm = async () => {
|
||||
try {
|
||||
await httpClient.delete(`api/tracks/${state.selectedTrack!.id}/${state.selectedTrack!.filename}/${logbookContext.state.selectedLogId}`);
|
||||
|
||||
await getTracks();
|
||||
|
||||
// tracks.splice(state.selectedTrack!.index, 1);
|
||||
// log.tracks = JSON.stringify(tracks);
|
||||
// await httpClient.put(`api/logs/log/${selectedRowKey}`, log, config);
|
||||
|
||||
// const updatedLog = await getLog();
|
||||
|
||||
// dispatch({ type: 'SET_TRACKS', payload: JSON.parse(updatedLog.tracks!) })
|
||||
dispatch({ type: 'SET_IS_CONFIRM_DIALOG_OPEN', payload: false })
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
const onConfirmDialogCancel = async () => {
|
||||
dispatch({ type: 'SET_IS_CONFIRM_DIALOG_OPEN', payload: false })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const getTracks = async () => {
|
||||
try {
|
||||
const response: AxiosResponse = await httpClient.get(
|
||||
`api/tracks/${logbookContext.state.selectedLogId}`
|
||||
)
|
||||
|
||||
const tracks = response.data;
|
||||
|
||||
dispatch({ type: 'SET_TRACKS', payload: tracks })
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError;
|
||||
|
||||
logbookContext.dispatch({ type: 'SET_FORM_ALERT', payload: { severity: 'danger', message: axiosError.message }})
|
||||
}
|
||||
}
|
||||
|
||||
if (logbookContext.state.selectedLogId) {
|
||||
getTracks();
|
||||
}
|
||||
}, [logbookContext.state.selectedLogId])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(logbookContext.state.formMode)
|
||||
if (logbookContext.state.formMode === FormMode.VIEW) {
|
||||
dispatch({ type: 'SET_IS_DISABLED', payload: true });
|
||||
}
|
||||
}, [logbookContext.state.formMode]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(state.isDisabled)
|
||||
}, [state.isDisabled])
|
||||
|
||||
return (
|
||||
<div className='grid grid-cols-12 gap-3'>
|
||||
<>
|
||||
{state.tracks.length > 0 &&
|
||||
<div className="col-span-12">
|
||||
<TrackMap height='400px' logId={logbookContext.state.selectedLogId!} tracks={state.tracks} />
|
||||
</div>
|
||||
}
|
||||
<>
|
||||
{state.tracks.length > 0 && state.tracks.map((track, index) => {
|
||||
const filename: string = track.url.substring(track.url.lastIndexOf('/') + 1);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='col-span-10'>
|
||||
<Input type='text' />
|
||||
<Input isDisabled={state.isDisabled} key={index} type='text' value={filename}/>
|
||||
</div>
|
||||
<div className='col-span-2'>
|
||||
<Button isIconOnly onPress={() => console.log('delete')}><FontAwesomeIcon icon={faTrash} /></Button>
|
||||
<Button isDisabled={state.isDisabled} key={index} isIconOnly onPress={() => onDeleteTrack(track.id, filename, index)}><FontAwesomeIcon icon={faTrash} /></Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
)
|
||||
})}
|
||||
<div className='col-span-12'>
|
||||
<Button
|
||||
as='label'
|
||||
disabled={state.isLoading ? true : false}
|
||||
color='primary'
|
||||
isDisabled={state.isDisabled}
|
||||
fullWidth={true}
|
||||
startContent={<FontAwesomeIcon icon={faUpload} />}
|
||||
>
|
||||
@@ -103,6 +145,16 @@ const TracksForm = () => {
|
||||
<input hidden onChange={handleFileUpload} type='file' />
|
||||
</Button>
|
||||
</div>
|
||||
{state.isConfirmDialogOpen && (
|
||||
<ConfirmationDialog
|
||||
contentText="Are you sure you want to delete this track?"
|
||||
isLoading={state.isConfirmDialogLoading}
|
||||
isOpen={state.isConfirmDialogOpen}
|
||||
onCancel={onConfirmDialogCancel}
|
||||
onConfirm={onConfirmDialogConfirm}
|
||||
title="Confirm Delete"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
@@ -136,31 +188,7 @@ const TracksForm = () => {
|
||||
// dispatch({ type: 'SET_ON_DELETE_TRACK', payload: { isConfirmDialogOpen: true, selectedTrack: { fileName: fileName, index: index }}})
|
||||
// }
|
||||
|
||||
// const onConfirmDialogConfirm = async () => {
|
||||
// try {
|
||||
// const config = await getConfig();
|
||||
|
||||
// await httpClient.delete(`api/logs/log/${selectedRowKey}/track?fileName=${state.selectedTrack!.fileName}`, config);
|
||||
|
||||
// const log = await getLog();
|
||||
// const tracks: string[] = JSON.parse(log.tracks!);
|
||||
|
||||
// tracks.splice(state.selectedTrack!.index, 1);
|
||||
// log.tracks = JSON.stringify(tracks);
|
||||
// await httpClient.put(`api/logs/log/${selectedRowKey}`, log, config);
|
||||
|
||||
// const updatedLog = await getLog();
|
||||
|
||||
// dispatch({ type: 'SET_TRACKS', payload: JSON.parse(updatedLog.tracks!) })
|
||||
// dispatch({ type: 'SET_IS_CONFIRM_DIALOG_OPEN', payload: false })
|
||||
// } catch (error) {
|
||||
// console.log(error);
|
||||
// }
|
||||
// }
|
||||
|
||||
// const onConfirmDialogCancel = async () => {
|
||||
// dispatch({ type: 'SET_IS_CONFIRM_DIALOG_OPEN', payload: false })
|
||||
// }
|
||||
|
||||
// useEffect(() => {
|
||||
// const updateTracks = async () => {
|
||||
@@ -204,19 +232,8 @@ const TracksForm = () => {
|
||||
// })}
|
||||
// </>
|
||||
// }
|
||||
{/* {logbookContext.state.formMode === FormMode.VIEW &&
|
||||
<LogTrackMaps logId={logbookContext.state.selectedLogId!} tracks={state.tracks} />
|
||||
} */}
|
||||
{/* {state.isConfirmDialogOpen && (
|
||||
<ConfirmationDialog
|
||||
contentText="Are you sure you want to delete this track?"
|
||||
isLoading={state.isConfirmDialogLoading}
|
||||
isOpen={state.isConfirmDialogOpen}
|
||||
onCancel={onConfirmDialogCancel}
|
||||
onConfirm={onConfirmDialogConfirm}
|
||||
title="Confirm Delete"
|
||||
/>
|
||||
)} */}
|
||||
|
||||
|
||||
// </div>
|
||||
// )
|
||||
// }
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
export interface TracksFormState {
|
||||
isConfirmDialogOpen: boolean;
|
||||
isConfirmDialogLoading: boolean;
|
||||
isDisabled: boolean;
|
||||
isLoading: boolean;
|
||||
selectedTrack: {
|
||||
fileName: string,
|
||||
id: string,
|
||||
filename: string,
|
||||
index: number
|
||||
} | undefined;
|
||||
tracks: { id: string; order: number; url: string; }[];
|
||||
|
||||
@@ -3,13 +3,15 @@ import { TracksFormState } from "./TracksFormState.interface";
|
||||
type Action =
|
||||
| { type: 'SET_IS_CONFIRM_DIALOG_OPEN'; payload: boolean }
|
||||
| { type: 'SET_IS_CONFORM_DIALOG_LOADING'; payload: boolean }
|
||||
| { type: 'SET_IS_DISABLED'; payload: boolean }
|
||||
| { type: 'SET_IS_LOADING'; payload: boolean }
|
||||
| { type: 'SET_ON_DELETE_TRACK'; payload: { isConfirmDialogOpen: boolean, selectedTrack: { fileName: string, index: number } }}
|
||||
| { type: 'SET_ON_DELETE_TRACK'; payload: { isConfirmDialogOpen: boolean, selectedTrack: { id: string, filename: string, index: number } }}
|
||||
| { type: 'SET_TRACKS'; payload: { id: string; order: number; url: string }[] };
|
||||
|
||||
export const initialState: TracksFormState = {
|
||||
isConfirmDialogOpen: false,
|
||||
isConfirmDialogLoading: false,
|
||||
isDisabled: false,
|
||||
isLoading: false,
|
||||
selectedTrack: undefined,
|
||||
tracks: []
|
||||
@@ -29,6 +31,12 @@ export const reducer = (state: TracksFormState, action: Action): TracksFormState
|
||||
isConfirmDialogLoading: action.payload
|
||||
}
|
||||
}
|
||||
case 'SET_IS_DISABLED': {
|
||||
return {
|
||||
...state,
|
||||
isDisabled: action.payload
|
||||
}
|
||||
}
|
||||
case 'SET_IS_LOADING': {
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -23,7 +23,7 @@ export const usePilots = () => {
|
||||
const response: AxiosResponse = await httpClient.get(
|
||||
`/api/pilots`
|
||||
);
|
||||
console.log(response)
|
||||
|
||||
setPilots(response.data);
|
||||
} catch (error) {
|
||||
return error;
|
||||
|
||||
@@ -7,7 +7,6 @@ import { BrowserRouter } from 'react-router-dom';
|
||||
import { OidcProvider } from './auth/oidcConfig.ts';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<OidcProvider>
|
||||
<AppContextProvider>
|
||||
<LogbookContextProvider>
|
||||
@@ -17,5 +16,4 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
</LogbookContextProvider>
|
||||
</AppContextProvider>
|
||||
</OidcProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
/* body {
|
||||
@apply bg-base-300;
|
||||
} */
|
||||
@theme inline {
|
||||
--color-primary: #000000;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1,7 +1,12 @@
|
||||
locals {
|
||||
app_name = {
|
||||
test = "flying-app-test"
|
||||
prod = "flying-app-prod"
|
||||
test = "flying-test"
|
||||
prod = "flying-prod"
|
||||
}
|
||||
|
||||
container_app_environment_name = {
|
||||
test = "noahspan-test"
|
||||
prod = "noahspan-prod"
|
||||
}
|
||||
|
||||
container_image = {
|
||||
|
||||
@@ -2,6 +2,10 @@ output "app_name" {
|
||||
value = local.app_name[var.environment]
|
||||
}
|
||||
|
||||
output "container_app_environment_name" {
|
||||
value = local.container_app_environment_name[var.environment]
|
||||
}
|
||||
|
||||
output "container_image" {
|
||||
value = local.container_image[var.environment]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
output "name" {
|
||||
value = local.name[var.environment]
|
||||
}
|
||||
|
||||
output "log_analytics_workspace_name" {
|
||||
value = local.log_analytics_workspace_name[var.environment]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
variable "environment" {}
|
||||
@@ -1,18 +1,8 @@
|
||||
module "api" {
|
||||
source = "./api"
|
||||
environment = var.environment
|
||||
}
|
||||
|
||||
module "app" {
|
||||
source = "./app"
|
||||
environment = var.environment
|
||||
}
|
||||
|
||||
module "container_app_environment" {
|
||||
source = "./container_app_environment"
|
||||
environment = var.environment
|
||||
}
|
||||
|
||||
module "storage" {
|
||||
source = "./storage"
|
||||
environment = var.environment
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
output "api" {
|
||||
value = module.api
|
||||
}
|
||||
|
||||
output "app" {
|
||||
value = module.app
|
||||
}
|
||||
|
||||
output "container_app_environment" {
|
||||
value = module.container_app_environment
|
||||
}
|
||||
|
||||
output "storage" {
|
||||
value = module.storage
|
||||
}
|
||||
165
infrastructure/container_app.tf
Normal file
165
infrastructure/container_app.tf
Normal 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 ]
|
||||
}
|
||||
}
|
||||
8
infrastructure/container_app_environment_storage.tf
Normal file
8
infrastructure/container_app_environment_storage.tf
Normal 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"
|
||||
}
|
||||
@@ -1,216 +1,10 @@
|
||||
module "storage" {
|
||||
source = "github.com/noahspannbauer/noahspan-terraform/modules/storage"
|
||||
resource_group_name = var.RESOURCE_GROUP_NAME
|
||||
storage_account_name = module.environment.storage.account_name
|
||||
storage_containers = module.environment.storage.containers
|
||||
storage_shares = module.environment.storage.shares
|
||||
storage_tables = module.environment.storage.tables
|
||||
data "azurerm_client_config" "current" {}
|
||||
|
||||
data "azurerm_resource_group" "resource_group" {
|
||||
name = var.RESOURCE_GROUP_NAME
|
||||
}
|
||||
|
||||
module "container_app_environment" {
|
||||
source = "github.com/noahspannbauer/noahspan-terraform/modules/container_app_environment"
|
||||
container_app_environment_name = module.environment.container_app_environment.name
|
||||
log_analytics_workspace_name = module.environment.container_app_environment.log_analytics_workspace_name
|
||||
log_analytics_workspace_retention_in_days = 30
|
||||
log_analytics_workspace_sku = "PerGB2018"
|
||||
data "azurerm_container_app_environment" "container_app_environment" {
|
||||
name = module.environment.app.container_app_environment_name
|
||||
resource_group_name = var.RESOURCE_GROUP_NAME
|
||||
}
|
||||
|
||||
module "container_app_environment_storage" {
|
||||
source = "github.com/noahspannbauer/noahspan-terraform/modules/container_app_environment_storage"
|
||||
name = "${module.container_app_environment.name}-files"
|
||||
container_app_environment_id = module.container_app_environment.id
|
||||
account_name = module.storage.name
|
||||
share_name = module.environment.storage.shares[0]
|
||||
access_key = module.storage.primary_access_key
|
||||
access_mode = "ReadWrite"
|
||||
}
|
||||
|
||||
module "api" {
|
||||
source = "github.com/noahspannbauer/noahspan-terraform/modules/container_app"
|
||||
containers = [
|
||||
{
|
||||
cpu = 0.25
|
||||
envs = [
|
||||
{
|
||||
name = "AZURE_STORAGE_CONNECTION_STRING"
|
||||
secret_name = "azure-storage-connection-string"
|
||||
},
|
||||
{
|
||||
name = "CLIENT_ID"
|
||||
secret_name = "client-id"
|
||||
},
|
||||
{
|
||||
name = "CLIENT_SECRET"
|
||||
secret_name = "client-secret"
|
||||
},
|
||||
{
|
||||
name = "TENANT_ID"
|
||||
secret_name = "tenant-id"
|
||||
},
|
||||
{
|
||||
name = "DB_PATH"
|
||||
secret_name = "db-path"
|
||||
},
|
||||
{
|
||||
name = "DB_SYNC"
|
||||
secret_name = "db-sync"
|
||||
}
|
||||
]
|
||||
image = module.environment.api.container_image
|
||||
memory = "0.5Gi"
|
||||
name = module.environment.api.container_name
|
||||
startup_probe = [
|
||||
{
|
||||
failure_count_threshold = 10
|
||||
initial_delay = 1
|
||||
interval_seconds = 2
|
||||
path = "/api/health"
|
||||
port = 3000
|
||||
transport = "HTTP"
|
||||
}
|
||||
]
|
||||
volume_mounts = [
|
||||
{
|
||||
name = "${module.container_app_environment.name}-emptydir"
|
||||
path = "/var/lib/data"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
args = ["replicate"]
|
||||
cpu = 0.25
|
||||
image = "docker.io/litestream/litestream:0.3.13"
|
||||
memory = "0.5Gi"
|
||||
name = "replicate"
|
||||
volume_mounts = [
|
||||
{
|
||||
name = "${module.container_app_environment.name}-emptydir"
|
||||
path = "/var/lib/data"
|
||||
},
|
||||
{
|
||||
name = "${module.container_app_environment.name}-fileshare"
|
||||
path = "/mnt/data"
|
||||
sub_path = "data"
|
||||
},
|
||||
{
|
||||
name = "${module.container_app_environment.name}-fileshare"
|
||||
path = "/etc"
|
||||
sub_path = "litestream"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
container_app_name = module.environment.api.app_name
|
||||
container_app_environment_id = module.container_app_environment.id
|
||||
custom_domain_count = module.environment.api.custom_domain_count
|
||||
init_containers = [
|
||||
{
|
||||
args = ["restore", "-if-db-not-exists", "-if-replica-exists", "/var/lib/data/flying.db"]
|
||||
cpu = 0.25
|
||||
image = "docker.io/litestream/litestream:0.3.13"
|
||||
memory = "0.5Gi"
|
||||
name = "restore"
|
||||
volume_mounts = [
|
||||
{
|
||||
name = "${module.container_app_environment.name}-emptydir"
|
||||
path = "/var/lib/data"
|
||||
},
|
||||
{
|
||||
name = "${module.container_app_environment.name}-fileshare"
|
||||
path = "/mnt/data"
|
||||
sub_path = "data"
|
||||
},
|
||||
{
|
||||
name = "${module.container_app_environment.name}-fileshare"
|
||||
path = "/etc"
|
||||
sub_path = "litestream"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
ingress_external_enabled = module.environment.api.ingress_external_enabled
|
||||
ingress_target_port = module.environment.api.ingress_target_port
|
||||
ingress_transport = module.environment.api.ingress_transport
|
||||
registry_password = var.DOCKER_IO_PASSWORD
|
||||
registry_username = var.DOCKER_IO_USERNAME
|
||||
registry_password_secret_name = "docker-io-password"
|
||||
registry_server_name = "docker.io"
|
||||
resource_group_name = var.RESOURCE_GROUP_NAME
|
||||
secrets = [
|
||||
{
|
||||
name = "azure-storage-connection-string"
|
||||
value = module.storage.primary_connection_string
|
||||
},
|
||||
{
|
||||
name = "docker-io-password"
|
||||
value = var.DOCKER_IO_PASSWORD
|
||||
},
|
||||
{
|
||||
name = "client-id"
|
||||
value = var.CLIENT_ID
|
||||
},
|
||||
{
|
||||
name = "client-secret"
|
||||
value = var.CLIENT_SECRET
|
||||
},
|
||||
{
|
||||
name = "tenant-id"
|
||||
value = var.TENANT_ID
|
||||
},
|
||||
{
|
||||
name = "db-path"
|
||||
value = var.DB_PATH
|
||||
},
|
||||
{
|
||||
name = "db-sync"
|
||||
value = var.DB_SYNC
|
||||
}
|
||||
]
|
||||
storage_account_primary_connection_string = module.storage.primary_connection_string
|
||||
traffic_weight_percentage = module.environment.api.traffic_weight_percentage
|
||||
template_min_replicas = module.environment.api.template_min_replicas
|
||||
template_max_replicas = module.environment.api.template_max_replicas
|
||||
volume = [
|
||||
{
|
||||
name = "${module.container_app_environment.name}-fileshare"
|
||||
storage_name = module.container_app_environment_storage.name
|
||||
storage_type = "AzureFile"
|
||||
},
|
||||
{
|
||||
name = "${module.container_app_environment.name}-emptydir"
|
||||
storage_type = "EmptyDir"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
module "app" {
|
||||
source = "github.com/noahspannbauer/noahspan-terraform/modules/container_app"
|
||||
containers = [
|
||||
{
|
||||
cpu = 0.25
|
||||
image = module.environment.app.container_image
|
||||
memory = "0.5Gi"
|
||||
name = module.environment.app.container_name
|
||||
}
|
||||
]
|
||||
container_app_name = module.environment.app.app_name
|
||||
container_app_environment_id = module.container_app_environment.id
|
||||
custom_domain_count = module.environment.app.custom_domain_count
|
||||
ingress_external_enabled = module.environment.app.ingress_external_enabled
|
||||
ingress_target_port = module.environment.app.ingress_target_port
|
||||
registry_password = var.DOCKER_IO_PASSWORD
|
||||
registry_username = var.DOCKER_IO_USERNAME
|
||||
registry_password_secret_name = "docker-io-password"
|
||||
registry_server_name = "docker.io"
|
||||
resource_group_name = var.RESOURCE_GROUP_NAME
|
||||
secrets = [
|
||||
{
|
||||
name = "docker-io-password"
|
||||
value = var.DOCKER_IO_PASSWORD
|
||||
}
|
||||
]
|
||||
traffic_weight_percentage = module.environment.app.traffic_weight_percentage
|
||||
template_max_replicas = module.environment.app.template_max_replicas
|
||||
template_min_replicas = module.environment.app.template_min_replicas
|
||||
}
|
||||
19
infrastructure/storage.tf
Normal file
19
infrastructure/storage.tf
Normal 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
|
||||
}
|
||||
@@ -1,10 +1,3 @@
|
||||
variable "API_SUBDOMAIN_NAME" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "APP_SUBDOMAIN_NAME" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "CLIENT_ID" {
|
||||
type = string
|
||||
@@ -15,18 +8,6 @@ variable "CLIENT_SECRET" {
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "DB_PATH" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "DB_SYNC" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "DNS_ZONE_RESOURCE_GROUP" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "DOCKER_IO_PASSWORD" {
|
||||
type = string
|
||||
sensitive = true
|
||||
@@ -36,10 +17,6 @@ variable "DOCKER_IO_USERNAME" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "DOMAIN_NAME" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "RESOURCE_GROUP_NAME" {
|
||||
type = string
|
||||
}
|
||||
|
||||
28
package-lock.json
generated
28
package-lock.json
generated
@@ -51,7 +51,7 @@
|
||||
"@nestjs/serve-static": "^5.0.3",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"@noahspan/azure-database": "^3.1.2",
|
||||
"@noahspan/noahspan-modules": "^1.2.8",
|
||||
"@noahspan/noahspan-modules": "^1.2.9",
|
||||
"@schematics/angular": "^17.3.7",
|
||||
"@types/multer": "^1.4.12",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
@@ -5148,6 +5148,25 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/mapped-types": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz",
|
||||
"integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||
"class-transformer": "^0.4.0 || ^0.5.0",
|
||||
"class-validator": "^0.13.0 || ^0.14.0",
|
||||
"reflect-metadata": "^0.1.12 || ^0.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"class-transformer": {
|
||||
"optional": true
|
||||
},
|
||||
"class-validator": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/passport": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz",
|
||||
@@ -5752,9 +5771,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@noahspan/noahspan-modules": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@noahspan/noahspan-modules/-/noahspan-modules-1.2.8.tgz",
|
||||
"integrity": "sha512-Zx4wtMAiRj2D132ng4DhlKt/O4kXj1NzCjxwVAXL1tinSsX9xHxjecY/GmYs9j/iMdFwAb5SpnyN5YeLX/THFw==",
|
||||
"version": "1.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@noahspan/noahspan-modules/-/noahspan-modules-1.2.9.tgz",
|
||||
"integrity": "sha512-YUkFpI7UvvCUA3cAkSsWJZS3telrlVraCgXY1TrSoimPcT48fO4HKNqKboSJAeVq03rK6mV+BGimF9ZXZjrJzg==",
|
||||
"dependencies": {
|
||||
"@azure/identity": "^4.2.0",
|
||||
"@azure/msal-node": "^2.9.2",
|
||||
@@ -5762,6 +5781,7 @@
|
||||
"@nestjs/common": "^11.0.11",
|
||||
"@nestjs/core": "^11.0.11",
|
||||
"@nestjs/jwt": "^11.0.0",
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.0.11",
|
||||
"axios": "^1.7.2",
|
||||
|
||||
Reference in New Issue
Block a user