Feature/4 pilots add (#20)
* changing api url to env variable * updating env variables * updating env variables * updating env variables * updating env variables * updating env variables * updating env variables * feature/4-pilots---add * feature/4-pilots---add * feature/4-pilots---add * feature/4-pilots---add * ad pilot * add pilot * add pilot * add pilot * add pilot * add pilot * adding pilots * add pilot * add pilot * add pilots * add pilot * add pilot * adding pilots * adding pilots * adding pilots * adding pilots * adding pilots * adding pilots * adding pilots * adding pilots * adding pilots * adding pilots * adding pilots * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * Testing menu button * Testing menu button * Testing menu button * Testing menu button * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot * adding pilot --------- Co-authored-by: Noah Spannbauer <nspannbauer@sensonix.com>
This commit was merged in pull request #20.
This commit is contained in:
11
.github/workflows/feature.yml
vendored
11
.github/workflows/feature.yml
vendored
@@ -5,6 +5,12 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- feature/**
|
- feature/**
|
||||||
|
|
||||||
|
env:
|
||||||
|
VITE_API_URL: ${{ vars.VITE_API_URL }}
|
||||||
|
VITE_CLIENT_ID: ${{ vars.VITE_CLIENT_ID }}
|
||||||
|
VITE_TENANT_ID: ${{ vars.VITE_TENANT_ID }}
|
||||||
|
VITE_REDIRECT_URL: ${{ vars.VITE_REDIRECT_URL }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_and_deploy_job:
|
build_and_deploy_job:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -20,11 +26,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
|
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
action: "upload"
|
action: 'upload'
|
||||||
app_build_command: ${{ vars.APP_BUILD_COMMAND }}
|
app_build_command: ${{ vars.APP_BUILD_COMMAND }}
|
||||||
app_location: ${{ vars.APP_LOCATION }}
|
app_location: ${{ vars.APP_LOCATION }}
|
||||||
api_build_command: ${{ vars.API_BUILD_COMMAND }}
|
api_build_command: ${{ vars.API_BUILD_COMMAND }}
|
||||||
api_location: ${{ vars.API_LOCATION}}
|
api_location: ${{ vars.API_LOCATION}}
|
||||||
output_location: ${{ vars.OUTPUT_LOCATION }}
|
output_location: ${{ vars.OUTPUT_LOCATION }}
|
||||||
deployment_environment: "development"
|
deployment_environment: 'development'
|
||||||
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
.vscode/*
|
.vscode/*
|
||||||
node_modules
|
node_modules
|
||||||
**/.env
|
**/.env
|
||||||
|
**/.env.*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -20,7 +20,9 @@
|
|||||||
"start:azure": "npm run build && func host start"
|
"start:azure": "npm run build && func host start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@azure/data-tables": "^13.2.2",
|
||||||
"@azure/functions": "^1.0.3",
|
"@azure/functions": "^1.0.3",
|
||||||
|
"@nestjs/axios": "^3.0.3",
|
||||||
"@nestjs/azure-database": "^3.0.0",
|
"@nestjs/azure-database": "^3.0.0",
|
||||||
"@nestjs/azure-func-http": "^0.10.0",
|
"@nestjs/azure-func-http": "^0.10.0",
|
||||||
"@nestjs/common": "^10.0.0",
|
"@nestjs/common": "^10.0.0",
|
||||||
@@ -28,13 +30,14 @@
|
|||||||
"@nestjs/core": "^10.0.0",
|
"@nestjs/core": "^10.0.0",
|
||||||
"@nestjs/passport": "^10.0.3",
|
"@nestjs/passport": "^10.0.3",
|
||||||
"@nestjs/platform-express": "^10.0.0",
|
"@nestjs/platform-express": "^10.0.0",
|
||||||
"@noahspan/noahspan-modules": "^0.2.7",
|
"@noahspan/noahspan-modules": "^0.3.9",
|
||||||
"@schematics/angular": "^17.3.7",
|
"@schematics/angular": "^17.3.7",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@microsoft/microsoft-graph-types": "^2.40.0",
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.0.0",
|
||||||
"@nestjs/testing": "^10.0.0",
|
"@nestjs/testing": "^10.0.0",
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { Controller, Get, Query } from '@nestjs/common';
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Headers,
|
||||||
|
Query,
|
||||||
|
Res,
|
||||||
|
StreamableFile
|
||||||
|
} from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
AppConfigService,
|
AppConfigService,
|
||||||
MsGraphService,
|
MsGraphService,
|
||||||
@@ -6,10 +13,17 @@ import {
|
|||||||
} from '@noahspan/noahspan-modules';
|
} from '@noahspan/noahspan-modules';
|
||||||
import { FeatureFlagValue } from '@azure/app-configuration';
|
import { FeatureFlagValue } from '@azure/app-configuration';
|
||||||
import { Public } from '@noahspan/noahspan-modules';
|
import { Public } from '@noahspan/noahspan-modules';
|
||||||
|
import { Person } from '@microsoft/microsoft-graph-types';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
import { createReadStream } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { arrayBuffer } from 'stream/consumers';
|
||||||
|
import type { Response } from 'express';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly appService: AppService,
|
||||||
private readonly appConfigService: AppConfigService,
|
private readonly appConfigService: AppConfigService,
|
||||||
private readonly msGraphService: MsGraphService
|
private readonly msGraphService: MsGraphService
|
||||||
) {}
|
) {}
|
||||||
@@ -37,21 +51,58 @@ export class AppController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Public()
|
@Get('userPhoto')
|
||||||
@Get('profilePhoto')
|
async getProfilePhoto(@Headers() headers: any): Promise<StreamableFile> {
|
||||||
async getProfilePhoto(@Query() query: any): Promise<any> {}
|
try {
|
||||||
|
const graphToken: string = await this.msGraphService.getMsGraphAuth(
|
||||||
|
headers.authorization.replace('Bearer ', ''),
|
||||||
|
['user.read']
|
||||||
|
);
|
||||||
|
const client: MsGraphClient =
|
||||||
|
await this.msGraphService.getMsGraphClientDelegated(graphToken);
|
||||||
|
const blob: Blob = await client.api(`me/photos('48x48')/$value`).get();
|
||||||
|
const arrayBuffer: ArrayBuffer = await blob.arrayBuffer();
|
||||||
|
const buffer: Buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
|
return new StreamableFile(buffer, {
|
||||||
|
type: 'application/json',
|
||||||
|
disposition: `attachment; filename="user_photo.png"`
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Get('userProfile')
|
@Get('userProfile')
|
||||||
async getUserProfile(@Query() query: any) {
|
async getUserProfile(@Headers() headers: any) {
|
||||||
try {
|
try {
|
||||||
const username: string = query.username;
|
const graphToken: string = await this.msGraphService.getMsGraphAuth(
|
||||||
|
headers.authorization.replace('Bearer ', ''),
|
||||||
|
['user.read']
|
||||||
|
);
|
||||||
const client: MsGraphClient =
|
const client: MsGraphClient =
|
||||||
await this.msGraphService.getMsGraphClient();
|
await this.msGraphService.getMsGraphClientDelegated(graphToken);
|
||||||
const userProfile = await client.api(`users/${username}`).get();
|
const userProfile = await client.api(`me`).get();
|
||||||
|
|
||||||
return userProfile;
|
return userProfile;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('personSearch')
|
||||||
|
async searchUsers(
|
||||||
|
@Headers() headers: any,
|
||||||
|
@Query('search') search: any
|
||||||
|
): Promise<Person[]> {
|
||||||
|
try {
|
||||||
|
const accessToken: string = headers.authorization.replace('Bearer ', '');
|
||||||
|
const personSearchResults: Person[] =
|
||||||
|
await this.appService.getPersonSearchResults(accessToken, search);
|
||||||
|
console.log(personSearchResults);
|
||||||
|
return personSearchResults;
|
||||||
|
} catch (error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import {
|
|||||||
} from '@noahspan/noahspan-modules';
|
} from '@noahspan/noahspan-modules';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { APP_GUARD } from '@nestjs/core';
|
import { APP_GUARD } from '@nestjs/core';
|
||||||
|
import { PilotModule } from './pilot/pilot.module';
|
||||||
|
import { APP_FILTER } from '@nestjs/core';
|
||||||
|
import { HttpExceptionFilter } from './filters/http-exception.filter';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -28,7 +31,8 @@ import { APP_GUARD } from '@nestjs/core';
|
|||||||
tenantId: process.env.TENANT_ID,
|
tenantId: process.env.TENANT_ID,
|
||||||
clientId: process.env.CLIENT_ID,
|
clientId: process.env.CLIENT_ID,
|
||||||
clientSecret: process.env.CLIENT_SECRET
|
clientSecret: process.env.CLIENT_SECRET
|
||||||
})
|
}),
|
||||||
|
PilotModule
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [
|
||||||
@@ -36,6 +40,10 @@ import { APP_GUARD } from '@nestjs/core';
|
|||||||
provide: APP_GUARD,
|
provide: APP_GUARD,
|
||||||
useClass: AuthGuard
|
useClass: AuthGuard
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: APP_FILTER,
|
||||||
|
useClass: HttpExceptionFilter
|
||||||
|
},
|
||||||
AppService
|
AppService
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,44 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { MsGraphService, MsGraphClient } from '@noahspan/noahspan-modules';
|
||||||
|
import { Person } from '@microsoft/microsoft-graph-types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AppService {
|
export class AppService {
|
||||||
|
constructor(private readonly msGraphService: MsGraphService) {}
|
||||||
|
|
||||||
getHello(): string {
|
getHello(): string {
|
||||||
return 'Hello World!';
|
return 'Hello World!';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPersonSearchResults(
|
||||||
|
accessToken: string,
|
||||||
|
search: string
|
||||||
|
): Promise<Person[]> {
|
||||||
|
try {
|
||||||
|
const graphToken: string = await this.msGraphService.getMsGraphAuth(
|
||||||
|
accessToken,
|
||||||
|
['user.read']
|
||||||
|
);
|
||||||
|
const client: MsGraphClient =
|
||||||
|
await this.msGraphService.getMsGraphClientDelegated(graphToken);
|
||||||
|
const results: any = await client
|
||||||
|
.api(`me/people/?$search=${search}`)
|
||||||
|
.get();
|
||||||
|
let personResults: Person[];
|
||||||
|
|
||||||
|
if (results.value) {
|
||||||
|
personResults = results.value.filter((result: Person) => {
|
||||||
|
if (result.userPrincipalName !== null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
personResults = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return personResults;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
api/src/customError/CustomError.ts
Normal file
9
api/src/customError/CustomError.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export class CustomError extends Error {
|
||||||
|
statusCode: number;
|
||||||
|
|
||||||
|
constructor(message, name, statusCode) {
|
||||||
|
super(message);
|
||||||
|
this.name = name;
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
api/src/filters/http-exception.filter.ts
Normal file
26
api/src/filters/http-exception.filter.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
ExceptionFilter,
|
||||||
|
Catch,
|
||||||
|
ArgumentsHost,
|
||||||
|
HttpException
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
@Catch(HttpException)
|
||||||
|
export class HttpExceptionFilter implements ExceptionFilter {
|
||||||
|
catch(excpetion: HttpException, host: ArgumentsHost) {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const response = ctx.getResponse<Response>();
|
||||||
|
const request = ctx.getRequest<Request>();
|
||||||
|
const status = excpetion.getStatus();
|
||||||
|
console.log(excpetion.cause);
|
||||||
|
|
||||||
|
response.status(status).json({
|
||||||
|
name: excpetion.cause,
|
||||||
|
message: excpetion.message,
|
||||||
|
statusCode: status,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
path: request.url
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,25 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
import { HttpExceptionFilter } from './filters/http-exception.filter';
|
||||||
|
import { InternalServerErrorException } from '@nestjs/common';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
|
const httpService = new HttpService();
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
|
app.useGlobalFilters(new HttpExceptionFilter());
|
||||||
|
httpService.axiosRef.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('Internal server error exception', error);
|
||||||
|
|
||||||
|
throw new InternalServerErrorException();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
await app.listen(3000);
|
await app.listen(3000);
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
7
api/src/pilot/certificate/certificate.entity.ts
Normal file
7
api/src/pilot/certificate/certificate.entity.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export class Certificate {
|
||||||
|
partitionKey: string;
|
||||||
|
rowKey: string;
|
||||||
|
type: string;
|
||||||
|
issueDate: Date;
|
||||||
|
number?: string;
|
||||||
|
}
|
||||||
37
api/src/pilot/certificate/certificate.service.ts
Normal file
37
api/src/pilot/certificate/certificate.service.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Repository, InjectRepository } from '@nestjs/azure-database';
|
||||||
|
import { Certificate } from './certificate.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CertificateService {
|
||||||
|
private readonly partitionKey: string = 'certificate';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Certificate)
|
||||||
|
private readonly certificateRepository: Repository<Certificate>
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async find(rowKey: string): Promise<Certificate> {
|
||||||
|
return await this.certificateRepository.find(this.partitionKey, rowKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(): Promise<Certificate[]> {
|
||||||
|
return await this.certificateRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(certificate: Certificate): Promise<Certificate> {
|
||||||
|
return await this.certificateRepository.create(certificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(rowKey: string, certificate: Certificate): Promise<Certificate> {
|
||||||
|
return await this.certificateRepository.update(
|
||||||
|
this.partitionKey,
|
||||||
|
rowKey,
|
||||||
|
certificate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(rowKey: string): Promise<void> {
|
||||||
|
await this.certificateRepository.delete(this.partitionKey, rowKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
api/src/pilot/endorsement/endorsement.entity.ts
Normal file
6
api/src/pilot/endorsement/endorsement.entity.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export class Endorsement {
|
||||||
|
partitionkey: string;
|
||||||
|
rowKey: string;
|
||||||
|
type: string;
|
||||||
|
issueDate: Date;
|
||||||
|
}
|
||||||
37
api/src/pilot/endorsement/endosement.service.ts
Normal file
37
api/src/pilot/endorsement/endosement.service.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { InjectRepository, Repository } from '@nestjs/azure-database';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Endorsement } from './endorsement.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EndosementService {
|
||||||
|
private readonly partitionKey: string = 'endorsement';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Endorsement)
|
||||||
|
private readonly endorsementRepository: Repository<Endorsement>
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async find(rowKey: string): Promise<Endorsement> {
|
||||||
|
return await this.endorsementRepository.find(this.partitionKey, rowKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(): Promise<Endorsement[]> {
|
||||||
|
return await this.endorsementRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(endorsement: Endorsement): Promise<Endorsement> {
|
||||||
|
return await this.endorsementRepository.create(endorsement);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(rowKey: string, endorsement: Endorsement): Promise<Endorsement> {
|
||||||
|
return await this.endorsementRepository.update(
|
||||||
|
this.partitionKey,
|
||||||
|
rowKey,
|
||||||
|
endorsement
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(rowKey: string): Promise<void> {
|
||||||
|
await this.endorsementRepository.delete(this.partitionKey, rowKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
api/src/pilot/info/pilot-info.dto.ts
Normal file
10
api/src/pilot/info/pilot-info.dto.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export class PilotInfoDto {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
address: string;
|
||||||
|
city: string;
|
||||||
|
state: string;
|
||||||
|
postalCode: string;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
}
|
||||||
12
api/src/pilot/info/pilot-info.entity.ts
Normal file
12
api/src/pilot/info/pilot-info.entity.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export class PilotInfoEntity {
|
||||||
|
partitionKey: string;
|
||||||
|
rowKey: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
address?: string;
|
||||||
|
city?: string;
|
||||||
|
state?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
}
|
||||||
83
api/src/pilot/info/pilot-info.service.ts
Normal file
83
api/src/pilot/info/pilot-info.service.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PilotInfoDto } from './pilot-info.dto';
|
||||||
|
import { PilotInfoEntity } from './pilot-info.entity';
|
||||||
|
import { TableClient, TableService } from '@noahspan/noahspan-modules';
|
||||||
|
import { odata, RestError, TableInsertEntityHeaders } from '@azure/data-tables';
|
||||||
|
import { CustomError } from '../../customError/CustomError';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PilotInfoService {
|
||||||
|
private readonly partitionKey: string = 'info';
|
||||||
|
|
||||||
|
constructor(private readonly tableService: TableService) {}
|
||||||
|
|
||||||
|
// async find(rowKey: string): Promise<PilotInfo> {
|
||||||
|
// return await this.pilotInfoRepository.find(this.partitionKey, rowKey);
|
||||||
|
// }
|
||||||
|
|
||||||
|
async findAll(): Promise<PilotInfoEntity[]> {
|
||||||
|
try {
|
||||||
|
const client: TableClient =
|
||||||
|
await this.tableService.getTableClient('Pilots');
|
||||||
|
const entities = await client.listEntities({
|
||||||
|
queryOptions: { filter: odata`PartitionKey eq 'pilot'` }
|
||||||
|
});
|
||||||
|
const pilots: PilotInfoEntity[] = [];
|
||||||
|
|
||||||
|
for await (const entity of entities) {
|
||||||
|
const pilot: PilotInfoEntity = {
|
||||||
|
partitionKey: entity.partitionKey,
|
||||||
|
rowKey: entity.rowKey,
|
||||||
|
id: entity.id.toString(),
|
||||||
|
name: entity.name.toString()
|
||||||
|
};
|
||||||
|
|
||||||
|
pilots.push(pilot);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pilots;
|
||||||
|
} catch (error) {
|
||||||
|
const restError: RestError = error as RestError;
|
||||||
|
|
||||||
|
throw new CustomError(
|
||||||
|
restError.details['odataError']['message']['value'],
|
||||||
|
restError.details['odataError']['code'],
|
||||||
|
restError.statusCode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(pilotInfoData: PilotInfoDto): Promise<TableInsertEntityHeaders> {
|
||||||
|
const client: TableClient =
|
||||||
|
await this.tableService.getTableClient('Pilots');
|
||||||
|
const pilotInfo: PilotInfoEntity = new PilotInfoEntity();
|
||||||
|
|
||||||
|
Object.assign(pilotInfo, pilotInfoData);
|
||||||
|
pilotInfo.partitionKey = 'pilot';
|
||||||
|
pilotInfo.rowKey = pilotInfo.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await client.createEntity(pilotInfo);
|
||||||
|
} catch (error) {
|
||||||
|
const restError: RestError = error as RestError;
|
||||||
|
|
||||||
|
throw new CustomError(
|
||||||
|
restError.details['odataError']['message']['value'],
|
||||||
|
restError.details['odataError']['code'],
|
||||||
|
restError.statusCode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// async update(rowKey: string, pilotInfo: PilotInfo): Promise<PilotInfo> {
|
||||||
|
// return await this.pilotInfoRepository.update(
|
||||||
|
// this.partitionKey,
|
||||||
|
// rowKey,
|
||||||
|
// pilotInfo
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async delete(rowKey: string): Promise<void> {
|
||||||
|
// await this.pilotInfoRepository.delete(this.partitionKey, rowKey);
|
||||||
|
// }
|
||||||
|
}
|
||||||
6
api/src/pilot/medical/medical.entity.ts
Normal file
6
api/src/pilot/medical/medical.entity.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export class Medical {
|
||||||
|
partitionKey: string;
|
||||||
|
rowKey: string;
|
||||||
|
certificateClass: string;
|
||||||
|
certificateExpiration: Date;
|
||||||
|
}
|
||||||
33
api/src/pilot/medical/medical.service.ts
Normal file
33
api/src/pilot/medical/medical.service.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Repository, InjectRepository } from '@nestjs/azure-database';
|
||||||
|
import { Medical } from './medical.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MedicalService {
|
||||||
|
private readonly partitionKey: string = 'medical';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Medical)
|
||||||
|
private readonly profileRepository: Repository<Medical>
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async find(rowKey: string): Promise<Medical> {
|
||||||
|
return this.profileRepository.find(this.partitionKey, rowKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(): Promise<Medical[]> {
|
||||||
|
return this.profileRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(profile: Medical): Promise<Medical> {
|
||||||
|
return this.profileRepository.create(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(rowKey: string, profile: Medical): Promise<Medical> {
|
||||||
|
return this.profileRepository.update(this.partitionKey, rowKey, profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(rowKey: string) {
|
||||||
|
return this.profileRepository.delete(this.partitionKey, rowKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
api/src/pilot/pilot.controller.ts
Normal file
40
api/src/pilot/pilot.controller.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Body, Controller, Get, HttpException, Post } from '@nestjs/common';
|
||||||
|
import { PilotInfoService } from './info/pilot-info.service';
|
||||||
|
import { PilotInfoDto } from './info/pilot-info.dto';
|
||||||
|
import { TableInsertEntityHeaders } from '@azure/data-tables';
|
||||||
|
import { CustomError } from '../customError/CustomError';
|
||||||
|
import { PilotInfoEntity } from './info/pilot-info.entity';
|
||||||
|
|
||||||
|
@Controller('pilots')
|
||||||
|
export class PilotController {
|
||||||
|
constructor(private readonly pilotInfoService: PilotInfoService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async findAll(): Promise<PilotInfoEntity[]> {
|
||||||
|
try {
|
||||||
|
const pilots: PilotInfoEntity[] = await this.pilotInfoService.findAll();
|
||||||
|
|
||||||
|
return pilots;
|
||||||
|
} catch (error) {
|
||||||
|
const customError = error as CustomError;
|
||||||
|
|
||||||
|
throw new HttpException(customError.message, customError.statusCode, {
|
||||||
|
cause: customError.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
async create(@Body() pilotInfoData: PilotInfoDto): Promise<void> {
|
||||||
|
try {
|
||||||
|
const response: TableInsertEntityHeaders =
|
||||||
|
await this.pilotInfoService.create(pilotInfoData);
|
||||||
|
} catch (error) {
|
||||||
|
const customError = error as CustomError;
|
||||||
|
|
||||||
|
throw new HttpException(customError.message, customError.statusCode, {
|
||||||
|
cause: customError.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
api/src/pilot/pilot.module.ts
Normal file
16
api/src/pilot/pilot.module.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PilotController } from './pilot.controller';
|
||||||
|
import { TableModule } from '@noahspan/noahspan-modules';
|
||||||
|
import { PilotInfoService } from './info/pilot-info.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TableModule.register({
|
||||||
|
accountName: process.env.AZURE_STORAGE_ACCOUNT_NAME,
|
||||||
|
accountKey: process.env.AZURE_STORAGE_ACCOUNT_KEY
|
||||||
|
})
|
||||||
|
],
|
||||||
|
controllers: [PilotController],
|
||||||
|
providers: [PilotInfoService]
|
||||||
|
})
|
||||||
|
export class PilotModule {}
|
||||||
7905
app/package-lock.json
generated
7905
app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,13 +10,13 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/msal-react": "^2.0.15",
|
"@azure/msal-browser": "^3.17.0",
|
||||||
|
"@azure/msal-react": "^2.0.19",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.5.2",
|
"@fortawesome/free-regular-svg-icons": "^6.5.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||||
"@nextui-org/react": "^2.3.6",
|
"@noahspan/noahspan-components": "^0.6.8",
|
||||||
"@noahspan/noahspan-components": "^0.2.5",
|
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"framer-motion": "^11.1.7",
|
"framer-motion": "^11.1.7",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
"react-router-dom": "^6.23.0"
|
"react-router-dom": "^6.23.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@microsoft/microsoft-graph-types": "^2.40.0",
|
||||||
"@types/react": "^18.2.66",
|
"@types/react": "^18.2.66",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.22",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
|||||||
@@ -2,21 +2,23 @@ import { useEffect } from 'react';
|
|||||||
import { Route, Routes } from 'react-router-dom';
|
import { Route, Routes } from 'react-router-dom';
|
||||||
import Pilots from './components/pilots/Pilots';
|
import Pilots from './components/pilots/Pilots';
|
||||||
import { useAppContext } from './hooks/appContext/UseAppContext';
|
import { useAppContext } from './hooks/appContext/UseAppContext';
|
||||||
import axios, { AxiosResponse } from 'axios';
|
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||||
|
import { useHttpClient } from './hooks/httpClient/UseHttpClient';
|
||||||
import { useFeatureFlag } from './hooks/featureFlag/UseFeatureFlag';
|
import { useFeatureFlag } from './hooks/featureFlag/UseFeatureFlag';
|
||||||
|
import SiteNav from './components/siteNav/SiteNav';
|
||||||
|
|
||||||
const App: React.FC<unknown> = () => {
|
const App: React.FC<unknown> = () => {
|
||||||
|
const httpClient: AxiosInstance = useHttpClient();
|
||||||
const appContext = useAppContext();
|
const appContext = useAppContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getFeatureFlags = async () => {
|
const getFeatureFlags = async () => {
|
||||||
try {
|
try {
|
||||||
const featureFlagKeys: string = 'flying-pilots';
|
const featureFlagKeys: string = 'flying-pilots';
|
||||||
const response: AxiosResponse = await axios.get(
|
const response: AxiosResponse = await httpClient.get(
|
||||||
`http://localhost:7071/api/featureFlags?keys=${featureFlagKeys}&label=${process.env.NODE_ENV}`
|
`api/featureFlags?keys=${featureFlagKeys}&label=${import.meta.env.MODE}`
|
||||||
);
|
);
|
||||||
const featureFlags: { key: string; enabled: boolean }[] = response.data;
|
const featureFlags: { key: string; enabled: boolean }[] = response.data;
|
||||||
console.log(featureFlags);
|
|
||||||
|
|
||||||
if (featureFlags.length > 0) {
|
if (featureFlags.length > 0) {
|
||||||
appContext.dispatch({
|
appContext.dispatch({
|
||||||
@@ -33,11 +35,14 @@ const App: React.FC<unknown> = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="container mx-auto">
|
||||||
|
<SiteNav />
|
||||||
<Routes>
|
<Routes>
|
||||||
{useFeatureFlag('flying-pilots')?.enabled && (
|
{useFeatureFlag('flying-pilots')?.enabled && (
|
||||||
<Route path="/" element={<Pilots />} />
|
<Route path="/" element={<Pilots />} />
|
||||||
)}
|
)}
|
||||||
</Routes>
|
</Routes>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import SiteNav from '../siteNav/SiteNav';
|
|
||||||
|
|
||||||
const Logbook: React.FC<unknown> = () => {
|
const Logbook: React.FC<unknown> = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SiteNav />
|
|
||||||
<div>Logbook goes here</div>
|
<div>Logbook goes here</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
5
app/src/components/pilotForm/IPilotFormProps.ts
Normal file
5
app/src/components/pilotForm/IPilotFormProps.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface IPilotFormProps {
|
||||||
|
pilotId?: string;
|
||||||
|
isDrawerOpen: boolean;
|
||||||
|
onOpenCloseDrawer: () => void;
|
||||||
|
}
|
||||||
@@ -1,85 +1,473 @@
|
|||||||
// import {
|
import { useEffect, useState } from 'react';
|
||||||
// Accordion,
|
import {
|
||||||
// AccordionItem,
|
useForm,
|
||||||
// Button,
|
Controller,
|
||||||
// DatePicker,
|
FormProvider,
|
||||||
// Input,
|
FieldValues
|
||||||
// Select,
|
} from 'react-hook-form';
|
||||||
// SelectItem
|
import {
|
||||||
// } from '@nextui-org/react';
|
Button,
|
||||||
// import { useForm, Controller, SubmitHandler } from 'react-hook-form';
|
DatePicker,
|
||||||
|
Drawer,
|
||||||
|
DrawerBody,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerFooter,
|
||||||
|
Input,
|
||||||
|
Option,
|
||||||
|
PeoplePicker,
|
||||||
|
SaveIcon,
|
||||||
|
Select,
|
||||||
|
StateSelect,
|
||||||
|
Typography,
|
||||||
|
XmarkIcon
|
||||||
|
} from '@noahspan/noahspan-components';
|
||||||
|
import { IPilotFormProps } from './IPilotFormProps';
|
||||||
|
import PilotFormCertificates from '../pilotFormCertificates/PilotFormCertificates';
|
||||||
|
import PilotFormEndorsements from '../pilotFormEndorsements/PilotFormEndorsements';
|
||||||
|
import { Person } from '@microsoft/microsoft-graph-types';
|
||||||
|
import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
|
||||||
|
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||||
|
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
||||||
|
import { IPilotFormCertificates } from '../pilotFormCertificates/IPilotFormCertificates';
|
||||||
|
import { IPilotFormEndorsements } from '../pilotFormEndorsements/IPilotFormEndorsements';
|
||||||
|
import { EventMessage, EventPayload, EventType } from '@azure/msal-browser';
|
||||||
|
|
||||||
// const PilotForm: React.FC<unknown> = () => {
|
const PilotForm: React.FC<IPilotFormProps> = ({
|
||||||
// const { control, handleSubmit } = useForm();
|
pilotId,
|
||||||
|
isDrawerOpen,
|
||||||
|
onOpenCloseDrawer
|
||||||
|
}: IPilotFormProps) => {
|
||||||
|
const httpClient: AxiosInstance = useHttpClient();
|
||||||
|
const [peoplePickerResults, setPeoplePickerResults] = useState<Person[]>([]);
|
||||||
|
const [isPeoplePickerLoading, setIsPeoplePickerLoading] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
const { getAccessToken } = useAccessToken();
|
||||||
|
const methods = useForm();
|
||||||
|
|
||||||
// const onSubmit = (data: unknown) => {
|
const handlePeoplePickerOnClick = (
|
||||||
// console.log(data);
|
event: React.MouseEvent<HTMLDivElement>
|
||||||
// };
|
) => {
|
||||||
|
const divElement: HTMLDivElement = event.target as HTMLDivElement;
|
||||||
|
|
||||||
// return (
|
methods.setValue('id', divElement.id);
|
||||||
// <form className="m-10" onSubmit={handleSubmit(onSubmit)}>
|
methods.setValue('name', divElement.textContent);
|
||||||
// <div className="grid grid-cols-2 gap-4">
|
setPeoplePickerResults([]);
|
||||||
// <div className="self-center">
|
};
|
||||||
// <label>First Name</label>
|
|
||||||
// </div>
|
|
||||||
// <div>
|
|
||||||
// <Controller
|
|
||||||
// name="firstName"
|
|
||||||
// control={control}
|
|
||||||
// render={({ field }) => <Input />}
|
|
||||||
// />
|
|
||||||
// </div>
|
|
||||||
// <div className="self-center">
|
|
||||||
// <label>Last Name</label>
|
|
||||||
// </div>
|
|
||||||
// <div>
|
|
||||||
// <Controller
|
|
||||||
// name="lastName"
|
|
||||||
// control={control}
|
|
||||||
// render={({ field }) => <Input />}
|
|
||||||
// />
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// <Accordion>
|
|
||||||
// <AccordionItem title="Medical Certificate">
|
|
||||||
// <div className="grid grid-cols-2 gap-4">
|
|
||||||
// <div className="self-center">
|
|
||||||
// <label>Class</label>
|
|
||||||
// </div>
|
|
||||||
// <div>
|
|
||||||
// <Controller
|
|
||||||
// name="medicalClass"
|
|
||||||
// control={control}
|
|
||||||
// render={({ field }) => (
|
|
||||||
// <Select>
|
|
||||||
// <SelectItem key="first" value="First">
|
|
||||||
// First
|
|
||||||
// </SelectItem>
|
|
||||||
// <SelectItem key="second" value="Second">
|
|
||||||
// Second
|
|
||||||
// </SelectItem>
|
|
||||||
// <SelectItem key="Third" value="Third">
|
|
||||||
// Third
|
|
||||||
// </SelectItem>
|
|
||||||
// </Select>
|
|
||||||
// )}
|
|
||||||
// />
|
|
||||||
// </div>
|
|
||||||
// <div className="self-center">
|
|
||||||
// <label>Expires</label>
|
|
||||||
// </div>
|
|
||||||
// <div>
|
|
||||||
// <Controller
|
|
||||||
// name="medicalExpiration"
|
|
||||||
// control={control}
|
|
||||||
// render={({ field }) => <DatePicker />}
|
|
||||||
// />
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </AccordionItem>
|
|
||||||
// </Accordion>
|
|
||||||
// </form>
|
|
||||||
// );
|
|
||||||
// };
|
|
||||||
|
|
||||||
// export default PilotForm;
|
const handlePeoplePickerOnChange = async (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
setIsPeoplePickerLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
methods.setValue('name', event.target.value);
|
||||||
|
|
||||||
|
const searchString: string = event.target.value;
|
||||||
|
const accessToken: string = await getAccessToken();
|
||||||
|
const response: AxiosResponse = await httpClient.get(
|
||||||
|
`api/personSearch?search=${searchString}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: accessToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log(response);
|
||||||
|
setPeoplePickerResults(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
setIsPeoplePickerLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: unknown) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const accessToken: string = await getAccessToken();
|
||||||
|
const response: AxiosResponse = await httpClient.post(
|
||||||
|
`api/pilots`,
|
||||||
|
data,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: accessToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response) console.log(response);
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const errResp = error.response;
|
||||||
|
|
||||||
|
console.log(errResp?.data.message);
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(methods.formState.errors);
|
||||||
|
}, [methods.formState.errors]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
open={isDrawerOpen}
|
||||||
|
placement="right"
|
||||||
|
size={1000}
|
||||||
|
data-testid="pilot-drawer"
|
||||||
|
>
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<DrawerHeader text="Add Pilot" onClose={onOpenCloseDrawer} />
|
||||||
|
<form onSubmit={methods.handleSubmit(onSubmit)}>
|
||||||
|
<DrawerBody>
|
||||||
|
<div className="grid grid-cols-4 gap-4">
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Typography variant="h6">Name *</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<Controller
|
||||||
|
name="name"
|
||||||
|
control={methods.control}
|
||||||
|
rules={{ required: 'A name must be selected' }}
|
||||||
|
render={({ field: { disabled, value } }) => (
|
||||||
|
<PeoplePicker
|
||||||
|
results={peoplePickerResults}
|
||||||
|
inputProps={{
|
||||||
|
disabled: disabled,
|
||||||
|
labelProps: {
|
||||||
|
className: 'before:content-none after:content-none'
|
||||||
|
},
|
||||||
|
onChange: (event) => handlePeoplePickerOnChange(event),
|
||||||
|
error: methods.formState.errors.name ? true : false,
|
||||||
|
helperText: methods.formState.errors.name
|
||||||
|
? methods.formState.errors.name.message?.toString()
|
||||||
|
: undefined,
|
||||||
|
value: value
|
||||||
|
}}
|
||||||
|
listItemProps={{
|
||||||
|
children: null,
|
||||||
|
onClick: handlePeoplePickerOnClick
|
||||||
|
}}
|
||||||
|
loading={isPeoplePickerLoading}
|
||||||
|
data-testid="pilot-form-people-picker"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Typography variant="h6">Address *</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<Controller
|
||||||
|
name="address"
|
||||||
|
control={methods.control}
|
||||||
|
rules={{ required: 'An address is required' }}
|
||||||
|
render={({ field: { disabled, onChange, value } }) => (
|
||||||
|
<Input
|
||||||
|
className="!border-t-blue-gray-200 focus:!border-t-gray-900"
|
||||||
|
disabled={disabled}
|
||||||
|
labelProps={{
|
||||||
|
className: 'before:content-none after:content-none'
|
||||||
|
}}
|
||||||
|
error={methods.formState.errors.address ? true : false}
|
||||||
|
helperText={
|
||||||
|
methods.formState.errors.address
|
||||||
|
? methods.formState.errors.address.message?.toString()
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
data-testid="pilot-form-address-input"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Typography variant="h6">City *</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<Controller
|
||||||
|
name="city"
|
||||||
|
control={methods.control}
|
||||||
|
rules={{ required: 'A city is required' }}
|
||||||
|
render={({ field: { disabled, onChange, value } }) => (
|
||||||
|
<Input
|
||||||
|
disabled={disabled}
|
||||||
|
labelProps={{
|
||||||
|
className: 'before:content-none after:content-none'
|
||||||
|
}}
|
||||||
|
error={methods.formState.errors.city ? true : false}
|
||||||
|
helperText={
|
||||||
|
methods.formState.errors.city
|
||||||
|
? methods.formState.errors.city.message?.toString()
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
data-testid="pilot-form-city-input"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Typography variant="h6">State *</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<Controller
|
||||||
|
name="state"
|
||||||
|
control={methods.control}
|
||||||
|
rules={{ required: 'A state must be selected' }}
|
||||||
|
render={({ field: { disabled, onChange, value } }) => (
|
||||||
|
<StateSelect
|
||||||
|
disabled={disabled}
|
||||||
|
error={methods.formState.errors.state ? true : false}
|
||||||
|
helperText={
|
||||||
|
methods.formState.errors.state
|
||||||
|
? methods.formState.errors.state.message?.toString()
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
variant="outlined"
|
||||||
|
data-testid="pilot-form-state-dropdown"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Typography variant="h6">Postal Code *</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<Controller
|
||||||
|
name="postalCode"
|
||||||
|
control={methods.control}
|
||||||
|
rules={{ required: 'A postal code is required' }}
|
||||||
|
render={({ field: { disabled, onChange, value } }) => (
|
||||||
|
<Input
|
||||||
|
disabled={disabled}
|
||||||
|
labelProps={{
|
||||||
|
className: 'before:content-none after:content-none'
|
||||||
|
}}
|
||||||
|
error={methods.formState.errors.postalCode ? true : false}
|
||||||
|
helperText={
|
||||||
|
methods.formState.errors.postalCode
|
||||||
|
? methods.formState.errors.postalCode.message?.toString()
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
data-testid="pilot-form-postal-code-input"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Typography variant="h6">Email</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<Controller
|
||||||
|
name="email"
|
||||||
|
control={methods.control}
|
||||||
|
rules={{
|
||||||
|
pattern: {
|
||||||
|
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
||||||
|
message: 'Invalid email address'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
render={({ field: { disabled, onChange, value } }) => (
|
||||||
|
<Input
|
||||||
|
disabled={disabled}
|
||||||
|
labelProps={{
|
||||||
|
className: 'before:content-none after:content-none'
|
||||||
|
}}
|
||||||
|
error={methods.formState.errors.email ? true : false}
|
||||||
|
helperText={
|
||||||
|
methods.formState.errors.email
|
||||||
|
? methods.formState.errors.email.message?.toString()
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
data-testid="pilot-form-email-input"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Typography variant="h6">Phone Number</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<Controller
|
||||||
|
name="phone"
|
||||||
|
control={methods.control}
|
||||||
|
rules={{
|
||||||
|
pattern: {
|
||||||
|
value: /^[0-9]{3}[-\s\.][0-9]{3}[-\s\.][0-9]{4}$/i,
|
||||||
|
message: 'Enter phone number as 123-456-7890'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
render={({ field: { disabled, onChange, value } }) => (
|
||||||
|
<Input
|
||||||
|
disabled={disabled}
|
||||||
|
labelProps={{
|
||||||
|
className: 'before:content-none after:content-none'
|
||||||
|
}}
|
||||||
|
error={methods.formState.errors.phone ? true : false}
|
||||||
|
helperText={
|
||||||
|
methods.formState.errors.phone
|
||||||
|
? methods.formState.errors.phone.message?.toString()
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
data-testid="pilot-form-phone-input"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{pilotId && (
|
||||||
|
<>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Typography variant="h6">Last Review</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<Controller
|
||||||
|
name="lastReview"
|
||||||
|
control={methods.control}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<DatePicker
|
||||||
|
handleDateChanged={(date: string) => {
|
||||||
|
methods.setValue('lastReview', date);
|
||||||
|
}}
|
||||||
|
inputProps={{
|
||||||
|
value: field.value
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{pilotId && (
|
||||||
|
<>
|
||||||
|
<div className="col-span-4">
|
||||||
|
<Typography variant="h5">Medical</Typography>
|
||||||
|
<hr className="my-3" />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Typography variant="h6">Class</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<Controller
|
||||||
|
name="medicalClass"
|
||||||
|
control={methods.control}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
labelProps={{
|
||||||
|
className:
|
||||||
|
'before:content-none after:content-none'
|
||||||
|
}}
|
||||||
|
{...field}
|
||||||
|
>
|
||||||
|
<Option key="first" value="First">
|
||||||
|
First
|
||||||
|
</Option>
|
||||||
|
<Option key="second" value="Second">
|
||||||
|
Second
|
||||||
|
</Option>
|
||||||
|
<Option key="third" value="Third">
|
||||||
|
Third
|
||||||
|
</Option>
|
||||||
|
<Option key="basicMed" value="Basic Med">
|
||||||
|
Basic Med
|
||||||
|
</Option>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Typography variant="h6">Expiration Date</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3">
|
||||||
|
<Controller
|
||||||
|
name="medicalExpiration"
|
||||||
|
control={methods.control}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<DatePicker
|
||||||
|
handleDateChanged={(date: string) => {
|
||||||
|
methods.setValue('medicalExpiration', date);
|
||||||
|
}}
|
||||||
|
inputProps={{
|
||||||
|
value: field.value
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{pilotId && (
|
||||||
|
<>
|
||||||
|
<div className="col-span-4">
|
||||||
|
<Typography variant="h5">Certificates</Typography>
|
||||||
|
<hr className="my-3" />
|
||||||
|
</div>
|
||||||
|
<PilotFormCertificates certificates={[]} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{pilotId && (
|
||||||
|
<>
|
||||||
|
<div className="col-span-4">
|
||||||
|
<Typography variant="h5">Endorsements</Typography>
|
||||||
|
<hr className="my-3" />
|
||||||
|
</div>
|
||||||
|
<PilotFormEndorsements endorsements={[]} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DrawerBody>
|
||||||
|
<DrawerFooter>
|
||||||
|
<div className="flex gap-2 justify-end justify-self-center pt-4">
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="flex items-center gap-3"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={onOpenCloseDrawer}
|
||||||
|
data-testid="pilot-cancel-button"
|
||||||
|
>
|
||||||
|
<XmarkIcon size="lg" />
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="flex items-center gap-3"
|
||||||
|
loading={isLoading}
|
||||||
|
variant="filled"
|
||||||
|
type="submit"
|
||||||
|
data-testid="pilot-save-button"
|
||||||
|
>
|
||||||
|
<SaveIcon size="lg" />
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DrawerFooter>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PilotForm;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { Certificate } from './certificate.type';
|
||||||
|
|
||||||
|
export interface IPilotFormCertificates {
|
||||||
|
certificates: Certificate[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { IPilotFormCertificates } from './IPilotFormCertificates';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DatePicker,
|
||||||
|
Input,
|
||||||
|
Option,
|
||||||
|
PlusIcon,
|
||||||
|
Select,
|
||||||
|
TrashIcon,
|
||||||
|
Typography
|
||||||
|
} from '@noahspan/noahspan-components';
|
||||||
|
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
const PilotFormCertificates: React.FC<IPilotFormCertificates> = ({
|
||||||
|
certificates
|
||||||
|
}: IPilotFormCertificates) => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
formState: { errors },
|
||||||
|
setValue
|
||||||
|
} = useFormContext();
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
name: 'certificates',
|
||||||
|
control
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{fields.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Typography variant="h6">Type</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Typography variant="h6">Number</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Typography variant="h6">Date of Issue</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1"></div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{fields.map((field, index) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Controller
|
||||||
|
name={`certificates.${index}.type`}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Select label="Type" {...field}>
|
||||||
|
<Option key="student" value="Student">
|
||||||
|
Student
|
||||||
|
</Option>
|
||||||
|
<Option key="private" value="Private">
|
||||||
|
Private
|
||||||
|
</Option>
|
||||||
|
<Option key="instrument" value="Instrument">
|
||||||
|
Instrument
|
||||||
|
</Option>
|
||||||
|
<Option key="recreational" value="Recreational">
|
||||||
|
Recreational
|
||||||
|
</Option>
|
||||||
|
<Option key="sport" value="Sport">
|
||||||
|
Sport
|
||||||
|
</Option>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Controller
|
||||||
|
name={`certificates.${index}.number`}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => {
|
||||||
|
return <Input label="Number" {...field} />;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Controller
|
||||||
|
name={`certificates.${index}.dateOfIssue`}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<DatePicker
|
||||||
|
handleDateChanged={(date: string) => {
|
||||||
|
setValue(`certificates.${index}.dateOfIssue`, date);
|
||||||
|
}}
|
||||||
|
inputProps={{
|
||||||
|
value: field.value
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Button
|
||||||
|
className="flex items-center gap-3"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
<TrashIcon size="lg" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div className="col-span-4">
|
||||||
|
<Button
|
||||||
|
className="flex items-center gap-3"
|
||||||
|
onClick={() => {
|
||||||
|
append({
|
||||||
|
type: '',
|
||||||
|
number: '',
|
||||||
|
dateOfIssue: null
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
<PlusIcon size="lg" />
|
||||||
|
Add Certificate
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PilotFormCertificates;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export type Certificate = {
|
||||||
|
type: string;
|
||||||
|
number: string;
|
||||||
|
dateOfIssue: Date;
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { Endorsement } from './endorsement.type';
|
||||||
|
|
||||||
|
export interface IPilotFormEndorsements {
|
||||||
|
endorsements: Endorsement[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { IPilotFormEndorsements } from './IPilotFormEndorsements';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DatePicker,
|
||||||
|
Input,
|
||||||
|
Option,
|
||||||
|
PlusIcon,
|
||||||
|
Select,
|
||||||
|
TrashIcon,
|
||||||
|
Typography
|
||||||
|
} from '@noahspan/noahspan-components';
|
||||||
|
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
const PilotFormEndorsements: React.FC<IPilotFormEndorsements> = ({
|
||||||
|
endorsements
|
||||||
|
}: IPilotFormEndorsements) => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
formState: { errors },
|
||||||
|
setValue
|
||||||
|
} = useFormContext();
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
name: 'endorsements',
|
||||||
|
control
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{fields.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Typography variant="h6">Type</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Typography variant="h6">Date of Issue</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1"></div>
|
||||||
|
<div className="col-span-1"></div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{fields.map((field, index) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Controller
|
||||||
|
name={`endorsements.${index}.type`}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Select label="Type" {...field}>
|
||||||
|
<Option key="complex" value="Complex">
|
||||||
|
Complex
|
||||||
|
</Option>
|
||||||
|
<Option key="highPerformance" value="High Performance">
|
||||||
|
High Performance
|
||||||
|
</Option>
|
||||||
|
<Option key="highAltitude" value="High Altitude">
|
||||||
|
High Altitude
|
||||||
|
</Option>
|
||||||
|
<Option key="tailwheel" value="Tailwheel">
|
||||||
|
Tailwheel
|
||||||
|
</Option>
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Controller
|
||||||
|
name={`endorsements.${index}.dateOfIssue`}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<DatePicker
|
||||||
|
handleDateChanged={(date: string) => {
|
||||||
|
setValue(`endorsements.${index}.dateOfIssue`, date);
|
||||||
|
}}
|
||||||
|
inputProps={{
|
||||||
|
value: field.value
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<Button
|
||||||
|
className="flex items-center gap-3"
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
<TrashIcon size="lg" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div className="col-span-4">
|
||||||
|
<Button
|
||||||
|
className="flex items-center gap-3"
|
||||||
|
onClick={() => {
|
||||||
|
append({
|
||||||
|
type: '',
|
||||||
|
number: '',
|
||||||
|
dateOfIssue: null
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
|
<PlusIcon size="lg" />
|
||||||
|
Add Endorsement
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PilotFormEndorsements;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export type Endorsement = {
|
||||||
|
type: string;
|
||||||
|
number: string;
|
||||||
|
dateOfIssue: Date;
|
||||||
|
};
|
||||||
@@ -1,65 +1,127 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import SiteNav from '../siteNav/SiteNav';
|
import PilotForm from '../pilotForm/PilotForm';
|
||||||
// import PilotForm from '../pilotForm/PilotForm';
|
|
||||||
import { Button } from '@nextui-org/react';
|
|
||||||
import {
|
import {
|
||||||
Drawer,
|
Button,
|
||||||
DrawerBody,
|
EllipsisVerticalIcon,
|
||||||
DrawerContent,
|
EyeIcon,
|
||||||
DrawerFooter,
|
IconButton,
|
||||||
DrawerHeader
|
Menu,
|
||||||
|
MenuHandler,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
PenIcon,
|
||||||
|
PlusIcon,
|
||||||
|
Table,
|
||||||
|
TableColumnDef,
|
||||||
|
TrashIcon,
|
||||||
|
Typography
|
||||||
} from '@noahspan/noahspan-components';
|
} from '@noahspan/noahspan-components';
|
||||||
import { faPlus } from '@fortawesome/free-solid-svg-icons';
|
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||||
|
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
||||||
|
|
||||||
const Pilots: React.FC<unknown> = () => {
|
const Pilots: React.FC<unknown> = () => {
|
||||||
|
const httpClient: AxiosInstance = useHttpClient();
|
||||||
|
const { getAccessToken } = useAccessToken();
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
|
const [pilots, setPilots] = useState<Pilot[]>([]);
|
||||||
const onOpenCloseDrawer = () => {
|
const onOpenCloseDrawer = () => {
|
||||||
setIsDrawerOpen(!isDrawerOpen);
|
setIsDrawerOpen(!isDrawerOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Pilot = {
|
||||||
|
partitionKey: string;
|
||||||
|
rowKey: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: TableColumnDef[] = [
|
||||||
|
{
|
||||||
|
accessorKey: 'name',
|
||||||
|
header: 'Name'
|
||||||
|
}
|
||||||
|
// {
|
||||||
|
// id: 'actions',
|
||||||
|
// header: 'Actions',
|
||||||
|
// cellProps: {
|
||||||
|
// className: 'text-center'
|
||||||
|
// },
|
||||||
|
// cell: () => {
|
||||||
|
// return (
|
||||||
|
// <Menu placement="bottom-end">
|
||||||
|
// <MenuHandler>
|
||||||
|
// <div>
|
||||||
|
// <IconButton variant="text">
|
||||||
|
// <EllipsisVerticalIcon size="xl" />
|
||||||
|
// </IconButton>
|
||||||
|
// </div>
|
||||||
|
// </MenuHandler>
|
||||||
|
// <MenuList>
|
||||||
|
// <MenuItem className="flex gap-3">
|
||||||
|
// <PenIcon size="lg" />
|
||||||
|
// Edit
|
||||||
|
// </MenuItem>
|
||||||
|
// <MenuItem className="flex gap-3">
|
||||||
|
// <EyeIcon size="lg" />
|
||||||
|
// View
|
||||||
|
// </MenuItem>
|
||||||
|
// <hr className="my-3" />
|
||||||
|
// <MenuItem className="flex gap-3">
|
||||||
|
// <TrashIcon size="lg" />
|
||||||
|
// Delete
|
||||||
|
// </MenuItem>
|
||||||
|
// </MenuList>
|
||||||
|
// </Menu>
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// enableSorting: false
|
||||||
|
// }
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getPilots = async () => {
|
||||||
|
try {
|
||||||
|
const accessToken: string = await getAccessToken();
|
||||||
|
const response: AxiosResponse = await httpClient.get(`api/pilots`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: accessToken
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(response.data);
|
||||||
|
setPilots(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getPilots();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto">
|
<>
|
||||||
<SiteNav />
|
<div className="grid grid-cols-1 gap-4 w-full rounded-xl py-4 px-8 shadow-md backdrop-saturate-200 backdrop-blur-2xl bg-opacity-80 border border-white/80 bg-white mt-6">
|
||||||
<div className="px-6">
|
<div className="col-span-1">
|
||||||
<h1 role="heading">Pilots</h1>
|
<Typography variant="h2">Pilots</Typography>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1 justify-self-end">
|
||||||
<Button
|
<Button
|
||||||
color="default"
|
className="flex justify-center gap-3"
|
||||||
variant="light"
|
variant="filled"
|
||||||
onClick={onOpenCloseDrawer}
|
onClick={onOpenCloseDrawer}
|
||||||
startContent={<FontAwesomeIcon icon={faPlus} />}
|
data-testid="pilot-add-button"
|
||||||
data-testid="new-pilot-button"
|
|
||||||
>
|
>
|
||||||
New
|
<PlusIcon size="lg" />
|
||||||
</Button>
|
Add Pilot
|
||||||
<Drawer isOpen={isDrawerOpen} data-testid="pilot-drawer">
|
|
||||||
<DrawerContent>
|
|
||||||
<DrawerHeader>
|
|
||||||
<h2>Add Pilot</h2>
|
|
||||||
</DrawerHeader>
|
|
||||||
<DrawerBody>{/* <PilotForm /> */}</DrawerBody>
|
|
||||||
<DrawerFooter>
|
|
||||||
<div className="flex gap-4 justify-end justify-self-center">
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
color="default"
|
|
||||||
onClick={onOpenCloseDrawer}
|
|
||||||
data-testid="pilot-drawer-cancel-button"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{pilots.length > 0 && <Table defaultData={pilots} columns={columns} />}
|
||||||
<Button color="primary" onClick={onOpenCloseDrawer}>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DrawerFooter>
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<PilotForm
|
||||||
|
isDrawerOpen={isDrawerOpen}
|
||||||
|
onOpenCloseDrawer={onOpenCloseDrawer}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
7
app/src/components/siteNav/ISiteNavProps.ts
Normal file
7
app/src/components/siteNav/ISiteNavProps.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { InteractionStatus } from '@azure/msal-browser';
|
||||||
|
|
||||||
|
export interface ISiteNavProps {
|
||||||
|
handleSignIn: () => void;
|
||||||
|
handleSignOut: () => void;
|
||||||
|
inProgress: InteractionStatus;
|
||||||
|
}
|
||||||
@@ -1,67 +1,258 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { ISiteNavProps } from './ISiteNavProps';
|
||||||
|
// import { Link as ReactRouterLink } from 'react-router-dom';
|
||||||
|
import { useAppContext } from '../../hooks/appContext/UseAppContext';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Button,
|
||||||
Link,
|
Menu,
|
||||||
|
MenuHandler,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
Navbar,
|
Navbar,
|
||||||
NavbarBrand,
|
NavbarBrand,
|
||||||
NavbarContent,
|
NavbarLinks,
|
||||||
NavbarItem,
|
NavbarMenu,
|
||||||
Button
|
NavbarItemProps,
|
||||||
} from '@nextui-org/react';
|
PlaneIcon,
|
||||||
import { Link as ReactRouterLink } from 'react-router-dom';
|
SignOutIcon,
|
||||||
|
Spinner,
|
||||||
|
Typography
|
||||||
|
} from '@noahspan/noahspan-components';
|
||||||
import { useIsAuthenticated, useMsal } from '@azure/msal-react';
|
import { useIsAuthenticated, useMsal } from '@azure/msal-react';
|
||||||
import { useAppContext } from '../../hooks/appContext/UseAppContext';
|
import { InteractionStatus } from '@azure/msal-browser';
|
||||||
import { Logo, Plane } from '@noahspan/noahspan-components';
|
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
||||||
|
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||||
|
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||||
|
import { User } from '@microsoft/microsoft-graph-types';
|
||||||
|
import { EventMessage, EventPayload, EventType } from '@azure/msal-browser';
|
||||||
|
|
||||||
|
type EventPayloadExtended = EventPayload & { accessToken: string };
|
||||||
|
|
||||||
const SiteNav: React.FC<unknown> = () => {
|
const SiteNav: React.FC<unknown> = () => {
|
||||||
const isAuthenticated = useIsAuthenticated();
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const { accounts, instance } = useMsal();
|
const [userPhoto, setUserPhoto] = useState<string>();
|
||||||
const initializeLogin = () => {
|
const httpClient: AxiosInstance = useHttpClient();
|
||||||
instance.loginRedirect();
|
|
||||||
};
|
|
||||||
const appContext = useAppContext();
|
const appContext = useAppContext();
|
||||||
|
const isAuthenticated = useIsAuthenticated();
|
||||||
|
const { getAccessToken } = useAccessToken();
|
||||||
|
const { inProgress, instance } = useMsal();
|
||||||
|
const navItems: NavbarItemProps[] = [
|
||||||
|
{
|
||||||
|
name: 'Pilots',
|
||||||
|
url: '#'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const handleSignIn = async () => {
|
||||||
|
await instance.loginRedirect({
|
||||||
|
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleSignOut = () => {
|
||||||
|
instance.logoutRedirect();
|
||||||
|
};
|
||||||
|
const getUserProfile = async (accessToken: string): Promise<User> => {
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse = await httpClient.get(`api/userProfile`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: accessToken
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const userProfile: User = response.data;
|
||||||
|
|
||||||
console.log(accounts);
|
return userProfile;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getUserPhoto = async (accessToken: string): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const response: AxiosResponse = await httpClient.get(`api/userPhoto`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: accessToken
|
||||||
|
},
|
||||||
|
responseType: 'arraybuffer'
|
||||||
|
});
|
||||||
|
const arrayBufferView = new Uint8Array(response.data);
|
||||||
|
const blob = new Blob([arrayBufferView], { type: 'image/png' });
|
||||||
|
const imageUrl = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
return imageUrl;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const callback = instance.addEventCallback(
|
||||||
|
async (message: EventMessage) => {
|
||||||
|
if (message.eventType === EventType.LOGIN_SUCCESS) {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const eventPayload: EventPayloadExtended =
|
||||||
|
message.payload as EventPayloadExtended;
|
||||||
|
const userProfile: User = await getUserProfile(
|
||||||
|
eventPayload.accessToken
|
||||||
|
);
|
||||||
|
const userPhoto = await getUserPhoto(eventPayload.accessToken);
|
||||||
|
|
||||||
|
appContext.dispatch({
|
||||||
|
type: 'SET_USER_PROFILE',
|
||||||
|
payload: userProfile
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (callback) {
|
||||||
|
instance.removeEventCallback(callback);
|
||||||
|
appContext.dispatch({ type: 'SET_USER_PROFILE', payload: {} });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const setUserProfile = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const accessToken: string = await getAccessToken();
|
||||||
|
const userProfile = await getUserProfile(accessToken);
|
||||||
|
const userPhoto = await getUserPhoto(accessToken);
|
||||||
|
|
||||||
|
setUserPhoto(userPhoto);
|
||||||
|
|
||||||
|
appContext.dispatch({
|
||||||
|
type: 'SET_USER_PROFILE',
|
||||||
|
payload: userProfile
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
isAuthenticated &&
|
||||||
|
Object.keys(appContext.state.userProfile).length === 0
|
||||||
|
) {
|
||||||
|
setUserProfile();
|
||||||
|
}
|
||||||
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Navbar
|
<Navbar className="mt-6">
|
||||||
classNames={{
|
|
||||||
base: 'bg-transparent z-0'
|
|
||||||
}}
|
|
||||||
isBlurred={false}
|
|
||||||
maxWidth="full"
|
|
||||||
data-testid="flying-navbar"
|
|
||||||
>
|
|
||||||
<NavbarBrand>
|
<NavbarBrand>
|
||||||
<Logo className="pr-3" height={50} width={50} data-testid="logo" />
|
<img height={40} width={40} src="noahspan-logo.png" />{' '}
|
||||||
<Plane size="2xl" />
|
<PlaneIcon size="2x" />
|
||||||
</NavbarBrand>
|
</NavbarBrand>
|
||||||
<NavbarContent justify="center">
|
<NavbarLinks items={navItems} />
|
||||||
<NavbarItem>
|
<NavbarMenu>
|
||||||
{appContext.state.featureFlags.find(
|
<div className="flex items-center gap-2 hidden lg:inline-block">
|
||||||
(featureFlag) => featureFlag.key === 'flying-pilots'
|
{!loading && isAuthenticated && (
|
||||||
)?.enabled && (
|
<Menu placement="bottom-end">
|
||||||
<Link>
|
<MenuHandler>
|
||||||
<ReactRouterLink to="/">Pilots</ReactRouterLink>
|
<>
|
||||||
</Link>
|
{/* {userPhoto && */}
|
||||||
|
{/* <img className='rounded-full' src={userPhoto} /> */}
|
||||||
|
{/* } */}
|
||||||
|
{/* {!userPhoto && */}
|
||||||
|
<div className="rounded-full text-white text-center pt-2 bg-black h-[40px] w-[40px]">
|
||||||
|
NS
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <div className="flex gap-2">
|
||||||
|
<div className='flex-none'>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<Button className='flex-1' variant="text" size="sm">
|
||||||
|
{appContext.state.userProfile.displayName}
|
||||||
|
</Button>
|
||||||
|
</div> */}
|
||||||
|
</>
|
||||||
|
</MenuHandler>
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem onClick={handleSignOut}>
|
||||||
|
<Typography
|
||||||
|
className="flex justify-center gap-3"
|
||||||
|
variant="small"
|
||||||
|
>
|
||||||
|
<SignOutIcon size="lg" />
|
||||||
|
Sign Out
|
||||||
|
</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
{loading && isAuthenticated && (
|
||||||
|
<div className="flex justify-center gap-3">
|
||||||
|
<Spinner size="xs" />
|
||||||
|
<Typography variant="small">Loading...</Typography>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</NavbarItem>
|
|
||||||
</NavbarContent>
|
|
||||||
<NavbarContent justify="end">
|
|
||||||
{!isAuthenticated && (
|
{!isAuthenticated && (
|
||||||
<Button
|
<Button
|
||||||
as={Link}
|
variant="text"
|
||||||
color="primary"
|
size="sm"
|
||||||
href="#"
|
onClick={handleSignIn}
|
||||||
onClick={initializeLogin}
|
loading={inProgress === InteractionStatus.Login ? true : false}
|
||||||
data-testid="login-link"
|
|
||||||
>
|
>
|
||||||
Login
|
Sign In
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{isAuthenticated && <Avatar name={accounts[0]?.username} />}
|
</div>
|
||||||
</NavbarContent>
|
{/* <IconButton
|
||||||
|
variant='text'
|
||||||
|
className='ml-auto h-6 w-6 text-inherit hover:bg-transparent focus:bg-transparent active:bg-transparent lg:hidden'
|
||||||
|
ripple={false}
|
||||||
|
onClick={() => setOpenNav(!openNav)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faBars} size='2x' />
|
||||||
|
</IconButton> */}
|
||||||
|
</NavbarMenu>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <Navbar
|
||||||
|
// classNames={{
|
||||||
|
// base: 'bg-transparent z-0'
|
||||||
|
// }}
|
||||||
|
// isBlurred={false}
|
||||||
|
// maxWidth="full"
|
||||||
|
// data-testid="flying-navbar"
|
||||||
|
// >
|
||||||
|
// <NavbarBrand>
|
||||||
|
// <Logo className="pr-3" height={50} width={50} data-testid="logo" />
|
||||||
|
// <Plane size="2xl" />
|
||||||
|
// </NavbarBrand>
|
||||||
|
// <NavbarContent justify="center">
|
||||||
|
// <NavbarItem>
|
||||||
|
// {appContext.state.featureFlags.find(
|
||||||
|
// (featureFlag) => featureFlag.key === 'flying-pilots'
|
||||||
|
// )?.enabled && (
|
||||||
|
// <Link>
|
||||||
|
// <ReactRouterLink to="/">Pilots</ReactRouterLink>
|
||||||
|
// </Link>
|
||||||
|
// )}
|
||||||
|
// </NavbarItem>
|
||||||
|
// </NavbarContent>
|
||||||
|
// <NavbarContent justify='end'>
|
||||||
|
// <Login
|
||||||
|
// loginCompleted={loginCompleted}
|
||||||
|
// loginView='compact'
|
||||||
|
// />
|
||||||
|
// </NavbarContent>
|
||||||
|
// </Navbar>
|
||||||
|
// );
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SiteNav;
|
export default SiteNav;
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ const AppContextProvider: React.FC<IAppContextProviderProps> = (
|
|||||||
props: IAppContextProviderProps
|
props: IAppContextProviderProps
|
||||||
) => {
|
) => {
|
||||||
const intialState: IAppContextState = {
|
const intialState: IAppContextState = {
|
||||||
featureFlags: []
|
featureFlags: [],
|
||||||
|
userProfile: {}
|
||||||
};
|
};
|
||||||
const [state, dispatch] = useReducer(reducer, intialState);
|
const [state, dispatch] = useReducer(reducer, intialState);
|
||||||
const contextValue: IAppContextProps = useMemo(() => {
|
const contextValue: IAppContextProps = useMemo(() => {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { User } from '@microsoft/microsoft-graph-types';
|
||||||
|
|
||||||
export interface IAppContextState {
|
export interface IAppContextState {
|
||||||
featureFlags: { key: string; enabled: boolean }[];
|
featureFlags: { key: string; enabled: boolean }[];
|
||||||
|
userProfile: User;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import { User } from '@microsoft/microsoft-graph-types';
|
||||||
import { IAppContextState } from './IAppContextState';
|
import { IAppContextState } from './IAppContextState';
|
||||||
|
|
||||||
export type Action = {
|
export type Action =
|
||||||
type: 'SET_FEATURE_FLAGS';
|
| { type: 'SET_FEATURE_FLAGS'; payload: { key: string; enabled: boolean }[] }
|
||||||
payload: { key: string; enabled: boolean }[];
|
| { type: 'SET_USER_PROFILE'; payload: User };
|
||||||
};
|
|
||||||
|
|
||||||
export const reducer = (
|
export const reducer = (
|
||||||
state: IAppContextState,
|
state: IAppContextState,
|
||||||
@@ -16,6 +16,12 @@ export const reducer = (
|
|||||||
featureFlags: action.payload
|
featureFlags: action.payload
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case 'SET_USER_PROFILE': {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
userProfile: action.payload
|
||||||
|
};
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
32
app/src/hooks/accessToken/UseAcessToken.tsx
Normal file
32
app/src/hooks/accessToken/UseAcessToken.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import {
|
||||||
|
AuthenticationResult,
|
||||||
|
InteractionRequiredAuthError
|
||||||
|
} from '@azure/msal-browser';
|
||||||
|
import { useMsal } from '@azure/msal-react';
|
||||||
|
|
||||||
|
export const useAccessToken = () => {
|
||||||
|
const { accounts, instance } = useMsal();
|
||||||
|
const getAccessToken = async () => {
|
||||||
|
const tokenRequest = {
|
||||||
|
account: accounts[0],
|
||||||
|
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`]
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response: AuthenticationResult =
|
||||||
|
await instance.acquireTokenSilent(tokenRequest);
|
||||||
|
|
||||||
|
return `Bearer ${response.accessToken}`;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof InteractionRequiredAuthError) {
|
||||||
|
await instance.acquireTokenRedirect(tokenRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getAccessToken
|
||||||
|
};
|
||||||
|
};
|
||||||
24
app/src/hooks/httpClient/UseHttpClient.tsx
Normal file
24
app/src/hooks/httpClient/UseHttpClient.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import axios, { AxiosInstance, CreateAxiosDefaults } from 'axios';
|
||||||
|
|
||||||
|
export const useHttpClient = () => {
|
||||||
|
let config: CreateAxiosDefaults<any>;
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
config = {
|
||||||
|
baseURL: import.meta.env.VITE_API_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
config = {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const httpClient: AxiosInstance = axios.create(config);
|
||||||
|
|
||||||
|
return httpClient;
|
||||||
|
};
|
||||||
@@ -3,10 +3,8 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
h1 {
|
body {
|
||||||
@apply text-2xl font-bold leading-7 text-gray-900;
|
@apply bg-[#ECEFF1];
|
||||||
}
|
@apply text-black;
|
||||||
h2 {
|
|
||||||
@apply text-2xl font-bold leading-7 text-gray-900;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,28 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { MsalProvider } from '@azure/msal-react';
|
|
||||||
import { Configuration, PublicClientApplication } from '@azure/msal-browser';
|
|
||||||
import { NextUIProvider } from '@nextui-org/react';
|
|
||||||
import App from './App.tsx';
|
import App from './App.tsx';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
|
||||||
import AppContextProvider from './context/appContext/AppContextProvider.tsx';
|
import AppContextProvider from './context/appContext/AppContextProvider.tsx';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import '@noahspan/noahspan-components/noahspan-components.css';
|
import '@noahspan/noahspan-components/noahspan-components.css';
|
||||||
|
import { PublicClientApplication } from '@azure/msal-browser';
|
||||||
|
import { MsalProvider } from '@azure/msal-react';
|
||||||
|
|
||||||
const configuration: Configuration = {
|
const pca: PublicClientApplication = new PublicClientApplication({
|
||||||
auth: {
|
auth: {
|
||||||
clientId: 'd3562a45-050d-4f9a-baed-0497c7156924',
|
clientId: import.meta.env.VITE_CLIENT_ID,
|
||||||
authority:
|
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}`,
|
||||||
'https://login.microsoftonline.com/0f23652e-4b15-420f-991e-3d6fc769a31d',
|
redirectUri: import.meta.env.VITE_REDIRECT_URL
|
||||||
redirectUri: 'http://localhost:5173'
|
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
const pca = new PublicClientApplication(configuration);
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<MsalProvider instance={pca}>
|
<MsalProvider instance={pca}>
|
||||||
<AppContextProvider>
|
<AppContextProvider>
|
||||||
<NextUIProvider>
|
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</NextUIProvider>
|
|
||||||
</AppContextProvider>
|
</AppContextProvider>
|
||||||
</MsalProvider>
|
</MsalProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
|
|||||||
11
app/src/vite-env.d.ts
vendored
11
app/src/vite-env.d.ts
vendored
@@ -1 +1,12 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_API_URL: string;
|
||||||
|
readonly VITE_CLIENT_ID: string;
|
||||||
|
readonly VITE_TENANT_ID: string;
|
||||||
|
readonly VITE_REDIRECT_URL: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
const { nextui } = require('@nextui-org/react');
|
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
export default {
|
export default {
|
||||||
content: [
|
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||||
'./index.html',
|
|
||||||
'./src/**/*.{js,ts,jsx,tsx}',
|
|
||||||
'../node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}'
|
|
||||||
],
|
|
||||||
theme: {
|
theme: {
|
||||||
extend: {}
|
extend: {}
|
||||||
},
|
},
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
plugins: [nextui()]
|
plugins: []
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": false,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
resource "azurerm_storage_account" "storage_account" {
|
resource "azurerm_storage_account" "storage_account_dev" {
|
||||||
name = var.STORAGE_ACCOUNT_NAME
|
name = "${var.STORAGE_ACCOUNT_NAME}dev"
|
||||||
resource_group_name = data.azurerm_resource_group.resource_group.name
|
resource_group_name = data.azurerm_resource_group.resource_group.name
|
||||||
location = data.azurerm_resource_group.resource_group.location
|
location = data.azurerm_resource_group.resource_group.location
|
||||||
account_tier = "Standard"
|
account_tier = "Standard"
|
||||||
account_replication_type = "GRS"
|
account_replication_type = "LRS"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "azurerm_storage_account" "storage_account_staging" {
|
||||||
|
name = "${var.STORAGE_ACCOUNT_NAME}staging"
|
||||||
|
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_account" "storage_account_prod" {
|
||||||
|
name = "${var.STORAGE_ACCOUNT_NAME}prod"
|
||||||
|
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"
|
||||||
}
|
}
|
||||||
7369
package-lock.json
generated
7369
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@
|
|||||||
"tests"
|
"tests"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"start": "concurrently 'npm run start:azure -w api' 'wait-on tcp:0.0.0.0:7071 && npm run dev -w app'",
|
||||||
"format": "prettier --write \"**/src/**/*.ts\" \"**/test/**/*.ts\"",
|
"format": "prettier --write \"**/src/**/*.ts\" \"**/test/**/*.ts\"",
|
||||||
"lint": "npm run lint -w api && npm run lint -w app",
|
"lint": "npm run lint -w api && npm run lint -w app",
|
||||||
"prepare": "husky || true"
|
"prepare": "husky || true"
|
||||||
@@ -14,13 +15,15 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||||
"@typescript-eslint/parser": "^7.2.0",
|
"@typescript-eslint/parser": "^7.2.0",
|
||||||
|
"concurrently": "^8.2.2",
|
||||||
"eslint": "^8.42.0",
|
"eslint": "^8.42.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.6",
|
"eslint-plugin-react-refresh": "^0.4.6",
|
||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
"lint-staged": "^15.2.2",
|
"lint-staged": "^15.2.2",
|
||||||
"prettier": "3.2.5"
|
"prettier": "3.2.5",
|
||||||
|
"wait-on": "^7.2.0"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"**/*": "prettier --write \"**/src/**/*.ts\" \"**/test/**/*.ts\" --ignore-unknown"
|
"**/*": "prettier --write \"**/src/**/*.ts\" \"**/test/**/*.ts\" --ignore-unknown"
|
||||||
|
|||||||
@@ -6,9 +6,18 @@ Feature: Pilots
|
|||||||
When the user clicks the "Pilots" navbar link
|
When the user clicks the "Pilots" navbar link
|
||||||
Then the user is on the "Pilots" page
|
Then the user is on the "Pilots" page
|
||||||
|
|
||||||
Scenario: Cancel Add Pilot drawer
|
# Scenario: Cancel Pilot drawer
|
||||||
Given the user is on the "Pilots" page
|
# Given the user is on the "Pilots" page
|
||||||
When the user clicks the New button
|
# When the user clicks the New button
|
||||||
Then the pilot drawer is visible
|
# Then the pilot drawer is visible
|
||||||
When the user clicks the pilot drawer cancel button
|
# When the user clicks the pilot drawer cancel button
|
||||||
Then the pilot drawer is no longer visible
|
# Then the pilot drawer is no longer visible
|
||||||
|
|
||||||
|
# Scenario: Add pilot
|
||||||
|
# Given the user is on the "Pilots" page
|
||||||
|
# When the user clicks the Add Pilot button
|
||||||
|
# And the pilot drawer is visible
|
||||||
|
# And the user enters the pilot's information
|
||||||
|
# And the user clicks the Save button
|
||||||
|
# Then the Add Pilot drawer is no longer visible
|
||||||
|
# And the pilot is in the Pilots list
|
||||||
@@ -3,24 +3,28 @@ import { config } from '../support/config';
|
|||||||
|
|
||||||
export class PilotsPage {
|
export class PilotsPage {
|
||||||
page: Page;
|
page: Page;
|
||||||
newButton: Locator;
|
|
||||||
pilotDrawer: Locator;
|
pilotDrawer: Locator;
|
||||||
pilotDrawerCancelButton: Locator;
|
pilotAddButton: Locator;
|
||||||
|
pilotSaveButton: Locator;
|
||||||
|
pilotCancelButton: Locator;
|
||||||
|
|
||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
this.newButton = page.getByTestId('new-pilot-button');
|
|
||||||
this.pilotDrawer = page.getByTestId('pilot-drawer');
|
this.pilotDrawer = page.getByTestId('pilot-drawer');
|
||||||
this.pilotDrawerCancelButton = page.getByTestId(
|
this.pilotAddButton = page.getByTestId('add-pilot-button');
|
||||||
'pilot-drawer-cancel-button'
|
this.pilotSaveButton = page.getByTestId('save-pilot-button');
|
||||||
);
|
this.pilotCancelButton = page.getByTestId('pilot-form-cancel-button');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async clickNewButton() {
|
public async clickAddPilotButton() {
|
||||||
await this.newButton.click();
|
await this.pilotAddButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async clickSaveButton() {
|
||||||
|
await this.pilotSaveButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async clickPilotDrawerCancelButton() {
|
public async clickPilotDrawerCancelButton() {
|
||||||
await this.pilotDrawerCancelButton.click();
|
await this.pilotCancelButton.click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,27 @@ import { config } from '../support/config';
|
|||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
import { PilotsPage } from '../pages/pilots.page';
|
import { PilotsPage } from '../pages/pilots.page';
|
||||||
|
|
||||||
When('the user clicks the New button', async function (this: ICustomWorld) {
|
When(
|
||||||
|
'the user clicks the Add Pilot button',
|
||||||
|
async function (this: ICustomWorld) {
|
||||||
const pilotsPage = new PilotsPage(this.page!);
|
const pilotsPage = new PilotsPage(this.page!);
|
||||||
|
|
||||||
await pilotsPage.clickNewButton();
|
await pilotsPage.clickAddPilotButton();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
When(
|
||||||
|
`the user enters the pilot's information`,
|
||||||
|
async function (this: ICustomWorld) {
|
||||||
|
const pilotsPage = new PilotsPage(this.page!);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// When(`the user clicks the Save button`, async function (this: ICustomWorld) {
|
||||||
|
// const pilotsPage = new PilotsPage(this.page!);
|
||||||
|
|
||||||
|
// await pil
|
||||||
|
// })
|
||||||
|
|
||||||
When(
|
When(
|
||||||
'the user clicks the pilot drawer cancel button',
|
'the user clicks the pilot drawer cancel button',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"@cucumber/cucumber": "^10.7.0",
|
"@cucumber/cucumber": "^10.7.0",
|
||||||
"@playwright/test": "^1.44.0",
|
"@playwright/test": "^1.44.0",
|
||||||
"@types/fs-extra": "^11.0.4",
|
"@types/fs-extra": "^11.0.4",
|
||||||
"fs-extra": "^11.2.0"
|
"fs-extra": "^11.2.0",
|
||||||
|
"otpauth": "^9.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user