Compare commits
1 Commits
102-switch
...
feature/82
| Author | SHA1 | Date | |
|---|---|---|---|
| d0f15ee160 |
@@ -15,11 +15,16 @@ jobs:
|
||||
environment: ${{ inputs.environment_name }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
api
|
||||
client
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
- name: Install Nest CLI
|
||||
run: |
|
||||
@@ -32,15 +37,16 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm ci
|
||||
pnpm install
|
||||
|
||||
- name: Build API
|
||||
- name: Build
|
||||
run: |
|
||||
npm run build -w api
|
||||
pnpm --filter api build
|
||||
|
||||
- name: Build Client
|
||||
- name: Deploy
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
run: |
|
||||
npm run build -w client
|
||||
pnpm --filter api --prod deploy ./.prod/api
|
||||
|
||||
- name: Log into Docker Hub
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
@@ -58,5 +64,6 @@ jobs:
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
tags: noahspan/flying:${{ inputs.version_number }}
|
||||
tags: noahspan/flying-api:${{ inputs.version_number }}
|
||||
context: .
|
||||
target: api
|
||||
70
.github/workflows/app_build.yaml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
name: 'App Build'
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
environment_name:
|
||||
required: true
|
||||
type: string
|
||||
version_number:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
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:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.environment_name }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
app
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
- name: 'Setup Node'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ vars.NODE_VERSION }}
|
||||
|
||||
- name: Install
|
||||
run: |
|
||||
pnpm install
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
pnpm --filter app build
|
||||
|
||||
- name: Deploy
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
run: |
|
||||
pnpm --filter app --prod deploy ./.prod/app
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log into Docker Hub
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push to registry
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
tags: noahspan/flying-app:${{ inputs.version_number }}
|
||||
context: .
|
||||
target: app
|
||||
11
.github/workflows/changes.yaml
vendored
@@ -4,8 +4,8 @@ on:
|
||||
outputs:
|
||||
api:
|
||||
value: ${{ jobs.changes.outputs.api }}
|
||||
client:
|
||||
value: ${{ jobs.changes.outputs.client }}
|
||||
app:
|
||||
value: ${{ jobs.changes.outputs.app }}
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
@@ -13,15 +13,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
api: ${{ steps.filter.outputs.api }}
|
||||
client: ${{ steps.filter.outputs.client }}
|
||||
app: ${{ steps.filter.outputs.app }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
initial-fetch-depth: 2
|
||||
filters: |
|
||||
api:
|
||||
- 'api/**'
|
||||
client:
|
||||
- 'client/**'
|
||||
app:
|
||||
- 'app/**'
|
||||
6
.github/workflows/deploy.yaml
vendored
@@ -23,13 +23,13 @@ jobs:
|
||||
- name: Log in to Azure
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ vars.AZURE_CLIENT_ID }}
|
||||
client-id: ${{ vars.VITE_CLIENT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant-id: ${{ vars.AZURE_TENANT_ID }}
|
||||
tenant-id: ${{ vars.VITE_TENANT_ID }}
|
||||
|
||||
- name: Azure CLI script
|
||||
uses: azure/cli@v2
|
||||
with:
|
||||
azcliversion: latest
|
||||
inlineScript: |
|
||||
az containerapp update --name ${{ inputs.app_name }}-${{ inputs.environment_name }} --container-name ${{ inputs.app_name }} --resource-group noahspan --image docker.io/noahspan/${{ inputs.app_name }}:${{ inputs.version_number }}
|
||||
az containerapp update --name ${{ inputs.app_name }}-${{ inputs.environment_name }} --resource-group noahspan-flying --image docker.io/noahspan/${{ inputs.app_name }}:${{ inputs.version_number }}
|
||||
39
.github/workflows/main.yaml
vendored
@@ -8,25 +8,48 @@ jobs:
|
||||
changes:
|
||||
uses: ./.github/workflows/changes.yaml
|
||||
|
||||
build:
|
||||
if: ${{ needs.changes.outputs.app == 'true' || needs.changes.outputs.client == 'true' }}
|
||||
name: build
|
||||
build-api:
|
||||
if: ${{ needs.changes.outputs.api == 'true' }}
|
||||
name: build-api
|
||||
needs:
|
||||
- changes
|
||||
uses: ./.github/workflows/build.yaml
|
||||
uses: ./.github/workflows/api_build.yaml
|
||||
with:
|
||||
environment_name: test
|
||||
version_number: ${{ github.run_id }}
|
||||
secrets: inherit
|
||||
|
||||
deploy:
|
||||
name: deploy
|
||||
deploy-api:
|
||||
name: deploy-api
|
||||
needs:
|
||||
- changes
|
||||
- build
|
||||
- build-api
|
||||
uses: ./.github/workflows/deploy.yaml
|
||||
with:
|
||||
app_name: flying
|
||||
app_name: flying-api
|
||||
environment_name: test
|
||||
version_number: ${{ github.run_id }}
|
||||
secrets: inherit
|
||||
|
||||
build-app:
|
||||
if: ${{ needs.changes.outputs.app == 'true' }}
|
||||
name: build-app
|
||||
needs:
|
||||
- changes
|
||||
uses: ./.github/workflows/app_build.yaml
|
||||
with:
|
||||
environment_name: test
|
||||
version_number: ${{ github.run_id }}
|
||||
secrets: inherit
|
||||
|
||||
deploy-app:
|
||||
name: deploy-app
|
||||
needs:
|
||||
- changes
|
||||
- build-app
|
||||
uses: ./.github/workflows/deploy.yaml
|
||||
with:
|
||||
app_name: flying-app
|
||||
environment_name: test
|
||||
version_number: ${{ github.run_id }}
|
||||
secrets: inherit
|
||||
|
||||
19
.github/workflows/pull_request.yaml
vendored
@@ -7,12 +7,23 @@ jobs:
|
||||
changes:
|
||||
uses: ./.github/workflows/changes.yaml
|
||||
|
||||
build:
|
||||
if: ${{ needs.changes.outputs.app == 'true' || needs.changes.outputs.client == 'true'}}
|
||||
name: build
|
||||
build-api:
|
||||
if: ${{ needs.changes.outputs.api == 'true' }}
|
||||
name: build-api
|
||||
needs:
|
||||
- changes
|
||||
uses: ./.github/workflows/build.yaml
|
||||
uses: ./.github/workflows/api_build.yaml
|
||||
with:
|
||||
environment_name: pull_request
|
||||
version_number: ${{ github.run_id }}
|
||||
secrets: inherit
|
||||
|
||||
build-app:
|
||||
if: ${{ needs.changes.outputs.app == 'true' }}
|
||||
name: build-app
|
||||
needs:
|
||||
- changes
|
||||
uses: ./.github/workflows/app_build.yaml
|
||||
with:
|
||||
environment_name: pull_request
|
||||
version_number: ${{ github.run_id }}
|
||||
|
||||
33
.github/workflows/tag.yaml
vendored
@@ -5,21 +5,40 @@ on:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build
|
||||
uses: ./.github/workflows/build.yaml
|
||||
build-api:
|
||||
name: build-api
|
||||
uses: ./.github/workflows/api_build.yaml
|
||||
with:
|
||||
environment_name: prod
|
||||
version_number: ${{ github.ref_name }}
|
||||
secrets: inherit
|
||||
|
||||
deploy:
|
||||
name: deploy
|
||||
deploy-api:
|
||||
name: deploy-api
|
||||
needs:
|
||||
- build
|
||||
- build-api
|
||||
uses: ./.github/workflows/deploy.yaml
|
||||
with:
|
||||
app_name: flying
|
||||
app_name: flying-api
|
||||
environment_name: prod
|
||||
version_number: ${{ github.ref_name }}
|
||||
secrets: inherit
|
||||
|
||||
build-app:
|
||||
name: build-app
|
||||
uses: ./.github/workflows/app_build.yaml
|
||||
with:
|
||||
environment_name: prod
|
||||
version_number: ${{ github.ref_name }}
|
||||
secrets: inherit
|
||||
|
||||
deploy-app:
|
||||
name: deploy-app
|
||||
needs:
|
||||
- build-app
|
||||
uses: ./.github/workflows/deploy.yaml
|
||||
with:
|
||||
app_name: flying-app
|
||||
environment_name: prod
|
||||
version_number: ${{ github.ref_name }}
|
||||
secrets: inherit
|
||||
|
||||
47
Dockerfile
@@ -1,18 +1,39 @@
|
||||
FROM --platform=linux/amd64 node:22-slim
|
||||
# FROM --platform=linux/amd64 node:18-alpine AS base
|
||||
# ENV PNPM_HOME="/pnpm"
|
||||
# ENV PATH="$PNPM_HOME:$PATH"
|
||||
# RUN corepack enable
|
||||
|
||||
WORKDIR app
|
||||
COPY ./api/dist ./api/dist
|
||||
COPY ./api/package.json package-lock.json ./api
|
||||
COPY ./client/dist ./client/dist
|
||||
# FROM base AS api
|
||||
# COPY /api/dist /app/dist
|
||||
# COPY /api/node_modules /app/node_modules
|
||||
# WORKDIR /app
|
||||
# EXPOSE 3000
|
||||
# # CMD ["node", "dist/main.js"]
|
||||
# ENTRYPOINT ["tail", "-f", "/dev/null"]
|
||||
|
||||
# FROM base AS app
|
||||
# COPY /app/dist /app/dist
|
||||
# WORKDIR /app
|
||||
# RUN npm i -g serve
|
||||
# EXPOSE 8080
|
||||
# CMD [ "serve", "-s", "/dist", "-p", "8080" ]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
FROM node:18-slim AS base
|
||||
|
||||
FROM base AS api
|
||||
WORKDIR api
|
||||
RUN npm ci
|
||||
|
||||
WORKDIR /
|
||||
COPY ./api/entrypoint.sh ./entrypoint.sh
|
||||
RUN chmod +x entrypoint.sh
|
||||
|
||||
COPY ./.prod/api .
|
||||
EXPOSE 3000
|
||||
CMD ["node", "dist/main.js"]
|
||||
# ENTRYPOINT ["tail", "-f", "/dev/null"]
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
# ENTRYPOINT ["tail", "-f", "/dev/null"]
|
||||
FROM base AS app
|
||||
WORKDIR /app
|
||||
COPY ./.prod/app .
|
||||
RUN npm i -g serve
|
||||
EXPOSE 8080
|
||||
CMD [ "serve", "-s", "dist", "-p", "8080" ]
|
||||
3
api/.gitignore
vendored
@@ -57,6 +57,3 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
|
||||
local.settings.json
|
||||
|
||||
|
||||
/src/database/*.db
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
npx typeorm migration:run -d ./app/api/dist/database/data-source.js
|
||||
node ./app/api/dist/main.js
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "api",
|
||||
"version": "2.0.0-alpha-3",
|
||||
"version": "1.3.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -16,40 +16,24 @@
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"typeorm": "npm run build && npx typeorm -d dist/database/data-source.js",
|
||||
"migration:generate": "npm run typeorm -- migration:generate",
|
||||
"migration:run": "npm run typeorm -- migration:run",
|
||||
"migration:revert": "npm run typeorm -- migration:revert"
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/storage-blob": "^12.27.0",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.7",
|
||||
"@nestjs/axios": "^4.0.1",
|
||||
"@nestjs/common": "^11.1.6",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.1.6",
|
||||
"@nestjs/jwt": "^11.0.0",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.1.6",
|
||||
"@nestjs/serve-static": "^5.0.3",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"@nestjs/axios": "^3.0.3",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.2.2",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@noahspan/azure-database": "^3.1.2",
|
||||
"@noahspan/noahspan-modules": "^1.2.9",
|
||||
"@noahspan/noahspan-modules": "^1.1.5",
|
||||
"@schematics/angular": "^17.3.7",
|
||||
"@types/multer": "^1.4.12",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"dotenv": "^16.6.1",
|
||||
"express-session": "^1.18.2",
|
||||
"jwks-rsa": "^3.2.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"node-gyp": "^11.4.1",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-openidconnect": "^0.1.2",
|
||||
"dotenv": "^16.4.7",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.25",
|
||||
"uuid": "^10.0.0",
|
||||
"uuidv4": "^6.2.13"
|
||||
},
|
||||
@@ -57,13 +41,11 @@
|
||||
"@microsoft/microsoft-graph-types": "^2.40.0",
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^11.1.6",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/express-session": "^1.18.2",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/passport-azure-ad": "^4.3.6",
|
||||
"@types/passport-openidconnect": "^0.1.3",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
|
||||
22
api/src/app.controller.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService]
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { FeatureFlagModule } from './featureFlag/feature-flag.module'
|
||||
import { LogModule } from './log/log.module';
|
||||
import { PilotModule } from './pilot/pilot.module';
|
||||
import { APP_FILTER, APP_GUARD, Reflector } from '@nestjs/core';
|
||||
import { HttpExceptionFilter } from './filters/http-exception.filter';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { AuthModule, MsGraphModule } from '@noahspan/noahspan-modules';
|
||||
import { AuthGuard, AuthModule, UserModule } from '@noahspan/noahspan-modules';
|
||||
import configuration from './config/configuration';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { dataSourceOptions } from './database/data-source';
|
||||
import { TrackModule } from './track/track.module';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { join } from 'path';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -20,41 +15,35 @@ import { join } from 'path';
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
return {
|
||||
audience: configService.get<string>('audience'),
|
||||
issuerUrl: configService.get<string>('issuer'),
|
||||
jwksUri: configService.get<string>('jwksUri')
|
||||
clientId: configService.get<string>('clientId'),
|
||||
clientSecret: configService.get<string>('clientSecret'),
|
||||
tenantId: configService.get<string>('tenantId')
|
||||
};
|
||||
},
|
||||
}),
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
load: [configuration]
|
||||
}),
|
||||
HealthModule,
|
||||
FeatureFlagModule,
|
||||
LogModule,
|
||||
// MsGraphModule.registerAsync({
|
||||
// inject: [ConfigService],
|
||||
// imports: [ConfigModule],
|
||||
// useFactory: async (configService: ConfigService) => {
|
||||
// return {
|
||||
// clientId: configService.get<string>('clientId'),
|
||||
// clientSecret: configService.get<string>('clientSecret'),
|
||||
// tenantId: configService.get<string>('tenantId')
|
||||
// }
|
||||
// }
|
||||
// }),
|
||||
PilotModule,
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: join(__dirname, '../..', 'client', 'dist')
|
||||
}),
|
||||
TrackModule,
|
||||
TypeOrmModule.forRoot(dataSourceOptions)
|
||||
UserModule.registerAsync({
|
||||
inject: [ConfigService],
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
return {
|
||||
clientId: configService.get<string>('clientId'),
|
||||
clientSecret: configService.get<string>('clientSecret'),
|
||||
tenantId: configService.get<string>('tenantId')
|
||||
}
|
||||
}
|
||||
})
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: HttpExceptionFilter
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
5
api/src/auth/auth.interface.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface AuthModuleOptions {
|
||||
tenantId: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
}
|
||||
4
api/src/auth/auth.module-definition.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { ConfigurableModuleBuilder } from '@nestjs/common';
|
||||
import { AuthModuleOptions } from './auth.interface';
|
||||
|
||||
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder<AuthModuleOptions>().build()
|
||||
14
api/src/auth/auth.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { AzureAdStrategy } from './auth.strategy';
|
||||
import { ConfigurableModuleClass } from './auth.module-definition';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PassportModule.register({
|
||||
defaultStrategy: 'azure-ad'
|
||||
})
|
||||
],
|
||||
providers: [AzureAdStrategy]
|
||||
})
|
||||
export class AuthModule extends ConfigurableModuleClass {}
|
||||
25
api/src/auth/auth.strategy.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Inject, Injectable } from "@nestjs/common";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { AuthModuleOptions } from './auth.interface'
|
||||
import { MODULE_OPTIONS_TOKEN } from "./auth.module-definition";
|
||||
import { BearerStrategy } from 'passport-azure-ad'
|
||||
|
||||
@Injectable()
|
||||
export class AzureAdStrategy extends PassportStrategy(
|
||||
BearerStrategy,
|
||||
'azure-ad'
|
||||
) {
|
||||
constructor(@Inject(MODULE_OPTIONS_TOKEN) authModuleOptions: AuthModuleOptions) {
|
||||
super({
|
||||
identityMetadata: `https://login.microsoftonline.com/${authModuleOptions.tenantId}/.well-known/openid-configuration`,
|
||||
clientID: authModuleOptions.clientId,
|
||||
audience: `api://${authModuleOptions.clientId}`,
|
||||
loggingLevel: 'info',
|
||||
loggingNoPII: false
|
||||
})
|
||||
}
|
||||
|
||||
async validate(data: any): Promise<any> {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { PilotEntity } from '../pilot/pilot.entity';
|
||||
|
||||
@Entity({ name: 'certificates'})
|
||||
export class CertificateEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
type: string;
|
||||
|
||||
@Column()
|
||||
number: string;
|
||||
|
||||
@Column()
|
||||
issueDate: Date
|
||||
|
||||
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.certificates, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
@JoinColumn({ name: 'pilotId' })
|
||||
pilot: PilotEntity;
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
export default () => ({
|
||||
azureStorageConnectionString: process.env.AZURE_STORAGE_CONNECTION_STRING,
|
||||
audience: process.env.AUDIENCE,
|
||||
clientId: process.env.CLIENT_ID,
|
||||
clientSecret: process.env.CLIENT_SECRET,
|
||||
issuer: process.env.ISSUER_URL,
|
||||
jwksUri: process.env.JWKS_URI,
|
||||
tenantId: process.env.TENANT_ID
|
||||
})
|
||||
@@ -1,19 +0,0 @@
|
||||
import { DataSource, DataSourceOptions } from 'typeorm';
|
||||
import { config } from 'dotenv';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
config();
|
||||
|
||||
const configService = new ConfigService();
|
||||
|
||||
export const dataSourceOptions: DataSourceOptions = {
|
||||
type: 'better-sqlite3',
|
||||
database: configService.get<string>('DB_PATH'),
|
||||
entities: ['../**/*.entity.js'],
|
||||
migrations: ['./migrations/*.js'],
|
||||
synchronize: configService.get<boolean>('DB_SYNC')
|
||||
}
|
||||
|
||||
const dataSource = new DataSource(dataSourceOptions);
|
||||
|
||||
export default dataSource;
|
||||
@@ -1,4 +0,0 @@
|
||||
dbs:
|
||||
- path: /var/lib/data/flying.db
|
||||
replicas:
|
||||
- path: /mnt/data/backup
|
||||
@@ -1,64 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class InitialMigration1758802917932 implements MigrationInterface {
|
||||
name = 'InitialMigration1758802917932'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "certificates" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar NOT NULL, "number" varchar NOT NULL, "issueDate" datetime NOT NULL, "pilotId" varchar)`);
|
||||
await queryRunner.query(`CREATE TABLE "endorsements" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar NOT NULL, "issueDate" datetime NOT NULL, "pilotId" varchar)`);
|
||||
await queryRunner.query(`CREATE TABLE "medical" ("id" varchar PRIMARY KEY NOT NULL, "class" varchar NOT NULL, "expirationDate" datetime NOT NULL, "pilotId" varchar)`);
|
||||
await queryRunner.query(`CREATE TABLE "pilots" ("id" varchar PRIMARY KEY NOT NULL, "name" varchar NOT NULL, "address" varchar NOT NULL, "city" varchar NOT NULL, "state" varchar NOT NULL, "postalCode" varchar NOT NULL, "email" varchar NOT NULL, "phone" varchar NOT NULL, "userId" varchar NOT NULL)`);
|
||||
await queryRunner.query(`CREATE TABLE "logs" ("id" varchar PRIMARY KEY NOT NULL, "date" datetime NOT NULL, "aircraftMakeModel" varchar NOT NULL, "aircraftIdentity" varchar NOT NULL, "routeFrom" varchar NOT NULL, "routeTo" varchar NOT NULL, "durationOfFlight" integer NOT NULL, "singleEngineLand" integer, "simulatorAtd" integer, "landingsDay" integer, "landingsNight" integer, "groundTrainingReceived" integer, "flightTrainingReceived" integer, "crossCountry" integer, "night" integer, "solo" integer, "pilotInCommand" integer, "instrumentActual" integer, "instrumentSimulated" integer, "instrumentApproaches" integer, "instrumentHolds" integer, "instrumentNavTrack" integer, "notes" varchar, "pilotId" varchar)`);
|
||||
await queryRunner.query(`CREATE TABLE "tracks" ("id" varchar PRIMARY KEY NOT NULL, "url" varchar NOT NULL, "order" integer NOT NULL, "logId" varchar)`);
|
||||
await queryRunner.query(`CREATE TABLE "temporary_certificates" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar NOT NULL, "number" varchar NOT NULL, "issueDate" datetime NOT NULL, "pilotId" varchar, CONSTRAINT "FK_05a68997dc2d27dfcc642a4cf51" FOREIGN KEY ("pilotId") REFERENCES "pilots" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
|
||||
await queryRunner.query(`INSERT INTO "temporary_certificates"("id", "type", "number", "issueDate", "pilotId") SELECT "id", "type", "number", "issueDate", "pilotId" FROM "certificates"`);
|
||||
await queryRunner.query(`DROP TABLE "certificates"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_certificates" RENAME TO "certificates"`);
|
||||
await queryRunner.query(`CREATE TABLE "temporary_endorsements" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar NOT NULL, "issueDate" datetime NOT NULL, "pilotId" varchar, CONSTRAINT "FK_96a69a0bfbdeccce6bffe34002d" FOREIGN KEY ("pilotId") REFERENCES "pilots" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
|
||||
await queryRunner.query(`INSERT INTO "temporary_endorsements"("id", "type", "issueDate", "pilotId") SELECT "id", "type", "issueDate", "pilotId" FROM "endorsements"`);
|
||||
await queryRunner.query(`DROP TABLE "endorsements"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_endorsements" RENAME TO "endorsements"`);
|
||||
await queryRunner.query(`CREATE TABLE "temporary_medical" ("id" varchar PRIMARY KEY NOT NULL, "class" varchar NOT NULL, "expirationDate" datetime NOT NULL, "pilotId" varchar, CONSTRAINT "FK_cb1f5d88fa2b105cc77513e9082" FOREIGN KEY ("pilotId") REFERENCES "pilots" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
|
||||
await queryRunner.query(`INSERT INTO "temporary_medical"("id", "class", "expirationDate", "pilotId") SELECT "id", "class", "expirationDate", "pilotId" FROM "medical"`);
|
||||
await queryRunner.query(`DROP TABLE "medical"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_medical" RENAME TO "medical"`);
|
||||
await queryRunner.query(`CREATE TABLE "temporary_logs" ("id" varchar PRIMARY KEY NOT NULL, "date" datetime NOT NULL, "aircraftMakeModel" varchar NOT NULL, "aircraftIdentity" varchar NOT NULL, "routeFrom" varchar NOT NULL, "routeTo" varchar NOT NULL, "durationOfFlight" integer NOT NULL, "singleEngineLand" integer, "simulatorAtd" integer, "landingsDay" integer, "landingsNight" integer, "groundTrainingReceived" integer, "flightTrainingReceived" integer, "crossCountry" integer, "night" integer, "solo" integer, "pilotInCommand" integer, "instrumentActual" integer, "instrumentSimulated" integer, "instrumentApproaches" integer, "instrumentHolds" integer, "instrumentNavTrack" integer, "notes" varchar, "pilotId" varchar, CONSTRAINT "FK_19598551658f7625a82c7f029c8" FOREIGN KEY ("pilotId") REFERENCES "pilots" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
|
||||
await queryRunner.query(`INSERT INTO "temporary_logs"("id", "date", "aircraftMakeModel", "aircraftIdentity", "routeFrom", "routeTo", "durationOfFlight", "singleEngineLand", "simulatorAtd", "landingsDay", "landingsNight", "groundTrainingReceived", "flightTrainingReceived", "crossCountry", "night", "solo", "pilotInCommand", "instrumentActual", "instrumentSimulated", "instrumentApproaches", "instrumentHolds", "instrumentNavTrack", "notes", "pilotId") SELECT "id", "date", "aircraftMakeModel", "aircraftIdentity", "routeFrom", "routeTo", "durationOfFlight", "singleEngineLand", "simulatorAtd", "landingsDay", "landingsNight", "groundTrainingReceived", "flightTrainingReceived", "crossCountry", "night", "solo", "pilotInCommand", "instrumentActual", "instrumentSimulated", "instrumentApproaches", "instrumentHolds", "instrumentNavTrack", "notes", "pilotId" FROM "logs"`);
|
||||
await queryRunner.query(`DROP TABLE "logs"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_logs" RENAME TO "logs"`);
|
||||
await queryRunner.query(`CREATE TABLE "temporary_tracks" ("id" varchar PRIMARY KEY NOT NULL, "url" varchar NOT NULL, "order" integer NOT NULL, "logId" varchar, CONSTRAINT "FK_71881df31cfff2362e39accc4b2" FOREIGN KEY ("logId") REFERENCES "logs" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION)`);
|
||||
await queryRunner.query(`INSERT INTO "temporary_tracks"("id", "url", "order", "logId") SELECT "id", "url", "order", "logId" FROM "tracks"`);
|
||||
await queryRunner.query(`DROP TABLE "tracks"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_tracks" RENAME TO "tracks"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "tracks" RENAME TO "temporary_tracks"`);
|
||||
await queryRunner.query(`CREATE TABLE "tracks" ("id" varchar PRIMARY KEY NOT NULL, "url" varchar NOT NULL, "order" integer NOT NULL, "logId" varchar)`);
|
||||
await queryRunner.query(`INSERT INTO "tracks"("id", "url", "order", "logId") SELECT "id", "url", "order", "logId" FROM "temporary_tracks"`);
|
||||
await queryRunner.query(`DROP TABLE "temporary_tracks"`);
|
||||
await queryRunner.query(`ALTER TABLE "logs" RENAME TO "temporary_logs"`);
|
||||
await queryRunner.query(`CREATE TABLE "logs" ("id" varchar PRIMARY KEY NOT NULL, "date" datetime NOT NULL, "aircraftMakeModel" varchar NOT NULL, "aircraftIdentity" varchar NOT NULL, "routeFrom" varchar NOT NULL, "routeTo" varchar NOT NULL, "durationOfFlight" integer NOT NULL, "singleEngineLand" integer, "simulatorAtd" integer, "landingsDay" integer, "landingsNight" integer, "groundTrainingReceived" integer, "flightTrainingReceived" integer, "crossCountry" integer, "night" integer, "solo" integer, "pilotInCommand" integer, "instrumentActual" integer, "instrumentSimulated" integer, "instrumentApproaches" integer, "instrumentHolds" integer, "instrumentNavTrack" integer, "notes" varchar, "pilotId" varchar)`);
|
||||
await queryRunner.query(`INSERT INTO "logs"("id", "date", "aircraftMakeModel", "aircraftIdentity", "routeFrom", "routeTo", "durationOfFlight", "singleEngineLand", "simulatorAtd", "landingsDay", "landingsNight", "groundTrainingReceived", "flightTrainingReceived", "crossCountry", "night", "solo", "pilotInCommand", "instrumentActual", "instrumentSimulated", "instrumentApproaches", "instrumentHolds", "instrumentNavTrack", "notes", "pilotId") SELECT "id", "date", "aircraftMakeModel", "aircraftIdentity", "routeFrom", "routeTo", "durationOfFlight", "singleEngineLand", "simulatorAtd", "landingsDay", "landingsNight", "groundTrainingReceived", "flightTrainingReceived", "crossCountry", "night", "solo", "pilotInCommand", "instrumentActual", "instrumentSimulated", "instrumentApproaches", "instrumentHolds", "instrumentNavTrack", "notes", "pilotId" FROM "temporary_logs"`);
|
||||
await queryRunner.query(`DROP TABLE "temporary_logs"`);
|
||||
await queryRunner.query(`ALTER TABLE "medical" RENAME TO "temporary_medical"`);
|
||||
await queryRunner.query(`CREATE TABLE "medical" ("id" varchar PRIMARY KEY NOT NULL, "class" varchar NOT NULL, "expirationDate" datetime NOT NULL, "pilotId" varchar)`);
|
||||
await queryRunner.query(`INSERT INTO "medical"("id", "class", "expirationDate", "pilotId") SELECT "id", "class", "expirationDate", "pilotId" FROM "temporary_medical"`);
|
||||
await queryRunner.query(`DROP TABLE "temporary_medical"`);
|
||||
await queryRunner.query(`ALTER TABLE "endorsements" RENAME TO "temporary_endorsements"`);
|
||||
await queryRunner.query(`CREATE TABLE "endorsements" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar NOT NULL, "issueDate" datetime NOT NULL, "pilotId" varchar)`);
|
||||
await queryRunner.query(`INSERT INTO "endorsements"("id", "type", "issueDate", "pilotId") SELECT "id", "type", "issueDate", "pilotId" FROM "temporary_endorsements"`);
|
||||
await queryRunner.query(`DROP TABLE "temporary_endorsements"`);
|
||||
await queryRunner.query(`ALTER TABLE "certificates" RENAME TO "temporary_certificates"`);
|
||||
await queryRunner.query(`CREATE TABLE "certificates" ("id" varchar PRIMARY KEY NOT NULL, "type" varchar NOT NULL, "number" varchar NOT NULL, "issueDate" datetime NOT NULL, "pilotId" varchar)`);
|
||||
await queryRunner.query(`INSERT INTO "certificates"("id", "type", "number", "issueDate", "pilotId") SELECT "id", "type", "number", "issueDate", "pilotId" FROM "temporary_certificates"`);
|
||||
await queryRunner.query(`DROP TABLE "temporary_certificates"`);
|
||||
await queryRunner.query(`DROP TABLE "tracks"`);
|
||||
await queryRunner.query(`DROP TABLE "logs"`);
|
||||
await queryRunner.query(`DROP TABLE "pilots"`);
|
||||
await queryRunner.query(`DROP TABLE "medical"`);
|
||||
await queryRunner.query(`DROP TABLE "endorsements"`);
|
||||
await queryRunner.query(`DROP TABLE "certificates"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Body, Controller, Delete, Get, HttpException, Param, Post, Put } from "@nestjs/common";
|
||||
import { EndorsementService } from "./endorsement.service";
|
||||
import { CustomError } from "@noahspan/noahspan-modules";
|
||||
import { EndorsementDto } from "./endorsement.dto";
|
||||
|
||||
|
||||
@Controller('endorsements')
|
||||
export class EndorsementController {
|
||||
constructor(private readonly endorsementService: EndorsementService) {}
|
||||
|
||||
@Get(':id')
|
||||
async find(@Param('id') id: string) {
|
||||
try {
|
||||
return await this.endorsementService.find(id);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Get()
|
||||
async findAll() {
|
||||
try {
|
||||
return await this.endorsementService.findAll();
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(@Body() endorsementDto: EndorsementDto) {
|
||||
try {
|
||||
return await this.endorsementService.create(endorsementDto);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
async update(@Param('id') id: string, @Body() endorsementDto: EndorsementDto) {
|
||||
try {
|
||||
return await this.endorsementService.update(id, endorsementDto);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode)
|
||||
}
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async delete(@Param('id') id: string) {
|
||||
try {
|
||||
return await this.endorsementService.delete(id);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export class EndorsementDto {
|
||||
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { PilotEntity } from 'src/pilot/pilot.entity';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'endorsements' })
|
||||
export class EndorsementEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string
|
||||
|
||||
@Column()
|
||||
type: string;
|
||||
|
||||
@Column()
|
||||
issueDate: Date;
|
||||
|
||||
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.endorsements, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
@JoinColumn({ name: 'pilotId' })
|
||||
pilot: PilotEntity
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { EndorsementEntity } from "./endorsement.entity";
|
||||
import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { EndorsementDto } from "./endorsement.dto";
|
||||
|
||||
@Injectable()
|
||||
export class EndorsementService {
|
||||
constructor(
|
||||
@InjectRepository(EndorsementEntity) private readonly endorsementRepository: Repository<EndorsementEntity>
|
||||
) {}
|
||||
|
||||
async find(id: string): Promise<EndorsementEntity> {
|
||||
return await this.endorsementRepository.findOneBy({ id });
|
||||
}
|
||||
|
||||
async findAll(): Promise<EndorsementEntity[]> {
|
||||
return await this.endorsementRepository.find();
|
||||
}
|
||||
|
||||
async create(endorsement: EndorsementDto): Promise<InsertResult> {
|
||||
return await this.endorsementRepository.insert(endorsement);
|
||||
}
|
||||
|
||||
async update(id: string, endorsement: EndorsementDto): Promise<UpdateResult> {
|
||||
return await this.endorsementRepository.update(id, endorsement);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<DeleteResult> {
|
||||
return await this.endorsementRepository.delete({ id });
|
||||
}
|
||||
}
|
||||
41
api/src/featureFlag/feature-flag.controller.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
HttpException,
|
||||
Param,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { FeatureFlagService } from './feature-flag.service';
|
||||
import { CustomError } from '../error/customError';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Controller('featureFlags')
|
||||
@UseGuards(AuthGuard('azure-ad'))
|
||||
export class FeatureFlagController {
|
||||
constructor(private readonly featureFlagService: FeatureFlagService) {}
|
||||
|
||||
@Get(':partitionKey/:rowKey')
|
||||
async find(
|
||||
@Param('partitionKey') partitionKey: string,
|
||||
@Param('rowKey') rowKey: string
|
||||
) {
|
||||
try {
|
||||
return await this.featureFlagService.find(partitionKey, rowKey);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Get()
|
||||
async findAll() {
|
||||
try {
|
||||
return await this.featureFlagService.findAll();
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
5
api/src/featureFlag/feature-flag.dto.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export class FeatureFlagDto {
|
||||
partitionKey: string;
|
||||
rowKey: string;
|
||||
active: string;
|
||||
}
|
||||
7
api/src/featureFlag/feature-flag.entity.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { EntityString } from '@noahspan/azure-database';
|
||||
|
||||
export class FeatureFlag {
|
||||
@EntityString() partitionKey: string;
|
||||
@EntityString() rowKey: string;
|
||||
@EntityString() active: string;
|
||||
}
|
||||
27
api/src/featureFlag/feature-flag.module.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FeatureFlagController } from './feature-flag.controller';
|
||||
import { FeatureFlagService } from './feature-flag.service';
|
||||
import { AzureTableStorageModule } from '@noahspan/azure-database';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { FeatureFlag } from './feature-flag.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
AzureTableStorageModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
return {
|
||||
connectionString: configService.get<string>('azureStorageConnectionString')
|
||||
};
|
||||
},
|
||||
inject: [ConfigService]
|
||||
}),
|
||||
AzureTableStorageModule.forFeature(FeatureFlag, {
|
||||
createTableIfNotExists: false,
|
||||
table: 'featureFlags'
|
||||
}),
|
||||
],
|
||||
controllers: [FeatureFlagController],
|
||||
providers: [FeatureFlagService]
|
||||
})
|
||||
export class FeatureFlagModule {}
|
||||
18
api/src/featureFlag/feature-flag.service.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository, Repository } from '@noahspan/azure-database';
|
||||
import { FeatureFlag } from './feature-flag.entity';
|
||||
|
||||
@Injectable()
|
||||
export class FeatureFlagService {
|
||||
constructor(
|
||||
@InjectRepository(FeatureFlag) private readonly featureFlagRepository: Repository<FeatureFlag>
|
||||
) {}
|
||||
|
||||
async find(partitionKey: string, rowKey: string): Promise<FeatureFlag> {
|
||||
return await this.featureFlagRepository.find(partitionKey, rowKey);
|
||||
}
|
||||
|
||||
async findAll(): Promise<FeatureFlag[]> {
|
||||
return await this.featureFlagRepository.findAll();
|
||||
}
|
||||
}
|
||||
@@ -37,10 +37,10 @@ import { ConfigService } from '@nestjs/config';
|
||||
return blockBlobClient;
|
||||
}
|
||||
|
||||
async uploadFile(file: Express.Multer.File, containerName: string, logId: string): Promise<string> {
|
||||
async uploadFile(file: Express.Multer.File, containerName: string, rowKey: string): Promise<string> {
|
||||
this.containerName = containerName;
|
||||
|
||||
const blockBlobClient = await this.getBlobClient(`${logId}/${file.originalname}`);
|
||||
const blockBlobClient = await this.getBlobClient(`${rowKey}/${file.originalname}`);
|
||||
const fileUrl = blockBlobClient.url;
|
||||
|
||||
await blockBlobClient.uploadData(file.buffer);
|
||||
@@ -58,32 +58,11 @@ import { ConfigService } from '@nestjs/config';
|
||||
return downloaded
|
||||
}
|
||||
|
||||
async deleteFile(containerName: string, logId: string, fileName: string): Promise<void> {
|
||||
async deleteFile(containerName: string, rowKey:string, fileName: string): Promise<void> {
|
||||
this.containerName = containerName;
|
||||
|
||||
const blockBlobClient = await this.getBlobClient(`${logId}/${fileName}`);
|
||||
const blockBlobClient = await this.getBlobClient(`${rowKey}/${fileName}`);
|
||||
|
||||
await blockBlobClient.deleteIfExists();
|
||||
}
|
||||
|
||||
async deleteFolder(containerName: string, logId: string): Promise<void> {
|
||||
const blobService = await this.getBlobServiceInstance();
|
||||
|
||||
this.containerName = containerName;
|
||||
|
||||
const containerClient = blobService.getContainerClient(containerName);
|
||||
const blobsToDelete = []
|
||||
|
||||
for await (const blob of containerClient.listBlobsFlat({ prefix: logId })) {
|
||||
blobsToDelete.push(blob.name)
|
||||
}
|
||||
|
||||
for (const blobName of blobsToDelete) {
|
||||
const blockBlobClient = containerClient.getBlockBlobClient(blobName);
|
||||
|
||||
await blockBlobClient.delete();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { HealthController } from './health.controller';
|
||||
import { HealthService } from './health.service';
|
||||
|
||||
describe('HealthController', () => {
|
||||
let controller; HealthController;
|
||||
|
||||
const mockHealthService = {
|
||||
isDatabaseConnected: jest.fn()
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [HealthController],
|
||||
providers: [HealthService]
|
||||
}).compile();
|
||||
|
||||
controller = module.get<HealthController>(HealthController);
|
||||
})
|
||||
|
||||
it('isHealthy => should return true', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return database connected', async () => {
|
||||
jest.spyOn(mockHealthService, 'isDatabaseConnected').mockReturnValue(true);
|
||||
|
||||
const result = await controller.isHealthy();
|
||||
|
||||
expect(mockHealthService.isDatabaseConnected).toHaveBeenCalled();
|
||||
expect(result).toEqual(true);
|
||||
})
|
||||
|
||||
it('isHealthy => should return error', async () => {
|
||||
jest.spyOn(mockHealthService, 'isDatabaseConnected').mockReturnValue(false);
|
||||
|
||||
const result = await controller.isHealthy();
|
||||
|
||||
expect(mockHealthService.isDatabaseConnected).toHaveBeenCalled();
|
||||
expect(result).toEqual(false);
|
||||
})
|
||||
})
|
||||
@@ -1,27 +0,0 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
HttpException,
|
||||
} from '@nestjs/common';
|
||||
import { HealthService } from './health.service';
|
||||
import { CustomError } from 'src/error/customError';
|
||||
|
||||
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
constructor(
|
||||
private readonly healthService: HealthService
|
||||
) {}
|
||||
|
||||
|
||||
@Get()
|
||||
async isHealthy(): Promise<boolean> {
|
||||
try {
|
||||
return await this.healthService.isDatabaseConnected();
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HealthController } from './health.controller';
|
||||
import { HealthService } from './health.service';
|
||||
|
||||
@Module({
|
||||
controllers: [HealthController],
|
||||
providers: [
|
||||
HealthService
|
||||
]
|
||||
})
|
||||
export class HealthModule {}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { HealthService } from './health.service';
|
||||
|
||||
describe('HealthService', () => {
|
||||
let service: HealthService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [HealthService]
|
||||
}).compile();
|
||||
|
||||
service = module.get<HealthService>(HealthService);
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CustomError } from '../error/customError';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class HealthService {
|
||||
constructor(private dataSource: DataSource) {}
|
||||
|
||||
async isDatabaseConnected(): Promise<boolean> {
|
||||
try {
|
||||
const isDatabaseConnected: boolean = this.dataSource.isInitialized;
|
||||
|
||||
if (isDatabaseConnected) {
|
||||
return isDatabaseConnected;
|
||||
} else {
|
||||
throw new CustomError('Database not connected', 'Database not connected', 400)
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { JwtPayload } from "jwt-decode";
|
||||
|
||||
export interface CustomJwtPayload extends JwtPayload {
|
||||
permissions: string[];
|
||||
}
|
||||
41
api/src/log/interceptors/log.interceptor.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
|
||||
import { Observable, map } from 'rxjs';
|
||||
|
||||
export class LogInterceptor implements NestInterceptor {
|
||||
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
console.log(token)
|
||||
|
||||
if (!token) {
|
||||
return handler.handle().pipe(
|
||||
map((data) => {
|
||||
if (data.length) {
|
||||
const logs = data.map((log) => {
|
||||
return {
|
||||
partitionKey: log.partitionKey,
|
||||
rowKey: log.rowKey,
|
||||
pilotId: log.pilotId,
|
||||
pilotName: log.pilotName,
|
||||
date: log.date,
|
||||
aircraftMakeModel: log.aircraftMakeModel,
|
||||
routeFrom: log.routeFrom,
|
||||
routeTo: log.routeTo,
|
||||
durationOfFlight: log.durationOfFlight,
|
||||
tracks: log.tracks,
|
||||
notes: log.notes
|
||||
};
|
||||
});
|
||||
|
||||
return logs;
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return handler.handle().pipe(map((data) => data));
|
||||
}
|
||||
}
|
||||
@@ -7,23 +7,22 @@ import {
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
StreamableFile,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
UseInterceptors
|
||||
} from '@nestjs/common';
|
||||
import { LogDto } from './log.dto';
|
||||
import { LogEntity } from './log.entity';
|
||||
import { Log } from './log.entity';
|
||||
import { LogService } from './log.service';
|
||||
import { CustomError } from '../error/customError';
|
||||
import { AuthGuard, Public } from '@noahspan/noahspan-modules';
|
||||
import { LogInterceptor } from './log.interceptor';
|
||||
import { AuthGuard } from '@noahspan/noahspan-modules';
|
||||
import { LogInterceptor } from './interceptors/log.interceptor';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { DeleteResult, InsertResult, UpdateResult } from 'typeorm';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
const reflector = new Reflector();
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
|
||||
@Controller('logs')
|
||||
@UseInterceptors(new LogInterceptor(reflector))
|
||||
export class LogController {
|
||||
constructor(
|
||||
private readonly fileService: FileService,
|
||||
@@ -31,13 +30,14 @@ export class LogController {
|
||||
) {}
|
||||
|
||||
|
||||
@Get(':id')
|
||||
@Public()
|
||||
@Get(':partitionKey/:rowKey')
|
||||
@UseInterceptors(new LogInterceptor())
|
||||
async find(
|
||||
@Param('id') id: string,
|
||||
): Promise<LogEntity> {
|
||||
@Param('partitionKey') partitionKey: string,
|
||||
@Param('rowKey') rowKey: string
|
||||
): Promise<Log> {
|
||||
try {
|
||||
return await this.logService.find(id);
|
||||
return await this.logService.find(partitionKey, rowKey);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
@@ -46,8 +46,8 @@ export class LogController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Public()
|
||||
async findAll(): Promise<LogEntity[]> {
|
||||
@UseInterceptors(new LogInterceptor())
|
||||
async findAll(): Promise<Log[]> {
|
||||
try {
|
||||
return await this.logService.findAll();
|
||||
} catch (error) {
|
||||
@@ -57,12 +57,15 @@ export class LogController {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Post()
|
||||
@UseGuards(AuthGuard)
|
||||
async create(@Body() logDto: LogDto): Promise<InsertResult> {
|
||||
async create(@Body() logDto: LogDto): Promise<Log> {
|
||||
try {
|
||||
return await this.logService.create(logDto);
|
||||
const log = new Log();
|
||||
|
||||
Object.assign(log, logDto);
|
||||
|
||||
return await this.logService.create(log);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
@@ -70,14 +73,19 @@ export class LogController {
|
||||
}
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@UseGuards(AuthGuard)
|
||||
@UseGuards(AuthGuard)
|
||||
@Put(':partitionKey/:rowKey')
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Param('partitionKey') partitionKey: string,
|
||||
@Param('rowKey') rowKey: string,
|
||||
@Body() logDto: LogDto
|
||||
): Promise<UpdateResult> {
|
||||
): Promise<Log> {
|
||||
try {
|
||||
return await this.logService.update(id, logDto);
|
||||
const log = new Log();
|
||||
|
||||
Object.assign(log, logDto);
|
||||
|
||||
return await this.logService.update(partitionKey, rowKey, log);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
@@ -85,13 +93,52 @@ export class LogController {
|
||||
}
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(AuthGuard)
|
||||
@Delete(':partitionKey/:rowKey')
|
||||
async delete(
|
||||
@Param('id') id: string,
|
||||
): Promise<DeleteResult> {
|
||||
@Param('partitionKey') partitionKey: string,
|
||||
@Param('rowKey') rowKey: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
return await this.logService.delete(id);
|
||||
return await this.logService.delete(partitionKey, rowKey);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Post(':partitionKey/:rowKey/track')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async createTrack(@Param('rowKey') rowKey: string, @UploadedFile() file: Express.Multer.File) {
|
||||
try {
|
||||
const containerName = 'tracks';
|
||||
const url = await this.fileService.uploadFile(file, containerName, rowKey);
|
||||
|
||||
return { url }
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':partitionKey/:rowKey/track')
|
||||
async downloadTrack(@Param('rowKey') rowKey: string, @Query('fileName') fileName: string): Promise<string> {
|
||||
const containerName = 'tracks';
|
||||
const downloadedFile: string = await this.fileService.downloadFile(containerName, rowKey, fileName)
|
||||
|
||||
return downloadedFile;
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Delete(':partitionKey/:rowKey/track')
|
||||
async deleteTrack(@Param('rowKey') rowKey: string, @Query('fileName') fileName: string): Promise<void> {
|
||||
try {
|
||||
const containerName = 'tracks';
|
||||
|
||||
return await this.fileService.deleteFile(containerName, rowKey, fileName)
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
import { PilotEntity } from "src/pilot/pilot.entity";
|
||||
|
||||
export class LogDto {
|
||||
pilotId: string;
|
||||
date: Date;
|
||||
pilotName: string;
|
||||
date: string;
|
||||
aircraftMakeModel: string;
|
||||
aircraftIdentity: string;
|
||||
routeFrom: string;
|
||||
routeTo: string;
|
||||
durationOfFlight?: number;
|
||||
singleEngineLand?: number;
|
||||
simulatorAtd?: number;
|
||||
landingsDay?: number;
|
||||
landingsNight?: number;
|
||||
instrumentActual?: number;
|
||||
instrumentSimulated?: number;
|
||||
instrumentApproaches?: number;
|
||||
instrumentHolds?: number;
|
||||
instrumentNavTrack?: number;
|
||||
groundTrainingReceived?: number;
|
||||
flightTrainingReceived?: number;
|
||||
crossCountry?: number;
|
||||
night?: number;
|
||||
solo?: number;
|
||||
pilotInCommand?: number;
|
||||
notes?: string;
|
||||
tracks?: []
|
||||
durationOfFlight: number;
|
||||
singleEngineLand: string;
|
||||
simulatorAtd: number;
|
||||
landingsDay: number;
|
||||
landingsNight: number;
|
||||
instrumentActual: number;
|
||||
instrumentSimulated: number;
|
||||
instrumentApproaches: number;
|
||||
instrumentHolds: number;
|
||||
instrumentNavTrack: number;
|
||||
groundTrainingReceived: number;
|
||||
flightTrainingReceived: number;
|
||||
crossCountry: number;
|
||||
night: number;
|
||||
solo: number;
|
||||
pilotInCommand: number;
|
||||
tracks: string[];
|
||||
notes: string;
|
||||
}
|
||||
|
||||
@@ -1,85 +1,29 @@
|
||||
import { PilotEntity } from 'src/pilot/pilot.entity';
|
||||
import { TrackEntity } from 'src/track/track.entity';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, OneToMany } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'logs' })
|
||||
export class LogEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string
|
||||
|
||||
@Column()
|
||||
export class Log {
|
||||
partitionKey: string;
|
||||
rowKey: string;
|
||||
pilotId: string;
|
||||
|
||||
@Column()
|
||||
date: Date;
|
||||
|
||||
@Column()
|
||||
pilotName: string;
|
||||
date: string;
|
||||
aircraftMakeModel: string;
|
||||
|
||||
@Column()
|
||||
aircraftIdentity: string;
|
||||
|
||||
@Column()
|
||||
routeFrom: string;
|
||||
|
||||
@Column()
|
||||
routeTo: string;
|
||||
|
||||
@Column()
|
||||
durationOfFlight: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
durationOfFlight: number | null;
|
||||
singleEngineLand: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
simulatorAtd: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
landingsDay: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
landingsNight: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
groundTrainingReceived: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
flightTrainingReceived: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
crossCountry: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
night: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
solo: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
pilotInCommand: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
instrumentActual: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
instrumentSimulated: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
instrumentApproaches: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
instrumentHolds: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
instrumentNavTrack: number | null;
|
||||
|
||||
@Column({ nullable: true })
|
||||
notes: string | null;
|
||||
|
||||
@OneToMany(() => TrackEntity, (track: TrackEntity) => track.log)
|
||||
tracks: TrackEntity[]
|
||||
|
||||
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.logs, {onDelete: 'CASCADE', onUpdate: 'CASCADE'})
|
||||
@JoinColumn({ name: 'pilotId' })
|
||||
pilot: PilotEntity;
|
||||
}
|
||||
simulatorAtd?: number | null;
|
||||
landingsDay?: number | null;
|
||||
landingsNight?: number | null;
|
||||
groundTrainingReceived?: number;
|
||||
flightTrainingReceived?: number;
|
||||
crossCountry?: number | null;
|
||||
night?: number | null;
|
||||
solo?: number | null;
|
||||
pilotInCommand?: number | null;
|
||||
instrumentActual?: number | null;
|
||||
instrumentSimulated?: number | null;
|
||||
instrumentApproaches?: number | null;
|
||||
instrumentHolds?: number | null;
|
||||
instrumentNavTrack?: number | null;
|
||||
tracks?: string[];
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { CallHandler, ExecutionContext, NestInterceptor, UnauthorizedException } from '@nestjs/common';
|
||||
import { Observable, map } from 'rxjs';
|
||||
import { LogEntity } from './log.entity';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { IS_PUBLIC_KEY } from '@noahspan/noahspan-modules';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
export class LogInterceptor implements NestInterceptor {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
return handler.handle().pipe(
|
||||
map((data: LogEntity[]) => {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const limitData = (data) => {
|
||||
return data.map((log: LogEntity) => {
|
||||
return {
|
||||
id: log.id,
|
||||
pilot: {
|
||||
name: log.pilot.name
|
||||
},
|
||||
date: log.date,
|
||||
aircraftMakeModel: log.aircraftMakeModel,
|
||||
routeFrom: log.routeFrom,
|
||||
routeTo: log.routeTo,
|
||||
durationOfFlight: log.durationOfFlight,
|
||||
tracks: log.tracks,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (req.headers.authorization) {
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
const jwtPayload = jwtDecode(token);
|
||||
const rolesKeyName = Object.keys(jwtPayload).find((key) => key.includes('roles'));
|
||||
|
||||
if (jwtPayload[rolesKeyName].includes('Flying.Read')) {
|
||||
const logs = limitData(data);
|
||||
|
||||
return logs;
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
} else if (!req.headers.authorization && isPublic) {
|
||||
const publicData = limitData(data)
|
||||
const logs = publicData.slice(0, 5)
|
||||
|
||||
return logs;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,28 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { LogController } from './log.controller';
|
||||
import { LogService } from './log.service';
|
||||
import { LogEntity } from './log.entity';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AzureTableStorageModule } from '@noahspan/azure-database';
|
||||
import { Log } from './log.entity';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { PilotModule } from 'src/pilot/pilot.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PilotModule,
|
||||
TypeOrmModule.forFeature([LogEntity])
|
||||
AzureTableStorageModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
return {
|
||||
connectionString: configService.get<string>('azureStorageConnectionString')
|
||||
};
|
||||
},
|
||||
inject: [ConfigService]
|
||||
}),
|
||||
AzureTableStorageModule.forFeature(Log, {
|
||||
createTableIfNotExists: false,
|
||||
table: 'logs'
|
||||
}),
|
||||
],
|
||||
controllers: [LogController],
|
||||
exports: [LogService],
|
||||
providers: [
|
||||
ConfigService,
|
||||
FileService,
|
||||
|
||||
@@ -1,63 +1,34 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { LogEntity } from './log.entity';
|
||||
import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm';
|
||||
import { LogDto } from './log.dto';
|
||||
import { PilotService } from 'src/pilot/pilot.service';
|
||||
import { PilotEntity } from 'src/pilot/pilot.entity';
|
||||
import { CustomError } from 'src/error/customError';
|
||||
import { FileService } from 'src/file/file.service';
|
||||
import { InjectRepository, Repository } from '@noahspan/azure-database';
|
||||
import { Log } from './log.entity';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@Injectable()
|
||||
export class LogService {
|
||||
constructor(
|
||||
@InjectRepository(LogEntity) private readonly logRepository: Repository<LogEntity>,
|
||||
private readonly fileService: FileService,
|
||||
private readonly pilotService: PilotService,
|
||||
@InjectRepository(Log) private readonly logRepository: Repository<Log>
|
||||
) {}
|
||||
|
||||
async find(id: string): Promise<LogEntity> {
|
||||
const logEntity: LogEntity = await this.logRepository.findOne({
|
||||
where: { id: id },
|
||||
relations: ['pilot', 'tracks']
|
||||
});
|
||||
|
||||
return logEntity;
|
||||
async find(partitionKey: string, rowKey: string): Promise<Log> {
|
||||
return await this.logRepository.find(partitionKey, rowKey);
|
||||
}
|
||||
|
||||
async findAll(): Promise<LogEntity[]> {
|
||||
return await this.logRepository.find({
|
||||
relations: ['pilot', 'tracks']
|
||||
});
|
||||
async findAll(): Promise<Log[]> {
|
||||
return await this.logRepository.findAll();
|
||||
}
|
||||
|
||||
async create(logDto: LogDto): Promise<InsertResult> {
|
||||
try{
|
||||
const pilotEntity: PilotEntity = await this.pilotService.find(logDto.pilotId);
|
||||
async create(log: Log): Promise<Log> {
|
||||
log.partitionKey = 'log';
|
||||
log.rowKey = uuidv4();
|
||||
|
||||
if (pilotEntity) {
|
||||
const { pilotId, ...newLogDto } = logDto;
|
||||
const log = this.logRepository.create({
|
||||
...newLogDto,
|
||||
pilot: pilotEntity
|
||||
})
|
||||
|
||||
return this.logRepository.insert(log);
|
||||
} else {
|
||||
throw new CustomError('Pilot not found', 'Not found', 404);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
return await this.logRepository.create(log);
|
||||
}
|
||||
|
||||
async update(id: string, log: LogDto): Promise<UpdateResult> {
|
||||
return await this.logRepository.update(id, log);
|
||||
async update(partitionKey: string, rowKey: string, log: Log): Promise<Log> {
|
||||
return await this.logRepository.update(partitionKey, rowKey, log);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<DeleteResult> {
|
||||
await this.fileService.deleteFolder('tracks', id);
|
||||
|
||||
return await this.logRepository.delete({ id });
|
||||
async delete(partitionKey: string, rowKey: string): Promise<void> {
|
||||
await this.logRepository.delete(partitionKey, rowKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,26 +3,15 @@ import { AppModule } from './app.module';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { HttpExceptionFilter } from './filters/http-exception.filter';
|
||||
import { InternalServerErrorException } from '@nestjs/common';
|
||||
import * as session from 'express-session';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
async function bootstrap() {
|
||||
const httpService = new HttpService();
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
app.enableCors({
|
||||
origin: 'http://localhost:8080', // Allow requests from your frontend's origin
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||
credentials: true, // If you need to send cookies or authorization headers
|
||||
});
|
||||
app.enableCors();
|
||||
app.setGlobalPrefix('api');
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
app.use(
|
||||
session({
|
||||
secret: 'blah',
|
||||
resave: false,
|
||||
saveUninitialized: false
|
||||
})
|
||||
)
|
||||
|
||||
httpService.axiosRef.interceptors.response.use(
|
||||
(response) => {
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { Body, Controller, Delete, Get, HttpException, Param, Post, Put, UseGuards } from "@nestjs/common";
|
||||
import { MedicalService } from "./medical.service";
|
||||
import { CustomError } from "src/error/customError";
|
||||
import { MedicalDto } from "./medical.dto";
|
||||
import { AuthGuard } from "@noahspan/noahspan-modules";
|
||||
|
||||
|
||||
@Controller('medical')
|
||||
export class MedicalController {
|
||||
constructor(private readonly medicalService: MedicalService) {}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Get(':id')
|
||||
async find(@Param('id') id: string) {
|
||||
try {
|
||||
return await this.medicalService.find(id)
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Get()
|
||||
async findAll() {
|
||||
try {
|
||||
return await this.medicalService.findAll();
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Post()
|
||||
async create(@Body() medicalDto: MedicalDto) {
|
||||
try {
|
||||
return await this.medicalService.create(medicalDto);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Put(':id')
|
||||
async update(@Param('id') id: string, @Body() medicalDto: MedicalDto) {
|
||||
try {
|
||||
return await this.medicalService.update(id, medicalDto);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Delete(':id')
|
||||
async delete(@Param('id') id: string) {
|
||||
try {
|
||||
return await this.medicalService.delete(id);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export class MedicalDto {
|
||||
id: string;
|
||||
class: string;
|
||||
expirationDate: string;
|
||||
pilotId: string;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { PilotEntity } from 'src/pilot/pilot.entity';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'medical' })
|
||||
export class MedicalEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
class: string;
|
||||
|
||||
@Column()
|
||||
expirationDate: Date;
|
||||
|
||||
@ManyToOne(() => PilotEntity, (pilot: PilotEntity) => pilot.medical, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
@JoinColumn({ name: 'pilotId' })
|
||||
pilot: PilotEntity;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
|
||||
import { MedicalEntity } from "./medical.entity";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm';
|
||||
import { MedicalDto } from "./medical.dto";
|
||||
|
||||
@Injectable()
|
||||
export class MedicalService {
|
||||
constructor(
|
||||
@InjectRepository(MedicalEntity) private readonly medicalRepository: Repository<MedicalEntity>
|
||||
) {}
|
||||
|
||||
async find(id: string): Promise<MedicalEntity> {
|
||||
return await this.medicalRepository.findOneBy({ id });
|
||||
}
|
||||
|
||||
async findAll(): Promise<MedicalEntity[]> {
|
||||
return await this.medicalRepository.find();
|
||||
}
|
||||
|
||||
async create(medical: MedicalDto): Promise<InsertResult> {
|
||||
return await this.medicalRepository.insert(medical)
|
||||
}
|
||||
|
||||
async update(id: string, medical: MedicalDto): Promise<UpdateResult> {
|
||||
return await this.medicalRepository.update(id, medical);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<DeleteResult> {
|
||||
return await this.medicalRepository.delete({ id });
|
||||
}
|
||||
}
|
||||
5
api/src/pilot/certificate/certificate.entity.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export class Certificate {
|
||||
type: string;
|
||||
issueDate: string;
|
||||
number: string;
|
||||
}
|
||||
4
api/src/pilot/endorsement/endorsement.entity.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export class Endorsement {
|
||||
type: string;
|
||||
issueDate: Date;
|
||||
}
|
||||
35
api/src/pilot/interceptors/pilot.interceptor.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
|
||||
import { Observable, map } from 'rxjs';
|
||||
|
||||
export class PilotInterceptor implements NestInterceptor {
|
||||
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
|
||||
if (!token) {
|
||||
return handler.handle().pipe(
|
||||
map((data) => {
|
||||
if (data.length) {
|
||||
const pilots = data.map((pilot) => {
|
||||
return {
|
||||
partitionKey: pilot.partitionKey,
|
||||
rowKey: pilot.rowKey,
|
||||
id: pilot.id,
|
||||
name: pilot.name,
|
||||
certificates: pilot.certificates,
|
||||
endorsements: pilot.endorsements
|
||||
};
|
||||
});
|
||||
|
||||
return pilots;
|
||||
} else {
|
||||
return data;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return handler.handle().pipe(map((data) => data));
|
||||
}
|
||||
}
|
||||
@@ -11,24 +11,24 @@ import {
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { PilotDto } from './pilot.dto';
|
||||
import { Pilot } from './pilot.entity';
|
||||
import { PilotService } from './pilot.service';
|
||||
import { CustomError } from '../error/customError';
|
||||
import { PilotInterceptor } from './pilot.interceptor';
|
||||
import { AuthGuard, Public } from '@noahspan/noahspan-modules';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
const reflector = new Reflector();
|
||||
import { AuthGuard } from '@noahspan/noahspan-modules'
|
||||
import { PilotInterceptor } from './interceptors/pilot.interceptor';
|
||||
|
||||
@Controller('pilots')
|
||||
@UseInterceptors(new PilotInterceptor(reflector))
|
||||
@UseInterceptors(new PilotInterceptor())
|
||||
export class PilotController {
|
||||
constructor(private readonly pilotService: PilotService) {}
|
||||
|
||||
@Get(':id')
|
||||
@Public()
|
||||
async find(@Param('id') id: string) {
|
||||
@Get(':partitionKey/:rowKey')
|
||||
async find(
|
||||
@Param('partitionKey') partitionKey: string,
|
||||
@Param('rowKey') rowKey: string
|
||||
) {
|
||||
try {
|
||||
return await this.pilotService.find(id);
|
||||
return await this.pilotService.find(partitionKey, rowKey);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
@@ -37,7 +37,6 @@ export class PilotController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Public()
|
||||
async findAll() {
|
||||
try {
|
||||
return await this.pilotService.findAll();
|
||||
@@ -48,12 +47,30 @@ export class PilotController {
|
||||
}
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UseGuards(AuthGuard)
|
||||
@Post()
|
||||
async create(@Body() pilotDto: PilotDto) {
|
||||
try {
|
||||
let pilot = new Pilot();
|
||||
|
||||
return await this.pilotService.create(pilotDto);
|
||||
pilot = {
|
||||
partitionKey: pilotDto.partitionKey,
|
||||
rowKey: pilotDto.rowKey,
|
||||
id: pilotDto.id,
|
||||
name: pilotDto.name,
|
||||
address: pilotDto.address,
|
||||
city: pilotDto.city,
|
||||
state: pilotDto.state,
|
||||
postalCode: pilotDto.postalCode,
|
||||
email: pilotDto.email,
|
||||
phone: pilotDto.phone,
|
||||
medicalClass: pilotDto.medicalClass,
|
||||
medicalExpiration: pilotDto.medicalExpiration,
|
||||
certificates: JSON.stringify(pilotDto.certificates),
|
||||
endorsements: JSON.stringify(pilotDto.endorsements)
|
||||
}
|
||||
|
||||
return await this.pilotService.create(pilot);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
@@ -61,14 +78,34 @@ export class PilotController {
|
||||
}
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@UseGuards(AuthGuard)
|
||||
@Put(':partitionKey/:rowKey')
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Param('partitionKey') partitionKey: string,
|
||||
@Param('rowKey') rowKey: string,
|
||||
@Body() pilotDto: PilotDto
|
||||
) {
|
||||
try {
|
||||
return await this.pilotService.update(id, pilotDto);
|
||||
let pilot = new Pilot();
|
||||
|
||||
pilot = {
|
||||
partitionKey: pilotDto.partitionKey,
|
||||
rowKey: pilotDto.rowKey,
|
||||
id: pilotDto.id,
|
||||
name: pilotDto.name,
|
||||
address: pilotDto.address,
|
||||
city: pilotDto.city,
|
||||
state: pilotDto.state,
|
||||
postalCode: pilotDto.postalCode,
|
||||
email: pilotDto.email,
|
||||
phone: pilotDto.phone,
|
||||
medicalClass: pilotDto.medicalClass,
|
||||
medicalExpiration: pilotDto.medicalExpiration,
|
||||
certificates: JSON.stringify(pilotDto.certificates),
|
||||
endorsements: JSON.stringify(pilotDto.endorsements)
|
||||
}
|
||||
|
||||
return await this.pilotService.update(partitionKey, rowKey, pilot);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
@@ -76,13 +113,14 @@ export class PilotController {
|
||||
}
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(AuthGuard)
|
||||
@Delete(':partitionKey/:rowKey')
|
||||
async delete(
|
||||
@Param('id') id: string,
|
||||
@Param('partitionKey') partitionKey: string,
|
||||
@Param('rowKey') rowKey: string
|
||||
) {
|
||||
try {
|
||||
return await this.pilotService.delete(id);
|
||||
return await this.pilotService.delete(partitionKey, rowKey);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { LogEntity } from "src/log/log.entity";
|
||||
import { Certificate } from "./certificate/certificate.entity";
|
||||
import { Endorsement } from "./endorsement/endorsement.entity";
|
||||
|
||||
export class PilotDto {
|
||||
partitionKey: string;
|
||||
rowKey: string;
|
||||
id: string;
|
||||
name: string;
|
||||
address: string;
|
||||
city: string;
|
||||
@@ -8,4 +12,8 @@ export class PilotDto {
|
||||
postalCode: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
medicalClass?: string;
|
||||
medicalExpiration?: string;
|
||||
certificates: Certificate;
|
||||
endorsements: Endorsement
|
||||
}
|
||||
|
||||
@@ -1,47 +1,18 @@
|
||||
import { LogEntity } from 'src/log/log.entity';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
|
||||
import { CertificateEntity } from '../certificate/certificate.entity';
|
||||
import { EndorsementEntity } from 'src/endorsement/endorsement.entity';
|
||||
import { MedicalEntity } from 'src/medical/medical.entity';
|
||||
import { EntityString } from '@noahspan/azure-database';
|
||||
|
||||
@Entity({ name: 'pilots' })
|
||||
export class PilotEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column()
|
||||
address: string
|
||||
|
||||
@Column()
|
||||
city: string;
|
||||
|
||||
@Column()
|
||||
state: string;
|
||||
|
||||
@Column()
|
||||
postalCode: string;
|
||||
|
||||
@Column()
|
||||
email: string;
|
||||
|
||||
@Column()
|
||||
phone: string;
|
||||
|
||||
@Column()
|
||||
userId: string | null;
|
||||
|
||||
@OneToMany(() => LogEntity, (log: LogEntity) => log.pilot)
|
||||
logs: LogEntity[];
|
||||
|
||||
@OneToMany(() => CertificateEntity, (certificate: CertificateEntity) => certificate.pilot)
|
||||
certificates: CertificateEntity[];
|
||||
|
||||
@OneToMany(() => EndorsementEntity, (endorsement: EndorsementEntity) => endorsement.pilot)
|
||||
endorsements: EndorsementEntity[];
|
||||
|
||||
@OneToMany(() => MedicalEntity, (medical: MedicalEntity) => medical.pilot)
|
||||
medical: MedicalEntity[];
|
||||
export class Pilot {
|
||||
@EntityString() partitionKey: string;
|
||||
@EntityString() rowKey: string;
|
||||
@EntityString() id: string;
|
||||
@EntityString() name: string;
|
||||
@EntityString() address?: string;
|
||||
@EntityString() city?: string;
|
||||
@EntityString() state?: string;
|
||||
@EntityString() postalCode?: string;
|
||||
@EntityString() email?: string;
|
||||
@EntityString() phone?: string;
|
||||
@EntityString() medicalClass?: string;
|
||||
@EntityString() medicalExpiration: string;
|
||||
@EntityString() certificates: string;
|
||||
@EntityString() endorsements: string;
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { Observable, map } from 'rxjs';
|
||||
import { PilotEntity } from './pilot.entity';
|
||||
import { IS_PUBLIC_KEY } from '@noahspan/noahspan-modules';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
export class PilotInterceptor implements NestInterceptor {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
return handler.handle().pipe(
|
||||
map((data: PilotEntity[]) => {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const limitData = (data) => {
|
||||
return data.map((pilot: PilotEntity) => {
|
||||
return {
|
||||
id: pilot.id,
|
||||
name: pilot.name
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
if (req.headers.authorization) {
|
||||
const authHeader = req.headers.authorization;
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
const jwtPayload = jwtDecode(token);
|
||||
const rolesKeyName = Object.keys(jwtPayload).find((key) => key.includes('roles'));
|
||||
|
||||
if (jwtPayload[rolesKeyName].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,15 +1,41 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PilotController } from './pilot.controller';
|
||||
import { PilotService } from './pilot.service';
|
||||
import { PilotEntity } from './pilot.entity';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AzureTableStorageModule } from '@noahspan/azure-database';
|
||||
import { Pilot } from './pilot.entity';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { Log } from 'src/log/log.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([PilotEntity])
|
||||
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(Pilot, {
|
||||
createTableIfNotExists: false,
|
||||
table: 'pilots'
|
||||
})
|
||||
],
|
||||
controllers: [PilotController],
|
||||
exports: [PilotService],
|
||||
providers: [PilotService]
|
||||
})
|
||||
export class PilotModule {}
|
||||
|
||||
@@ -1,46 +1,54 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PilotEntity } from './pilot.entity';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { DeleteResult, InsertResult, Repository, UpdateResult } from 'typeorm';
|
||||
import { PilotDto } from './pilot.dto';
|
||||
import { CustomError } from 'src/error/customError';
|
||||
import { InjectRepository, Repository } from '@noahspan/azure-database';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Pilot } from './pilot.entity';
|
||||
import { Log } from 'src/log/log.entity';
|
||||
import { LogService } from 'src/log/log.service';
|
||||
|
||||
@Injectable()
|
||||
export class PilotService {
|
||||
constructor(
|
||||
@InjectRepository(PilotEntity) private readonly pilotRepository: Repository<PilotEntity>
|
||||
@InjectRepository(Pilot) private readonly pilotRepository: Repository<Pilot>,
|
||||
@InjectRepository(Log) private readonly logRepository: Repository<Log>
|
||||
) {}
|
||||
|
||||
async find(id: string): Promise<PilotEntity> {
|
||||
try {
|
||||
const pilotEntity = await this.pilotRepository.findOneBy({ id });
|
||||
|
||||
if (pilotEntity) {
|
||||
return pilotEntity
|
||||
} else {
|
||||
throw new CustomError('Pilot not found', 'Not found', 404)
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
async find(partitionKey: string, rowKey: string): Promise<Pilot> {
|
||||
return await this.pilotRepository.find(partitionKey, rowKey);
|
||||
}
|
||||
|
||||
async findAll(): Promise<PilotEntity[]> {
|
||||
return await this.pilotRepository.find();
|
||||
async findAll(): Promise<Pilot[]> {
|
||||
return await this.pilotRepository.findAll();
|
||||
}
|
||||
|
||||
async create(pilot: PilotDto): Promise<InsertResult> {
|
||||
return await this.pilotRepository.insert(pilot);
|
||||
async create(pilot: Pilot): Promise<Pilot> {
|
||||
// try {
|
||||
// return await this.pilotRepository.create(pilot);
|
||||
// } catch (error) {
|
||||
// throw new Error(error);
|
||||
// }
|
||||
return await this.pilotRepository.create(pilot);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
pilot: PilotDto
|
||||
): Promise<UpdateResult> {
|
||||
return await this.pilotRepository.update(id, pilot);
|
||||
partitionKey: string,
|
||||
rowKey: string,
|
||||
pilot: Pilot
|
||||
): Promise<Pilot> {
|
||||
return await this.pilotRepository.update(partitionKey, rowKey, pilot);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<DeleteResult> {
|
||||
return await this.pilotRepository.delete({ id });
|
||||
async delete(partitionKey: string, rowKey: string): Promise<void> {
|
||||
const pilotLogs: Log[] = await this.logRepository.findAll({
|
||||
queryOptions: {
|
||||
filter: `pilotId eq '${rowKey}'`
|
||||
}
|
||||
})
|
||||
|
||||
for (const pilotLog of pilotLogs) {
|
||||
await this.logRepository.delete(pilotLog.partitionKey, pilotLog.rowKey);
|
||||
}
|
||||
|
||||
await this.pilotRepository.delete(partitionKey, rowKey);
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import {
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpException,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
UploadedFile,
|
||||
UseGuards,
|
||||
UseInterceptors
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard } from '@noahspan/noahspan-modules';
|
||||
import { CustomError } from '../error/customError';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { TrackService } from './track.service';
|
||||
import { TrackEntity } from './track.entity';
|
||||
import { DeleteResult, InsertResult } from 'typeorm';
|
||||
|
||||
@Controller('tracks')
|
||||
export class TrackController {
|
||||
constructor(
|
||||
private readonly fileService: FileService,
|
||||
private readonly trackService: TrackService
|
||||
) {}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Get(':logId')
|
||||
async findAll(@Param('logId') logId: string): Promise<TrackEntity[]> {
|
||||
try {
|
||||
return await this.trackService.findAll(logId);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Post(':logId/:order')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async create(@Param('logId') logId: string, @Param('order') order: number, @UploadedFile() file: Express.Multer.File): Promise<InsertResult> {
|
||||
try {
|
||||
return await this.trackService.create(logId, order, file);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Delete(':id/:filename/:logId')
|
||||
async delete(@Param('id') id: string, @Query('fileName') filename: string, @Query('logId') logId: string): Promise<DeleteResult> {
|
||||
try {
|
||||
return await this.trackService.delete(id, logId, filename);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':logId/:fileName')
|
||||
async downloadTrack(@Param('logId') logId: string, @Param('fileName') fileName: string): Promise<string> {
|
||||
try {
|
||||
return await this.trackService.downloadTrackFile(logId, fileName);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export class TrackDto {
|
||||
logId: string;
|
||||
order: number;
|
||||
url?: string;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { LogEntity } from 'src/log/log.entity';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||
|
||||
@Entity({ name: 'tracks' })
|
||||
export class TrackEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
url: string;
|
||||
|
||||
@Column()
|
||||
order: number;
|
||||
|
||||
@ManyToOne(() => LogEntity, (log: LogEntity) => log.tracks, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
@JoinColumn({ name: 'logId' })
|
||||
log: LogEntity;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { TrackEntity } from "./track.entity";
|
||||
import { TrackController } from "./track.controller";
|
||||
import { FileService } from "../file/file.service";
|
||||
import { TrackService } from './track.service';
|
||||
import { LogModule } from 'src/log/log.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
LogModule,
|
||||
TypeOrmModule.forFeature([TrackEntity])
|
||||
],
|
||||
controllers: [TrackController],
|
||||
providers: [
|
||||
FileService,
|
||||
TrackService
|
||||
]
|
||||
})
|
||||
export class TrackModule {}
|
||||
@@ -1,98 +0,0 @@
|
||||
import { DeleteResult, InsertResult, Repository, UpdateResult } from "typeorm";
|
||||
import { TrackDto } from "./track.dto";
|
||||
import { TrackEntity } from "./track.entity";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { LogService } from "src/log/log.service";
|
||||
import { LogEntity } from "src/log/log.entity";
|
||||
import { CustomError } from "src/error/customError";
|
||||
import { FileService } from "src/file/file.service";
|
||||
|
||||
@Injectable()
|
||||
export class TrackService {
|
||||
private readonly containerName: string = 'tracks'
|
||||
|
||||
constructor(
|
||||
@InjectRepository(TrackEntity) private readonly trackRepository: Repository<TrackEntity>,
|
||||
private readonly fileService: FileService,
|
||||
private readonly logService: LogService
|
||||
) {}
|
||||
|
||||
async find(id: string): Promise<TrackEntity> {
|
||||
try {
|
||||
return await this.trackRepository.findOneBy({ id });
|
||||
} catch (error) {
|
||||
throw new CustomError('Track not found', 'Not found', 404)
|
||||
}
|
||||
}
|
||||
|
||||
async findAll(logId: string): Promise<TrackEntity[]> {
|
||||
try {
|
||||
const logEntity: LogEntity = await this.logService.find(logId);
|
||||
|
||||
if (logEntity) {
|
||||
const tracks = await this.trackRepository.find({
|
||||
where: { log:
|
||||
{
|
||||
id: logEntity.id
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return tracks;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
throw new CustomError('Tracks not found', 'Not found', 404);
|
||||
}
|
||||
}
|
||||
|
||||
async create(logId: string, order: number, file: Express.Multer.File): Promise<InsertResult> {
|
||||
try {
|
||||
const logEntity: LogEntity = await this.logService.find(logId);
|
||||
|
||||
if (logEntity) {
|
||||
const url = await this.fileService.uploadFile(file, this.containerName, logId);
|
||||
const track = this.trackRepository.create({
|
||||
log: logEntity,
|
||||
order: order,
|
||||
url: url
|
||||
});
|
||||
|
||||
return await this.trackRepository.insert(track);
|
||||
} else {
|
||||
throw new CustomError('Log not found', 'Not found', 404)
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: string, track: TrackDto): Promise<UpdateResult> {
|
||||
try {
|
||||
return await this.trackRepository.update(id, track);
|
||||
} catch(error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async downloadTrackFile(logId: string, fileName: string): Promise<string> {
|
||||
try {
|
||||
const downloadedFile: string = await this.fileService.downloadFile(this.containerName, logId, fileName);
|
||||
|
||||
return downloadedFile;
|
||||
} catch (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
|
||||
}
|
||||
}
|
||||
}
|
||||
0
client/.gitignore → app/.gitignore
vendored
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "client",
|
||||
"name": "app",
|
||||
"private": true,
|
||||
"version": "2.0.0-alpha-3",
|
||||
"version": "1.3.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -11,34 +11,25 @@
|
||||
"serve": "serve -s dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||
"@fortawesome/react-fontawesome": "^3.1.0",
|
||||
"@heroui/react": "^2.8.5",
|
||||
"@noahspan/noahspan-components": "^2.0.0-alpha-14",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@azure/msal-browser": "^4.0.1",
|
||||
"@azure/msal-react": "^3.0.1",
|
||||
"@noahspan/noahspan-components": "^1.9.1",
|
||||
"axios": "^1.7.2",
|
||||
"dotenv": "^16.4.7",
|
||||
"framer-motion": "^12.23.24",
|
||||
"leaflet": "^1.9.4",
|
||||
"oidc-spa": "^7.2.4",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react": "19.0.0-rc.1",
|
||||
"react-dom": "19.0.0-rc.1",
|
||||
"react-hook-form": "^7.51.4",
|
||||
"react-leaflet": "^4",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-leaflet-kml": "^2.1.2",
|
||||
"react-oidc-context": "^3.3.0",
|
||||
"react-router-dom": "^6.23.0",
|
||||
"swiper": "^11.2.6",
|
||||
"tailwindcss": "^4.1.13"
|
||||
"swiper": "^11.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/microsoft-graph-types": "^2.40.0",
|
||||
"@types/leaflet": "^1.9.16",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/react-dom": "^19.1.8",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.2.0"
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 696 B After Width: | Height: | Size: 696 B |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 618 B After Width: | Height: | Size: 618 B |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -3,21 +3,29 @@ import Flights from './components/flights/Flights';
|
||||
import Logbook from './components/logbook/Logbook';
|
||||
import Pilots from './components/pilots/Pilots';
|
||||
import SiteNav from './components/siteNav/SiteNav';
|
||||
import { HeroUIProvider } from '@heroui/react';
|
||||
import { useHref, useNavigate } from 'react-router-dom';
|
||||
import { useIsAuthenticated } from '@azure/msal-react';
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
const navigate = useNavigate();
|
||||
const isAuthenticated = useIsAuthenticated()
|
||||
|
||||
const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
||||
return isAuthenticated ? children : <Navigate to='/' />
|
||||
}
|
||||
|
||||
return (
|
||||
<HeroUIProvider navigate={navigate} useHref={useHref}>
|
||||
<>
|
||||
<SiteNav />
|
||||
<Routes>
|
||||
<Route path='/' element={<Flights />} />
|
||||
<Route path="/logbook" element={<Logbook />} />
|
||||
<Route path="/pilots" element={<Pilots />} />
|
||||
|
||||
</Routes>
|
||||
</HeroUIProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
70
app/src/auth/msalConfig.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License.
|
||||
*/
|
||||
|
||||
import { LogLevel } from '@azure/msal-browser';
|
||||
|
||||
/**
|
||||
* Configuration object to be passed to MSAL instance on creation.
|
||||
* For a full list of MSAL.js configuration parameters, visit:
|
||||
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md
|
||||
*/
|
||||
|
||||
export const msalConfig = {
|
||||
auth: {
|
||||
clientId: import.meta.env.VITE_CLIENT_ID, // This is the ONLY mandatory field that you need to supply.
|
||||
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}`, // Replace the placeholder with your tenant subdomain
|
||||
redirectUri: import.meta.env.VITE_REDIRECT_URL, // Points to window.location.origin. You must register this URI on Microsoft Entra admin center/App Registration.
|
||||
postLogoutRedirectUri: '/', // Indicates the page to navigate after logout.
|
||||
navigateToLoginRequestUrl: false, // If "true", will navigate back to the original request location before processing the auth code response.
|
||||
},
|
||||
cache: {
|
||||
cacheLocation: 'sessionStorage', // Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO between tabs.
|
||||
storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
|
||||
},
|
||||
system: {
|
||||
loggerOptions: {
|
||||
loggerCallback: (level: any, message: any, containsPii: any) => {
|
||||
if (containsPii) {
|
||||
return;
|
||||
}
|
||||
switch (level) {
|
||||
case LogLevel.Error:
|
||||
console.error(message);
|
||||
return;
|
||||
case LogLevel.Info:
|
||||
console.info(message);
|
||||
return;
|
||||
case LogLevel.Verbose:
|
||||
console.debug(message);
|
||||
return;
|
||||
case LogLevel.Warning:
|
||||
console.warn(message);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Scopes you add here will be prompted for user consent during sign-in.
|
||||
* By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request.
|
||||
* For more information about OIDC scopes, visit:
|
||||
* https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
|
||||
*/
|
||||
export const loginRequest = {
|
||||
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`],
|
||||
};
|
||||
|
||||
/**
|
||||
* An optional silentRequest object can be used to achieve silent SSO
|
||||
* between applications by providing a "login_hint" property.
|
||||
*/
|
||||
// export const silentRequest = {
|
||||
// scopes: ["openid", "profile"],
|
||||
// loginHint: "example@domain.net"
|
||||
// };
|
||||
81
app/src/components/actionMenu/ActionMenu.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { useState } from 'react';
|
||||
import { IActionMenuProps } from './IActionMenuProps';
|
||||
import {
|
||||
IconButton,
|
||||
Icon,
|
||||
IconName,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuItem
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
import { useIsAuthenticated } from '@azure/msal-react';
|
||||
|
||||
const ActionMenu = ({ id, onDelete, onOpenCloseForm, onOpenCloseTracks }: IActionMenuProps) => {
|
||||
const [anchorElAction, setAnchorElAction] = useState<null | HTMLElement>(
|
||||
null
|
||||
);
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
const onOpenActionMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorElAction(event.currentTarget);
|
||||
};
|
||||
|
||||
const onCloseActionMenu = () => {
|
||||
setAnchorElAction(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IconButton onClick={onOpenActionMenu}>
|
||||
<Icon iconName={IconName.ELLIPSIS_VERTICAL} size="sm" />
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={anchorElAction}
|
||||
keepMounted
|
||||
open={Boolean(anchorElAction)}
|
||||
onClose={onCloseActionMenu}
|
||||
>
|
||||
{isAuthenticated &&
|
||||
<>
|
||||
<MenuItem onClick={() => onOpenCloseForm(FormMode.EDIT, id)}>
|
||||
<ListItemIcon>
|
||||
<Icon iconName={IconName.PEN} size="lg" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Edit</ListItemText>
|
||||
</MenuItem>
|
||||
{onOpenCloseTracks &&
|
||||
<MenuItem onClick={() => onOpenCloseTracks!(FormMode.EDIT, id)}>
|
||||
<ListItemIcon>
|
||||
<Icon iconName={IconName.MAP_LOCATION_DOT} size="lg" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Tracks</ListItemText>
|
||||
</MenuItem>
|
||||
}
|
||||
</>
|
||||
|
||||
}
|
||||
<MenuItem onClick={() => onOpenCloseForm(FormMode.VIEW, id)}>
|
||||
<ListItemIcon>
|
||||
<Icon iconName={IconName.EYE} size="lg" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>View</ListItemText>
|
||||
</MenuItem>
|
||||
{isAuthenticated &&
|
||||
<>
|
||||
<hr className="my-3" />
|
||||
<MenuItem onClick={() => onDelete(id)}>
|
||||
<ListItemIcon>
|
||||
<Icon iconName={IconName.TRASH} size="lg" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Delete</ListItemText>
|
||||
</MenuItem>
|
||||
</>
|
||||
}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionMenu;
|
||||
52
app/src/components/confirmationDialog/ConfirmationDialog.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Icon,
|
||||
IconName,
|
||||
Spinner
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { IDialogConfirmationProps } from './IConfirmationDialogProps';
|
||||
|
||||
const ConfirmationDialog = ({
|
||||
contentText,
|
||||
isLoading,
|
||||
isOpen,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
title
|
||||
}: IDialogConfirmationProps) => {
|
||||
return (
|
||||
<Dialog
|
||||
onClose={onCancel}
|
||||
open={isOpen}
|
||||
sx={{
|
||||
'& .MuiDialog-paper': { width: '2000px' }
|
||||
}}
|
||||
>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogContent sx={{ textAlign: 'center' }}>
|
||||
{!isLoading && <DialogContentText>{contentText}</DialogContentText>}
|
||||
{isLoading && <Spinner />}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel} variant="outlined" startIcon={<Icon iconName={IconName.XMARK} />}>
|
||||
No
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
variant="contained"
|
||||
startIcon={<Icon iconName={IconName.CIRCLE_CHECK} />}
|
||||
>
|
||||
Yes
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmationDialog;
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface DialogConfirmationProps {
|
||||
export interface IDialogConfirmationProps {
|
||||
contentText: string;
|
||||
isLoading: boolean;
|
||||
isOpen: boolean;
|
||||
76
app/src/components/flights/Flights.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Box, Card, CardContent, Container, Grid, Skeleton, Spinner, Stack, theme, Typography, useMediaQuery } from "@noahspan/noahspan-components";
|
||||
import LogbookCard from "../logbookCard/LogbookCard";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLogs } from "../../hooks/logs/UseLogs";
|
||||
import { ILogbookEntry } from "../logbook/ILogbookEntry";
|
||||
|
||||
const Flights = () => {
|
||||
const [flights, setFlights] = useState<ILogbookEntry[]>([]);
|
||||
const { logs, isLoading } = useLogs();
|
||||
const isMedium = useMediaQuery(theme.breakpoints.up('md'));
|
||||
|
||||
useEffect(() => {
|
||||
const flights: ILogbookEntry[] | undefined = logs?.filter((log: ILogbookEntry) => {
|
||||
if (log.tracks && JSON.parse(log.tracks).length > 0) {
|
||||
return log;
|
||||
}
|
||||
})
|
||||
|
||||
if (flights && flights.length > 0) {
|
||||
setFlights(flights)
|
||||
}
|
||||
}, [logs])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Box sx={{ margin: '20px' }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={isMedium ? 11 : 6}>
|
||||
<Typography variant="h4">Flights</Typography>
|
||||
</Grid>
|
||||
{!isLoading &&
|
||||
<Grid size={12}>
|
||||
<LogbookCard logs={flights} mode='flights' />
|
||||
</Grid>
|
||||
}
|
||||
{isLoading && [...Array(6)].map((_element, index) => {
|
||||
return (
|
||||
<Grid display='flex' justifyContent='center' size={12}>
|
||||
<Card
|
||||
key={index}
|
||||
sx={{
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Grid container>
|
||||
<Grid size={12}>
|
||||
<Skeleton height={60} width={200} />
|
||||
<Skeleton height={30} width={200} />
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<Skeleton height={300} />
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
{[...Array(6)].map((_element, index) => {
|
||||
return (
|
||||
<>
|
||||
<Skeleton height={20} width={300} />
|
||||
<Skeleton height={40} width={300} />
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default Flights;
|
||||
8
app/src/components/logForm/ILogFormProps.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
|
||||
export interface ILogFormProps {
|
||||
entryId?: string;
|
||||
isDrawerOpen: boolean;
|
||||
mode: FormMode;
|
||||
onOpenClose: (mode: FormMode) => void;
|
||||
}
|
||||
9
app/src/components/logForm/ILogFormState.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Alert } from "../../interfaces/Alert.interface";
|
||||
|
||||
export interface ILogFormState {
|
||||
alert: Alert | undefined;
|
||||
isDisabled: boolean;
|
||||
isLoading: boolean;
|
||||
pilotOptions: { label: string; value: string }[];
|
||||
selectedEntryPilotName: string;
|
||||
}
|
||||
947
app/src/components/logForm/LogForm.tsx
Normal file
@@ -0,0 +1,947 @@
|
||||
import React, { useEffect, useReducer } from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Alert,
|
||||
Button,
|
||||
DatePicker,
|
||||
Drawer,
|
||||
Grid,
|
||||
Icon,
|
||||
IconButton,
|
||||
IconName,
|
||||
Select,
|
||||
TextField,
|
||||
theme,
|
||||
Typography,
|
||||
useMediaQuery
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { useForm, Controller, FormProvider } from 'react-hook-form';
|
||||
import { ILogFormProps } from './ILogFormProps';
|
||||
import { initialState, reducer } from './reducer';
|
||||
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
||||
import { useIsAuthenticated } from '@azure/msal-react';
|
||||
import { FormMode } from '../../enums/formMode';
|
||||
import { usePilots } from '../../hooks/pilots/UsePilots';
|
||||
|
||||
const LogForm: React.FC<ILogFormProps> = ({
|
||||
entryId,
|
||||
isDrawerOpen,
|
||||
mode,
|
||||
onOpenClose
|
||||
}) => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const httpClient: AxiosInstance = useHttpClient();
|
||||
const { getAccessToken } = useAccessToken();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const defaultValues = {
|
||||
pilotId: '',
|
||||
pilotName: '',
|
||||
date: null,
|
||||
aircraftMakeModel: '',
|
||||
aircraftIdentity: '',
|
||||
routeFrom: '',
|
||||
routeTo: '',
|
||||
durationOfFlight: null,
|
||||
singleEngineLand: null,
|
||||
simulatorAtd: null,
|
||||
landingsDay: null,
|
||||
landingsNight: null,
|
||||
groundTrainingReceived: null,
|
||||
flightTrainingReceived: null,
|
||||
crossCountry: null,
|
||||
night: null,
|
||||
solo: null,
|
||||
pilotInCommand: null,
|
||||
instrumentActual: null,
|
||||
instrumentSimulated: null,
|
||||
instrumentApproaches: null,
|
||||
instrumentHolds: null,
|
||||
instrumentNavTrack: null,
|
||||
notes: ''
|
||||
};
|
||||
const methods = useForm();
|
||||
const { pilots } = usePilots();
|
||||
const isMedium = useMediaQuery(theme.breakpoints.up('md'));
|
||||
|
||||
const onCancel = () => {
|
||||
methods.reset(defaultValues);
|
||||
dispatch({ type: 'SET_IS_DISABLED', payload: false });
|
||||
onOpenClose(FormMode.CANCEL);
|
||||
};
|
||||
|
||||
const onSubmit = async (data: unknown) => {
|
||||
try {
|
||||
dispatch({ type: 'SET_IS_LOADING', payload: true });
|
||||
|
||||
const accessToken: string = await getAccessToken();
|
||||
|
||||
if (!entryId) {
|
||||
await httpClient.post(`api/logs`, data, {
|
||||
headers: {
|
||||
Authorization: accessToken
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await httpClient.put(`api/logs/log/${entryId}`, data, {
|
||||
headers: {
|
||||
Authorization: accessToken
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
methods.reset(defaultValues);
|
||||
dispatch({ type: 'SET_IS_DISABLED', payload: false });
|
||||
onOpenClose(FormMode.CANCEL);
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError;
|
||||
|
||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'error', message: axiosError.message }});
|
||||
} finally {
|
||||
dispatch({ type: 'SET_IS_LOADING', payload: false });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === FormMode.VIEW) {
|
||||
dispatch({ type: 'SET_IS_DISABLED', payload: true });
|
||||
}
|
||||
}, [mode]);
|
||||
|
||||
useEffect(() => {
|
||||
const getEntry = async () => {
|
||||
try {
|
||||
dispatch({ type: 'SET_IS_LOADING', payload: true });
|
||||
|
||||
const config = isAuthenticated
|
||||
? { headers: { Authorization: await getAccessToken() } }
|
||||
: {};
|
||||
const response: AxiosResponse = await httpClient.get(
|
||||
`api/logs/log/${entryId}`,
|
||||
config
|
||||
);
|
||||
const entry = response.data;
|
||||
|
||||
// if (mode !== FormMode.ADD) {
|
||||
// const pilot = pilots?.find((pilot) => pilot.id === entry.pilotId);
|
||||
// console.log(pilot.name)
|
||||
// dispatch({
|
||||
// type: 'SET_SELECTED_ENTRY_PILOT_NAME',
|
||||
// payload: pilot.name
|
||||
// });
|
||||
// }
|
||||
|
||||
methods.reset(entry);
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError;
|
||||
|
||||
dispatch({ type: 'SET_ALERT', payload: { severity: 'error', message: axiosError.message }});
|
||||
} finally {
|
||||
dispatch({ type: 'SET_IS_LOADING', payload: false });
|
||||
}
|
||||
};
|
||||
|
||||
if (entryId && isDrawerOpen) {
|
||||
getEntry();
|
||||
}
|
||||
}, [entryId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pilots && FormMode.ADD) {
|
||||
const newPilotsOptions = pilots.map((pilot) => {
|
||||
return {
|
||||
label: pilot.name,
|
||||
value: pilot.id
|
||||
};
|
||||
});
|
||||
|
||||
dispatch({ type: 'SET_PILOT_OPTIONS', payload: newPilotsOptions });
|
||||
}
|
||||
}, [pilots]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open={isDrawerOpen}
|
||||
anchor="right"
|
||||
PaperProps={{
|
||||
sx: {
|
||||
padding: '30px',
|
||||
width: isMedium ? '33%' : '75%'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={methods.handleSubmit(onSubmit)} style={{ paddingBottom: '50px' }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={11}>
|
||||
<Typography variant="h4">{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Entry`}</Typography>
|
||||
</Grid>
|
||||
<Grid display="flex" justifyContent="right" size={1}>
|
||||
<IconButton onClick={onCancel}>
|
||||
<Icon iconName={IconName.XMARK} />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
{state.alert && (
|
||||
<Grid display="flex" justifyContent="center" size={12}>
|
||||
<Alert
|
||||
onClose={() =>
|
||||
dispatch({ type: 'SET_ALERT', payload: undefined })
|
||||
}
|
||||
severity={state.alert.severity}
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
{state.alert.message}
|
||||
</Alert>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">Pilot *</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="pilotId"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return (
|
||||
<Select
|
||||
disabled={state.isDisabled}
|
||||
fullWidth
|
||||
onChange={(event: any) => {
|
||||
const pilot = pilots?.find(
|
||||
(pilot) => (pilot.id = event.target.value)
|
||||
);
|
||||
|
||||
if (pilot) {
|
||||
methods.setValue('pilotName', pilot.name);
|
||||
}
|
||||
|
||||
methods.setValue('pilotId', event.target.value);
|
||||
}}
|
||||
options={
|
||||
state.pilotOptions && state.pilotOptions.length > 0
|
||||
? state.pilotOptions
|
||||
: []
|
||||
}
|
||||
value={value ? value : ''}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">Date *</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="date"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<DatePicker
|
||||
disabled={state.isDisabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">Aircraft Make and Model *</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="aircraftMakeModel"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={methods.formState.errors.address ? true : false}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
{isAuthenticated &&
|
||||
<>
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">Aircraft Identity *</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="aircraftIdentity"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={methods.formState.errors.address ? true : false}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
}
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">Route From</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="routeFrom"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={methods.formState.errors.address ? true : false}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">Route To</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="routeTo"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={methods.formState.errors.address ? true : false}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">Duration Of Flight</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="durationOfFlight"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={methods.formState.errors.address ? true : false}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
{isAuthenticated &&
|
||||
<>
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">Single Engine Land</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="singleEngineLand"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={methods.formState.errors.address ? true : false}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
}
|
||||
{isAuthenticated &&
|
||||
<>
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">Simulator or ATD</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="simulatorAtd"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={methods.formState.errors.address ? true : false}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
}
|
||||
{isAuthenticated &&
|
||||
<Grid size={12}>
|
||||
<Accordion defaultExpanded>
|
||||
<AccordionSummary expandIcon={<Icon iconName={IconName.CHEVRON_DOWN} />}>
|
||||
<Typography variant="body1">Landings</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container spacing={2}>
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">Day</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="landingsDay"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">Night</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="landingsNight"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Grid>
|
||||
}
|
||||
{isAuthenticated &&
|
||||
<Grid size={12}>
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<Icon iconName={IconName.CHEVRON_DOWN} />}>
|
||||
Instrument
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container spacing={2}>
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">Actual</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="instrumentActual"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">Simulated</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="instrumentSimulated"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">
|
||||
Instrument Approaches
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="instrumentApproaches"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={4}>
|
||||
<Typography variant="body1">Holds</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="instrumentHolds"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">Nav / Track</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="instrumentNavTrack"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Grid>
|
||||
}
|
||||
{isAuthenticated &&
|
||||
<Grid size={12}>
|
||||
<Accordion defaultExpanded>
|
||||
<AccordionSummary expandIcon={<Icon iconName={IconName.CHEVRON_DOWN} />}>
|
||||
Type of Pilot Experience or Training
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container spacing={2}>
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">
|
||||
Ground Training Received
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="groundTrainingReceived"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">
|
||||
Flight Training Received
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="flightTrainingReceived"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">Cross Country</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="crossCountry"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">Night</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="night"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">Solo</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="solo"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid alignItems="center" display="flex" size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">Pilot in Command</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="pilotInCommand"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={
|
||||
methods.formState.errors.address ? true : false
|
||||
}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
onChange={onChange}
|
||||
slotProps={{
|
||||
htmlInput: {
|
||||
step: 0.1
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Grid>
|
||||
}
|
||||
<Grid size={isMedium ? 4 : 12}>
|
||||
<Typography variant="body1">Notes</Typography>
|
||||
</Grid>
|
||||
<Grid size={isMedium ? 8 : 12}>
|
||||
<Controller
|
||||
name="notes"
|
||||
control={methods.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<TextField
|
||||
disabled={state.isDisabled}
|
||||
error={methods.formState.errors.address ? true : false}
|
||||
fullWidth
|
||||
helperText={
|
||||
methods.formState.errors.address
|
||||
? methods.formState.errors.address.message?.toString()
|
||||
: undefined
|
||||
}
|
||||
multiline
|
||||
rows={3}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid display="flex" gap={2} justifyContent="right" size={12}>
|
||||
<Button
|
||||
disabled={
|
||||
state.isDisabled && mode.toString() !== FormMode.VIEW
|
||||
? state.isDisabled
|
||||
: false
|
||||
}
|
||||
startIcon={<Icon iconName={IconName.XMARK} />}
|
||||
variant="outlined"
|
||||
onClick={onCancel}
|
||||
size="small"
|
||||
>
|
||||
{mode.toString() !== FormMode.VIEW ? 'Cancel' : 'Close'}
|
||||
</Button>
|
||||
{mode.toString() !== FormMode.VIEW && (
|
||||
<Button
|
||||
disabled={state.isDisabled}
|
||||
startIcon={<Icon iconName={IconName.SAVE} />}
|
||||
size="small"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</FormProvider>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogForm;
|
||||
58
app/src/components/logForm/reducer.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Alert } from '../../interfaces/Alert.interface';
|
||||
import { ILogFormState } from './ILogFormState';
|
||||
|
||||
type Action =
|
||||
| { type: 'SET_ALERT'; payload: Alert | undefined }
|
||||
| { type: 'SET_IS_DISABLED'; payload: boolean }
|
||||
| { type: 'SET_IS_LOADING'; payload: boolean }
|
||||
| { type: 'SET_PILOT_OPTIONS'; payload: { label: string; value: string }[] }
|
||||
| { type: 'SET_SELECTED_ENTRY_PILOT_NAME'; payload: string };
|
||||
|
||||
export const initialState: ILogFormState = {
|
||||
alert: undefined,
|
||||
isDisabled: false,
|
||||
isLoading: true,
|
||||
pilotOptions: [],
|
||||
selectedEntryPilotName: ''
|
||||
};
|
||||
|
||||
export const reducer = (
|
||||
state: ILogFormState,
|
||||
action: Action
|
||||
): ILogFormState => {
|
||||
switch (action.type) {
|
||||
case 'SET_ALERT': {
|
||||
return {
|
||||
...state,
|
||||
alert: action.payload
|
||||
};
|
||||
}
|
||||
case 'SET_IS_DISABLED': {
|
||||
return {
|
||||
...state,
|
||||
isDisabled: action.payload
|
||||
};
|
||||
}
|
||||
case 'SET_IS_LOADING': {
|
||||
return {
|
||||
...state,
|
||||
isLoading: action.payload
|
||||
};
|
||||
}
|
||||
case 'SET_PILOT_OPTIONS': {
|
||||
return {
|
||||
...state,
|
||||
pilotOptions: action.payload
|
||||
};
|
||||
}
|
||||
case 'SET_SELECTED_ENTRY_PILOT_NAME': {
|
||||
return {
|
||||
...state,
|
||||
selectedEntryPilotName: action.payload
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
64
app/src/components/logTrackMaps/LogTrackMaps.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { LogTrackMapsProps } from './LogTrackMapsProps.interface';
|
||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
||||
import { useIsAuthenticated } from '@azure/msal-react';
|
||||
import { MapContainer, TileLayer } from 'react-leaflet';
|
||||
import ReactLeafletKml from 'react-leaflet-kml';
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/pagination';
|
||||
import 'swiper/css';
|
||||
import './LogTrackMaps.css';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
|
||||
const LogTrackMaps = ({ rowKey, trackUrls }: LogTrackMapsProps) => {
|
||||
const [tracks, setTracks] = useState<any[]>([])
|
||||
const httpClient: AxiosInstance = useHttpClient();
|
||||
const { getAccessToken } = useAccessToken();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
useEffect(() => {
|
||||
const getTracks = async () => {
|
||||
const convertedTracks: any[] = []
|
||||
|
||||
for (const trackUrl of trackUrls) {
|
||||
const trackUrlSplit = trackUrl.split('/')
|
||||
const filename = trackUrlSplit[trackUrlSplit.length - 1];
|
||||
const config = isAuthenticated
|
||||
? { headers: { Authorization: await getAccessToken() } }
|
||||
: {};
|
||||
const response: AxiosResponse = await httpClient.get(
|
||||
`api/logs/log/${rowKey}/track?fileName=${filename}`,
|
||||
config
|
||||
);
|
||||
const kml = new DOMParser().parseFromString(response.data, 'text/xml')
|
||||
|
||||
convertedTracks.push(kml);
|
||||
}
|
||||
|
||||
setTracks(convertedTracks)
|
||||
}
|
||||
|
||||
getTracks();
|
||||
}, [trackUrls])
|
||||
|
||||
return (
|
||||
<MapContainer
|
||||
center={[45.14489, -93.21019]}
|
||||
scrollWheelZoom={false}
|
||||
style={{ height: '500px', width: '100%' }}
|
||||
zoom={8}
|
||||
>
|
||||
<TileLayer
|
||||
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
{tracks.length > 0 && tracks.map((track) => (
|
||||
<ReactLeafletKml kml={track} />
|
||||
))}
|
||||
</MapContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogTrackMaps;
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface LogTrackMapsProps {
|
||||
rowKey: string;
|
||||
trackUrls: string[];
|
||||
}
|
||||
209
app/src/components/logTracks/LogTracks.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import { useEffect, useReducer } from "react";
|
||||
import { Button, Drawer, Grid, Icon, IconButton, IconName, Spinner, TextField, theme, Typography, useMediaQuery } from "@noahspan/noahspan-components";
|
||||
import { useHttpClient } from "../../hooks/httpClient/UseHttpClient";
|
||||
import { AxiosInstance, AxiosResponse } from "axios";
|
||||
import { useAccessToken } from "../../hooks/accessToken/UseAcessToken";
|
||||
import { LogTracksProps } from "./LogTracksProps.interface";
|
||||
import { FormMode } from "../../enums/formMode";
|
||||
import { useIsAuthenticated } from "@azure/msal-react";
|
||||
import { ILogbookEntry } from "../logbook/ILogbookEntry";
|
||||
import ConfirmationDialog from "../confirmationDialog/ConfirmationDialog";
|
||||
import { initialState, reducer } from "./reducer";
|
||||
import LogTrackMaps from "../logTrackMaps/LogTrackMaps";
|
||||
|
||||
const LogTracks = ({ isDrawerOpen, mode, onOpenClose, selectedRowKey }: LogTracksProps) => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState)
|
||||
const httpClient: AxiosInstance = useHttpClient();
|
||||
const { getAccessToken } = useAccessToken();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
|
||||
const isMedium = useMediaQuery(theme.breakpoints.up('md'));
|
||||
|
||||
const getConfig = async () => {
|
||||
const config = isAuthenticated
|
||||
? { headers: { Authorization: await getAccessToken() } }
|
||||
: {};
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
const getLog = async (): Promise<ILogbookEntry> => {
|
||||
const logResponse: AxiosResponse = await httpClient.get(
|
||||
`api/logs/log/${selectedRowKey}`,
|
||||
await getConfig()
|
||||
);
|
||||
const logData: ILogbookEntry = logResponse.data;
|
||||
|
||||
return logData
|
||||
}
|
||||
|
||||
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
try {
|
||||
dispatch({ type: 'SET_IS_LOADING', payload: true})
|
||||
|
||||
const file = event.target.files![0]
|
||||
const formData = new FormData();
|
||||
const config = await getConfig();
|
||||
const formDataConfig = {
|
||||
headers: {
|
||||
...config.headers,
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
}
|
||||
|
||||
formData.append('file', file);
|
||||
|
||||
const uploadResponse: AxiosResponse = await httpClient.post(`api/logs/log/${selectedRowKey}/track`, formData, formDataConfig);
|
||||
const uploadUrl = uploadResponse.data.url;
|
||||
const log = await getLog();
|
||||
const tracks: string[] = log.tracks ? JSON.parse(log.tracks!) : [];
|
||||
|
||||
tracks.push(uploadUrl)
|
||||
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!) })
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
} finally {
|
||||
dispatch({ type: 'SET_IS_LOADING', payload: false });
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
onOpenClose(FormMode.CANCEL)
|
||||
}
|
||||
|
||||
const onDeleteTrack = async (fileName: string, index: number) => {
|
||||
dispatch({ type: 'SET_ON_DELETE_TRACK', payload: { isConfirmDialogOpen: true, selectedTrack: { fileName: fileName, index: index }}})
|
||||
}
|
||||
|
||||
const onConfirmDialogConfirm = async () => {
|
||||
try {
|
||||
const config = await getConfig();
|
||||
|
||||
await httpClient.delete(`api/logs/log/${selectedRowKey}/track?fileName=${state.selectedTrack!.fileName}`, config);
|
||||
|
||||
const log = await getLog();
|
||||
const tracks: string[] = JSON.parse(log.tracks!);
|
||||
|
||||
tracks.splice(state.selectedTrack!.index, 1);
|
||||
log.tracks = JSON.stringify(tracks);
|
||||
await httpClient.put(`api/logs/log/${selectedRowKey}`, log, config);
|
||||
|
||||
const updatedLog = await getLog();
|
||||
|
||||
dispatch({ type: 'SET_TRACKS', payload: JSON.parse(updatedLog.tracks!) })
|
||||
dispatch({ type: 'SET_IS_CONFIRM_DIALOG_OPEN', payload: false })
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
const onConfirmDialogCancel = async () => {
|
||||
dispatch({ type: 'SET_IS_CONFIRM_DIALOG_OPEN', payload: false })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const updateTracks = async () => {
|
||||
const log = await getLog();
|
||||
|
||||
dispatch({ type: 'SET_TRACKS', payload: JSON.parse(log.tracks!) });
|
||||
}
|
||||
|
||||
updateTracks();
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open={isDrawerOpen}
|
||||
anchor='right'
|
||||
PaperProps={{
|
||||
sx: {
|
||||
padding: '30px',
|
||||
width: isMedium ? '33%' : '75%'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={11}>
|
||||
<Typography variant="h4">{`${mode.toString().toLowerCase().charAt(0).toUpperCase() + mode.toString().slice(1).toLowerCase()} Tracks`}</Typography>
|
||||
</Grid>
|
||||
<Grid display="flex" justifyContent="right" size={1}>
|
||||
<IconButton disabled={state.isLoading ? true : false} onClick={onCancel}>
|
||||
<Icon iconName={IconName.XMARK} />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
{mode === FormMode.EDIT &&
|
||||
<>
|
||||
{state.isLoading &&
|
||||
<>
|
||||
<Grid display="flex" justifyContent="center" size={12}>
|
||||
<Spinner />
|
||||
</Grid>
|
||||
<Grid display="flex" justifyContent="center" size={12}>
|
||||
Loading...
|
||||
</Grid>
|
||||
</>
|
||||
}
|
||||
{!state.isLoading && state.tracks.length > 0 && state.tracks.map((track, index) => {
|
||||
const trackSplit = track.split('/')
|
||||
const filename = trackSplit[trackSplit.length - 1];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid size={11}>
|
||||
<TextField disabled={true} fullWidth value={filename} />
|
||||
</Grid>
|
||||
<Grid size={1}>
|
||||
<IconButton onClick={() => onDeleteTrack(filename, index)}><Icon iconName={IconName.TRASH} /></IconButton>
|
||||
</Grid>
|
||||
</>
|
||||
)
|
||||
})}
|
||||
<Grid display='flex' gap={2} justifyContent='right' size={12}>
|
||||
<Button
|
||||
disabled={state.isLoading ? true : false}
|
||||
startIcon={<Icon iconName={IconName.XMARK} />}
|
||||
variant="outlined"
|
||||
onClick={onCancel}
|
||||
size="small"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
{mode.toString() !== FormMode.VIEW && (
|
||||
<Button
|
||||
component='label'
|
||||
disabled={state.isLoading ? true : false}
|
||||
startIcon={<Icon iconName={IconName.UPLOAD} />}
|
||||
variant='contained'
|
||||
>
|
||||
Upload Track
|
||||
<input hidden onChange={handleFileUpload} type='file' />
|
||||
</Button>
|
||||
)}
|
||||
</Grid>
|
||||
</>
|
||||
}
|
||||
{mode === FormMode.VIEW &&
|
||||
<LogTrackMaps rowKey={selectedRowKey!} trackUrls={state.tracks} />
|
||||
}
|
||||
</Grid>
|
||||
{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"
|
||||
/>
|
||||
)}
|
||||
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
export default LogTracks;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { FormMode } from "../../enums/formMode";
|
||||
|
||||
export interface TracksFormProps {
|
||||
export interface LogTracksProps {
|
||||
isDrawerOpen: boolean;
|
||||
mode: FormMode;
|
||||
onOpenClose: (mode: FormMode) => void;
|
||||
selectedLogId: string | undefined;
|
||||
selectedRowKey: string | undefined;
|
||||
}
|
||||
10
app/src/components/logTracks/LogTracksState.interface.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface LogTracksState {
|
||||
isConfirmDialogOpen: boolean;
|
||||
isConfirmDialogLoading: boolean;
|
||||
isLoading: boolean;
|
||||
selectedTrack: {
|
||||
fileName: string,
|
||||
index: number
|
||||
} | undefined;
|
||||
tracks: string[];
|
||||
}
|
||||