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