Feature/4 pilots add #20
@@ -1,4 +1,11 @@
|
||||
import { Controller, Get, Headers, Query } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Headers,
|
||||
Query,
|
||||
Res,
|
||||
StreamableFile
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
AppConfigService,
|
||||
MsGraphService,
|
||||
@@ -8,6 +15,10 @@ import { FeatureFlagValue } from '@azure/app-configuration';
|
||||
import { Public } from '@noahspan/noahspan-modules';
|
||||
import { Person } from '@microsoft/microsoft-graph-types';
|
||||
import { AppService } from './app.service';
|
||||
import { createReadStream } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { arrayBuffer } from 'stream/consumers';
|
||||
import type { Response } from 'express';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
@@ -23,7 +34,6 @@ export class AppController {
|
||||
@Query() query: any
|
||||
): Promise<{ key: string; enabled: boolean }[]> {
|
||||
try {
|
||||
console.log(query);
|
||||
const featureFlagKeys: string[] =
|
||||
query.keys && query.keys.toString().includes(';')
|
||||
? query.keys.split(';')
|
||||
@@ -41,9 +51,27 @@ export class AppController {
|
||||
}
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('profilePhoto')
|
||||
async getProfilePhoto(@Query() query: any): Promise<any> {}
|
||||
@Get('userPhoto')
|
||||
async getProfilePhoto(@Headers() headers: any): Promise<StreamableFile> {
|
||||
try {
|
||||
const graphToken: string = await this.msGraphService.getMsGraphAuth(
|
||||
headers.authorization.replace('Bearer ', ''),
|
||||
['user.read']
|
||||
);
|
||||
const client: MsGraphClient =
|
||||
await this.msGraphService.getMsGraphClientDelegated(graphToken);
|
||||
const blob: Blob = await client.api(`me/photos('48x48')/$value`).get();
|
||||
const arrayBuffer: ArrayBuffer = await blob.arrayBuffer();
|
||||
const buffer: Buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
return new StreamableFile(buffer, {
|
||||
type: 'application/json',
|
||||
disposition: `attachment; filename="user_photo.png"`
|
||||
});
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
@Get('userProfile')
|
||||
async getUserProfile(@Headers() headers: any) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export class PilotInfoDto {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
name: string;
|
||||
address: string;
|
||||
city: string;
|
||||
state: string;
|
||||
|
||||
@@ -2,12 +2,11 @@ export class PilotInfoEntity {
|
||||
partitionKey: string;
|
||||
rowKey: string;
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
address: string;
|
||||
city: string;
|
||||
state: string;
|
||||
postalCode: string;
|
||||
name: string;
|
||||
address?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
postalCode?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,51 @@
|
||||
import { HttpException, Injectable } from '@nestjs/common';
|
||||
// import { Repository, InjectRepository } from '@nestjs/azure-database';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PilotInfoDto } from './pilot-info.dto';
|
||||
import { PilotInfoEntity } from './pilot-info.entity';
|
||||
import { TableClient, TableService } from '@noahspan/noahspan-modules';
|
||||
import { RestError, TableInsertEntityHeaders } from '@azure/data-tables';
|
||||
import { odata, RestError, TableInsertEntityHeaders } from '@azure/data-tables';
|
||||
import { CustomError } from '../../customError/CustomError';
|
||||
|
||||
@Injectable()
|
||||
export class PilotInfoService {
|
||||
private readonly partitionKey: string = 'info';
|
||||
|
||||
constructor(
|
||||
// @InjectRepository(PilotInfo)
|
||||
// private readonly pilotInfoRepository: Repository<PilotInfo>
|
||||
private readonly tableService: TableService
|
||||
) {}
|
||||
constructor(private readonly tableService: TableService) {}
|
||||
|
||||
// async find(rowKey: string): Promise<PilotInfo> {
|
||||
// return await this.pilotInfoRepository.find(this.partitionKey, rowKey);
|
||||
// }
|
||||
|
||||
// async findAll(): Promise<PilotInfo[]> {
|
||||
// return await this.pilotInfoRepository.findAll();
|
||||
// }
|
||||
async findAll(): Promise<PilotInfoEntity[]> {
|
||||
try {
|
||||
const client: TableClient =
|
||||
await this.tableService.getTableClient('Pilots');
|
||||
const entities = await client.listEntities({
|
||||
queryOptions: { filter: odata`PartitionKey eq 'pilot'` }
|
||||
});
|
||||
const pilots: PilotInfoEntity[] = [];
|
||||
|
||||
for await (const entity of entities) {
|
||||
const pilot: PilotInfoEntity = {
|
||||
partitionKey: entity.partitionKey,
|
||||
rowKey: entity.rowKey,
|
||||
id: entity.id.toString(),
|
||||
name: entity.name.toString()
|
||||
};
|
||||
|
||||
pilots.push(pilot);
|
||||
}
|
||||
|
||||
return pilots;
|
||||
} catch (error) {
|
||||
const restError: RestError = error as RestError;
|
||||
|
||||
throw new CustomError(
|
||||
restError.details['odataError']['message']['value'],
|
||||
restError.details['odataError']['code'],
|
||||
restError.statusCode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async create(pilotInfoData: PilotInfoDto): Promise<TableInsertEntityHeaders> {
|
||||
const client: TableClient =
|
||||
|
||||
@@ -1,33 +1,40 @@
|
||||
import { Body, Controller, HttpException, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Get, HttpException, Post } from '@nestjs/common';
|
||||
import { PilotInfoService } from './info/pilot-info.service';
|
||||
import { PilotInfoDto } from './info/pilot-info.dto';
|
||||
import { TableInsertEntityHeaders } from '@azure/data-tables';
|
||||
import { CustomError } from '../customError/CustomError';
|
||||
import { PilotInfoEntity } from './info/pilot-info.entity';
|
||||
|
||||
@Controller('pilots')
|
||||
export class PilotController {
|
||||
constructor(private readonly pilotInfoService: PilotInfoService) {}
|
||||
|
||||
@Post()
|
||||
async createPilot(@Body() pilotInfoData: PilotInfoDto): Promise<void> {
|
||||
@Get()
|
||||
async findAll(): Promise<PilotInfoEntity[]> {
|
||||
try {
|
||||
const response: TableInsertEntityHeaders =
|
||||
await this.pilotInfoService.create(pilotInfoData);
|
||||
const pilots: PilotInfoEntity[] = await this.pilotInfoService.findAll();
|
||||
|
||||
console.log(`Not Broken: ${response}`);
|
||||
return pilots;
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
console.log(customError.name);
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode, {
|
||||
cause: customError.name
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// throw new HttpException({
|
||||
// status: customError.statusCode,
|
||||
// error: customError.message
|
||||
// }, customError.statusCode, {
|
||||
// cause: customError.name
|
||||
// });
|
||||
@Post()
|
||||
async create(@Body() pilotInfoData: PilotInfoDto): Promise<void> {
|
||||
try {
|
||||
const response: TableInsertEntityHeaders =
|
||||
await this.pilotInfoService.create(pilotInfoData);
|
||||
} catch (error) {
|
||||
const customError = error as CustomError;
|
||||
|
||||
throw new HttpException(customError.message, customError.statusCode, {
|
||||
cause: customError.name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@noahspan/noahspan-components": "^0.6.5",
|
||||
"@noahspan/noahspan-components": "^0.6.8",
|
||||
"axios": "^1.7.2",
|
||||
"framer-motion": "^11.1.7",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -5,25 +5,11 @@ import { useAppContext } from './hooks/appContext/UseAppContext';
|
||||
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { useHttpClient } from './hooks/httpClient/UseHttpClient';
|
||||
import { useFeatureFlag } from './hooks/featureFlag/UseFeatureFlag';
|
||||
import { useMsal } from '@azure/msal-react';
|
||||
import SiteNav from './components/siteNav/SiteNav';
|
||||
import { EventMessage, EventPayload, EventType } from '@azure/msal-browser';
|
||||
import { User } from '@microsoft/microsoft-graph-types';
|
||||
|
||||
type EventPayloadExtended = EventPayload & { accessToken: string };
|
||||
|
||||
const App: React.FC<unknown> = () => {
|
||||
const httpClient: AxiosInstance = useHttpClient();
|
||||
const appContext = useAppContext();
|
||||
const { inProgress, instance } = useMsal();
|
||||
const handleSignIn = async () => {
|
||||
await instance.loginRedirect({
|
||||
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`]
|
||||
});
|
||||
};
|
||||
const handleSignOut = () => {
|
||||
instance.logoutRedirect();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getFeatureFlags = async () => {
|
||||
@@ -48,49 +34,9 @@ const App: React.FC<unknown> = () => {
|
||||
getFeatureFlags();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const callback = instance.addEventCallback(
|
||||
async (message: EventMessage) => {
|
||||
if (message.eventType === EventType.LOGIN_SUCCESS) {
|
||||
try {
|
||||
const eventPayload: EventPayloadExtended =
|
||||
message.payload as EventPayloadExtended;
|
||||
const response: AxiosResponse = await httpClient.get(
|
||||
`api/userProfile`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${eventPayload.accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
const userProfile: User = response.data;
|
||||
|
||||
appContext.dispatch({
|
||||
type: 'SET_USER_PROFILE',
|
||||
payload: userProfile
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
if (callback) {
|
||||
instance.removeEventCallback(callback);
|
||||
appContext.dispatch({ type: 'SET_USER_PROFILE', payload: {} });
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto">
|
||||
<SiteNav
|
||||
handleSignIn={handleSignIn}
|
||||
handleSignOut={handleSignOut}
|
||||
inProgress={inProgress}
|
||||
/>
|
||||
<SiteNav />
|
||||
<Routes>
|
||||
{useFeatureFlag('flying-pilots')?.enabled && (
|
||||
<Route path="/" element={<Pilots />} />
|
||||
|
||||
@@ -154,6 +154,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
onClick: handlePeoplePickerOnClick
|
||||
}}
|
||||
loading={isPeoplePickerLoading}
|
||||
data-testid="pilot-form-people-picker"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -181,6 +182,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
data-testid="pilot-form-address-input"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -207,6 +209,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
data-testid="pilot-form-city-input"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -231,6 +234,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
variant="outlined"
|
||||
data-testid="pilot-form-state-dropdown"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -257,6 +261,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
data-testid="pilot-form-postal-code-input"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -288,6 +293,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
data-testid="pilot-form-email-input"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -319,6 +325,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
data-testid="pilot-form-phone-input"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -437,7 +444,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
className="flex items-center gap-3"
|
||||
variant="outlined"
|
||||
onClick={onOpenCloseDrawer}
|
||||
data-testid="pilot-drawer-cancel-button"
|
||||
data-testid="pilot-cancel-button"
|
||||
>
|
||||
<XmarkIcon size="lg" />
|
||||
Cancel
|
||||
@@ -449,6 +456,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
|
||||
loading={isLoading}
|
||||
variant="filled"
|
||||
type="submit"
|
||||
data-testid="pilot-save-button"
|
||||
>
|
||||
<SaveIcon size="lg" />
|
||||
Save
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import PilotForm from '../pilotForm/PilotForm';
|
||||
import {
|
||||
Button,
|
||||
@@ -16,133 +16,88 @@ import {
|
||||
TrashIcon,
|
||||
Typography
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
||||
|
||||
const Pilots: React.FC<unknown> = () => {
|
||||
const httpClient: AxiosInstance = useHttpClient();
|
||||
const { getAccessToken } = useAccessToken();
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||
const [pilots, setPilots] = useState<Pilot[]>([]);
|
||||
const onOpenCloseDrawer = () => {
|
||||
setIsDrawerOpen(!isDrawerOpen);
|
||||
};
|
||||
|
||||
type Person = {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
age: number;
|
||||
visits: number;
|
||||
status: string;
|
||||
progress: number;
|
||||
type Pilot = {
|
||||
partitionKey: string;
|
||||
rowKey: string;
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const data: Person[] = [
|
||||
{
|
||||
firstName: 'tanner',
|
||||
lastName: 'linsley',
|
||||
age: 24,
|
||||
visits: 100,
|
||||
status: 'In Relationship',
|
||||
progress: 50
|
||||
},
|
||||
{
|
||||
firstName: 'tandy',
|
||||
lastName: 'miller',
|
||||
age: 40,
|
||||
visits: 40,
|
||||
status: 'Single',
|
||||
progress: 80
|
||||
},
|
||||
{
|
||||
firstName: 'joe',
|
||||
lastName: 'dirte',
|
||||
age: 45,
|
||||
visits: 20,
|
||||
status: 'Complicated',
|
||||
progress: 10
|
||||
}
|
||||
];
|
||||
|
||||
const columns: TableColumnDef[] = [
|
||||
{
|
||||
accessorKey: 'firstName',
|
||||
header: 'First Name',
|
||||
cell: ({ getValue }) => (
|
||||
<div>
|
||||
{getValue<string>().charAt(0).toUpperCase() +
|
||||
getValue<string>().slice(1)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: 'lastName',
|
||||
header: 'Last Name',
|
||||
cell: ({ getValue }) => (
|
||||
<div>
|
||||
{getValue<string>().charAt(0).toUpperCase() +
|
||||
getValue<string>().slice(1)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: 'age',
|
||||
header: 'Age',
|
||||
cellProps: {
|
||||
className: 'text-right'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'visits',
|
||||
header: 'Visits',
|
||||
cellProps: {
|
||||
className: 'text-right'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Status'
|
||||
},
|
||||
{
|
||||
accessorKey: 'progress',
|
||||
header: 'Progress',
|
||||
cellProps: {
|
||||
className: 'text-right'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
cellProps: {
|
||||
className: 'text-center'
|
||||
},
|
||||
cell: () => {
|
||||
return (
|
||||
<Menu placement="bottom-end">
|
||||
<MenuHandler>
|
||||
<div>
|
||||
<IconButton variant="text">
|
||||
<EllipsisVerticalIcon size="xl" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</MenuHandler>
|
||||
<MenuList>
|
||||
<MenuItem className="flex gap-3">
|
||||
<PenIcon size="lg" />
|
||||
Edit
|
||||
</MenuItem>
|
||||
<MenuItem className="flex gap-3">
|
||||
<EyeIcon size="lg" />
|
||||
View
|
||||
</MenuItem>
|
||||
<hr className="my-3" />
|
||||
<MenuItem className="flex gap-3">
|
||||
<TrashIcon size="lg" />
|
||||
Delete
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
},
|
||||
enableSorting: false
|
||||
accessorKey: 'name',
|
||||
header: 'Name'
|
||||
}
|
||||
// {
|
||||
// id: 'actions',
|
||||
// header: 'Actions',
|
||||
// cellProps: {
|
||||
// className: 'text-center'
|
||||
// },
|
||||
// cell: () => {
|
||||
// return (
|
||||
// <Menu placement="bottom-end">
|
||||
// <MenuHandler>
|
||||
// <div>
|
||||
// <IconButton variant="text">
|
||||
// <EllipsisVerticalIcon size="xl" />
|
||||
// </IconButton>
|
||||
// </div>
|
||||
// </MenuHandler>
|
||||
// <MenuList>
|
||||
// <MenuItem className="flex gap-3">
|
||||
// <PenIcon size="lg" />
|
||||
// Edit
|
||||
// </MenuItem>
|
||||
// <MenuItem className="flex gap-3">
|
||||
// <EyeIcon size="lg" />
|
||||
// View
|
||||
// </MenuItem>
|
||||
// <hr className="my-3" />
|
||||
// <MenuItem className="flex gap-3">
|
||||
// <TrashIcon size="lg" />
|
||||
// Delete
|
||||
// </MenuItem>
|
||||
// </MenuList>
|
||||
// </Menu>
|
||||
// );
|
||||
// },
|
||||
// enableSorting: false
|
||||
// }
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const getPilots = async () => {
|
||||
try {
|
||||
const accessToken: string = await getAccessToken();
|
||||
const response: AxiosResponse = await httpClient.get(`api/pilots`, {
|
||||
headers: {
|
||||
Authorization: accessToken
|
||||
}
|
||||
});
|
||||
console.log(response.data);
|
||||
setPilots(response.data);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
getPilots();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-4 w-full rounded-xl py-4 px-8 shadow-md backdrop-saturate-200 backdrop-blur-2xl bg-opacity-80 border border-white/80 bg-white mt-6">
|
||||
@@ -154,13 +109,13 @@ const Pilots: React.FC<unknown> = () => {
|
||||
className="flex justify-center gap-3"
|
||||
variant="filled"
|
||||
onClick={onOpenCloseDrawer}
|
||||
data-testid="add-pilot-button"
|
||||
data-testid="pilot-add-button"
|
||||
>
|
||||
<PlusIcon size="lg" />
|
||||
Add Pilot
|
||||
</Button>
|
||||
</div>
|
||||
<Table defaultData={data} columns={columns}></Table>
|
||||
{pilots.length > 0 && <Table defaultData={pilots} columns={columns} />}
|
||||
</div>
|
||||
<PilotForm
|
||||
isDrawerOpen={isDrawerOpen}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ISiteNavProps } from './ISiteNavProps';
|
||||
// import { Link as ReactRouterLink } from 'react-router-dom';
|
||||
import { useAppContext } from '../../hooks/appContext/UseAppContext';
|
||||
@@ -13,25 +14,139 @@ import {
|
||||
NavbarMenu,
|
||||
NavbarItemProps,
|
||||
PlaneIcon,
|
||||
SignOutIcon,
|
||||
Spinner,
|
||||
Typography
|
||||
} from '@noahspan/noahspan-components';
|
||||
import { useIsAuthenticated } from '@azure/msal-react';
|
||||
import { useIsAuthenticated, useMsal } from '@azure/msal-react';
|
||||
import { InteractionStatus } from '@azure/msal-browser';
|
||||
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
|
||||
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
|
||||
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { User } from '@microsoft/microsoft-graph-types';
|
||||
import { EventMessage, EventPayload, EventType } from '@azure/msal-browser';
|
||||
|
||||
const SiteNav: React.FC<ISiteNavProps> = ({
|
||||
handleSignIn,
|
||||
handleSignOut,
|
||||
inProgress
|
||||
}: ISiteNavProps) => {
|
||||
type EventPayloadExtended = EventPayload & { accessToken: string };
|
||||
|
||||
const SiteNav: React.FC<unknown> = () => {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [userPhoto, setUserPhoto] = useState<string>();
|
||||
const httpClient: AxiosInstance = useHttpClient();
|
||||
const appContext = useAppContext();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const { getAccessToken } = useAccessToken();
|
||||
const { inProgress, instance } = useMsal();
|
||||
const navItems: NavbarItemProps[] = [
|
||||
{
|
||||
name: 'Pilots',
|
||||
url: '#'
|
||||
}
|
||||
];
|
||||
const handleSignIn = async () => {
|
||||
await instance.loginRedirect({
|
||||
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`]
|
||||
});
|
||||
};
|
||||
const handleSignOut = () => {
|
||||
instance.logoutRedirect();
|
||||
};
|
||||
const getUserProfile = async (accessToken: string): Promise<User> => {
|
||||
try {
|
||||
const response: AxiosResponse = await httpClient.get(`api/userProfile`, {
|
||||
headers: {
|
||||
Authorization: accessToken
|
||||
}
|
||||
});
|
||||
const userProfile: User = response.data;
|
||||
|
||||
return userProfile;
|
||||
} catch (error) {
|
||||
throw new Error();
|
||||
}
|
||||
};
|
||||
const getUserPhoto = async (accessToken: string): Promise<string> => {
|
||||
try {
|
||||
const response: AxiosResponse = await httpClient.get(`api/userPhoto`, {
|
||||
headers: {
|
||||
Authorization: accessToken
|
||||
},
|
||||
responseType: 'arraybuffer'
|
||||
});
|
||||
const arrayBufferView = new Uint8Array(response.data);
|
||||
const blob = new Blob([arrayBufferView], { type: 'image/png' });
|
||||
const imageUrl = window.URL.createObjectURL(blob);
|
||||
|
||||
return imageUrl;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new Error();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const callback = instance.addEventCallback(
|
||||
async (message: EventMessage) => {
|
||||
if (message.eventType === EventType.LOGIN_SUCCESS) {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const eventPayload: EventPayloadExtended =
|
||||
message.payload as EventPayloadExtended;
|
||||
const userProfile: User = await getUserProfile(
|
||||
eventPayload.accessToken
|
||||
);
|
||||
const userPhoto = await getUserPhoto(eventPayload.accessToken);
|
||||
|
||||
appContext.dispatch({
|
||||
type: 'SET_USER_PROFILE',
|
||||
payload: userProfile
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
if (callback) {
|
||||
instance.removeEventCallback(callback);
|
||||
appContext.dispatch({ type: 'SET_USER_PROFILE', payload: {} });
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const setUserProfile = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const accessToken: string = await getAccessToken();
|
||||
const userProfile = await getUserProfile(accessToken);
|
||||
const userPhoto = await getUserPhoto(accessToken);
|
||||
|
||||
setUserPhoto(userPhoto);
|
||||
|
||||
appContext.dispatch({
|
||||
type: 'SET_USER_PROFILE',
|
||||
payload: userProfile
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
isAuthenticated &&
|
||||
Object.keys(appContext.state.userProfile).length === 0
|
||||
) {
|
||||
setUserProfile();
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
return (
|
||||
<Navbar className="mt-6">
|
||||
@@ -42,23 +157,47 @@ const SiteNav: React.FC<ISiteNavProps> = ({
|
||||
<NavbarLinks items={navItems} />
|
||||
<NavbarMenu>
|
||||
<div className="flex items-center gap-2 hidden lg:inline-block">
|
||||
{isAuthenticated && (
|
||||
{!loading && isAuthenticated && (
|
||||
<Menu placement="bottom-end">
|
||||
<MenuHandler>
|
||||
<div>
|
||||
<Button variant="text" size="sm">
|
||||
<>
|
||||
{/* {userPhoto && */}
|
||||
{/* <img className='rounded-full' src={userPhoto} /> */}
|
||||
{/* } */}
|
||||
{/* {!userPhoto && */}
|
||||
<div className="rounded-full text-white text-center pt-2 bg-black h-[40px] w-[40px]">
|
||||
NS
|
||||
</div>
|
||||
|
||||
{/* <div className="flex gap-2">
|
||||
<div className='flex-none'>
|
||||
|
||||
</div>
|
||||
<Button className='flex-1' variant="text" size="sm">
|
||||
{appContext.state.userProfile.displayName}
|
||||
</Button>
|
||||
</div>
|
||||
</div> */}
|
||||
</>
|
||||
</MenuHandler>
|
||||
<MenuList>
|
||||
{/* <MenuItem onClick={handleSignOut}> */}
|
||||
<MenuItem>
|
||||
<Typography variant="small">Sign Out</Typography>
|
||||
<MenuItem onClick={handleSignOut}>
|
||||
<Typography
|
||||
className="flex justify-center gap-3"
|
||||
variant="small"
|
||||
>
|
||||
<SignOutIcon size="lg" />
|
||||
Sign Out
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
{loading && isAuthenticated && (
|
||||
<div className="flex justify-center gap-3">
|
||||
<Spinner size="xs" />
|
||||
<Typography variant="small">Loading...</Typography>
|
||||
</div>
|
||||
)}
|
||||
{!isAuthenticated && (
|
||||
<Button
|
||||
variant="text"
|
||||
|
||||
33
package-lock.json
generated
33
package-lock.json
generated
@@ -76,7 +76,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@noahspan/noahspan-components": "^0.6.5",
|
||||
"@noahspan/noahspan-components": "^0.6.8",
|
||||
"axios": "^1.7.2",
|
||||
"framer-motion": "^11.1.7",
|
||||
"react": "^18.2.0",
|
||||
@@ -3397,9 +3397,9 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@noahspan/noahspan-components": {
|
||||
"version": "0.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@noahspan/noahspan-components/-/noahspan-components-0.6.5.tgz",
|
||||
"integrity": "sha512-rqwjSWY1mKzqoWBCDNB3WfvLb43DVet2J7a64414wLU/uCrTn4Q3/zpZxtKSE1Squ/geub25AUN0Ke302/4KvQ==",
|
||||
"version": "0.6.8",
|
||||
"resolved": "https://registry.npmjs.org/@noahspan/noahspan-components/-/noahspan-components-0.6.8.tgz",
|
||||
"integrity": "sha512-X4ftKtH9/ICyOCRKUnPMAh/u27Bi391edSKvIwWOxxawyIEZj58whjB0RfgcHV0O/Ap84lGt7YYctZTTzJHBEQ==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||
@@ -3455,6 +3455,17 @@
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
|
||||
"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -9961,6 +9972,17 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/otpauth": {
|
||||
"version": "9.3.2",
|
||||
"resolved": "https://registry.npmjs.org/otpauth/-/otpauth-9.3.2.tgz",
|
||||
"integrity": "sha512-KixtXWN9RGdS8WHPfDo7qsOYiivCbl+VeLBT+7HBTtJebBO6aXr/bpZXr+TwY2COecdY82VeBghm31mLYQVZlQ==",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/hectorm/otpauth?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
@@ -13153,7 +13175,8 @@
|
||||
"@cucumber/cucumber": "^10.7.0",
|
||||
"@playwright/test": "^1.44.0",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"fs-extra": "^11.2.0"
|
||||
"fs-extra": "^11.2.0",
|
||||
"otpauth": "^9.3.2"
|
||||
}
|
||||
},
|
||||
"tests/node_modules/fs-extra": {
|
||||
|
||||
@@ -6,9 +6,18 @@ Feature: Pilots
|
||||
When the user clicks the "Pilots" navbar link
|
||||
Then the user is on the "Pilots" page
|
||||
|
||||
Scenario: Cancel Add Pilot drawer
|
||||
Given the user is on the "Pilots" page
|
||||
When the user clicks the New button
|
||||
Then the pilot drawer is visible
|
||||
When the user clicks the pilot drawer cancel button
|
||||
Then the pilot drawer is no longer visible
|
||||
# Scenario: Cancel Pilot drawer
|
||||
# Given the user is on the "Pilots" page
|
||||
# When the user clicks the New button
|
||||
# Then the pilot drawer is visible
|
||||
# When the user clicks the pilot drawer cancel button
|
||||
# Then the pilot drawer is no longer visible
|
||||
|
||||
# Scenario: Add pilot
|
||||
# Given the user is on the "Pilots" page
|
||||
# When the user clicks the Add Pilot button
|
||||
# And the pilot drawer is visible
|
||||
# And the user enters the pilot's information
|
||||
# And the user clicks the Save button
|
||||
# Then the Add Pilot drawer is no longer visible
|
||||
# And the pilot is in the Pilots list
|
||||
@@ -3,24 +3,28 @@ import { config } from '../support/config';
|
||||
|
||||
export class PilotsPage {
|
||||
page: Page;
|
||||
newButton: Locator;
|
||||
pilotDrawer: Locator;
|
||||
pilotDrawerCancelButton: Locator;
|
||||
pilotAddButton: Locator;
|
||||
pilotSaveButton: Locator;
|
||||
pilotCancelButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.newButton = page.getByTestId('new-pilot-button');
|
||||
this.pilotDrawer = page.getByTestId('pilot-drawer');
|
||||
this.pilotDrawerCancelButton = page.getByTestId(
|
||||
'pilot-drawer-cancel-button'
|
||||
);
|
||||
this.pilotAddButton = page.getByTestId('add-pilot-button');
|
||||
this.pilotSaveButton = page.getByTestId('save-pilot-button');
|
||||
this.pilotCancelButton = page.getByTestId('pilot-form-cancel-button');
|
||||
}
|
||||
|
||||
public async clickNewButton() {
|
||||
await this.newButton.click();
|
||||
public async clickAddPilotButton() {
|
||||
await this.pilotAddButton.click();
|
||||
}
|
||||
|
||||
public async clickSaveButton() {
|
||||
await this.pilotSaveButton.click();
|
||||
}
|
||||
|
||||
public async clickPilotDrawerCancelButton() {
|
||||
await this.pilotDrawerCancelButton.click();
|
||||
await this.pilotCancelButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,27 @@ import { config } from '../support/config';
|
||||
import { expect } from '@playwright/test';
|
||||
import { PilotsPage } from '../pages/pilots.page';
|
||||
|
||||
When('the user clicks the New button', async function (this: ICustomWorld) {
|
||||
const pilotsPage = new PilotsPage(this.page!);
|
||||
When(
|
||||
'the user clicks the Add Pilot button',
|
||||
async function (this: ICustomWorld) {
|
||||
const pilotsPage = new PilotsPage(this.page!);
|
||||
|
||||
await pilotsPage.clickNewButton();
|
||||
});
|
||||
await pilotsPage.clickAddPilotButton();
|
||||
}
|
||||
);
|
||||
|
||||
When(
|
||||
`the user enters the pilot's information`,
|
||||
async function (this: ICustomWorld) {
|
||||
const pilotsPage = new PilotsPage(this.page!);
|
||||
}
|
||||
);
|
||||
|
||||
// When(`the user clicks the Save button`, async function (this: ICustomWorld) {
|
||||
// const pilotsPage = new PilotsPage(this.page!);
|
||||
|
||||
// await pil
|
||||
// })
|
||||
|
||||
When(
|
||||
'the user clicks the pilot drawer cancel button',
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"@cucumber/cucumber": "^10.7.0",
|
||||
"@playwright/test": "^1.44.0",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"fs-extra": "^11.2.0"
|
||||
"fs-extra": "^11.2.0",
|
||||
"otpauth": "^9.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user