From 12f635efb1650eb2dcfb2da37697b01f2eedcb41 Mon Sep 17 00:00:00 2001 From: Benjamin Beret Date: Tue, 4 Oct 2022 14:35:10 +0200 Subject: [PATCH 01/10] Prepare UI for XMAN list filtering --- .../xman/components/FlightList/FlightList.tsx | 15 ++- .../xman/components/FlightList/index.ts | 3 + .../FlightList/useFlightListFilter.ts | 40 +++++++ .../components/FlightList/useXmanFlights.ts | 11 +- .../src/modules/xman/components/Main.tsx | 109 +++++++++++++++--- .../modules/xman/components/Widget/Widget.tsx | 18 ++- 6 files changed, 168 insertions(+), 28 deletions(-) create mode 100644 packages/frontend/src/modules/xman/components/FlightList/useFlightListFilter.ts diff --git a/packages/frontend/src/modules/xman/components/FlightList/FlightList.tsx b/packages/frontend/src/modules/xman/components/FlightList/FlightList.tsx index c89064a14..482535025 100644 --- a/packages/frontend/src/modules/xman/components/FlightList/FlightList.tsx +++ b/packages/frontend/src/modules/xman/components/FlightList/FlightList.tsx @@ -39,21 +39,24 @@ import Loader from '../../../../shared/components/Loader'; import Center from '../../../../shared/components/Center'; import ErrorModal from '../../../../shared/components/ErrorModal'; import { useXmanActions } from './useXmanActions'; +import type { FlightListFilter } from './useFlightListFilter'; type OwnProps = { header?: boolean; loader?: boolean; - viewAll?: boolean; compact?: boolean; + flightListFilter: FlightListFilter; }; -const FlightList: React.FC = ({ +const FlightList: React.FC = function FlightList({ header = true, loader = true, - viewAll = false, compact = false, -}) => { - const { loading, error, data, refetch } = useXmanFlights({ viewAll }); + flightListFilter, +}) { + const { loading, error, data, refetch } = useXmanFlights({ + flightListFilter, + }); const [xmanActions] = useXmanActions(); const classes = useStyles(); @@ -107,7 +110,7 @@ const FlightList: React.FC = ({ flight.xmanStatus.needsAction && classes.needsAction, )} - key={flight.ifplId!} + key={flight.ifplId ?? `${flight.id}`} > diff --git a/packages/frontend/src/modules/xman/components/FlightList/index.ts b/packages/frontend/src/modules/xman/components/FlightList/index.ts index 453f0dd69..7be66b4e8 100644 --- a/packages/frontend/src/modules/xman/components/FlightList/index.ts +++ b/packages/frontend/src/modules/xman/components/FlightList/index.ts @@ -1,3 +1,6 @@ import FlightList from './FlightList'; +export { useFlightListFilter } from './useFlightListFilter'; +export type { FlightListFilter } from './useFlightListFilter'; + export default FlightList; diff --git a/packages/frontend/src/modules/xman/components/FlightList/useFlightListFilter.ts b/packages/frontend/src/modules/xman/components/FlightList/useFlightListFilter.ts new file mode 100644 index 000000000..e29dc29c3 --- /dev/null +++ b/packages/frontend/src/modules/xman/components/FlightList/useFlightListFilter.ts @@ -0,0 +1,40 @@ +export type FlightListFilter = { + interestArea: 'all' | 'center' | 'sector'; + onlyTodo: boolean; +}; + +import { atom, useRecoilState } from 'recoil'; +import { useMemo, useEffect } from 'react'; +import { tuple } from '../../../../shared/utils/types'; +import { + useViewer, + hasBoundSector, +} from '../../../../shared/containers/withViewer'; + +const xmanFlightListFilterAtom = atom({ + key: 'xman:flightListFilter', + default: { + interestArea: 'sector', + onlyTodo: false, + }, +}); + +export function useFlightListFilter() { + const [filterState, setFilter] = useRecoilState( + xmanFlightListFilterAtom, + ); + + const actions = useMemo(() => ({ setFilter }), [setFilter]); + const viewer = useViewer(); + const hasSector = hasBoundSector(viewer); + + useEffect(() => { + if (hasSector) { + setFilter((prev) => ({ ...prev, interestArea: 'sector' })); + } else { + setFilter((prev) => ({ ...prev, interestArea: 'center' })); + } + }, [setFilter, hasSector]); + + return tuple(filterState, actions); +} diff --git a/packages/frontend/src/modules/xman/components/FlightList/useXmanFlights.ts b/packages/frontend/src/modules/xman/components/FlightList/useXmanFlights.ts index 4e33b0a00..1ceaf7d14 100644 --- a/packages/frontend/src/modules/xman/components/FlightList/useXmanFlights.ts +++ b/packages/frontend/src/modules/xman/components/FlightList/useXmanFlights.ts @@ -19,6 +19,8 @@ import { FRAGMENTS as CopFragments } from './Cop'; import { FRAGMENTS as DelayFragments } from './Delay'; import { FRAGMENTS as ActionButtonsFragments } from './ActionButtons'; +import type { FlightListFilter } from './useFlightListFilter'; + const XMAN_FLIGHTS_QUERY = gql` query XmanFlights($interestSectors: [ElementarySectorInput!]) { flights( @@ -46,14 +48,17 @@ const XMAN_FLIGHTS_QUERY = gql` ${ActionButtonsFragments.flight} `; -export function useXmanFlights(opts: { viewAll: boolean }) { +export function useXmanFlights(opts: { flightListFilter: FlightListFilter }) { const viewer = useViewer(); + const { flightListFilter } = opts; const variables = useMemo(() => { return { interestSectors: - opts?.viewAll || !hasBoundSector(viewer) ? null : getSectors(viewer), + flightListFilter.interestArea !== 'sector' || !hasBoundSector(viewer) + ? null + : getSectors(viewer), }; - }, [viewer, opts?.viewAll]); + }, [viewer, flightListFilter.interestArea]); return useQuery( XMAN_FLIGHTS_QUERY, diff --git a/packages/frontend/src/modules/xman/components/Main.tsx b/packages/frontend/src/modules/xman/components/Main.tsx index 1e39f5f46..cbdafe259 100644 --- a/packages/frontend/src/modules/xman/components/Main.tsx +++ b/packages/frontend/src/modules/xman/components/Main.tsx @@ -7,9 +7,18 @@ import { Checkbox, makeStyles, Theme, + Select, + FormControl, + InputLabel, + MenuItem, + Chip, } from '@material-ui/core'; -import FlightList from './FlightList'; +import FlightList, { + useFlightListFilter, + FlightListFilter, +} from './FlightList'; + import * as DestinationStatus from './DestinationStatus'; import { isSupervisor, @@ -29,13 +38,14 @@ const useStyles = makeStyles((theme: Theme) => ({ })); const XmanRoot: React.FC<{}> = () => { - const [viewAll, setViewAll] = useState(false); + const [flightListFilter, flightListFilterActions] = useFlightListFilter(); + const viewer = useViewer(); const classes = useStyles(); const [runtime] = useRuntimeEnvironment(); - const showFilterControl = hasBoundSector(viewer); const showSupervisorControl = isSupervisor(viewer) || runtime.isDev; + const hasSector = hasBoundSector(viewer); return ( = () => { padding={1} justifyContent="space-between" > - {showFilterControl ? ( - setViewAll((prev) => !prev)} - /> - } - label="View all" - /> - ) : ( -
- )} + + + + flightListFilterActions.setFilter((prev) => ({ + ...prev, + interestArea: 'all', + })) + } + color={ + flightListFilter.interestArea === 'all' ? 'primary' : undefined + } + variant={ + flightListFilter.interestArea === 'all' ? 'default' : 'outlined' + } + className={classes.chips} + label="All" + /> + + flightListFilterActions.setFilter((prev) => ({ + ...prev, + interestArea: 'center', + })) + } + color={ + flightListFilter.interestArea === 'center' + ? 'primary' + : undefined + } + variant={ + flightListFilter.interestArea === 'center' + ? 'default' + : 'outlined' + } + className={classes.chips} + label="My center" + /> + + flightListFilterActions.setFilter((prev) => ({ + ...prev, + interestArea: 'sector', + })) + } + disabled={!hasSector} + color={ + flightListFilter.interestArea === 'sector' + ? 'primary' + : undefined + } + variant={ + flightListFilter.interestArea === 'sector' + ? 'default' + : 'outlined' + } + className={classes.chips} + label="My sector" + /> + + flightListFilterActions.setFilter((prev) => ({ + ...prev, + onlyTodo: !prev.onlyTodo, + })) + } + color={!flightListFilter.onlyTodo ? 'primary' : undefined} + variant={!flightListFilter.onlyTodo ? 'default' : 'outlined'} + className={classes.chips} + label="Flights without action" + /> + + = () => { flexGrow={1} overflow="auto" > - + ); diff --git a/packages/frontend/src/modules/xman/components/Widget/Widget.tsx b/packages/frontend/src/modules/xman/components/Widget/Widget.tsx index dfa63a0b6..74a942649 100644 --- a/packages/frontend/src/modules/xman/components/Widget/Widget.tsx +++ b/packages/frontend/src/modules/xman/components/Widget/Widget.tsx @@ -3,14 +3,30 @@ import React from 'react'; import Widget from '../../../../shared/components/Widget'; import FlightList from '../FlightList'; +import { + hasBoundSector, + useViewer, +} from '../../../../shared/containers/withViewer'; + type Props = { pathName: string; }; const XmanWidget: React.FC = ({ pathName }) => { + const viewer = useViewer(); + const hasSector = hasBoundSector(viewer); + return ( - + ); }; -- GitLab From 834b8aea685453c151d8b18f7ed9ae31fceed838 Mon Sep 17 00:00:00 2001 From: Benjamin Beret Date: Tue, 4 Oct 2022 15:50:47 +0200 Subject: [PATCH 02/10] Implement graphql server changes for xman list filtering --- .../src/queries/XmanFlights.ts | 28 ++ packages/benchmark-tools/src/queries/index.ts | 1 + packages/benchmark-tools/src/start.ts | 5 +- packages/graphql.server/schema.graphql | 8 + .../xman/XmanFlightConnector.test.ts | 285 +++++++++++++----- .../connectors/xman/XmanFlightConnector.ts | 148 ++++++--- .../src/schema/queries/flights.ts | 11 +- .../src/schema/types/common/Flight.gql | 8 + packages/graphql.server/src/types.ts | 6 + 9 files changed, 390 insertions(+), 110 deletions(-) create mode 100644 packages/benchmark-tools/src/queries/XmanFlights.ts diff --git a/packages/benchmark-tools/src/queries/XmanFlights.ts b/packages/benchmark-tools/src/queries/XmanFlights.ts new file mode 100644 index 000000000..cc9086bb8 --- /dev/null +++ b/packages/benchmark-tools/src/queries/XmanFlights.ts @@ -0,0 +1,28 @@ +import gql from 'graphql-tag'; + +export const xmanFlights = gql` + query XmanFlights { + flights( + limit: 2000 + filter: { + xman: { + hasXMAN: true + # interestFilter: { centers: ["LFEE", "LFRR"] } + # interestSectors: [ + # { center: "LFEE", name: "KD" } + # { center: "LFEE", name: "UF" } + # { center: "LFEE", name: "KF" } + # { center: "LFEE", name: "UH" } + # { center: "LFEE", name: "XH" } + # { center: "LFEE", name: "KH" } + # { center: "LFEE", name: "HH" } + # ] + } + } + ) { + __typename + id + callsign + } + } +`; diff --git a/packages/benchmark-tools/src/queries/index.ts b/packages/benchmark-tools/src/queries/index.ts index 363d17a55..4ae22b34c 100644 --- a/packages/benchmark-tools/src/queries/index.ts +++ b/packages/benchmark-tools/src/queries/index.ts @@ -1,5 +1,6 @@ export { makeQuery } from './makeQuery'; export { mapFlights } from './MapFlights'; +export { xmanFlights } from './XmanFlights'; export { componentStatuses } from './componentStatuses'; export { controlRoomStatus } from './controlRoomStatus'; diff --git a/packages/benchmark-tools/src/start.ts b/packages/benchmark-tools/src/start.ts index 123a0fe93..8000a9db6 100644 --- a/packages/benchmark-tools/src/start.ts +++ b/packages/benchmark-tools/src/start.ts @@ -2,17 +2,18 @@ import autocannon from 'autocannon'; import { makeQuery, mapFlights, + xmanFlights, componentStatuses, controlRoomStatus, } from './queries'; const instance = autocannon( { - title: 'ControlRoomStatus', + title: 'XmanFlights', url: 'http://localhost:3101/graphql', method: 'POST', duration: '15s', - body: makeQuery(mapFlights), + body: makeQuery(xmanFlights), headers: { 'content-type': 'application/json', }, diff --git a/packages/graphql.server/schema.graphql b/packages/graphql.server/schema.graphql index 5037a6189..a8e310219 100644 --- a/packages/graphql.server/schema.graphql +++ b/packages/graphql.server/schema.graphql @@ -788,10 +788,18 @@ input FlightFilter_XmanInput { """ hasXMAN: Boolean + """ + Only list flights in a provided interest sector. + + Will only filter the flight list if `hasXMAN` is set to true. + """ + interestFilter: InterestFilterInput + """ Only list flights in provided sectors interest area """ interestSectors: [ElementarySectorInput!] + @deprecated(reason: "Use `interestFilter` instead") } type FlightPlan { diff --git a/packages/graphql.server/src/connectors/xman/XmanFlightConnector.test.ts b/packages/graphql.server/src/connectors/xman/XmanFlightConnector.test.ts index 84f2b7c1d..6936946ba 100644 --- a/packages/graphql.server/src/connectors/xman/XmanFlightConnector.test.ts +++ b/packages/graphql.server/src/connectors/xman/XmanFlightConnector.test.ts @@ -137,90 +137,231 @@ describe('XmanFlightConnector', () => { expect(singleFlight).toBe(res[0]); }); - test('should allow filtering of flights', async () => { - const apiResponse: API['/flights']['/']['response'] = { - total: sampleFlights.length, - flights: sampleFlights, - }; + describe('filtering of flights', () => { + test('should allow filtering of flights via `interestSectors`', async () => { + const apiResponse: API['/flights']['/']['response'] = { + total: sampleFlights.length, + flights: sampleFlights, + }; - nock(TestServiceConfiguration.xman.baseUrl!) - .get('/flights') - .reply(200, apiResponse) - .persist(); - - const spy = jest.spyOn(c.flight.position.byCallsign, 'loadMany'); - const PILON = { long: 5.6919, lat: 48.002 }; - spy.mockResolvedValue([ - { - when: new Date(), - callsign: sampleFlights[0].callsign, - icaoAddr: 'ABCDFE', - ...PILON, - altitude: 36000, - }, - { - when: new Date(), - callsign: sampleFlights[1].callsign, - icaoAddr: 'ABCDFE', - ...PILON, - altitude: 31000, - }, - ]); + nock(TestServiceConfiguration.xman.baseUrl!) + .get('/flights') + .reply(200, apiResponse) + .persist(); + + const spy = jest.spyOn(c.flight.position.byCallsign, 'loadMany'); + const PILON = { long: 5.6919, lat: 48.002 }; + spy.mockResolvedValue([ + { + when: new Date(), + callsign: sampleFlights[0].callsign, + icaoAddr: 'ABCDFE', + ...PILON, + altitude: 36000, + }, + { + when: new Date(), + callsign: sampleFlights[1].callsign, + icaoAddr: 'ABCDFE', + ...PILON, + altitude: 31000, + }, + ]); - // No interest sectors - let res = await c.xman.flight.getFlights({ - filter: { interestSectors: [] }, - }); + // No interest sectors + let res = await c.xman.flight.getFlights({ + filter: { interestSectors: [] }, + }); - expect(res).toEqual([]); + expect(res).toEqual([]); - // Unknown 4ME environment - res = await c.xman.flight.getFlights({ - filter: { interestSectors: [{ center: 'ZZZZ', name: 'ABC' }] }, - }); + // Unknown 4ME environment + res = await c.xman.flight.getFlights({ + filter: { interestSectors: [{ center: 'ZZZZ', name: 'ABC' }] }, + }); - expect(res).toEqual([]); + expect(res).toEqual([]); - res = await c.xman.flight.getFlights({ - filter: { - interestSectors: [{ center: 'LFEE', name: 'KF' }], - }, - }); + res = await c.xman.flight.getFlights({ + filter: { + interestSectors: [{ center: 'LFEE', name: 'KF' }], + }, + }); - expect(res).toEqual( - expect.arrayContaining([ - expect.objectContaining({ callsign: sampleFlights[0].callsign }), - ]), - ); + expect(res).toEqual( + expect.arrayContaining([ + expect.objectContaining({ callsign: sampleFlights[0].callsign }), + ]), + ); - res = await c.xman.flight.getFlights({ - filter: { - interestSectors: [{ center: 'LFEE', name: 'UH' }], - }, - }); + res = await c.xman.flight.getFlights({ + filter: { + interestSectors: [{ center: 'LFEE', name: 'UH' }], + }, + }); - expect(res).toEqual( - expect.arrayContaining([ - expect.objectContaining({ callsign: sampleFlights[1].callsign }), - ]), - ); + expect(res).toEqual( + expect.arrayContaining([ + expect.objectContaining({ callsign: sampleFlights[1].callsign }), + ]), + ); - // With multiple sectors - res = await c.xman.flight.getFlights({ - filter: { - interestSectors: [ - { center: 'LFEE', name: 'UH' }, - { center: 'LFEE', name: 'KF' }, - ], - }, + // With multiple sectors + res = await c.xman.flight.getFlights({ + filter: { + interestSectors: [ + { center: 'LFEE', name: 'UH' }, + { center: 'LFEE', name: 'KF' }, + ], + }, + }); + + expect(res).toEqual( + expect.arrayContaining([ + expect.objectContaining({ callsign: sampleFlights[1].callsign }), + expect.objectContaining({ callsign: sampleFlights[0].callsign }), + ]), + ); }); - expect(res).toEqual( - expect.arrayContaining([ - expect.objectContaining({ callsign: sampleFlights[1].callsign }), - expect.objectContaining({ callsign: sampleFlights[0].callsign }), - ]), - ); + test('should allow filtering of flights via `interestFilter`', async () => { + const apiResponse: API['/flights']['/']['response'] = { + total: sampleFlights.length, + flights: sampleFlights, + }; + + nock(TestServiceConfiguration.xman.baseUrl!) + .get('/flights') + .reply(200, apiResponse) + .persist(); + + const spy = jest.spyOn(c.flight.position.byCallsign, 'loadMany'); + const PILON = { long: 5.6919, lat: 48.002 }; + spy.mockResolvedValue([ + { + when: new Date(), + callsign: sampleFlights[0].callsign, + icaoAddr: 'ABCDFE', + ...PILON, + altitude: 36000, + }, + { + when: new Date(), + callsign: sampleFlights[1].callsign, + icaoAddr: 'ABCDFE', + ...PILON, + altitude: 31000, + }, + ]); + + // No interest sectors + let res = await c.xman.flight.getFlights({ + filter: { interestFilter: {} }, + }); + + expect(res).toEqual([]); + + // Unknown 4ME environment + res = await c.xman.flight.getFlights({ + filter: { + interestFilter: { + elementarySectors: [{ center: 'ZZZZ', name: 'ABC' }], + }, + }, + }); + + expect(res).toEqual([]); + + res = await c.xman.flight.getFlights({ + filter: { + interestFilter: { + elementarySectors: [{ center: 'LFEE', name: 'KF' }], + }, + }, + }); + + expect(res).toEqual( + expect.arrayContaining([ + expect.objectContaining({ callsign: sampleFlights[0].callsign }), + ]), + ); + + res = await c.xman.flight.getFlights({ + filter: { + interestFilter: { + elementarySectors: [{ center: 'LFEE', name: 'UH' }], + }, + }, + }); + + expect(res).toEqual( + expect.arrayContaining([ + expect.objectContaining({ callsign: sampleFlights[1].callsign }), + ]), + ); + + // With multiple sectors + res = await c.xman.flight.getFlights({ + filter: { + interestFilter: { + elementarySectors: [ + { center: 'LFEE', name: 'UH' }, + { center: 'LFEE', name: 'KF' }, + ], + }, + }, + }); + + expect(res).toEqual( + expect.arrayContaining([ + expect.objectContaining({ callsign: sampleFlights[1].callsign }), + expect.objectContaining({ callsign: sampleFlights[0].callsign }), + ]), + ); + + // With a center + res = await c.xman.flight.getFlights({ + filter: { + interestFilter: { + centers: ['LFEE'], + }, + }, + }); + + expect(res).toEqual( + expect.arrayContaining([ + expect.objectContaining({ callsign: sampleFlights[1].callsign }), + expect.objectContaining({ callsign: sampleFlights[0].callsign }), + ]), + ); + + // With a non matching center + res = await c.xman.flight.getFlights({ + filter: { + interestFilter: { + centers: ['LFRR'], + }, + }, + }); + + expect(res).toEqual([]); + + // With a non matching center and matching sectors + res = await c.xman.flight.getFlights({ + filter: { + interestFilter: { + centers: ['LFRR'], + elementarySectors: [{ center: 'LFEE', name: 'UH' }], + }, + }, + }); + + expect(res).toEqual( + expect.arrayContaining([ + expect.objectContaining({ callsign: sampleFlights[1].callsign }), + ]), + ); + }); }); }); diff --git a/packages/graphql.server/src/connectors/xman/XmanFlightConnector.ts b/packages/graphql.server/src/connectors/xman/XmanFlightConnector.ts index 6e56a032c..199b79d96 100644 --- a/packages/graphql.server/src/connectors/xman/XmanFlightConnector.ts +++ b/packages/graphql.server/src/connectors/xman/XmanFlightConnector.ts @@ -9,7 +9,7 @@ import type { API } from './api_types'; import * as R from '@4me/backend-tools/dist/ramda'; import { toDate } from '../utils'; import DataLoader from 'dataloader'; -import type { IXmanActionInput } from '../../types'; +import type { IXmanActionInput, IInterestFilterInput } from '../../types'; import { XmanAction as XmanActionValidator } from './schema'; import { isInAirspace } from '@4me/dataset-loader'; @@ -52,70 +52,148 @@ export class XmanFlightConnector extends BaseDataConnector { }: { filter?: { interestSectors?: null | Array<{ center: string; name: string }>; + interestFilter?: null | IInterestFilterInput; }; } = {}): Promise> => { const flights = await this.fetchFlights(); - flights.forEach((f) => { + for (const f of flights) { this.byCallsign.clear(f.callsign).prime(f.callsign, f); - }); + } /** * If we have no filter, return early */ - if (!filter || !filter.interestSectors) { + if (!filter || !(filter.interestFilter || filter.interestSectors)) { return flights; } - const { interestSectors } = filter; + /** + * First, we expand our interest filters to a list of elementary sector ids + */ + + const sectorOfInterestIds = new Set(); + if (filter.interestFilter) { + /** + * Loop over all centers, add all elementary sectors + */ + for (const c of filter.interestFilter.centers ?? []) { + const elementarySectors = this.dataset.sectors.elementary.byCenter(c); + for (const es of elementarySectors) { + sectorOfInterestIds.add(es.id); + } + } + + /** + * Loop over all elementary sectors, add them to our sectorOfInterestIds set + */ + + for (const centerName of filter.interestFilter.elementarySectors ?? []) { + const es = this.dataset.sectors.elementary.byCenterName(centerName); + if (es) { + sectorOfInterestIds.add(es.id); + } + } + } else if (filter.interestSectors) { + for (const centerName of filter.interestSectors) { + const es = this.dataset.sectors.elementary.byCenterName(centerName); + if (es) { + sectorOfInterestIds.add(es.id); + } + } + } + + if (sectorOfInterestIds.size === 0) { + return []; + } + + /** + * Now we build a two dimension cache : + * XmanDestination -> elementarySectorId -> InterestArea + * + * For instance, EGLL -> LFEEUF -> {} + */ + + type InterestArea = NonNullable< + ReturnType + >['interestAreas'][number]['geometry']; + const interestAreaCache = new Map>(); + for (const xmanDestination of this.dataset.xman.all()) { + let destinationCache = interestAreaCache.get(xmanDestination.id); + if (!destinationCache) { + destinationCache = new Map(); + interestAreaCache.set(xmanDestination.id, destinationCache); + } + + for (const area of xmanDestination.interestAreas) { + destinationCache.set(area.elementarySectorId, area.geometry); + } + } + + /** + * Now we build a flight position map + */ const callsigns = flights.map(({ callsign }) => callsign); const positions = await this.parent.flight.position.byCallsign.loadMany( callsigns, ); + const positionCache = new Map(); + + for (const p of positions) { + if (!p || !p.callsign) { + continue; + } + + positionCache.set(p.callsign, p); + } /** - * This is a filter builder. - * - * Each filter is a function which takes an XmanFlight and returns - * true or false if the flight matches the filter. - * + * This function will take an XmanFlight, and return true or false depending on if the + * flight is currently in the previously built interest area */ - const singleSectorFilter = - (elementarySector: { center: string; name: string }) => - (xmanFlight: XmanFlight): boolean => { - const { callsign, destination } = xmanFlight; + function isFlightInInterestArea(f: XmanFlight): boolean { + const position = positionCache.get(f.callsign); + + if (!position) { + return false; + } - const position = positions.find((f) => f?.callsign === callsign); + const destination = f.destination; + const interestAreaByEsId = interestAreaCache.get(destination); + if (!interestAreaByEsId) { + return false; + } - const xmanAirport = this.dataset.xman.byId(destination); - const es = - this.dataset.sectors.elementary.byCenterName(elementarySector); + for (const esId of sectorOfInterestIds) { + const geometry = interestAreaByEsId.get(esId); - if (!position || !xmanAirport || !es) { - return false; + /** + * No geometry for this (destination, esId) tuple + */ + if (!geometry) { + continue; } - const interestArea = xmanAirport.interestAreas.find( - ({ elementarySectorId }) => elementarySectorId === es.id, - ); + const { lat, long, altitude } = position; - if (!interestArea) { - return false; + if ( + !isInAirspace(geometry, { + latitude: lat, + longitude: long, + altitude, + }) + ) { + continue; } - const { lat, long, altitude } = position; + return true; + } - return isInAirspace(interestArea.geometry, { - latitude: lat, - longitude: long, - altitude, - }); - }; + return false; + } - return flights.filter((f) => - R.anyPass(interestSectors.map(singleSectorFilter))(f), - ); + return flights.filter(isFlightInInterestArea); }; /** diff --git a/packages/graphql.server/src/schema/queries/flights.ts b/packages/graphql.server/src/schema/queries/flights.ts index 0ceb5b01d..2f5286828 100644 --- a/packages/graphql.server/src/schema/queries/flights.ts +++ b/packages/graphql.server/src/schema/queries/flights.ts @@ -41,10 +41,19 @@ const flights: IQueryResolvers['flights'] = async ( ); } + if ( + inputFilter?.xman?.hasXMAN === false && + inputFilter?.xman?.interestFilter + ) { + throw new Error( + `Invalid filter: filter.xman.hasXMAN is assumed to be true when filter.xman.interestFilter is defined.`, + ); + } + /** * Set xman.hasXMAN flag to true */ - if (inputFilter?.xman?.interestSectors) { + if (inputFilter?.xman?.interestSectors || inputFilter?.xman?.interestFilter) { inputFilter.xman.hasXMAN = true; } diff --git a/packages/graphql.server/src/schema/types/common/Flight.gql b/packages/graphql.server/src/schema/types/common/Flight.gql index 606edee89..76ed79984 100644 --- a/packages/graphql.server/src/schema/types/common/Flight.gql +++ b/packages/graphql.server/src/schema/types/common/Flight.gql @@ -139,6 +139,14 @@ input FlightFilter_XmanInput { Only list flights in provided sectors interest area """ interestSectors: [ElementarySectorInput!] + @deprecated(reason: "Use `interestFilter` instead") + + """ + Only list flights in a provided interest sector. + + Will only filter the flight list if `hasXMAN` is set to true. + """ + interestFilter: InterestFilterInput } """ diff --git a/packages/graphql.server/src/types.ts b/packages/graphql.server/src/types.ts index 84caa4157..e2b11ea89 100644 --- a/packages/graphql.server/src/types.ts +++ b/packages/graphql.server/src/types.ts @@ -713,6 +713,12 @@ export type IFlightFilter_XmanInput = { * If false, will only return flights without an XMAN status */ hasXMAN?: InputMaybe; + /** + * Only list flights in a provided interest sector. + * + * Will only filter the flight list if `hasXMAN` is set to true. + */ + interestFilter?: InputMaybe; /** Only list flights in provided sectors interest area */ interestSectors?: InputMaybe>; }; -- GitLab From 097e4d1c50680862c5de154894261eab14b3b875 Mon Sep 17 00:00:00 2001 From: Benjamin Beret Date: Tue, 4 Oct 2022 15:54:48 +0200 Subject: [PATCH 03/10] Generated typescript for frontend --- packages/frontend/src/__generated__/globalTypes.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/frontend/src/__generated__/globalTypes.ts b/packages/frontend/src/__generated__/globalTypes.ts index 787ada152..7a6ed5a6b 100644 --- a/packages/frontend/src/__generated__/globalTypes.ts +++ b/packages/frontend/src/__generated__/globalTypes.ts @@ -638,6 +638,12 @@ export type IFlightFilter_XmanInput = { * If false, will only return flights without an XMAN status */ hasXMAN?: InputMaybe; + /** + * Only list flights in a provided interest sector. + * + * Will only filter the flight list if `hasXMAN` is set to true. + */ + interestFilter?: InputMaybe; /** Only list flights in provided sectors interest area */ interestSectors?: InputMaybe>; }; -- GitLab From cd18b6285d0f744d35a53a18dae88e30829676e0 Mon Sep 17 00:00:00 2001 From: Benjamin Beret Date: Tue, 4 Oct 2022 18:13:00 +0200 Subject: [PATCH 04/10] Implement XMAN list filtering in frontend --- CHANGELOG.md | 1 + .../frontend/src/__generated__/globalTypes.ts | 2 + .../__generated__/useXmanFlights.ts | 3 +- .../FlightList/useFlightListFilter.ts | 15 +++--- .../components/FlightList/useXmanFlights.ts | 46 +++++++++++++++---- .../src/shared/containers/withViewer.tsx | 32 ++++++++++++- packages/graphql.server/schema.graphql | 5 ++ .../connectors/xman/XmanFlightConnector.ts | 40 +++++++++++++--- .../src/connectors/xman/utils.ts | 39 ++++++++-------- .../src/schema/types/common/Flight.gql | 5 ++ packages/graphql.server/src/types.ts | 2 + 11 files changed, 147 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 466c48514..cc255112a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## master +- XMAN flight list filtering (#823) - LFBB dataset (#852) ## v2.10.1 (26-09-2022) diff --git a/packages/frontend/src/__generated__/globalTypes.ts b/packages/frontend/src/__generated__/globalTypes.ts index 7a6ed5a6b..998e7704d 100644 --- a/packages/frontend/src/__generated__/globalTypes.ts +++ b/packages/frontend/src/__generated__/globalTypes.ts @@ -646,6 +646,8 @@ export type IFlightFilter_XmanInput = { interestFilter?: InputMaybe; /** Only list flights in provided sectors interest area */ interestSectors?: InputMaybe>; + /** Only list flights with a speed reduction asked (either applied or required) */ + withSpeedReduction?: InputMaybe; }; export type IFlightPlan = { diff --git a/packages/frontend/src/modules/xman/components/FlightList/__generated__/useXmanFlights.ts b/packages/frontend/src/modules/xman/components/FlightList/__generated__/useXmanFlights.ts index 2d4bf517b..ce8c63944 100644 --- a/packages/frontend/src/modules/xman/components/FlightList/__generated__/useXmanFlights.ts +++ b/packages/frontend/src/modules/xman/components/FlightList/__generated__/useXmanFlights.ts @@ -1,7 +1,8 @@ import type * as Types from '../../../../../__generated__/globalTypes'; export type IXmanFlightsQueryVariables = Types.Exact<{ - interestSectors?: Types.InputMaybe>; + interestFilter?: Types.InputMaybe; + withSpeedReduction?: Types.InputMaybe; }>; export type IXmanFlightsQuery = { diff --git a/packages/frontend/src/modules/xman/components/FlightList/useFlightListFilter.ts b/packages/frontend/src/modules/xman/components/FlightList/useFlightListFilter.ts index e29dc29c3..66d24614e 100644 --- a/packages/frontend/src/modules/xman/components/FlightList/useFlightListFilter.ts +++ b/packages/frontend/src/modules/xman/components/FlightList/useFlightListFilter.ts @@ -28,13 +28,14 @@ export function useFlightListFilter() { const viewer = useViewer(); const hasSector = hasBoundSector(viewer); - useEffect(() => { - if (hasSector) { - setFilter((prev) => ({ ...prev, interestArea: 'sector' })); - } else { - setFilter((prev) => ({ ...prev, interestArea: 'center' })); + const r = useMemo(() => { + const r = { ...filterState }; + if (!hasSector && r.interestArea === 'sector') { + r.interestArea = 'center'; } - }, [setFilter, hasSector]); - return tuple(filterState, actions); + return r; + }, [filterState, hasSector]); + + return tuple(r, actions); } diff --git a/packages/frontend/src/modules/xman/components/FlightList/useXmanFlights.ts b/packages/frontend/src/modules/xman/components/FlightList/useXmanFlights.ts index 1ceaf7d14..973a36f35 100644 --- a/packages/frontend/src/modules/xman/components/FlightList/useXmanFlights.ts +++ b/packages/frontend/src/modules/xman/components/FlightList/useXmanFlights.ts @@ -3,8 +3,7 @@ import { useQuery } from '@apollo/react-hooks'; import gql from 'graphql-tag'; import { useViewer, - getSectors, - hasBoundSector, + useOwnElementarySectors, } from '../../../../shared/containers/withViewer'; import type { @@ -22,10 +21,19 @@ import { FRAGMENTS as ActionButtonsFragments } from './ActionButtons'; import type { FlightListFilter } from './useFlightListFilter'; const XMAN_FLIGHTS_QUERY = gql` - query XmanFlights($interestSectors: [ElementarySectorInput!]) { + query XmanFlights( + $interestFilter: InterestFilterInput + $withSpeedReduction: Boolean + ) { flights( limit: 100 - filter: { xman: { hasXMAN: true, interestSectors: $interestSectors } } + filter: { + xman: { + hasXMAN: true + interestFilter: $interestFilter + withSpeedReduction: $withSpeedReduction + } + } ) { id ifplId @@ -51,14 +59,34 @@ const XMAN_FLIGHTS_QUERY = gql` export function useXmanFlights(opts: { flightListFilter: FlightListFilter }) { const viewer = useViewer(); const { flightListFilter } = opts; + const myCenter = viewer.centerId; + const mySectors = useOwnElementarySectors(); + + const interestSectors = useMemo(() => { + if (flightListFilter.interestArea !== 'sector') { + return null; + } + + return mySectors; + }, [mySectors, flightListFilter.interestArea]); + + const interestCenters = useMemo(() => { + if (flightListFilter.interestArea !== 'center' || !myCenter) { + return null; + } + + return [myCenter]; + }, [myCenter, flightListFilter.interestArea]); + const variables = useMemo(() => { return { - interestSectors: - flightListFilter.interestArea !== 'sector' || !hasBoundSector(viewer) - ? null - : getSectors(viewer), + withSpeedReduction: flightListFilter.onlyTodo === true ? true : null, + interestFilter: { + elementarySectors: interestSectors, + centers: interestCenters, + }, }; - }, [viewer, flightListFilter.interestArea]); + }, [interestSectors, interestCenters, flightListFilter.onlyTodo]); return useQuery( XMAN_FLIGHTS_QUERY, diff --git a/packages/frontend/src/shared/containers/withViewer.tsx b/packages/frontend/src/shared/containers/withViewer.tsx index 902ee74e3..aaa5dde63 100644 --- a/packages/frontend/src/shared/containers/withViewer.tsx +++ b/packages/frontend/src/shared/containers/withViewer.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { Viewer, @@ -33,6 +33,36 @@ export function useViewer(): Viewer { return ctx; } +/** + * Returns own elementary sectors as an array of { center, name } objects. + * + * Memoized array. + */ +export function useOwnElementarySectors(): null | Array<{ + center: string; + name: string; +}> { + const viewer = useViewer(); + + const rawSectors = useMemo(() => { + if (!hasBoundSector(viewer)) { + return null; + } + + return viewer.controllerWorkingPosition.boundSector.elementarySectors; + }, [viewer]); + + const r = useMemo(() => { + if (!rawSectors) { + return null; + } + + return rawSectors.map(({ name, center }) => ({ name, center })); + }, [rawSectors]); + + return r; +} + // Convenience functions export function isCWP(viewer: Viewer | null): viewer is CwpClient { if (!viewer) { diff --git a/packages/graphql.server/schema.graphql b/packages/graphql.server/schema.graphql index a8e310219..11807e735 100644 --- a/packages/graphql.server/schema.graphql +++ b/packages/graphql.server/schema.graphql @@ -800,6 +800,11 @@ input FlightFilter_XmanInput { """ interestSectors: [ElementarySectorInput!] @deprecated(reason: "Use `interestFilter` instead") + + """ + Only list flights with a speed reduction asked (either applied or required) + """ + withSpeedReduction: Boolean } type FlightPlan { diff --git a/packages/graphql.server/src/connectors/xman/XmanFlightConnector.ts b/packages/graphql.server/src/connectors/xman/XmanFlightConnector.ts index 199b79d96..76f131fba 100644 --- a/packages/graphql.server/src/connectors/xman/XmanFlightConnector.ts +++ b/packages/graphql.server/src/connectors/xman/XmanFlightConnector.ts @@ -11,6 +11,7 @@ import { toDate } from '../utils'; import DataLoader from 'dataloader'; import type { IXmanActionInput, IInterestFilterInput } from '../../types'; import { XmanAction as XmanActionValidator } from './schema'; +import { hasSpeedReduction } from './utils'; import { isInAirspace } from '@4me/dataset-loader'; @@ -53,6 +54,7 @@ export class XmanFlightConnector extends BaseDataConnector { filter?: { interestSectors?: null | Array<{ center: string; name: string }>; interestFilter?: null | IInterestFilterInput; + withSpeedReduction?: boolean; }; } = {}): Promise> => { const flights = await this.fetchFlights(); @@ -64,15 +66,20 @@ export class XmanFlightConnector extends BaseDataConnector { /** * If we have no filter, return early */ - if (!filter || !(filter.interestFilter || filter.interestSectors)) { + if (!filter) { return flights; } + const shouldFilterByInterestArea = + !!filter?.interestSectors || + !!filter?.interestFilter?.centers || + !!filter?.interestFilter?.elementarySectors; + /** * First, we expand our interest filters to a list of elementary sector ids */ - const sectorOfInterestIds = new Set(); + if (filter.interestFilter) { /** * Loop over all centers, add all elementary sectors @@ -103,10 +110,9 @@ export class XmanFlightConnector extends BaseDataConnector { } } - if (sectorOfInterestIds.size === 0) { - return []; - } - + /** + * TODO: Extract this stuff in a separate function, only call it when required. + */ /** * Now we build a two dimension cache : * XmanDestination -> elementarySectorId -> InterestArea @@ -193,7 +199,27 @@ export class XmanFlightConnector extends BaseDataConnector { return false; } - return flights.filter(isFlightInInterestArea); + function filterFlight(f: XmanFlight): boolean { + /** + * Perform some quick checks before geographical checks + */ + if ( + filter?.withSpeedReduction === true || + filter?.withSpeedReduction === false + ) { + if (hasSpeedReduction(f) !== filter.withSpeedReduction) { + return false; + } + } + + if (shouldFilterByInterestArea && !isFlightInInterestArea(f)) { + return false; + } + + return true; + } + + return flights.filter(filterFlight); }; /** diff --git a/packages/graphql.server/src/connectors/xman/utils.ts b/packages/graphql.server/src/connectors/xman/utils.ts index 3b318ac91..07a90e4d7 100644 --- a/packages/graphql.server/src/connectors/xman/utils.ts +++ b/packages/graphql.server/src/connectors/xman/utils.ts @@ -11,16 +11,33 @@ export function needsAction( ): boolean { const { advisory, action } = xmanFlight; + const speedReduction = hasSpeedReduction(xmanFlight); + + if (!speedReduction) { + return false; + } + + if (advisory?.type === 'minCleanSpeed' && action?.type !== 'minCleanSpeed') { + return true; + } + + return !action; +} + +/** + * Checks if a flight has a speed reduction required + */ +export function hasSpeedReduction( + xmanFlight: Pick, +): boolean { + const { advisory } = xmanFlight; + if (!advisory) { return false; } switch (advisory.type) { case 'machReduction': { - if (action) { - return false; - } - // Mach reduction is null or === 0 if (!advisory.machReduction) { return false; @@ -29,10 +46,6 @@ export function needsAction( return true; } case 'speed': { - if (action) { - return false; - } - if (!advisory.speed) { return false; } @@ -40,10 +53,6 @@ export function needsAction( return true; } case 'mixed': { - if (action) { - return false; - } - if (!advisory.speed && !advisory.machReduction) { return false; } @@ -51,13 +60,7 @@ export function needsAction( return true; } case 'minCleanSpeed': { - if (action && action.type !== 'minCleanSpeed') { - return true; - } - return true; } } - - return false; } diff --git a/packages/graphql.server/src/schema/types/common/Flight.gql b/packages/graphql.server/src/schema/types/common/Flight.gql index 76ed79984..082e8ecbd 100644 --- a/packages/graphql.server/src/schema/types/common/Flight.gql +++ b/packages/graphql.server/src/schema/types/common/Flight.gql @@ -147,6 +147,11 @@ input FlightFilter_XmanInput { Will only filter the flight list if `hasXMAN` is set to true. """ interestFilter: InterestFilterInput + + """ + Only list flights with a speed reduction asked (either applied or required) + """ + withSpeedReduction: Boolean } """ diff --git a/packages/graphql.server/src/types.ts b/packages/graphql.server/src/types.ts index e2b11ea89..dde2ae672 100644 --- a/packages/graphql.server/src/types.ts +++ b/packages/graphql.server/src/types.ts @@ -721,6 +721,8 @@ export type IFlightFilter_XmanInput = { interestFilter?: InputMaybe; /** Only list flights in provided sectors interest area */ interestSectors?: InputMaybe>; + /** Only list flights with a speed reduction asked (either applied or required) */ + withSpeedReduction?: InputMaybe; }; export type IFlightPlan = { -- GitLab From 49190fb618dd3f432ac67445caa232ca5a1f1783 Mon Sep 17 00:00:00 2001 From: Benjamin Beret Date: Wed, 5 Oct 2022 08:48:56 +0200 Subject: [PATCH 05/10] Fix typescript error, fix typo in tests --- .../src/connectors/xman/XmanFlightConnector.test.ts | 7 ++++++- .../src/connectors/xman/XmanFlightConnector.ts | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/graphql.server/src/connectors/xman/XmanFlightConnector.test.ts b/packages/graphql.server/src/connectors/xman/XmanFlightConnector.test.ts index 6936946ba..44d9b0500 100644 --- a/packages/graphql.server/src/connectors/xman/XmanFlightConnector.test.ts +++ b/packages/graphql.server/src/connectors/xman/XmanFlightConnector.test.ts @@ -259,7 +259,12 @@ describe('XmanFlightConnector', () => { filter: { interestFilter: {} }, }); - expect(res).toEqual([]); + expect(res).toEqual( + expect.arrayContaining([ + expect.objectContaining({ callsign: sampleFlights[1].callsign }), + expect.objectContaining({ callsign: sampleFlights[0].callsign }), + ]), + ); // Unknown 4ME environment res = await c.xman.flight.getFlights({ diff --git a/packages/graphql.server/src/connectors/xman/XmanFlightConnector.ts b/packages/graphql.server/src/connectors/xman/XmanFlightConnector.ts index 76f131fba..a59e60971 100644 --- a/packages/graphql.server/src/connectors/xman/XmanFlightConnector.ts +++ b/packages/graphql.server/src/connectors/xman/XmanFlightConnector.ts @@ -54,7 +54,7 @@ export class XmanFlightConnector extends BaseDataConnector { filter?: { interestSectors?: null | Array<{ center: string; name: string }>; interestFilter?: null | IInterestFilterInput; - withSpeedReduction?: boolean; + withSpeedReduction?: null | boolean; }; } = {}): Promise> => { const flights = await this.fetchFlights(); -- GitLab From 782fe76b2881a6cef65ebfd7358ee4780a3877f7 Mon Sep 17 00:00:00 2001 From: Benjamin Beret Date: Wed, 5 Oct 2022 09:31:59 +0200 Subject: [PATCH 06/10] Switch XMAN list filter state from global state to local state --- .../FlightList/useFlightListFilter.ts | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/frontend/src/modules/xman/components/FlightList/useFlightListFilter.ts b/packages/frontend/src/modules/xman/components/FlightList/useFlightListFilter.ts index 66d24614e..9f7d62350 100644 --- a/packages/frontend/src/modules/xman/components/FlightList/useFlightListFilter.ts +++ b/packages/frontend/src/modules/xman/components/FlightList/useFlightListFilter.ts @@ -3,30 +3,33 @@ export type FlightListFilter = { onlyTodo: boolean; }; -import { atom, useRecoilState } from 'recoil'; -import { useMemo, useEffect } from 'react'; +import { useMemo, useState } from 'react'; import { tuple } from '../../../../shared/utils/types'; import { useViewer, hasBoundSector, } from '../../../../shared/containers/withViewer'; -const xmanFlightListFilterAtom = atom({ - key: 'xman:flightListFilter', - default: { - interestArea: 'sector', +const initialStates = { + CWP_WITH_SECTOR: { + interestArea: 'sector' as const, + onlyTodo: true, + }, + OTHER: { + interestArea: 'center' as const, onlyTodo: false, }, -}); +}; export function useFlightListFilter() { - const [filterState, setFilter] = useRecoilState( - xmanFlightListFilterAtom, + const viewer = useViewer(); + const hasSector = hasBoundSector(viewer); + + const [filterState, setFilter] = useState( + hasSector ? initialStates.CWP_WITH_SECTOR : initialStates.OTHER, ); const actions = useMemo(() => ({ setFilter }), [setFilter]); - const viewer = useViewer(); - const hasSector = hasBoundSector(viewer); const r = useMemo(() => { const r = { ...filterState }; -- GitLab From 3593097795d177e171140e013a8ae6271cc6f25e Mon Sep 17 00:00:00 2001 From: Benjamin Beret Date: Wed, 5 Oct 2022 09:32:23 +0200 Subject: [PATCH 07/10] Debug info in CI --- .../graphql.server/src/tests/PostgresTestEnvironment.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/graphql.server/src/tests/PostgresTestEnvironment.ts b/packages/graphql.server/src/tests/PostgresTestEnvironment.ts index 1dbe2fcdb..874373c1c 100644 --- a/packages/graphql.server/src/tests/PostgresTestEnvironment.ts +++ b/packages/graphql.server/src/tests/PostgresTestEnvironment.ts @@ -55,13 +55,17 @@ class PostgreSQLEnvironment extends NodeEnvironment { if (redisConfig.url) { this.redis = new Redis(redisConfig.url); await new Promise((resolve, reject) => { - this.redis?.on('error', (err) => reject(err)); + this.redis?.on('error', (err) => { + err.redisUrl = redisConfig.url; + reject(err); + }); this.redis?.on('ready', () => resolve()); }); } } catch (err) { throw new Error( "Unable to connect to Redis. Ensure it's up with `docker-compose up -d redis` command" + + `\nRedis URL: ${err.redisUrl}\n` + `\nOriginal error message:\n\t${err.message}`, ); } -- GitLab From c378b022cfa9215c644c316115276b386a6a414d Mon Sep 17 00:00:00 2001 From: Benjamin Beret Date: Wed, 5 Oct 2022 09:50:40 +0200 Subject: [PATCH 08/10] Use 12 jest workers maximum in CI, to prevent redis breakage --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ff7b9631f..5c81aaf45 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build": "lerna run build", "flow:libdef": "lerna run flow:libdef", "test": "jest", - "test:ci": "jest --colors --coverage --reporters=default --reporters=jest-junit", + "test:ci": "jest --colors --coverage --reporters=default --reporters=jest-junit --maxWorkers=16", "typecheck:ci": "lerna run typecheck", "typecheck": "lerna run typecheck", "doc": "node scripts/docusaurusApiGenerator.js", -- GitLab From 1ee123a56b0bfc1eaa7b7b2a880af77e3b5487ec Mon Sep 17 00:00:00 2001 From: Benjamin Beret Date: Wed, 5 Oct 2022 10:11:17 +0200 Subject: [PATCH 09/10] Reduce workers to 8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5c81aaf45..fc00b631d 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build": "lerna run build", "flow:libdef": "lerna run flow:libdef", "test": "jest", - "test:ci": "jest --colors --coverage --reporters=default --reporters=jest-junit --maxWorkers=16", + "test:ci": "jest --colors --coverage --reporters=default --reporters=jest-junit --maxWorkers=8", "typecheck:ci": "lerna run typecheck", "typecheck": "lerna run typecheck", "doc": "node scripts/docusaurusApiGenerator.js", -- GitLab From 3df72a4371e117a81ea37b13614684f918605c75 Mon Sep 17 00:00:00 2001 From: Benjamin Beret Date: Wed, 5 Oct 2022 10:38:22 +0200 Subject: [PATCH 10/10] Tweak RedisConfig class in graphql to properly handle redis connection URLs with strings --- CHANGELOG.md | 3 ++- .../src/redis/RedisConfig.test.ts | 17 +++++++++++++ .../graphql.server/src/redis/RedisConfig.ts | 24 ++++++++++++------- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc255112a..9eba935d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # CHANGELOG -## master +## Master +- Fix REDIS_URL password parsing (#854) - XMAN flight list filtering (#823) - LFBB dataset (#852) diff --git a/packages/graphql.server/src/redis/RedisConfig.test.ts b/packages/graphql.server/src/redis/RedisConfig.test.ts index 91ab156fc..75740a9e1 100644 --- a/packages/graphql.server/src/redis/RedisConfig.test.ts +++ b/packages/graphql.server/src/redis/RedisConfig.test.ts @@ -54,6 +54,23 @@ describe('without process.env.REDIS_URL', () => { expect(config.url).toEqual(expect.stringMatching(/redis:\/\/redis/)); }); + test('should handle passwords', () => { + const log: any = { + info: jest.fn(), + error: jest.fn(), + fatal: jest.fn(), + }; + + const config = new RedisConfig({ + log, + url: 'redis://redis:mypassword@redis', + }); + + expect(config.url).toEqual(expect.stringContaining('mypassword')); + expect(config.printableUrl).toEqual(expect.stringContaining('********')); + expect(log.info).toHaveBeenCalledWith(expect.stringContaining('********')); + }); + test('should handle correct pathname', () => { const log: any = { info: jest.fn(), diff --git a/packages/graphql.server/src/redis/RedisConfig.ts b/packages/graphql.server/src/redis/RedisConfig.ts index a62be2744..62f62457d 100644 --- a/packages/graphql.server/src/redis/RedisConfig.ts +++ b/packages/graphql.server/src/redis/RedisConfig.ts @@ -5,12 +5,10 @@ import { logger } from '../logger'; import { URL } from 'url'; -export const config = { - url: null as string | null, -}; - export class RedisConfig { private _url: null | string = null; + private _printableUrl: null | string = null; + private log: typeof logger; constructor({ @@ -32,10 +30,6 @@ export class RedisConfig { try { const parsedUrl = new URL(url); - if (parsedUrl.password) { - parsedUrl.password = '********'; - } - if (parsedUrl.protocol !== 'redis:') { throw new Error( `Invalid URL protocol ${parsedUrl.protocol}. REDIS_URL should start with 'redis://'`, @@ -57,9 +51,14 @@ export class RedisConfig { } } - this.log.info(`Using redis URL ${parsedUrl.toString()}`); + const printableUrl = new URL(parsedUrl.toString()); + if (printableUrl.password) { + printableUrl.password = '********'; + } + this.log.info(`Using redis URL ${printableUrl.toString()}`); this._url = parsedUrl.toString(); + this._printableUrl = printableUrl.toString(); } catch (err) { this.log.error(`Invalid redis URL in REDIS_URL environment variable.`); this.log.error(err.message); @@ -73,4 +72,11 @@ export class RedisConfig { get url(): string | null { return this._url; } + + /** + * + */ + get printableUrl(): string | null { + return this._printableUrl; + } } -- GitLab