Path: blob/trunk/javascript/grid-ui/src/screens/Overview/Overview.tsx
2887 views
// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617import Grid from '@mui/material/Grid'18import Paper from '@mui/material/Paper'19import { loader } from 'graphql.macro'20import React from 'react'21import { useState, useEffect, useMemo } from 'react'22import {23Box,24Checkbox,25FormControl,26FormControlLabel,27InputLabel,28MenuItem,29Select30} from '@mui/material'31import Node from '../../components/Node/Node'32import { useQuery } from '@apollo/client'33import NodeInfo from '../../models/node-info'34import OsInfo from '../../models/os-info'35import NoData from '../../components/NoData/NoData'36import Loading from '../../components/Loading/Loading'37import Error from '../../components/Error/Error'38import StereotypeInfo from '../../models/stereotype-info'39import browserVersion from '../../util/browser-version'40import Capabilities from '../../models/capabilities'41import { GridConfig } from '../../config'42import { NODES_QUERY } from '../../graphql/nodes'43import { GRID_SESSIONS_QUERY } from '../../graphql/sessions'4445function Overview (): JSX.Element {46const { loading, error, data } = useQuery(NODES_QUERY, {47pollInterval: GridConfig.status.xhrPollingIntervalMillis,48fetchPolicy: 'network-only'49})5051const { data: sessionsData } = useQuery(GRID_SESSIONS_QUERY, {52pollInterval: GridConfig.status.xhrPollingIntervalMillis,53fetchPolicy: 'network-only'54})5556function compareSlotStereotypes(a: NodeInfo, b: NodeInfo, attribute: string): number {57const joinA = a.slotStereotypes.length === 158? a.slotStereotypes[0][attribute]59: a.slotStereotypes.slice().map(st => st[attribute]).reverse().join(',')60const joinB = b.slotStereotypes.length === 161? b.slotStereotypes[0][attribute]62: b.slotStereotypes.slice().map(st => st[attribute]).reverse().join(',')63return joinA.localeCompare(joinB)64}6566const sortProperties = {67'platformName': (a, b) => compareSlotStereotypes(a, b, 'platformName'),68'status': (a, b) => (a.status ?? '').localeCompare(b.status ?? ''),69'uri': (a, b) => (a.uri ?? '').localeCompare(b.uri ?? ''),70'browserName': (a, b) => compareSlotStereotypes(a, b, 'browserName'),71'browserVersion': (a, b) => compareSlotStereotypes(a, b, 'browserVersion'),72'slotCount': (a, b) => {73const valueA = a.slotStereotypes.reduce((sum, st) => sum + st.slotCount, 0)74const valueB = b.slotStereotypes.reduce((sum, st) => sum + st.slotCount, 0)75return valueA < valueB ? -1 : 176},77'id': (a, b) => (a.id < b.id ? -1 : 1)78}7980const sortPropertiesLabel = {81'platformName': 'Platform Name',82'status': 'Status',83'uri': 'URI',84'browserName': 'Browser Name',85'browserVersion': 'Browser Version',86'slotCount': 'Slot Count',87'id': 'ID'88}8990const [sortOption, setSortOption] = useState(Object.keys(sortProperties)[0])91const [sortOrder, setSortOrder] = useState(1)92const [sortedNodes, setSortedNodes] = useState<NodeInfo[]>([])93const [isDescending, setIsDescending] = useState(false)9495const handleSortChange = (event: React.ChangeEvent<{ value: unknown }>) => {96setSortOption(event.target.value as string)97}9899const handleOrderChange = (event: React.ChangeEvent<HTMLInputElement>) => {100setIsDescending(event.target.checked)101setSortOrder(event.target.checked ? -1 : 1)102}103104const sortNodes = useMemo(() => {105return (nodes: NodeInfo[], option: string, order: number) => {106const sortFn = sortProperties[option] || (() => 0)107return nodes.sort((a, b) => order * sortFn(a, b))108}109}, [sortOption, sortOrder])110111useEffect(() => {112if (data) {113const unSortedNodes = data.nodesInfo.nodes.map((node) => {114const osInfo: OsInfo = {115name: node.osInfo.name,116version: node.osInfo.version,117arch: node.osInfo.arch118}119120interface StereoTypeData {121stereotype: Capabilities;122slots: number;123}124125const slotStereotypes = (JSON.parse(126node.stereotypes) as StereoTypeData[]).map((item) => {127const slotStereotype: StereotypeInfo = {128browserName: item.stereotype.browserName ?? '',129browserVersion: browserVersion(130item.stereotype.browserVersion ?? item.stereotype.version),131platformName: (item.stereotype.platformName132?? item.stereotype.platform) ?? '',133slotCount: item.slots,134rawData: item135}136return slotStereotype137})138139const newNode: NodeInfo = {140uri: node.uri,141id: node.id,142status: node.status,143maxSession: node.maxSession,144slotCount: node.slotCount,145version: node.version,146osInfo: osInfo,147sessionCount: node.sessionCount ?? 0,148slotStereotypes: slotStereotypes149}150return newNode151})152153setSortedNodes(sortNodes(unSortedNodes, sortOption, sortOrder))154}155}, [data, sortOption, sortOrder])156157if (error !== undefined) {158const message = 'There has been an error while loading the Nodes from the Grid.'159const errorMessage = error?.networkError?.message160return (161<Grid container spacing={3}>162<Error message={message} errorMessage={errorMessage}/>163</Grid>164)165}166167if (loading) {168return (169<Grid container spacing={3}>170<Loading/>171</Grid>172)173}174175if (sortedNodes.length === 0) {176const shortMessage = 'The Grid has no registered Nodes yet.'177return (178<Grid container spacing={3}>179<NoData message={shortMessage}/>180</Grid>181)182}183184return (185<Grid container>186<Grid item xs={12}187style={{ display: 'flex', justifyContent: 'flex-start' }}>188<FormControl variant="outlined" style={{ marginBottom: '16px' }}>189<InputLabel>Sort By</InputLabel>190<Box display="flex" alignItems="center">191<Select value={sortOption} onChange={handleSortChange}192label="Sort By" style={{ minWidth: '170px' }}>193{Object.keys(sortProperties).map((key) => (194<MenuItem key={key} value={key}>195{sortPropertiesLabel[key]}196</MenuItem>197))}198</Select>199<FormControlLabel200control={<Checkbox checked={isDescending}201onChange={handleOrderChange}/>}202label="Descending"203style={{ marginLeft: '8px' }}204/>205</Box>206</FormControl>207</Grid>208{sortedNodes.map((node, index) => {209return (210<Grid211item212lg={6}213sm={12}214xl={4}215xs={12}216key={index}217paddingX={1}218paddingY={1}219>220<Paper221sx={{222display: 'flex',223overflow: 'auto',224flexDirection: 'column'225}}226>227<Node228node={node}229sessions={sessionsData?.sessionsInfo?.sessions?.filter(230session => session.nodeId === node.id231) || []}232origin={window.location.origin}233/>234</Paper>235</Grid>236)237})}238</Grid>239)240}241242export default Overview243244245