Merge pull request #180 from OktopUSP/dev

Dev
This commit is contained in:
Leandro Antônio Farias Machado 2024-01-11 19:33:06 -03:00 committed by GitHub
commit 15953af96d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 279 additions and 50 deletions

View File

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2023] [Leandro Antônio Farias Machado]
Copyright 2024 Leandro Antônio Farias Machado
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@ -1,7 +1,13 @@
<p align="center">
<img src="https://github.com/OktopUSP/oktopus/assets/83298718/fc05c512-951d-448c-8c31-1e0881783460"/>
</p>
<img src="https://github.com/OktopUSP/oktopus/assets/83298718/fc05c512-951d-448c-8c31-1e0881783460"/></p>
<br/>
<ul>
<li>
<h4>Overview:</h4>
</li>
</ul>
<img src="https://github.com/OktopUSP/oktopus/assets/83298718/11401f40-2e73-4610-97bb-29fea7e1e5e0"/>
image source = <a href="https://oktopus.app.br/controller">https://oktopus.app.br/controller</a>
<ul>
<li>
<h4>Introduction:</h4>
@ -239,7 +245,6 @@ OBS: Do not use those instructions in production. To implement the project in pr
<img src="https://github.com/OktopUSP/oktopus/assets/83298718/4599d566-eada-4313-8ae1-31dae82391de"/>
<img src="https://github.com/OktopUSP/oktopus/assets/83298718/501b4ccd-6147-4957-9096-695134e34b5e"/>
</li>
<h4>Agent Simulation:</h4>
<p>
In case you want a more complete and real-world simulation of devices you might use <a href="https://github.com/OktopUSP/agent-sim">Oktopus TR-369 Agent Simulator</a>.
</p>

1
agent/run.sh Normal file
View File

@ -0,0 +1 @@
obuspa -p -v 4 -r ./oktopus-mqtt-obuspa.txt -i lo

10
backend/services/controller/go.mod Executable file → Normal file
View File

