diff --git a/.eslintignore b/.eslintignore index 862cc4906..a10aafae3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -11,4 +11,6 @@ server/build .cache locales src/models -packages \ No newline at end of file +packages +tempServer +temp \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 425f720b5..45c6a2bdd 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,4 +5,6 @@ build dist locales coverage -packages \ No newline at end of file +packages +tempServer +temp \ No newline at end of file diff --git a/public/cardIcons/StationControl.svg b/public/cardIcons/StationControl.svg new file mode 100644 index 000000000..4ce83ceee --- /dev/null +++ b/public/cardIcons/StationControl.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/server/bootstrap/apollo.js b/server/bootstrap/apollo.js index e2d1d5f46..1554dab65 100644 --- a/server/bootstrap/apollo.js +++ b/server/bootstrap/apollo.js @@ -27,9 +27,9 @@ export default (app, SERVER_PORT) => { introspection: true, playground: true, uploads: false, - context: ({req}) => ({ - clientId: req && req.headers.clientid, - core: req && req.headers.core, + context: ({req, connection}) => ({ + clientId: req?.headers.clientid || connection?.context.clientId, + core: req?.headers.core, }), }; const apollo = new ApolloServer(graphqlOptions); diff --git a/server/classes/simulator.js b/server/classes/simulator.js index ce0cc6ea2..363a4ee09 100644 --- a/server/classes/simulator.js +++ b/server/classes/simulator.js @@ -177,6 +177,13 @@ export default class Simulator { this.midiSets = params.midiSets || []; this.crackedClients = params.crackedClients || {}; + // The name of the current card which each + // station is on. + this.clientCards = params.clientCards || {}; + + // Cards assigned to another station from a different station. + this.stationAssignedCards = params.stationAssignedCards || {}; + this.flipped = params.flipped || false; // Set up the teams if (params.teams) { @@ -256,6 +263,28 @@ export default class Simulator { setLayout(layout) { this.layout = layout; } + setClientCard(client, cardName) { + this.clientCards[client] = cardName; + } + addStationAssignedCard(station, card) { + const stationCards = this.stationAssignedCards[station]; + this.stationAssignedCards[station] = stationCards + ? stationCards.concat(card) + : [card]; + } + removeStationAssignedCard(cardName) { + const stationEntry = Object.entries( + this.stationAssignedCards, + ).find(([key, value]) => value.find(c => c.name === cardName)); + const station = stationEntry?.[0]; + if (!station) return; + + const stationCards = this.stationAssignedCards[station]; + this.stationAssignedCards[station] = stationCards + ? stationCards.filter(c => c.name !== cardName) + : []; + } + setTimelineStep(step, timelineId) { if (timelineId) { this.setAuxTimelineStep(timelineId, step); diff --git a/server/events/clients.js b/server/events/clients.js index 25502dd0a..86bc5661b 100644 --- a/server/events/clients.js +++ b/server/events/clients.js @@ -96,6 +96,13 @@ App.on("clientSetStation", ({client, stationName, cb}) => { } cb && cb(); }); +App.on("clientSetCard", ({id, card}) => { + const clientObj = App.clients.find(c => c.id === id); + const {simulatorId} = clientObj; + const simulator = App.simulators.find(s => s.id === simulatorId); + simulator.setClientCard(id, card); + pubsub.publish("clientChanged", App.clients); +}); App.on("clientLogin", ({client, loginName}) => { const clientObj = App.clients.find(c => c.id === client); clientObj.login(loginName); diff --git a/server/events/simulator.js b/server/events/simulator.js index 9c43230f8..7289a55af 100644 --- a/server/events/simulator.js +++ b/server/events/simulator.js @@ -447,3 +447,22 @@ App.on("flipSimulator", ({simulatorId, flip}) => { pubsub.publish("simulatorsUpdate", App.simulators); }); +App.on("stationAssignCard", ({simulatorId, assignedToStation, cardName}) => { + const simulator = App.simulators.find(s => s.id === simulatorId); + const card = simulator.stations + .reduce((acc, next) => acc.concat(next.cards), []) + .find(c => c.name === cardName); + + // Remove it so it isn't double-assigned + simulator.removeStationAssignedCard(cardName); + + simulator.addStationAssignedCard(assignedToStation, card); + pubsub.publish("simulatorsUpdate", App.simulators); + pubsub.publish("clientChanged", App.clients); +}); +App.on("stationUnassignCard", ({simulatorId, cardName}) => { + const simulator = App.simulators.find(s => s.id === simulatorId); + simulator.removeStationAssignedCard(cardName); + pubsub.publish("simulatorsUpdate", App.simulators); + pubsub.publish("clientChanged", App.clients); +}); diff --git a/server/typeDefs/clients.js b/server/typeDefs/clients.js index 41bf9db9d..00fb1a323 100644 --- a/server/typeDefs/clients.js +++ b/server/typeDefs/clients.js @@ -26,6 +26,8 @@ const schema = gql` cracked: Boolean commandLineOutput: [String] commandLineFeedback: [CommandLineFeedback] + currentCard: Card + # Space EdVentures token: String email: String @@ -176,6 +178,7 @@ const schema = gql` ): String setClientOverlay(id: ID!, overlay: Boolean!): String clientCrack(id: ID!, crack: Boolean!): String + clientSetCard(id: ID!, card: String!): String setKeypadCode(id: ID!, code: [Int]): String setKeypadEnteredCode(id: ID!, code: [Int!]): String @@ -282,6 +285,31 @@ const resolver = { mobile(client) { return Boolean(client.mobile); }, + currentCard(client) { + const simulator = App.simulators.find(s => s.id === client.simulatorId); + if (!simulator) return null; + const station = StationResolver(client); + if (!station) return null; + + // Get any assigned cards + const {stationAssignedCards = {}} = simulator; + const assignedCardList = Object.values(stationAssignedCards) + .reduce((acc, next) => acc.concat(next), []) + .map(c => c.name) + .filter(Boolean); + + const concatCards = stationAssignedCards[station.name] || []; + let cards = station.cards + .map(c => ({...c, assigned: assignedCardList.includes(c.name)})) + .concat(concatCards.map(c => ({...c, newStation: true}))) + .filter(Boolean); + + const card = cards + .filter(c => !c.hidden) + .find(c => c.name === simulator.clientCards[client.id]); + + return card || cards[0]; + }, }, Keypad: { giveHints(client) { diff --git a/server/typeDefs/simulator.js b/server/typeDefs/simulator.js index 8860c866c..71034510d 100644 --- a/server/typeDefs/simulator.js +++ b/server/typeDefs/simulator.js @@ -96,6 +96,14 @@ const schema = gql` Macro: Station: Unhide Card """ unhideSimulatorCard(simulatorId: ID!, cardName: String!): String + + stationAssignCard( + simulatorId: ID! + assignedToStation: String! + cardName: String! + ): String + stationUnassignCard(simulatorId: ID!, cardName: String!): String + """ Macro: Simulator: Flip Simulator """ diff --git a/server/typeDefs/station.js b/server/typeDefs/station.js index 8f4d5cb4f..722436244 100644 --- a/server/typeDefs/station.js +++ b/server/typeDefs/station.js @@ -29,6 +29,16 @@ const schema = gql` name: String component: String hidden: Boolean + # Whether this card is assigned to another station + assigned: Boolean + + # Whether this card is assigned to a station + # it doesn't belong to + newStation: Boolean + } + input CardInput { + name: String + component: String } extend type Simulator { stationSets: [StationSet] @@ -120,9 +130,28 @@ const resolver = { }, }, Station: { - cards(station, {showHidden}) { - if (showHidden) return station.cards; - return station.cards.filter(c => !c.hidden); + cards(station, {showHidden}, context) { + let simulator = context.simulator; + if (!simulator) { + const clientObj = App.clients.find(c => c.id === context.clientId); + simulator = App.simulators.find(s => s.id === clientObj?.simulatorId); + context.simulator = simulator; + } + const {stationAssignedCards = {}} = simulator || {}; + + const assignedCardList = Object.values(stationAssignedCards) + .reduce((acc, next) => acc.concat(next), []) + .map(c => c.name) + .filter(Boolean); + const concatCards = stationAssignedCards[station.name] || []; + let cards = station.cards + .map(c => ({...c, assigned: assignedCardList.includes(c.name)})) + .concat(concatCards.map(c => ({...c, newStation: true}))) + .filter(Boolean); + + // Indicate cards assigned to other stations + if (showHidden) return cards; + return cards.filter(c => !c.hidden); }, }, Query: { diff --git a/src/components/client/Card.js b/src/components/client/Card.js index e62c6db3b..2af8779a5 100644 --- a/src/components/client/Card.js +++ b/src/components/client/Card.js @@ -1,4 +1,4 @@ -import React, {Component} from "react"; +import React from "react"; import Layouts from "../layouts"; import Keyboard from "components/views/Keyboard"; import InterfaceCard from "components/views/Interfaces"; @@ -8,7 +8,7 @@ import SoundPlayer from "./soundPlayer"; import Reset from "./reset"; import TrainingPlayer from "helpers/trainingPlayer"; import {subscribe, publish} from "../../helpers/pubsub"; -import {Mutation} from "react-apollo"; +import {useMutation} from "react-apollo"; import gql from "graphql-tag.macro"; import {playSound} from "../generic/SoundPlayer"; import {randomFromList} from "helpers/randomFromList"; @@ -94,164 +94,121 @@ export function isMedia(src = "") { ]; return extensions.find(e => src.toLowerCase().indexOf(e) > -1); } -export default class CardFrame extends Component { - constructor(props) { - super(props); - if (props.test) { - this.state = { - card: "Test", - }; - } else { - this.state = { - card: this.props.station.cards && this.props.station.cards[0].name, - }; - } + +const SET_TRAINING_MUTATION = gql` + mutation ClientSetTraining($id: ID!, $training: Boolean!) { + clientSetTraining(client: $id, training: $training) + } +`; + +const CHANGE_CARD_MUTATION = gql` + mutation SetCard($id: ID!, $card: String!) { + clientSetCard(id: $id, card: $card) } - componentDidMount() { - setTimeout(() => { - this.setState({visible: true}); - }, 500); - this.cardChangeRequestSubscription = subscribe( - "cardChangeRequest", - payload => { - // Searching in order of priority, find a matching card by component (card - // names may have been changed to protect the innocent) then change to that card's name. - let found = false; - for (let i = 0; i < payload.changeToCard.length; i++) { - let matchingCard = this.props.station.cards.find( - c => c.component === payload.changeToCard[i], - ); - if (matchingCard) { - this.changeCard(matchingCard.name); - found = true; - break; - } +`; + +const CardFrame = props => { + const { + station: {cards, widgets, training: stationTraining}, + station, + simulator, + simulator: {soundEffects, caps, flipped, training: simTraining}, + client, + } = props; + const cardChanged = React.useRef(false); + const [visible, setVisible] = React.useState(false); + const cardName = client?.currentCard?.name; + React.useEffect(() => { + setTimeout(() => setVisible(true), 500); + }, []); + const [changeCardMutation] = useMutation(CHANGE_CARD_MUTATION); + const changeCard = React.useCallback( + name => { + const card = cards.find(c => c.name === name) ? name : cards?.[0]?.name; + if (cardChanged.current || cardName === card) return; + cardChanged.current = true; + setTimeout(() => (cardChanged.current = false), 500); + if (soundEffects?.cardChange) { + playSound({ + url: `/assets${randomFromList(soundEffects.cardChange)}`, + }); + } + changeCardMutation({variables: {id: client.id, card: name}}); + }, + [cards, changeCardMutation, cardName, client.id, soundEffects], + ); + + React.useEffect(() => { + return subscribe("cardChangeRequest", payload => { + // Searching in order of priority, find a matching card by component (card + // names may have been changed to protect the innocent) then change to that card's name. + let found = false; + for (let i = 0; i < payload.changeToCard.length; i++) { + let matchingCard = cards.find( + c => c.component === payload.changeToCard[i], + ); + if (matchingCard) { + changeCard(matchingCard.name); + found = true; + break; } - if (!found) { - const widgetName = payload.changeToCard.find(c => - this.props.station.widgets.includes(c), - ); - if (widgetName) { - publish("widgetOpen", widgetName); - } + } + if (!found) { + const widgetName = payload.changeToCard.find(c => widgets.includes(c)); + if (widgetName) { + publish("widgetOpen", widgetName); } - }, - ); - } - componentWillUnmount() { - // Unsubscribe - this.cardChangeRequestSubscription(); - } - componentDidUpdate(prevProps) { - if (prevProps.station.name !== this.props.station.name) { - this.setState({ - card: - this.props.station.cards && - this.props.station.cards[0] && - this.props.station.cards[0].name, - }); - } - } - changeCard = name => { - const card = this.props.station.cards.find(c => c.name === name) - ? name - : this.props.station.cards && - this.props.station.cards[0] && - this.props.station.cards[0].name; - if (this.cardChanged || this.state.card === card) return; - this.cardChanged = true; - setTimeout(() => (this.cardChanged = false), 500); - if ( - this.props.simulator && - this.props.simulator.soundEffects && - this.props.simulator.soundEffects.cardChange - ) { - playSound({ - url: `/assets${randomFromList( - this.props.simulator.soundEffects.cardChange, - )}`, - }); - } - this.setState({ - card, + } }); - }; - render() { - const { - station: {training: stationTraining}, - simulator: {caps, flipped, training: simTraining}, - client, - } = this.props; - const {visible} = this.state; + }, [cards, changeCard, widgets]); - return ( -
- - {client.cracked &&
} - - {this.props.client && ( - - this.setState({ - card: - this.props.station.cards && - this.props.station.cards[0] && - this.props.station.cards[0].name, - }) - } - /> - )} - {simTraining && - stationTraining && - client.training && - isMedia(stationTraining) && ( - - {action => ( - - )} - - )} - {client.offlineState !== "blackout" && ( - + + {client.cracked &&
} + + {client && } + {simTraining && + stationTraining && + client.training && + isMedia(stationTraining) && ( + )} -
- ); - } -} + {client.offlineState !== "blackout" && ( + + )} +
+ ); +}; + +export default CardFrame; diff --git a/src/components/client/index.js b/src/components/client/index.js index c1dd6cbeb..024c80151 100644 --- a/src/components/client/index.js +++ b/src/components/client/index.js @@ -26,6 +26,10 @@ const fragments = { station { name } + currentCard { + name + component + } loginName loginState offlineState diff --git a/src/components/client/simulatorData.js b/src/components/client/simulatorData.js index e46ea2e0b..b595b7b89 100644 --- a/src/components/client/simulatorData.js +++ b/src/components/client/simulatorData.js @@ -42,6 +42,8 @@ const fragments = { name component hidden + assigned + newStation } } } diff --git a/src/components/core/CoreComponents.js b/src/components/core/CoreComponents.js index 285a4d516..d2052924d 100644 --- a/src/components/core/CoreComponents.js +++ b/src/components/core/CoreComponents.js @@ -174,7 +174,6 @@ class CoreComponents extends Component { {simulator && ( changeCard(name)} alt="Card" ref={this.card} - className={`card-icon ${name === currentCard ? "active" : ""}`} + className={`card-icon ${name === currentCard ? "active" : ""} ${ + assigned ? "card-icon-assigned" : "" + }`} src={`/cardIcons/${cardName}.svg`} draggable="false" /> @@ -59,6 +61,7 @@ const CardSwitcher = ({cards, changeCard, currentCard}) => { changeCard={changeCard} currentCard={currentCard} component={card.component} + assigned={card.assigned || card.newStation} /> ))}
diff --git a/src/components/layouts/LayoutBlack/style.scss b/src/components/layouts/LayoutBlack/style.scss index 92eacfb8c..5261c6261 100644 --- a/src/components/layouts/LayoutBlack/style.scss +++ b/src/components/layouts/LayoutBlack/style.scss @@ -28,7 +28,21 @@ $alertp: #7a24cf; box-sizing: content-box; transition: background-color 0.2s ease, border-color 0.2s ease; } + img.card-icon-assigned.card-icon-assigned { + background-color: gold; + border: solid 2px gold; + + &:hover { + background-color: darken(gold, 20%); + border: solid 2px darken(gold, 20%); + } + &.active { + background-color: lighten(gold, 20%); + border: solid 2px lighten(gold, 20%); + } + } } + @mixin cardSwitcherColors($color) { .card-switcher { img { @@ -63,7 +77,6 @@ $alertp: #7a24cf; &.alertColorp { @include cardSwitcherColors($alertp); } - .card-frame { width: 100vw; height: 100vh; diff --git a/src/components/layouts/LayoutCorners/CardSwitcher.js b/src/components/layouts/LayoutCorners/CardSwitcher.js index 23435f49c..1af20323a 100644 --- a/src/components/layouts/LayoutCorners/CardSwitcher.js +++ b/src/components/layouts/LayoutCorners/CardSwitcher.js @@ -17,6 +17,7 @@ class CardSwitcher extends Component { cardNum={index} name={card.name} component={card.component} + assigned={card.assigned || card.newStation} {...this.props} /> ); @@ -51,7 +52,11 @@ const CardButton = props => { src={`/cardIcons/${cardName}.svg`} draggable="false" /> -
+
); diff --git a/src/components/layouts/LayoutCorners/cardSwitcher.scss b/src/components/layouts/LayoutCorners/cardSwitcher.scss index a5b50b76f..4a0d5a4a5 100644 --- a/src/components/layouts/LayoutCorners/cardSwitcher.scss +++ b/src/components/layouts/LayoutCorners/cardSwitcher.scss @@ -84,9 +84,15 @@ background: #bbb; border-radius: 50%; } + .card-icon-assigned { + background: gold; + } .active .card-icon-background { background: #fff; } + .active .card-icon-assigned { + background: lighten(gold, 10%); + } .card-icon-color { position: absolute; top: 4px; diff --git a/src/components/layouts/LayoutEpsilon/cardSwitcher.js b/src/components/layouts/LayoutEpsilon/cardSwitcher.js index c4388a748..7f9211e9b 100644 --- a/src/components/layouts/LayoutEpsilon/cardSwitcher.js +++ b/src/components/layouts/LayoutEpsilon/cardSwitcher.js @@ -41,7 +41,7 @@ const CardSwitcher = ({cards, cardName, changeCard, hyperCard}) => { key={`card-${c.name}`} className={`navigation-card ${ currentCard.component === cardName ? " active" : "" - }`} + } ${c.assigned || c.newStation ? "text-warning" : ""}`} onClick={() => { setShown(false); changeCard(c.name); diff --git a/src/components/layouts/LayoutLine/cardSwitcher.js b/src/components/layouts/LayoutLine/cardSwitcher.js index 362d4aca9..544e1f53b 100644 --- a/src/components/layouts/LayoutLine/cardSwitcher.js +++ b/src/components/layouts/LayoutLine/cardSwitcher.js @@ -15,7 +15,7 @@ class CardButton extends Component { }); }; render() { - const {component, changeCard, currentCard, name} = this.props; + const {component, changeCard, currentCard, name, assigned} = this.props; const cardName = component.match(intregex) ? "Interface" : component.match(spregex) @@ -28,7 +28,9 @@ class CardButton extends Component { onClick={() => changeCard(name)} alt="Card" ref={this.card} - className={`card-icon ${name === currentCard ? "active" : ""}`} + className={`card-icon ${name === currentCard ? "active" : ""} ${ + assigned ? "card-icon-assigned" : "" + }`} src={`/cardIcons/${cardName}.svg`} draggable="false" /> @@ -59,6 +61,7 @@ const CardSwitcher = ({cards, changeCard, currentCard}) => { changeCard={changeCard} currentCard={currentCard} component={card.component} + assigned={card.assigned || card.newStation} /> ))} diff --git a/src/components/layouts/LayoutLine/style.scss b/src/components/layouts/LayoutLine/style.scss index cca8b093d..489e7c0e8 100644 --- a/src/components/layouts/LayoutLine/style.scss +++ b/src/components/layouts/LayoutLine/style.scss @@ -26,7 +26,21 @@ $alertp: #7a24cf; box-sizing: content-box; transition: background-color 0.2s ease, border-color 0.2s ease; } + img.card-icon-assigned.card-icon-assigned { + background-color: gold; + border: solid 2px gold; + + &:hover { + background-color: darken(gold, 20%); + border: solid 2px darken(gold, 20%); + } + &.active { + background-color: lighten(gold, 20%); + border: solid 2px lighten(gold, 20%); + } + } } + @mixin cardSwitcherColors($color) { .card-switcher { img { diff --git a/src/components/layouts/LayoutPhoenix/cardSwitcher.js b/src/components/layouts/LayoutPhoenix/cardSwitcher.js index 932c56962..a8875e751 100644 --- a/src/components/layouts/LayoutPhoenix/cardSwitcher.js +++ b/src/components/layouts/LayoutPhoenix/cardSwitcher.js @@ -22,7 +22,9 @@ class CardSwitcher extends Component { {(cards || []).map(c => (
{ this.setState({shown: false}); changeCard(c.name); diff --git a/src/components/layouts/cardRenderer.js b/src/components/layouts/cardRenderer.js index 129727f43..7338d879b 100644 --- a/src/components/layouts/cardRenderer.js +++ b/src/components/layouts/cardRenderer.js @@ -3,7 +3,31 @@ import Views from "components/views"; import {Transition} from "react-transition-group"; import {Button} from "helpers/reactstrap"; import ErrorBoundary from "helpers/errorBoundary"; +import gql from "graphql-tag.macro"; +import {useMutation} from "react-apollo"; +const UNASSIGN_CARD = gql` + mutation UnassignCard($simulatorId: ID!, $cardName: String!) { + stationUnassignCard(simulatorId: $simulatorId, cardName: $cardName) + } +`; +const CardAssigned = ({cardName, simulatorId}) => { + const [unassign] = useMutation(UNASSIGN_CARD, { + variables: {simulatorId, cardName}, + }); + return ( +
+

Screen Unavailable

+

+ This screen has been assigned to another station and is not currently + available. Click the button below to restore control. +

+ +
+ ); +}; const CardWrapper = ({card}) => { return ( @@ -31,7 +55,14 @@ const CardWrapper = ({card}) => {
} > - + {card.assigned ? ( + + ) : ( + + )} ); @@ -59,7 +90,7 @@ function renderCards(props) { Views[clientObj.hypercard] ) { const Comp = Views[clientObj.hypercard]; - return ; + return [{name: "Hypercard", component: Comp, in: true, props}]; } function getCompName(name) { if (name.indexOf("interface-id:") > -1) { diff --git a/src/components/macros/updateViewscreenComponent.js b/src/components/macros/updateViewscreenComponent.js index 9f07a8fe5..daaaa62d5 100644 --- a/src/components/macros/updateViewscreenComponent.js +++ b/src/components/macros/updateViewscreenComponent.js @@ -57,7 +57,6 @@ export default ({updateArgs, args, clients, steps = []}) => { ); }, []) .reduce((acc, next) => acc.concat(next), []); - console.log(clients); return (
diff --git a/src/components/views/Clients/core.js b/src/components/views/Clients/core.js index 0d77d21f8..a66051f36 100644 --- a/src/components/views/Clients/core.js +++ b/src/components/views/Clients/core.js @@ -14,6 +14,9 @@ const fragment = gql` station { name } + currentCard { + name + } loginName hypercard } @@ -110,6 +113,7 @@ const ClientCore = ({clients, simulator}) => ( Station Name + Card Client Hypercard @@ -127,6 +131,7 @@ const ClientCore = ({clients, simulator}) => ( {c.station.name} {c.loginName} + {c.currentCard.name} {c.id} {" "} diff --git a/src/components/views/CommDecoding/decodingCanvas.js b/src/components/views/CommDecoding/decodingCanvas.js index 314132e6d..c6c542a2d 100644 --- a/src/components/views/CommDecoding/decodingCanvas.js +++ b/src/components/views/CommDecoding/decodingCanvas.js @@ -45,7 +45,6 @@ export default function DecodingCanvas({ function loop() { setP(p => { const newP = p + 20; - console.log(newP, width * 2); if (newP < width * 2) { return newP; } diff --git a/src/components/views/Midi/LiveData/lightingIntensity.js b/src/components/views/Midi/LiveData/lightingIntensity.js index a7df907fb..15b99f490 100644 --- a/src/components/views/Midi/LiveData/lightingIntensity.js +++ b/src/components/views/Midi/LiveData/lightingIntensity.js @@ -27,7 +27,7 @@ const LightingIntensity = ({simulatorId, value, setValue}) => { valueSet.current = true; setTimeout(() => { valueSet.current = false; - }, 1000 / 30); + }, 1000 / 10); setValue(intensity); }, [intensity, setValue]); diff --git a/src/components/views/Cards/Cards.stories.js b/src/components/views/StationControl/Cards.stories.js similarity index 100% rename from src/components/views/Cards/Cards.stories.js rename to src/components/views/StationControl/Cards.stories.js diff --git a/src/components/views/Cards/CardsCore.test.js b/src/components/views/StationControl/CardsCore.test.js similarity index 100% rename from src/components/views/Cards/CardsCore.test.js rename to src/components/views/StationControl/CardsCore.test.js diff --git a/src/components/views/Cards/core.js b/src/components/views/StationControl/core.js similarity index 100% rename from src/components/views/Cards/core.js rename to src/components/views/StationControl/core.js diff --git a/src/components/views/StationControl/index.js b/src/components/views/StationControl/index.js new file mode 100644 index 000000000..c2d4d9833 --- /dev/null +++ b/src/components/views/StationControl/index.js @@ -0,0 +1,217 @@ +import React from "react"; +import gql from "graphql-tag.macro"; +import { + Container, + Row, + Col, + Button, + ListGroup, + ListGroupItem, +} from "reactstrap"; +import useQueryAndSubscription from "helpers/hooks/useQueryAndSubscribe"; +import "./style.scss"; +import {useMutation} from "react-apollo"; + +const fragment = gql` + fragment StationClientData on Client { + id + station { + name + cards { + name + assigned + newStation + } + } + currentCard { + name + } + } +`; + +export const STATION_CLIENT_QUERY = gql` + query Clients($simulatorId: ID!) { + clients(simulatorId: $simulatorId) { + ...StationClientData + } + } + ${fragment} +`; +export const STATION_CLIENT_SUB = gql` + subscription TemplateUpdate($simulatorId: ID!) { + clientChanged(simulatorId: $simulatorId) { + ...StationClientData + } + } + ${fragment} +`; + +const UNASSIGN_CARD = gql` + mutation UnassignCard($simulatorId: ID!, $cardName: String!) { + stationUnassignCard(simulatorId: $simulatorId, cardName: $cardName) + } +`; + +const ASSIGN_CARD = gql` + mutation AssignCard( + $simulatorId: ID! + $assignedToStation: String! + $cardName: String! + ) { + stationAssignCard( + simulatorId: $simulatorId + assignedToStation: $assignedToStation + cardName: $cardName + ) + } +`; + +const StationControl = props => { + const [selectedClient, setSelectedClient] = React.useState(null); + const [selectedCard, setSelectedCard] = React.useState(null); + const {simulator} = props; + const {loading, data} = useQueryAndSubscription( + {query: STATION_CLIENT_QUERY, variables: {simulatorId: simulator.id}}, + {query: STATION_CLIENT_SUB, variables: {simulatorId: simulator.id}}, + ); + + const [removeAssignment] = useMutation(UNASSIGN_CARD, { + variables: {simulatorId: simulator.id, cardName: selectedCard}, + }); + const [performAssignment] = useMutation(ASSIGN_CARD); + + if (loading || !data) return null; + const {clients} = data; + if (!clients) return
No Clients
; + const stationNameCount = {}; + + const clientList = clients + .map((client, i, arr) => { + if (!client.station) return null; + const count = stationNameCount?.[client.station.name] + 1 || 1; + stationNameCount[client.station.name] = count; + let showCount = count > 1; + if (count === 1) { + if ( + arr.find( + (c, ii) => ii > i && c?.station?.name === client?.station?.name, + ) + ) { + showCount = true; + } + } + return { + id: client.id, + currentCard: client.currentCard.name, + label: `${client.station.name}${showCount ? ` (${count})` : ""}`, + station: client.station, + cards: client.station.cards, + }; + }) + .filter(Boolean); + const stations = clients + .reduce((acc, next) => acc.concat(next?.station?.name), []) + .filter((a, i, arr) => arr.indexOf(a) === i) + .filter(Boolean); + + const client = clients.find(c => c.id === selectedClient); + const assignCard = client?.station.cards.find(c => c.name === selectedCard); + const assignCardAssignedTo = clientList.find(client => + client.cards.find( + card => card.name === assignCard?.name && card.newStation, + ), + ); + + function assignCardToClient(station) { + performAssignment({ + variables: { + simulatorId: simulator.id, + cardName: selectedCard, + assignedToStation: station, + }, + }); + } + + return ( + + + +

Stations

+ + {clientList.map(c => ( + { + setSelectedClient(c.id); + setSelectedCard(null); + }} + active={selectedClient === c.id} + > +
{c.label}
+
+ {c.currentCard} +
+
+ ))} +
+ + {client && ( + +

Screens

+ + {client.station.cards + .filter(c => !c.newStation) + .map(c => { + const assignedTo = + c.assigned && + clientList.find(client => + client.cards.find( + card => card.name === c.name && card.newStation, + ), + ); + return ( + setSelectedCard(c.name)} + active={c.name === selectedCard} + > +
{c.name}
+
+ + {assignedTo && `Assigned To: ${assignedTo.label}`} + +
+
+ ); + })} +
+ + )} + {assignCard && ( + +

Assign To:

+ + {stations + .filter(station => station !== client.station.name) + .map(c => ( + assignCardToClient(c)} + active={assignCardAssignedTo?.station.name === c} + > + {c} + + ))} + + {assignCardAssignedTo && ( + + )} + + )} +
+
+ ); +}; +export default StationControl; diff --git a/src/components/views/StationControl/style.scss b/src/components/views/StationControl/style.scss new file mode 100644 index 000000000..2e4a3ff0d --- /dev/null +++ b/src/components/views/StationControl/style.scss @@ -0,0 +1,17 @@ +.card-stationControl { + // Stuff goes here + height: 100%; + display: block; + .row { + height: 100%; + } + .col-sm-4 { + display: flex; + flex-direction: column; + height: 100%; + } + .list-group { + flex: 1; + overflow-y: auto; + } +} diff --git a/src/components/views/Timeline/TimelineThumbnailCore.test.js b/src/components/views/Timeline/TimelineThumbnailCore.test.js index 5195fa177..01702d003 100644 --- a/src/components/views/Timeline/TimelineThumbnailCore.test.js +++ b/src/components/views/Timeline/TimelineThumbnailCore.test.js @@ -12,7 +12,6 @@ it("should render", async () => { debug(); await waitForElementToBeRemoved(() => getByText("Loading...")); await wait(); - console.log(container.innerHTML); expect(container.innerHTML).toBeTruthy(); expect(container.innerHTML).not.toBe("Error"); }); diff --git a/src/components/views/TorpedoLoading/inventory.js b/src/components/views/TorpedoLoading/inventory.js index 9d10c3ef5..a7b008abe 100644 --- a/src/components/views/TorpedoLoading/inventory.js +++ b/src/components/views/TorpedoLoading/inventory.js @@ -39,9 +39,8 @@ const TorpedoInventory = ({id, inventory, refetchQueries = []}) => { - console.log(count) || - ((parseInt(count, 10) || parseInt(count, 10) === 0) && - updateCount({variables: {id, type: "photon", count}})) + (parseInt(count, 10) || parseInt(count, 10) === 0) && + updateCount({variables: {id, type: "photon", count}}) } > {types.photon} diff --git a/src/components/views/index.js b/src/components/views/index.js index ddaa431d6..841c2fb7e 100644 --- a/src/components/views/index.js +++ b/src/components/views/index.js @@ -113,6 +113,7 @@ const SpaceEdventuresToken = React.lazy(() => import("./SpaceEdventuresToken")); const Crm = React.lazy(() => import("./Crm")); const CrmFighter = React.lazy(() => import("./CrmFighter")); const Records = React.lazy(() => import("./Records")); +const StationControl = React.lazy(() => import("./StationControl")); // Cores const EngineControlCore = React.lazy(() => import("./EngineControl/core")); @@ -203,7 +204,7 @@ const SpaceEdventuresTokenCore = React.lazy(() => import("./SpaceEdventuresToken/core"), ); const CrmCore = React.lazy(() => import("./Crm/core")); -const CardsCore = React.lazy(() => import("./Cards/core")); +const CardsCore = React.lazy(() => import("./StationControl/core")); const CommandLineCore = React.lazy(() => import("./CommandLine/core")); const AuxTimelineCore = React.lazy(() => import("./Timeline/auxTimelineData")); const LightingCore = React.lazy(() => import("./Lighting")); @@ -313,6 +314,7 @@ const Views = { Crm, CrmFighter, Records, + StationControl, }; export const Widgets = { diff --git a/src/helpers/graphqlClient.js b/src/helpers/graphqlClient.js index 91e185e4a..c261052c2 100644 --- a/src/helpers/graphqlClient.js +++ b/src/helpers/graphqlClient.js @@ -18,6 +18,19 @@ const graphqlUrl = ? "/graphql" : `http://${hostname}:${parseInt(window.location.port || 3000, 10) + 1}/graphql`; + +const webSocketLink = new WebSocketLink({ + uri: + process.env.NODE_ENV === "production" + ? `ws://${window.location.host}/graphql` + : `ws://${hostname}:${parseInt(window.location.port || 3000, 10) + + 1}/graphql`, + options: { + reconnect: true, + connectionParams: () => getClientId().then(clientId => ({clientId})), + }, +}); + const wsLink = ApolloLink.from([ onError(args => { const {response, graphQLErrors, networkError} = args; @@ -37,16 +50,7 @@ const wsLink = ApolloLink.from([ } if (response) response.errors = null; }), - new WebSocketLink({ - uri: - process.env.NODE_ENV === "production" - ? `ws://${window.location.host}/graphql` - : `ws://${hostname}:${parseInt(window.location.port || 3000, 10) + - 1}/graphql`, - options: { - reconnect: true, - }, - }), + webSocketLink, ]); const headersMiddleware = setContext((operation, {headers}) => { diff --git a/src/mocks/cards/Cards.mock.js b/src/mocks/cards/Cards.mock.js index 7ee39cdef..c0b5070cb 100644 --- a/src/mocks/cards/Cards.mock.js +++ b/src/mocks/cards/Cards.mock.js @@ -1,8 +1,8 @@ +import simulators from "../data/simulators"; import { CARDS_CORE_QUERY, CARDS_CORE_SUB, -} from "../../components/views/Cards/core"; -import simulators from "../data/simulators"; +} from "components/views/StationControl/core"; export default [ { diff --git a/src/mocks/data/clients.js b/src/mocks/data/clients.js index f7b14c2b9..363eb19fe 100644 --- a/src/mocks/data/clients.js +++ b/src/mocks/data/clients.js @@ -18,6 +18,11 @@ export default [ description: null, __typename: "Station", }, + currentCard: { + name: "Test", + component: "TestCard", + __typename: "Card", + }, __typename: "Client", }, { @@ -39,6 +44,11 @@ export default [ description: "", __typename: "Station", }, + currentCard: { + name: "Test", + component: "TestCard", + __typename: "Card", + }, __typename: "Client", }, ];