// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import React from 'react'; import './Overview.css'; import { API, graphqlOperation, I18n } from 'aws-amplify'; import { IMachineReferenceDataItem, IMessageFormatConfigItem, IUIReferenceMappingItem, ILocationReferenceDataItem, ILineReferenceDataItem, ReferenceDataTypes, IReferenceDataItem, ConfigType } from '../../util/data-structures'; import { getConfigItem, IGetConfigItemResponse, getUIReferenceItems, IGetUIReferenceItemsResponse } from '../../graphql/queries'; import { onUpdateUIReferenceItem, IOnUpdateUIReferenceItemResponse } from '../../graphql/subscriptions'; import { updateMachineGrouping } from '../../graphql/mutations'; import { LargeNotification } from '../shared/LargeNotification'; import { MachineStatusCard } from '../machines/MachineStatusCard'; import { LineRow } from '../lines/LineRow'; import { Form } from 'react-bootstrap'; import DropdownButton from 'react-bootstrap/DropdownButton'; import Dropdown from 'react-bootstrap/Dropdown' import Container from 'react-bootstrap/Container'; import Row from 'react-bootstrap/Row'; import Col from 'react-bootstrap/Col'; import Spinner from 'react-bootstrap/Spinner'; import Modal from 'react-bootstrap/Modal'; import Button from 'react-bootstrap/Button'; type OverviewProps = { selectedLocationStorageKeyName: string; }; type OverviewState = { locations: ILocationReferenceDataItem[]; lines: ILineReferenceDataItem[]; machines: IMachineReferenceDataItem[]; uiReferenceMapping?: IUIReferenceMappingItem; isLoading: boolean; isMutating: boolean; showConfigModal: boolean; machineIdDelimiter: string; newLocationUIReferenceMapping: Set; newLineUIReferenceMapping: Set; }; export class Overview extends React.Component { private _isMounted = false; private _selectedLocationStorageKeyName: string; private UNASSIGNED_MACHINES_LOCATION_ID = 'UNASSIGNED_MACHINES_LOCATION_ID'; private UNASSIGNED_MACHINES_LOCATION_NAME = 'Unassigned Machines'; private referenceDataUpdatedSubscription: any; constructor(props: OverviewProps) { super(props); this._selectedLocationStorageKeyName = props.selectedLocationStorageKeyName; this.state = { isLoading: false, isMutating: false, showConfigModal: false, machines: [], locations: [], lines: [], machineIdDelimiter: '/', newLocationUIReferenceMapping: new Set(), newLineUIReferenceMapping: new Set() }; this.onLocationSelect = this.onLocationSelect.bind(this); this.onClickConfig = this.onClickConfig.bind(this); this.onCloseConfigModal = this.onCloseConfigModal.bind(this); this.onChangeUIReferenceMapping = this.onChangeUIReferenceMapping.bind(this); this.onUpdateUIReferenceMapping = this.onUpdateUIReferenceMapping.bind(this); } async componentDidMount() { this._isMounted = true; await this.getReferenceData(); await this.getMessageFormatConfigItem(); // Retrieve the last selected location and set that to be the current selected location try { const selectedLocationId = window.localStorage.getItem(this._selectedLocationStorageKeyName); if (selectedLocationId) { this.onLocationSelect(selectedLocationId); } } catch (err) { console.error(err); } // @ts-ignore this.referenceDataUpdatedSubscription = API.graphql(graphqlOperation(onUpdateUIReferenceItem)).subscribe({ next: (response: IOnUpdateUIReferenceItemResponse) => this.handleUpdatedReferenceData(response.value.data.onUpdateUIReferenceItem), error: (err: any) => { // If there's an error (e.g. connection closed), reload the window. console.error('Subscription error', err); window.location.reload(); } }); } componentWillUnmount() { this._isMounted = false; if (this.referenceDataUpdatedSubscription) { this.referenceDataUpdatedSubscription.unsubscribe(); } } async getReferenceData() { if (!this._isMounted) { return; } this.setState({ isLoading: true }); const resp = (await API.graphql({ query: getUIReferenceItems }) as IGetUIReferenceItemsResponse); for (const referenceDataItem of resp.data.getUIReferenceItems) { this.handleUpdatedReferenceData(referenceDataItem); } this.checkSelectedLocation(); this.setState({ isLoading: false }); } async getMessageFormatConfigItem() { if (!this._isMounted) { return; } this.setState({ isLoading: true }); const resp = (await API.graphql({ query: getConfigItem, variables: { input: { id: 'DEFAULT', type: ConfigType.MESSAGE_FORMAT } } }) as IGetConfigItemResponse); const msgFormatConfigItem = resp.data.getConfigItem as IMessageFormatConfigItem; this.setState({ isLoading: false, machineIdDelimiter: msgFormatConfigItem.msgFormatDataAliasDelimiter }); } checkSelectedLocation() { // Set the selected location to the first location by default if (this.state.locations.length > 0 && !this.state.locations.some(loc => loc.isSelected)) { this.onLocationSelect(this.state.locations[0].id); } } handleUpdatedReferenceData(newRefData: IReferenceDataItem) { if (!this._isMounted) { return; } switch (newRefData.type) { case ReferenceDataTypes.LOCATION: this.addLocation(newRefData as ILocationReferenceDataItem); break; case ReferenceDataTypes.LINE: this.addLine(newRefData as ILineReferenceDataItem); break; case ReferenceDataTypes.MACHINE: this.addMachine(newRefData as IMachineReferenceDataItem); break; case ReferenceDataTypes.UI_REFERENCE_MAPPING: this.addUIReferenceMapping(newRefData as IUIReferenceMappingItem); break; } // Check if there are any machines not assigned to a location const machines = this.state.machines; const locations = this.state.locations; if (machines.some(machine => !machine.locationId)) { // Check if the list of locations has an item for unassigned machines. If not, create one if (!locations.some(loc => loc.id === this.UNASSIGNED_MACHINES_LOCATION_ID)) { locations.unshift({ id: this.UNASSIGNED_MACHINES_LOCATION_ID, type: ReferenceDataTypes.LOCATION, name: this.UNASSIGNED_MACHINES_LOCATION_NAME, // Only select the location for unassigned machines if there is no previous location selected isSelected: locations.find(loc => loc.isSelected) ? false : true }); } } this.setState({ ...this.state, locations, machines }); } addLocation(newLocation: ILocationReferenceDataItem) { if (!this._isMounted) { return; } const locations = this.state.locations; const matchingLocationIdx = locations.findIndex(loc => loc.id === newLocation.id); let isSelected = (locations.length === 0); if (matchingLocationIdx > -1) { // Remove the previous location from the array const previousLocation = locations.splice(matchingLocationIdx, 1); isSelected = previousLocation[0].isSelected; } locations.push({ ...newLocation, isSelected }); locations.sort(this.sortByNameOrId); this.setState({ ...this.state, locations }); } addLine(newLine: ILineReferenceDataItem) { if (!this._isMounted) { return; } const lines = this.state.lines; const matchingLineIdx = lines.findIndex(line => line.id === newLine.id); if (matchingLineIdx > -1) { // Remove the previous line from the array lines.splice(matchingLineIdx, 1); } lines.push(newLine); lines.sort(this.sortByNameOrId); this.setState({ ...this.state, lines }); } addMachine(newMachine: IMachineReferenceDataItem) { if (!this._isMounted) { return; } const machines = this.state.machines; const matchingMachineIdx = machines.findIndex(machine => machine.id === newMachine.id); if (matchingMachineIdx > -1) { machines.splice(matchingMachineIdx, 1); } if (this.state.uiReferenceMapping) { const delim = this.state.machineIdDelimiter; const splitMachineAlias = newMachine.id.split(delim); const splitLocationKeys = this.state.uiReferenceMapping.uiReferenceMappingLocationKeys .split(delim) .map(key => parseInt(key, 10)); const locationId = splitLocationKeys .map(idx => splitMachineAlias[idx]) .join(delim); const splitLineKeys = this.state.uiReferenceMapping.uiReferenceMappingLineKeys .split(delim) .map(key => parseInt(key, 10)); const lineId = splitLineKeys .map(idx => splitMachineAlias[idx]) .join(delim); if (locationId) { this.addLocation({ id: locationId, type: ReferenceDataTypes.LOCATION, isSelected: false }); } this.addLine({ id: lineId, type: ReferenceDataTypes.LINE, locationId }); newMachine.locationId = locationId; newMachine.lineId = lineId; } machines.push(newMachine); machines.sort(this.sortByNameOrId); this.setState({ ...this.state, machines }); } addUIReferenceMapping(uiReferenceMapping: IUIReferenceMappingItem): void { if (!this._isMounted) { return; } // Create a temporary copy of the machines array as we will be modifying the state // while iterating over each machine const machines = this.state.machines.slice(); this.setState({ uiReferenceMapping, locations: [], lines: [] }); // Setting the machine again will recreate all locations and lines machines.forEach(machine => { this.addMachine({ id: machine.id, type: ReferenceDataTypes.MACHINE, machineStatus: machine.machineStatus, machineStatusUpdatedTimestamp: machine.machineStatusUpdatedTimestamp, name: machine.name }); }); } onLocationSelect(selectedLocationId: string) { if (!this._isMounted) { return; } const locations = this.state.locations; // Set the location only if the selectedLocationId matches one of the locations if (locations.find(loc => loc.id === selectedLocationId)) { locations.forEach(loc => loc.isSelected = (loc.id === selectedLocationId)); window.localStorage.setItem(this._selectedLocationStorageKeyName, selectedLocationId); this.setState({ locations }); } } sortByNameOrId(a: IReferenceDataItem, b: IReferenceDataItem) { if (a.name && b.name) { return a.name.localeCompare(b.name); } else { return a.id.localeCompare(b.id); } } loadingIndicator() { return (); } showLineHeader(willDisplayLines: boolean) { if (!willDisplayLines) { return null; } return ( {`${I18n.get('legend.lines')}`}: {`${I18n.get('legend.machines')}`}: ); } showMachines() { if (!this.state.machines || this.state.machines.length === 0) { return (); } const selectedLocation = this.state.locations.find(loc => loc.isSelected); if (!selectedLocation) { return (); } const machinesInLines: { [key: string]: IMachineReferenceDataItem[] } = {}; const unassignedMachines: IMachineReferenceDataItem[] = []; this.state.machines .filter(machine => { if ((selectedLocation.id === this.UNASSIGNED_MACHINES_LOCATION_ID && !machine.locationId) || (machine.locationId === selectedLocation.id)) { return true; } return false; }) .forEach(machine => { if (machine.lineId) { if (!machinesInLines[machine.lineId]) { machinesInLines[machine.lineId] = []; } machinesInLines[machine.lineId].push(machine); } else { unassignedMachines.push(machine); } }); return ( <> {this.showLineHeader(Object.keys(machinesInLines).length > 0)} {Object.keys(machinesInLines).map( (lineId) => ( line.id === lineId)} machines={machinesInLines[lineId]}> ))} {unassignedMachines.map( (machine) => ( ))} ); } async onClickConfig() { if (!this._isMounted) { return; } const locationSelection = new Set(); const lineSelection = new Set(); if (this.state.uiReferenceMapping) { // Retrieve the current configuration from the state const delim = this.state.machineIdDelimiter; this.state.uiReferenceMapping.uiReferenceMappingLocationKeys .split(delim) .forEach(key => locationSelection.add(key)); this.state.uiReferenceMapping.uiReferenceMappingLineKeys .split(delim) .forEach(key => lineSelection.add(key)); } this.setState({ showConfigModal: true, newLineUIReferenceMapping: lineSelection, newLocationUIReferenceMapping: locationSelection }); } onCloseConfigModal() { if (!this._isMounted) { return; } this.setState({ showConfigModal: false }); } showModalBody() { if (!this.state.machines || this.state.machines.length === 0) { return null; } const delim = this.state.machineIdDelimiter; const sampleMachineId = this.state.machines[0].id; const sampleMachineIdTokens = sampleMachineId.split(delim); sampleMachineIdTokens.pop(); // Remove the machine name return ( {`${I18n.get('modal.configGrouping.sampleMachineLabel')}`}: {sampleMachineId} {`${I18n.get('modal.configGrouping.machineLocationSelectionLabel')}`} {Object.keys(sampleMachineIdTokens).map(idx => ( ))} {`${I18n.get('modal.configGrouping.machineLineSelectionLabel')}`} {Object.keys(sampleMachineIdTokens).map(idx => ( ))} {`${I18n.get('modal.configGrouping.locationPreviewLabel')}`}: {Array.from(this.state.newLocationUIReferenceMapping) .map(key => (sampleMachineIdTokens[parseInt(key, 10)])) .join(delim)} {`${I18n.get('modal.configGrouping.linePreviewLabel')}`}: {Array.from(this.state.newLineUIReferenceMapping) .map(key => (sampleMachineIdTokens[parseInt(key, 10)])) .join(delim)} ); } onChangeUIReferenceMapping(event: any) { if (!this._isMounted) { return; } const checkboxTarget = event.currentTarget; const checkboxId = checkboxTarget.id; const checkboxType = checkboxId.split('-')[0]; const checkboxValue = checkboxId.split('-')[1]; if (checkboxType === 'line') { const lineKeys = this.state.newLineUIReferenceMapping; if (checkboxTarget.checked) { if (this.state.newLocationUIReferenceMapping.has(checkboxValue)) { alert('This already selected as part of the Location grouping'); checkboxTarget.checked = false; } else { lineKeys.add(checkboxValue); } } else { lineKeys.delete(checkboxValue); } lineKeys.delete(''); this.setState({ newLineUIReferenceMapping: lineKeys }); } else if (checkboxType === 'location') { const locationKeys = this.state.newLocationUIReferenceMapping; if (checkboxTarget.checked) { if (this.state.newLineUIReferenceMapping.has(checkboxValue)) { alert('This already selected as part of the Line grouping'); checkboxTarget.checked = false; } else { locationKeys.add(checkboxValue); } } else { locationKeys.delete(checkboxValue); } locationKeys.delete(''); this.setState({ newLocationUIReferenceMapping: locationKeys }); } } async onUpdateUIReferenceMapping() { if (!this._isMounted) { return; } this.setState({ isMutating: true }); await API.graphql({ query: updateMachineGrouping, variables: { input: { uiReferenceMappingLocationKeys: Array.from(this.state.newLocationUIReferenceMapping).join(this.state.machineIdDelimiter), uiReferenceMappingLineKeys: Array.from(this.state.newLineUIReferenceMapping).join(this.state.machineIdDelimiter) } } }); this.setState({ isMutating: false, showConfigModal: false }); } render() { if (this.state.isLoading) { return this.loadingIndicator(); } const selectedLocation = this.state.locations.find(loc => loc.isSelected); if (!selectedLocation) { return (); } let configBtn = null; if (this.state.machines && this.state.machines.length > 0) { configBtn = ( ); } return ( <>
{`${I18n.get('legend.location')}`}: 
this.onLocationSelect(eventKey || '')}> {this.state.locations.map( (location) => ( {location.name || location.id} ) )} {configBtn}
{this.showMachines()}
{`${I18n.get('modal.configGrouping.header')}`} {this.showModalBody()} ); } }