@ -3,21 +3,22 @@ module github.com/leandrofars/oktopus
go 1.18
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/eclipse/paho.golang v0.10.0
github.com/go-stomp/stomp v2.1.4+incompatible
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/google/uuid v1.3.0
github.com/googollee/go-socket.io v1.7.0
github.com/gorilla/mux v1.8.0
github.com/joho/godotenv v1.5.1
github.com/rs/cors v1.9.0
go.mongodb.org/mongo-driver v1.11.3
golang.org/x/crypto v0.14.0
golang.org/x/crypto v0.17.0
golang.org/x/net v0.17.0
golang.org/x/sys v0.15.0
google.golang.org/protobuf v1.28.1
)
require (
github.com/go-stomp/stomp v2.1.4+incompatible // indirect
github.com/gofrs/uuid v4.0.0+incompatible // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/gomodule/redigo v1.8.4 // indirect
@ -30,6 +31,5 @@ require (
github.com/xdg-go/stringprep v1.0.3 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/text v0.14.0 // indirect
)

View File

@ -1,14 +1,14 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/eclipse/paho.golang v0.10.0 h1:oUGPjRwWcZQRgDD9wVDV7y7i7yBSxts3vcvcNJo8B4Q=
github.com/eclipse/paho.golang v0.10.0/go.mod h1:rhrV37IEwauUyx8FHrvmXOKo+QRKng5ncoN1vJiJMcs=
github.com/go-stomp/stomp v2.1.4+incompatible h1:D3SheUVDOz9RsjVWkoh/1iCOwD0qWjyeTZMUZ0EXg2Y=
github.com/go-stomp/stomp v2.1.4+incompatible/go.mod h1:VqCtqNZv1226A1/79yh+rMiFUcfY3R109np+7ke4n0c=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@ -60,8 +60,8 @@ github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7Jul
go.mongodb.org/mongo-driver v1.11.3 h1:Ql6K6qYHEzB6xvu4+AU0BoRoqf9vFPcc4o7MUIdPW8Y=
go.mongodb.org/mongo-driver v1.11.3/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
@ -71,13 +71,13 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -85,6 +85,7 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -2,9 +2,12 @@ package auth
import (
"errors"
"github.com/dgrijalva/jwt-go"
"fmt"
"log"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
)
func getJwtKey() []byte {
@ -18,16 +21,17 @@ func getJwtKey() []byte {
type JWTClaim struct {
Username string `json:"username"`
Email string `json:"email"`
jwt.StandardClaims
jwt.RegisteredClaims
}
func GenerateJWT(email string, username string) (tokenString string, err error) {
expirationTime := time.Now().Add(4 * time.Hour)
claims := &JWTClaim{
Email: email,
Username: username,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
username,
email,
jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
Issuer: "Oktopus",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
@ -40,21 +44,25 @@ func ValidateToken(signedToken string) (email string, err error) {
signedToken,
&JWTClaim{},
func(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
// hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
return getJwtKey(), nil
},
)
if err != nil {
log.Println(err)
return
}
claims, ok := token.Claims.(*JWTClaim)
if !ok {
err = errors.New("couldn't parse claims")
return
}
if claims.ExpiresAt < time.Now().Local().Unix() {
err = errors.New("token expired")
return
}
email = claims.Email

View File

@ -2,11 +2,12 @@ package api
import (
"encoding/json"
"go.mongodb.org/mongo-driver/bson"
"log"
"net/http"
"strconv"
"go.mongodb.org/mongo-driver/bson"
"github.com/gorilla/mux"
"github.com/leandrofars/oktopus/internal/db"
usp_msg "github.com/leandrofars/oktopus/internal/usp_message"
@ -37,11 +38,15 @@ func (a *Api) retrieveDevices(w http.ResponseWriter, r *http.Request) {
const PAGE_SIZE_DEFAULT = 20
// Get specific device
id := mux.Vars(r)["id"]
id := r.URL.Query().Get("id")
if id != "" {
device, err := a.Db.RetrieveDevice(id)
if err != nil {
log.Println(err)
if err == mongo.ErrNoDocuments {
json.NewEncoder(w).Encode("Device id: " + id + " not found")
return
}
json.NewEncoder(w).Encode(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
@ -59,7 +64,7 @@ func (a *Api) retrieveDevices(w http.ResponseWriter, r *http.Request) {
var page_number int64
if page_n == "" {
page_number = 1
page_number = 0
} else {
page_number, err = strconv.ParseInt(page_n, 10, 64)
if err != nil {
@ -93,6 +98,7 @@ func (a *Api) retrieveDevices(w http.ResponseWriter, r *http.Request) {
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode("Unable to get devices count from database")
return
}
skip := page_number * (page_size - 1)
@ -103,9 +109,12 @@ func (a *Api) retrieveDevices(w http.ResponseWriter, r *http.Request) {
//TODO: Create filters
//TODO: Create sorting
sort := bson.M{}
sort["status"] = 1
filter := bson.A{
//bson.M{"$match": filter},
//bson.M{"$sort": sort},
bson.M{"$sort": sort}, // shows online devices first
bson.M{"$skip": skip},
bson.M{"$limit": page_size},
}
@ -117,12 +126,15 @@ func (a *Api) retrieveDevices(w http.ResponseWriter, r *http.Request) {
return
}
err = json.NewEncoder(w).Encode(devices)
err = json.NewEncoder(w).Encode(map[string]interface{}{
"pages": total / page_size,
"page": page_number,
"size": page_size,
"devices": devices,
})
if err != nil {
log.Println(err)
}
return
}
func (a *Api) deviceCreateMsg(w http.ResponseWriter, r *http.Request) {

View File

@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@koa/cors": "^4.0.0",
"@koa/cors": "^5.0.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
@ -18,9 +18,9 @@
}
},
"node_modules/@koa/cors": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@koa/cors/-/cors-4.0.0.tgz",
"integrity": "sha512-Y4RrbvGTlAaa04DBoPBWJqDR5gPj32OOz827ULXfgB1F7piD1MB/zwn8JR2LAnvdILhxUbXbkXGWuNVsFuVFCQ==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@koa/cors/-/cors-5.0.0.tgz",
"integrity": "sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==",
"dependencies": {
"vary": "^1.1.2"
},
@ -1084,9 +1084,9 @@
},
"dependencies": {
"@koa/cors": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@koa/cors/-/cors-4.0.0.tgz",
"integrity": "sha512-Y4RrbvGTlAaa04DBoPBWJqDR5gPj32OOz827ULXfgB1F7piD1MB/zwn8JR2LAnvdILhxUbXbkXGWuNVsFuVFCQ==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@koa/cors/-/cors-5.0.0.tgz",
"integrity": "sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==",
"requires": {
"vary": "^1.1.2"
}

View File

@ -11,7 +11,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"@koa/cors": "^4.0.0",
"@koa/cors": "^5.0.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",

View File

@ -34,6 +34,44 @@ http {
# for more information.
include /etc/nginx/conf.d/*.conf;
server {
if ($host = oktopus.app.br) {
return 301 https://$host$request_uri;
}
listen 80;
listen [::]:80;
server_name oktopus.app.br;
return 404;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name oktopus.app.br;
root /usr/share/nginx/html;
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
ssl_certificate "/etc/letsencrypt/live/oktopus.app.br/fullchain.pem";
ssl_certificate_key "/etc/letsencrypt/live/oktopus.app.br/privkey.pem";
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
error_page 404 /404.html;
location = /404.html {
}
location / {
proxy_pass http://127.0.0.1:3001;
proxy_read_timeout 60;
proxy_connect_timeout 60;
proxy_redirect off;
}
}
server {
if ($host = oktopustr369.com) {
return 301 https://$host$request_uri;

View File

@ -1,6 +1,18 @@
import React, { useState, useEffect } from 'react';
import Head from 'next/head';
import { Box, Container, Unstable_Grid2 as Grid } from '@mui/material';
import {
Box,
Container,
Unstable_Grid2 as Grid,
Card,
OutlinedInput,
InputAdornment,
SvgIcon,
Stack,
Pagination,
CircularProgress
} from '@mui/material';
import MagnifyingGlassIcon from '@heroicons/react/24/solid/MagnifyingGlassIcon';
import { Layout as DashboardLayout } from 'src/layouts/dashboard/layout';
import { OverviewLatestOrders } from 'src/sections/overview/overview-latest-orders';
import { useAuth } from 'src/hooks/use-auth';
@ -10,9 +22,14 @@ const Page = () => {
const router = useRouter()
const auth = useAuth();
const [devices, setDevices] = useState([]);
const [deviceFound, setDeviceFound] = useState(false)
const [pages, setPages] = useState(0);
const [page, setPage] = useState(null);
const [Loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true)
if (auth.user.token) {
console.log("auth.user.token =", auth.user.token)
}else{
@ -36,13 +53,106 @@ const Page = () => {
return response.json()
})
.then(json => {
return setDevices(json)
setPages(json.pages + 1)
setPage(json.page)
setDevices(json.devices)
setLoading(false)
return setDeviceFound(true)
})
.catch(error => {
return console.error('Error:', error)
});
}, [auth.user]);
const handleChangePage = (event, value) => {
console.log("new page: ", value)
setPage(value)
fetchDevicePerPage(value)
}
const fetchDevicePerPage = async (p) => {
setLoading(true)
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("Authorization", auth.user.token);
var requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
}
p = p - 1
p = p.toString()
fetch(process.env.NEXT_PUBLIC_REST_ENPOINT+'/device?page_number='+p, requestOptions)
.then(response => {
if (response.status === 401)
router.push("/auth/login")
return response.json()
})
.then(json => {
setDevices(json.devices)
setLoading(false)
return
})
.catch(error => {
return console.error('Error:', error)
});
}
const fetchDevicePerId = async (id) => {
setLoading(true)
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("Authorization", auth.user.token);
var requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
}
if (id == ""){
return fetch(process.env.NEXT_PUBLIC_REST_ENPOINT+'/device', requestOptions)
.then(response => {
if (response.status === 401)
router.push("/auth/login")
return response.json()
})
.then(json => {
setPages(json.pages + 1)
setPage(json.page)
setDevices(json.devices)
setLoading(false)
return setDeviceFound(true)
})
.catch(error => {
return console.error('Error:', error)
});
}
let response = await fetch(process.env.NEXT_PUBLIC_REST_ENPOINT+'/device?id='+id, requestOptions)
if (response.status === 401)
router.push("/auth/login")
let json = await response.json()
if (json.MTP != undefined){
setDevices([json])
setDeviceFound(true)
setLoading(false)
setPages(1)
setPage(1)
}else{
setDeviceFound(false)
setDevices([])
setPages(1)
setPage(1)
setLoading(false)
}
}
return (
<>
<Head>
@ -50,6 +160,7 @@ const Page = () => {
Oktopus | TR-369
</title>
</Head>
<Box
component="main"
sx={{
@ -58,15 +169,68 @@ const Page = () => {
}}
>
<Container maxWidth="xl" >
<Stack spacing={3}>
<Grid
container
spacing={3}
>
<Grid xs={8}>
</Grid>
<OutlinedInput
xs={4}
defaultValue=""
fullWidth
placeholder="Search Device"
onKeyDownCapture={(e) => {
if (e.key === 'Enter') {
console.log("Fetch devices per id: ", e.target.value)
fetchDevicePerId(e.target.value)
}
}}
startAdornment={(
<InputAdornment position="start">
<SvgIcon
color="action"
fontSize="small"
>
<MagnifyingGlassIcon />
</SvgIcon>
</InputAdornment>
)}
sx={{ maxWidth: 500 }}
/>
</Grid>
{deviceFound ?
( !Loading ?
<OverviewLatestOrders
orders={devices}
sx={{ height: '100%' }}
/>
/> : <CircularProgress></CircularProgress>
)
:
<Box
sx={{
display: 'flex',
justifyContent: 'center'
}}
>
<p>Device Not Found</p>
</Box>
}
<Box
sx={{
display: 'flex',
justifyContent: 'center'
}}
>
{pages ? <Pagination
count={pages}
size="small"
page={page}
onChange={handleChangePage}
/>: null}
{/* //TODO: show loading */}
</Box>
</Stack>
</Container>
</Box>
</>