import React, { useEffect, useState, useRef } from 'react' import { API, graphqlOperation, Auth } from 'aws-amplify' import { Heading, Grid, useTheme, Button, SliderField } from '@aws-amplify/ui-react'; import { useNavigate } from "react-router-dom"; import * as customStatements from '../graphql/custom-statements' import { CORE_OVERVIEW_ID, RACE_STATES, RACE_OPERATIONS, RACE_CONFIRMATION_MESSAGES, RACE_STATE_ICONS, CARS } from '../utils/constants' import { MQTT_TOPICS } from '../utils/constants'; import { PahoMqttClient, IotCoreMqttClient } from '../utils/mqttClient' import awsExports from "../aws-exports"; import * as queries from '../graphql/queries' const { REACT_APP_MQTT_ENDPOINT_HOST_REMOTE, REACT_APP_MQTT_ENDPOINT_HOST_LOCAL, REACT_APP_MQTT_ENDPOINT_PORT } = process.env export default function Admin() { const [race, setRace] = useState() const [nrOfLaps, setNrOfLaps] = useState(30) const [secretPin, setSecretPin] = useState("") const [loading, setLoading] = useState(false) const [overview, setOverview] = useState() const [raceSubscription, setRaceSubscription] = useState() const raceSubscriptionRef = useRef(raceSubscription) const clientRef = useRef() const navigate = useNavigate(); const loadingText = "Loading ..." useEffect(() => { checkPin() init() return () => { tearDownConnections() } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const tearDownConnections = () => { if (raceSubscriptionRef.current) { raceSubscriptionRef.current.unsubscribe() } if (clientRef.current) { clientRef.current.disconnect() } } const checkPin = () => { const adminPin = prompt("Provide admin PIN") if (adminPin !== process.env.REACT_APP_ADMIN_PIN) { alert("Incorrect PIN"); navigate(`/`) } setSecretPin(adminPin) } const init = async () => { await initWsClient() const raceId = await setup() if (raceId) { const updateRaceSubscription = await API.graphql({ query: customStatements.customOnUpdateRaceById, variables: { id: raceId } }).subscribe({ next: ({ _, value }) => { console.log("Updated Race: ", value); fetchCurrentRace(value.data.onUpdateRaceById.id) }, error: error => console.warn(error) }); raceSubscriptionRef.current = updateRaceSubscription setRaceSubscription(updateRaceSubscription) return updateRaceSubscription } return null } const initWsClient = async () => { const isLocal = awsExports.aws_appsync_graphqlEndpoint.startsWith('http://') if (clientRef.current) { return } if (isLocal === false) { console.log("Remote Client") const { accessKeyId, secretAccessKey, sessionToken } = await Auth.currentCredentials() const client = new IotCoreMqttClient( REACT_APP_MQTT_ENDPOINT_HOST_REMOTE, accessKeyId, secretAccessKey, sessionToken, [MQTT_TOPICS.GAME_STATE_UPDATE] ) clientRef.current = client } else { console.log("Local Client") const client = new PahoMqttClient( REACT_APP_MQTT_ENDPOINT_HOST_LOCAL, REACT_APP_MQTT_ENDPOINT_PORT, [MQTT_TOPICS.GAME_STATE_UPDATE] ) clientRef.current = client } } const setup = async () => { const overview = await getOverview() setOverview(overview) if (overview && overview.currentRace !== null) { await fetchCurrentRace(overview.currentRace.id) return overview?.currentRace?.id } return null } const getOverview = async () => { var overview = null; try { const overviewData = await API.graphql(graphqlOperation(customStatements.customGetOverview, { id: CORE_OVERVIEW_ID })) overview = overviewData.data.getOverview || null } catch (error) { console.error("No overview found, likely that you need to initialise") console.log(error) } return overview } const fetchCurrentRace = async (raceId) => { setLoading(true) try { const raceData = await API.graphql(graphqlOperation(customStatements.customGetRace, { id: raceId })) const currentRace = raceData.data.getRace const carClaims = currentRace.currentRaceState === RACE_STATES.FORMATION_LAPS ? getFormationLapClaimsArray(currentRace) : getCarClaimsArray(currentRace) const updatedState = { raceId: currentRace.id, gameState: currentRace.currentRaceState, carClaims } clientRef.current.sendPayload(JSON.stringify(updatedState), MQTT_TOPICS.GAME_STATE_UPDATE) setRace(currentRace) } catch (err) { console.error(err) } setLoading(false) } const getCarClaimsArray = (raceData) => { var array = [] for (const player of raceData.players.items) { var object = { carId: parseInt(player.playerCarId), playerId: "" } if (player.claims.items.length > 0) { object.playerId = player.claims.items[0].id } array.push(object) } return array } const getFormationLapClaimsArray = (raceData) => { var array = [] for (const player of raceData.players.items) { const carId = parseInt(player.playerCarId) array.push({ carId, playerId: CARS[carId-1]?.claimable ? player.id : "" }) } return array } const initialise = async () => { setLoading(true) try { await API.graphql(graphqlOperation(queries.raceOperations, { input: { operation: RACE_OPERATIONS.INITIALISE, secretPin, additionalParams: { nrOfLaps } } })) } catch (error) { console.error(error) } window.location.reload(); setLoading(false) } const createNewRace = async () => { const operation = RACE_OPERATIONS.CREATE_NEW_RACE setLoading(true) // eslint-disable-next-line no-restricted-globals const confirmed = confirm(RACE_CONFIRMATION_MESSAGES[operation]) if (confirmed === true) { try { await API.graphql(graphqlOperation(queries.raceOperations, { input: { operation: operation, secretPin, additionalParams: { nrOfLaps } } })) raceSubscriptionRef.current.unsubscribe() await init() } catch (error) { console.error(error) } } setLoading(false) } const { tokens } = useTheme(); const raceIsOngoing = () => { const state = race?.currentRaceState return race && state !== RACE_STATES.CHECKERED_FLAG && state !== RACE_STATES.LOBBY && state !== RACE_STATES.ABORTED && state !== RACE_STATES.PRACTICE } const canChangeToThisActiveRaceState = (desiredState) => { const state = race?.currentRaceState return raceIsOngoing() && state !== RACE_STATES.LOBBY && state !== desiredState } const raceOperation = async (operation) => { setLoading(true) if (RACE_CONFIRMATION_MESSAGES[operation] !== undefined) { // eslint-disable-next-line no-restricted-globals const confirmed = confirm(RACE_CONFIRMATION_MESSAGES[operation]) if (!confirmed) { setLoading(false); return } } try { await API.graphql(graphqlOperation(queries.raceOperations, { input: { operation: operation, secretPin, additionalParams: { raceId: race.id } } })) } catch (error) { console.error(error) setLoading(false) } } const actionButtons = () => { if (!overview) { return ( <> One-time initialisation .... ) } else { return ( <> ) } } return ( <> Admin {race && <> Current race id: {race?.id} Current race state: {race?.currentRaceState} } {actionButtons()} ) }