Merge pull request #254 from OktopUSP/dev
Kubernetes Improvements + Frontend Features
This commit is contained in:
commit
c48a44f6ec
|
|
@ -4,11 +4,13 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/mail"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/leandrofars/oktopus/internal/api/auth"
|
"github.com/leandrofars/oktopus/internal/api/auth"
|
||||||
"github.com/leandrofars/oktopus/internal/db"
|
"github.com/leandrofars/oktopus/internal/db"
|
||||||
"github.com/leandrofars/oktopus/internal/utils"
|
"github.com/leandrofars/oktopus/internal/utils"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *Api) retrieveUsers(w http.ResponseWriter, r *http.Request) {
|
func (a *Api) retrieveUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -20,6 +22,11 @@ func (a *Api) retrieveUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, x := range users {
|
for _, x := range users {
|
||||||
|
objectID, ok := x["_id"].(primitive.ObjectID)
|
||||||
|
if ok {
|
||||||
|
creationTime := objectID.Timestamp()
|
||||||
|
x["createdAt"] = creationTime.Format("02/01/2006")
|
||||||
|
}
|
||||||
delete(x, "password")
|
delete(x, "password")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,7 +34,6 @@ func (a *Api) retrieveUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Api) registerUser(w http.ResponseWriter, r *http.Request) {
|
func (a *Api) registerUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -64,12 +70,27 @@ func (a *Api) registerUser(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user.Email == "" || user.Password == "" || !valid(user.Email) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := a.db.RegisterUser(user); err != nil {
|
if err := a.db.RegisterUser(user); err != nil {
|
||||||
|
if err == db.ErrorUserExists {
|
||||||
|
w.WriteHeader(http.StatusConflict)
|
||||||
|
w.Write([]byte("User with this email already exists"))
|
||||||
|
return
|
||||||
|
}
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func valid(email string) bool {
|
||||||
|
_, err := mail.ParseAddress(email)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Api) deleteUser(w http.ResponseWriter, r *http.Request) {
|
func (a *Api) deleteUser(w http.ResponseWriter, r *http.Request) {
|
||||||
tokenString := r.Header.Get("Authorization")
|
tokenString := r.Header.Get("Authorization")
|
||||||
if tokenString == "" {
|
if tokenString == "" {
|
||||||
|
|
@ -84,21 +105,21 @@ func (a *Api) deleteUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
//Check if user which is requesting deletion has the necessary privileges
|
//Check if user which is requesting deletion has the necessary privileges
|
||||||
rUser, err := a.db.FindUser(email)
|
rUser, err := a.db.FindUser(email)
|
||||||
if rUser.Level != AdminUser {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userEmail := mux.Vars(r)["user"]
|
userEmail := mux.Vars(r)["user"]
|
||||||
if userEmail == email {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := a.db.DeleteUser(userEmail); err != nil {
|
if rUser.Email == userEmail || (rUser.Level == AdminUser && rUser.Email != userEmail) { //Admin can delete any account, but admin account can never be deleted
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
if err := a.db.DeleteUser(userEmail); err != nil {
|
||||||
json.NewEncoder(w).Encode(err)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
return
|
json.NewEncoder(w).Encode(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
"go.mongodb.org/mongo-driver/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
"go.mongodb.org/mongo-driver/mongo/options"
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
)
|
)
|
||||||
|
|
@ -33,6 +34,15 @@ func NewDatabase(ctx context.Context, mongoUri string) Database {
|
||||||
log.Println("Connected to MongoDB-->", mongoUri)
|
log.Println("Connected to MongoDB-->", mongoUri)
|
||||||
|
|
||||||
db.users = client.Database("account-mngr").Collection("users")
|
db.users = client.Database("account-mngr").Collection("users")
|
||||||
|
indexField := bson.M{"email": 1}
|
||||||
|
_, err = db.users.Indexes().CreateOne(ctx, mongo.IndexModel{
|
||||||
|
Keys: indexField,
|
||||||
|
Options: options.Index().SetUnique(true),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
db.ctx = ctx
|
db.ctx = ctx
|
||||||
|
|
||||||
return db
|
return db
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"go.mongodb.org/mongo-driver/bson"
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
|
|
@ -13,8 +14,11 @@ type User struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
Level int `json:"level"`
|
Level int `json:"level"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ErrorUserExists = errors.New("User already exists")
|
||||||
|
|
||||||
func (d *Database) RegisterUser(user User) error {
|
func (d *Database) RegisterUser(user User) error {
|
||||||
err := d.users.FindOne(d.ctx, bson.D{{"email", user.Email}}).Err()
|
err := d.users.FindOne(d.ctx, bson.D{{"email", user.Email}}).Err()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -23,8 +27,10 @@ func (d *Database) RegisterUser(user User) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
return ErrorUserExists
|
||||||
}
|
}
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Database) UpdatePassword(user User) error {
|
func (d *Database) UpdatePassword(user User) error {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,15 @@ git clone https://github.com/OktopUSP/oktopus
|
||||||
export DEPLOYMENT_PATH=oktopus/deploy/kubernetes
|
export DEPLOYMENT_PATH=oktopus/deploy/kubernetes
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## HAProxy Ingress Controller
|
||||||
|
|
||||||
|
```shell
|
||||||
|
helm install haproxy-kubernetes-ingress haproxytech/kubernetes-ingress \
|
||||||
|
--create-namespace \
|
||||||
|
--namespace haproxy-controller \
|
||||||
|
--set controller.kind=DaemonSet \
|
||||||
|
--set controller.daemonset.useHostPort=true
|
||||||
|
```
|
||||||
|
|
||||||
## MongoBD
|
## MongoBD
|
||||||
|
|
||||||
|
|
@ -49,27 +58,6 @@ helm install nats nats/nats --set config.jetstream.enabled=true
|
||||||
|
|
||||||
## Oktopus
|
## Oktopus
|
||||||
|
|
||||||
|
|
||||||
<b>Node Ports</b>
|
|
||||||
|
|
||||||
For this deployment, we are not using a load balancer and kubernetes is deployed on-premises so we are using Nodeports to insource the client traffic into cluster. below the ports set on deployment files:
|
|
||||||
|
|
||||||
1. MQTT broker service (mqtt-svc): 30000
|
|
||||||
2. Frontend (frontend-svc): 30001
|
|
||||||
3. SocketIO: (socketio-svc): 30002
|
|
||||||
4. Controller (controller-svc): 30003
|
|
||||||
5. WebSocket (ws-svc): 30005
|
|
||||||
|
|
||||||
Before deploying the files, edit the frontend.yaml file to set the correct enviroment variables:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
env:
|
|
||||||
- name: NEXT_PUBLIC_REST_ENDPOINT
|
|
||||||
value: "<FRONTEND_IP>:30003"
|
|
||||||
- name: NEXT_PUBLIC_WS_ENDPOINT
|
|
||||||
value: "<FRONTEND_IP>:30005"
|
|
||||||
```
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
kubectl apply -f $DEPLOYMENT_PATH/mqtt.yaml
|
kubectl apply -f $DEPLOYMENT_PATH/mqtt.yaml
|
||||||
kubectl apply -f $DEPLOYMENT_PATH/mqtt-adapter.yaml
|
kubectl apply -f $DEPLOYMENT_PATH/mqtt-adapter.yaml
|
||||||
|
|
@ -88,4 +76,4 @@ kubectl apply -f $DEPLOYMENT_PATH/ws-adapter.yaml
|
||||||
kubectl get pods
|
kubectl get pods
|
||||||
kubectl get svc
|
kubectl get svc
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ spec:
|
||||||
- name: NATS_VERIFY_CERTIFICATES
|
- name: NATS_VERIFY_CERTIFICATES
|
||||||
value: "false"
|
value: "false"
|
||||||
- name: MONGO_URI
|
- name: MONGO_URI
|
||||||
value: "mongodb://oktopusp:oktopusp@mongodb-0.mongodb-svc.mongodb.svc.cluster.local:27017,mongodb-1.mongodb-svc.mongodb.svc.cluster.local:27017,mongodb-2.mongodb-svc.mongodb.svc.cluster.local:27017/adapter?replicaSet=mongodb&ssl=false"
|
value: "mongodb://oktopusp:oktopusp@mongodb-0.mongodb-svc.mongodb.svc.cluster.local:27017,mongodb-1.mongodb-svc.mongodb.svc.cluster.local:27017,mongodb-2.mongodb-svc.mongodb.svc.cluster.local:27017/?replicaSet=mongodb&ssl=false"
|
||||||
- name: CONTROLLER_ID
|
- name: CONTROLLER_ID
|
||||||
value: "oktopusController"
|
value: "oktopusController"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,5 @@ spec:
|
||||||
- protocol: TCP
|
- protocol: TCP
|
||||||
port: 8000
|
port: 8000
|
||||||
targetPort: 8000
|
targetPort: 8000
|
||||||
nodePort: 30003
|
type: ClusterIP
|
||||||
type: NodePort
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,8 @@ spec:
|
||||||
- containerPort: 3000
|
- containerPort: 3000
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
env:
|
env:
|
||||||
- name: NEXT_PUBLIC_REST_ENDPOINT
|
- name: NEXT_PUBLIC_REST_ENDPOINT
|
||||||
value: "192.168.1.130:30003"
|
value: "/api"
|
||||||
- name: NEXT_PUBLIC_WS_ENDPOINT
|
|
||||||
value: "192.168.1.130:30005"
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
|
|
@ -44,5 +42,5 @@ spec:
|
||||||
- protocol: TCP
|
- protocol: TCP
|
||||||
port: 3000
|
port: 3000
|
||||||
targetPort: 3000
|
targetPort: 3000
|
||||||
nodePort: 30001
|
#externalTrafficPolicy: Local
|
||||||
type: NodePort
|
type: ClusterIP
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: haproxy-kubernetes-ingress
|
|
||||||
namespace: haproxy-controller
|
|
||||||
data:
|
|
||||||
syslog-server: "address:stdout, format: raw, facility:daemon"
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: haproxy-tcp
|
|
||||||
namespace: default
|
|
||||||
data:
|
|
||||||
1883: # Port where the frontend is going to listen to.
|
|
||||||
default/mqtt-svc:1883 # Kubernetes service in the format NS/ServiceName:ServicePort
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
controller:
|
|
||||||
service:
|
|
||||||
tcpPorts:
|
|
||||||
- name: mqtt
|
|
||||||
port: 1883
|
|
||||||
targetPort: 1883
|
|
||||||
extraArgs:
|
|
||||||
- --configmap-tcp-services=default/haproxy-tcp
|
|
||||||
34
deploy/kubernetes/ingress.yaml
Normal file
34
deploy/kubernetes/ingress.yaml
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: web-ingress
|
||||||
|
namespace: default
|
||||||
|
annotations:
|
||||||
|
spec:
|
||||||
|
ingressClassName: "haproxy"
|
||||||
|
rules:
|
||||||
|
- host: oktopus.rdss.cloud
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: frontend-svc
|
||||||
|
port:
|
||||||
|
number: 3000
|
||||||
|
- path: /api
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: controller-svc
|
||||||
|
port:
|
||||||
|
number: 8000
|
||||||
|
- path: /socket.io
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: socketio-svc
|
||||||
|
port:
|
||||||
|
number: 5000
|
||||||
|
|
||||||
|
|
@ -31,7 +31,7 @@ spec:
|
||||||
- name: NATS_VERIFY_CERTIFICATES
|
- name: NATS_VERIFY_CERTIFICATES
|
||||||
value: "false"
|
value: "false"
|
||||||
- name: MQTT_URL
|
- name: MQTT_URL
|
||||||
value: "tcp://mqtt:1883"
|
value: "tcp://mqtt-svc:1883"
|
||||||
- name: MQTT_CLIENT_ID
|
- name: MQTT_CLIENT_ID
|
||||||
value: "mqtt-adapter"
|
value: "mqtt-adapter"
|
||||||
- name: MQTT_USERNAME
|
- name: MQTT_USERNAME
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ spec:
|
||||||
- protocol: TCP
|
- protocol: TCP
|
||||||
port: 1883
|
port: 1883
|
||||||
targetPort: 1883
|
targetPort: 1883
|
||||||
nodePort: 30000
|
externalTrafficPolicy: Local
|
||||||
type: NodePort
|
type: LoadBalancer
|
||||||
---
|
---
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
|
|
@ -46,3 +46,6 @@ spec:
|
||||||
value: "false"
|
value: "false"
|
||||||
- name: LOG_LEVEL
|
- name: LOG_LEVEL
|
||||||
value: "0" # 0 - DEBUG
|
value: "0" # 0 - DEBUG
|
||||||
|
- name: REDIS_ENABLE
|
||||||
|
value: "false"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ spec:
|
||||||
env:
|
env:
|
||||||
- name: NATS_URL
|
- name: NATS_URL
|
||||||
value: "nats:4222"
|
value: "nats:4222"
|
||||||
|
- name: CORS_ALLOWED_ORIGINS
|
||||||
|
value: ""
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
|
|
@ -31,5 +33,3 @@ spec:
|
||||||
- protocol: TCP
|
- protocol: TCP
|
||||||
port: 5000
|
port: 5000
|
||||||
targetPort: 5000
|
targetPort: 5000
|
||||||
nodePort: 30002
|
|
||||||
type: NodePort
|
|
||||||
|
|
|
||||||
|
|
@ -82,18 +82,22 @@ export const AuthProvider = (props) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("isAuthenticated: ", isAuthenticated)
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
const user = {
|
const user = {
|
||||||
id: '5e86809283e28b96d2d38537',
|
id: '5e86809283e28b96d2d38537',
|
||||||
avatar: '/assets/avatars/default-avatar.png',
|
avatar: '/assets/avatars/default-avatar.png',
|
||||||
name: 'Oktopus',
|
name: 'Oktopus',
|
||||||
email: 'anika.visser@devias.io',
|
email: 'anika.visser@devias.io',
|
||||||
|
token: localStorage.getItem("token")
|
||||||
};
|
};
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: HANDLERS.INITIALIZE,
|
type: HANDLERS.INITIALIZE,
|
||||||
payload: user
|
payload: user
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("AUTH CONTEXT --> auth.user.token:", user.token)
|
||||||
} else {
|
} else {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: HANDLERS.INITIALIZE
|
type: HANDLERS.INITIALIZE
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import ChartBarIcon from '@heroicons/react/24/solid/ChartBarIcon';
|
||||||
import CogIcon from '@heroicons/react/24/solid/CogIcon';
|
import CogIcon from '@heroicons/react/24/solid/CogIcon';
|
||||||
import ChatBubbleLeftRightIcon from '@heroicons/react/24/solid/ChatBubbleLeftRightIcon'
|
import ChatBubbleLeftRightIcon from '@heroicons/react/24/solid/ChatBubbleLeftRightIcon'
|
||||||
import MapIcon from '@heroicons/react/24/solid/MapIcon'
|
import MapIcon from '@heroicons/react/24/solid/MapIcon'
|
||||||
|
import UserGroupIcon from '@heroicons/react/24/solid/UserGroupIcon'
|
||||||
|
import KeyIcon from '@heroicons/react/24/solid/KeyIcon'
|
||||||
import CpuChip from '@heroicons/react/24/solid/CpuChipIcon';
|
import CpuChip from '@heroicons/react/24/solid/CpuChipIcon';
|
||||||
import { SvgIcon } from '@mui/material';
|
import { SvgIcon } from '@mui/material';
|
||||||
|
|
||||||
|
|
@ -42,6 +44,24 @@ export const items = [
|
||||||
// </SvgIcon>
|
// </SvgIcon>
|
||||||
// )
|
// )
|
||||||
// },
|
// },
|
||||||
|
// {
|
||||||
|
// title: 'Credentials',
|
||||||
|
// path: '/credentials',
|
||||||
|
// icon: (
|
||||||
|
// <SvgIcon fontSize="small">
|
||||||
|
// <KeyIcon/>
|
||||||
|
// </SvgIcon>
|
||||||
|
// )
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
title: 'Users',
|
||||||
|
path: '/users',
|
||||||
|
icon: (
|
||||||
|
<SvgIcon fontSize="small">
|
||||||
|
<UserGroupIcon/>
|
||||||
|
</SvgIcon>
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Settings',
|
title: 'Settings',
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
|
|
|
||||||
79
frontend/src/pages/403.js
Normal file
79
frontend/src/pages/403.js
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import Head from 'next/head';
|
||||||
|
import NextLink from 'next/link';
|
||||||
|
import ArrowLeftIcon from '@heroicons/react/24/solid/ArrowLeftIcon';
|
||||||
|
import { Box, Button, Container, SvgIcon, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
const Page = () => (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>
|
||||||
|
403 | Oktopus TR-369
|
||||||
|
</title>
|
||||||
|
</Head>
|
||||||
|
<Box
|
||||||
|
component="main"
|
||||||
|
sx={{
|
||||||
|
alignItems: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
flexGrow: 1,
|
||||||
|
minHeight: '100%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container maxWidth="md">
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
alignItems: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mb: 3,
|
||||||
|
textAlign: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="Under development"
|
||||||
|
src="/assets/errors/error-401.png"
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
maxWidth: '100%',
|
||||||
|
width: 400
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Typography
|
||||||
|
align="center"
|
||||||
|
sx={{ mb: 3 }}
|
||||||
|
variant="h3"
|
||||||
|
>
|
||||||
|
403: You're not allowed to perform this action
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
align="center"
|
||||||
|
color="text.secondary"
|
||||||
|
variant="body1"
|
||||||
|
>
|
||||||
|
You either tried to perform an action you're not authorized to do or you came here by mistake.
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
component={NextLink}
|
||||||
|
href="/"
|
||||||
|
startIcon={(
|
||||||
|
<SvgIcon fontSize="small">
|
||||||
|
<ArrowLeftIcon />
|
||||||
|
</SvgIcon>
|
||||||
|
)}
|
||||||
|
sx={{ mt: 3 }}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Go back to dashboard
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
||||||
|
|
@ -1,290 +0,0 @@
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
|
||||||
import Head from 'next/head';
|
|
||||||
import { subDays, subHours } from 'date-fns';
|
|
||||||
import ArrowDownOnSquareIcon from '@heroicons/react/24/solid/ArrowDownOnSquareIcon';
|
|
||||||
import ArrowUpOnSquareIcon from '@heroicons/react/24/solid/ArrowUpOnSquareIcon';
|
|
||||||
import PlusIcon from '@heroicons/react/24/solid/PlusIcon';
|
|
||||||
import { Box, Button, Container, Stack, SvgIcon, Typography } from '@mui/material';
|
|
||||||
import { useSelection } from 'src/hooks/use-selection';
|
|
||||||
import { Layout as DashboardLayout } from 'src/layouts/dashboard/layout';
|
|
||||||
import { CustomersTable } from 'src/sections/customer/customers-table';
|
|
||||||
import { CustomersSearch } from 'src/sections/customer/customers-search';
|
|
||||||
import { applyPagination } from 'src/utils/apply-pagination';
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
const data = [
|
|
||||||
{
|
|
||||||
id: '5e887ac47eed253091be10cb',
|
|
||||||
address: {
|
|
||||||
city: 'Cleveland',
|
|
||||||
country: 'USA',
|
|
||||||
state: 'Ohio',
|
|
||||||
street: '2849 Fulton Street'
|
|
||||||
},
|
|
||||||
avatar: '/assets/avatars/avatar-carson-darrin.png',
|
|
||||||
createdAt: subDays(subHours(now, 7), 1).getTime(),
|
|
||||||
email: 'carson.darrin@devias.io',
|
|
||||||
name: 'Carson Darrin',
|
|
||||||
phone: '304-428-3097'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5e887b209c28ac3dd97f6db5',
|
|
||||||
address: {
|
|
||||||
city: 'Atlanta',
|
|
||||||
country: 'USA',
|
|
||||||
state: 'Georgia',
|
|
||||||
street: '1865 Pleasant Hill Road'
|
|
||||||
},
|
|
||||||
avatar: '/assets/avatars/avatar-fran-perez.png',
|
|
||||||
createdAt: subDays(subHours(now, 1), 2).getTime(),
|
|
||||||
email: 'fran.perez@devias.io',
|
|
||||||
name: 'Fran Perez',
|
|
||||||
phone: '712-351-5711'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5e887b7602bdbc4dbb234b27',
|
|
||||||
address: {
|
|
||||||
city: 'North Canton',
|
|
||||||
country: 'USA',
|
|
||||||
state: 'Ohio',
|
|
||||||
street: '4894 Lakeland Park Drive'
|
|
||||||
},
|
|
||||||
avatar: '/assets/avatars/avatar-jie-yan-song.png',
|
|
||||||
createdAt: subDays(subHours(now, 4), 2).getTime(),
|
|
||||||
email: 'jie.yan.song@devias.io',
|
|
||||||
name: 'Jie Yan Song',
|
|
||||||
phone: '770-635-2682'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5e86809283e28b96d2d38537',
|
|
||||||
address: {
|
|
||||||
city: 'Madrid',
|
|
||||||
country: 'Spain',
|
|
||||||
name: 'Anika Visser',
|
|
||||||
street: '4158 Hedge Street'
|
|
||||||
},
|
|
||||||
avatar: '/assets/avatars/avatar-anika-visser.png',
|
|
||||||
createdAt: subDays(subHours(now, 11), 2).getTime(),
|
|
||||||
email: 'anika.visser@devias.io',
|
|
||||||
name: 'Anika Visser',
|
|
||||||
phone: '908-691-3242'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5e86805e2bafd54f66cc95c3',
|
|
||||||
address: {
|
|
||||||
city: 'San Diego',
|
|
||||||
country: 'USA',
|
|
||||||
state: 'California',
|
|
||||||
street: '75247'
|
|
||||||
},
|
|
||||||
avatar: '/assets/avatars/avatar-miron-vitold.png',
|
|
||||||
createdAt: subDays(subHours(now, 7), 3).getTime(),
|
|
||||||
email: 'miron.vitold@devias.io',
|
|
||||||
name: 'Miron Vitold',
|
|
||||||
phone: '972-333-4106'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5e887a1fbefd7938eea9c981',
|
|
||||||
address: {
|
|
||||||
city: 'Berkeley',
|
|
||||||
country: 'USA',
|
|
||||||
state: 'California',
|
|
||||||
street: '317 Angus Road'
|
|
||||||
},
|
|
||||||
avatar: '/assets/avatars/avatar-penjani-inyene.png',
|
|
||||||
createdAt: subDays(subHours(now, 5), 4).getTime(),
|
|
||||||
email: 'penjani.inyene@devias.io',
|
|
||||||
name: 'Penjani Inyene',
|
|
||||||
phone: '858-602-3409'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5e887d0b3d090c1b8f162003',
|
|
||||||
address: {
|
|
||||||
city: 'Carson City',
|
|
||||||
country: 'USA',
|
|
||||||
state: 'Nevada',
|
|
||||||
street: '2188 Armbrester Drive'
|
|
||||||
},
|
|
||||||
avatar: '/assets/avatars/avatar-omar-darboe.png',
|
|
||||||
createdAt: subDays(subHours(now, 15), 4).getTime(),
|
|
||||||
email: 'omar.darobe@devias.io',
|
|
||||||
name: 'Omar Darobe',
|
|
||||||
phone: '415-907-2647'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5e88792be2d4cfb4bf0971d9',
|
|
||||||
address: {
|
|
||||||
city: 'Los Angeles',
|
|
||||||
country: 'USA',
|
|
||||||
state: 'California',
|
|
||||||
street: '1798 Hickory Ridge Drive'
|
|
||||||
},
|
|
||||||
avatar: '/assets/avatars/avatar-siegbert-gottfried.png',
|
|
||||||
createdAt: subDays(subHours(now, 2), 5).getTime(),
|
|
||||||
email: 'siegbert.gottfried@devias.io',
|
|
||||||
name: 'Siegbert Gottfried',
|
|
||||||
phone: '702-661-1654'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5e8877da9a65442b11551975',
|
|
||||||
address: {
|
|
||||||
city: 'Murray',
|
|
||||||
country: 'USA',
|
|
||||||
state: 'Utah',
|
|
||||||
street: '3934 Wildrose Lane'
|
|
||||||
},
|
|
||||||
avatar: '/assets/avatars/avatar-iulia-albu.png',
|
|
||||||
createdAt: subDays(subHours(now, 8), 6).getTime(),
|
|
||||||
email: 'iulia.albu@devias.io',
|
|
||||||
name: 'Iulia Albu',
|
|
||||||
phone: '313-812-8947'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5e8680e60cba5019c5ca6fda',
|
|
||||||
address: {
|
|
||||||
city: 'Salt Lake City',
|
|
||||||
country: 'USA',
|
|
||||||
state: 'Utah',
|
|
||||||
street: '368 Lamberts Branch Road'
|
|
||||||
},
|
|
||||||
avatar: '/assets/avatars/avatar-nasimiyu-danai.png',
|
|
||||||
createdAt: subDays(subHours(now, 1), 9).getTime(),
|
|
||||||
email: 'nasimiyu.danai@devias.io',
|
|
||||||
name: 'Nasimiyu Danai',
|
|
||||||
phone: '801-301-7894'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const useCustomers = (page, rowsPerPage) => {
|
|
||||||
return useMemo(
|
|
||||||
() => {
|
|
||||||
return applyPagination(data, page, rowsPerPage);
|
|
||||||
},
|
|
||||||
[page, rowsPerPage]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const useCustomerIds = (customers) => {
|
|
||||||
return useMemo(
|
|
||||||
() => {
|
|
||||||
return customers.map((customer) => customer.id);
|
|
||||||
},
|
|
||||||
[customers]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Page = () => {
|
|
||||||
const [page, setPage] = useState(0);
|
|
||||||
const [rowsPerPage, setRowsPerPage] = useState(5);
|
|
||||||
const customers = useCustomers(page, rowsPerPage);
|
|
||||||
const customersIds = useCustomerIds(customers);
|
|
||||||
const customersSelection = useSelection(customersIds);
|
|
||||||
|
|
||||||
const handlePageChange = useCallback(
|
|
||||||
(event, value) => {
|
|
||||||
setPage(value);
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRowsPerPageChange = useCallback(
|
|
||||||
(event) => {
|
|
||||||
setRowsPerPage(event.target.value);
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>
|
|
||||||
Customers | Devias Kit
|
|
||||||
</title>
|
|
||||||
</Head>
|
|
||||||
<Box
|
|
||||||
component="main"
|
|
||||||
sx={{
|
|
||||||
flexGrow: 1,
|
|
||||||
py: 8
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Container maxWidth="xl">
|
|
||||||
<Stack spacing={3}>
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
justifyContent="space-between"
|
|
||||||
spacing={4}
|
|
||||||
>
|
|
||||||
<Stack spacing={1}>
|
|
||||||
<Typography variant="h4">
|
|
||||||
Customers
|
|
||||||
</Typography>
|
|
||||||
<Stack
|
|
||||||
alignItems="center"
|
|
||||||
direction="row"
|
|
||||||
spacing={1}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
color="inherit"
|
|
||||||
startIcon={(
|
|
||||||
<SvgIcon fontSize="small">
|
|
||||||
<ArrowUpOnSquareIcon />
|
|
||||||
</SvgIcon>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Import
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="inherit"
|
|
||||||
startIcon={(
|
|
||||||
<SvgIcon fontSize="small">
|
|
||||||
<ArrowDownOnSquareIcon />
|
|
||||||
</SvgIcon>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Export
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
startIcon={(
|
|
||||||
<SvgIcon fontSize="small">
|
|
||||||
<PlusIcon />
|
|
||||||
</SvgIcon>
|
|
||||||
)}
|
|
||||||
variant="contained"
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
<CustomersSearch />
|
|
||||||
<CustomersTable
|
|
||||||
count={data.length}
|
|
||||||
items={customers}
|
|
||||||
onDeselectAll={customersSelection.handleDeselectAll}
|
|
||||||
onDeselectOne={customersSelection.handleDeselectOne}
|
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onRowsPerPageChange={handleRowsPerPageChange}
|
|
||||||
onSelectAll={customersSelection.handleSelectAll}
|
|
||||||
onSelectOne={customersSelection.handleSelectOne}
|
|
||||||
page={page}
|
|
||||||
rowsPerPage={rowsPerPage}
|
|
||||||
selected={customersSelection.selected}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Page.getLayout = (page) => (
|
|
||||||
<DashboardLayout>
|
|
||||||
{page}
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
|
|
@ -22,7 +22,7 @@ const Page = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
const [devices, setDevices] = useState([]);
|
const [devices, setDevices] = useState([]);
|
||||||
const [deviceFound, setDeviceFound] = useState(false)
|
const [deviceFound, setDeviceFound] = useState(true)
|
||||||
const [pages, setPages] = useState(0);
|
const [pages, setPages] = useState(0);
|
||||||
const [page, setPage] = useState(null);
|
const [page, setPage] = useState(null);
|
||||||
const [Loading, setLoading] = useState(true);
|
const [Loading, setLoading] = useState(true);
|
||||||
|
|
@ -104,6 +104,7 @@ const Page = () => {
|
||||||
|
|
||||||
const fetchDevicePerId = async (id) => {
|
const fetchDevicePerId = async (id) => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
setDeviceFound(true)
|
||||||
var myHeaders = new Headers();
|
var myHeaders = new Headers();
|
||||||
myHeaders.append("Content-Type", "application/json");
|
myHeaders.append("Content-Type", "application/json");
|
||||||
myHeaders.append("Authorization", auth.user.token);
|
myHeaders.append("Authorization", auth.user.token);
|
||||||
|
|
|
||||||
449
frontend/src/pages/users.js
Normal file
449
frontend/src/pages/users.js
Normal file
|
|
@ -0,0 +1,449 @@
|
||||||
|
import { useCallback, useMemo, useState, useEffect } from 'react';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import { subDays, subHours } from 'date-fns';
|
||||||
|
import ArrowDownOnSquareIcon from '@heroicons/react/24/solid/ArrowDownOnSquareIcon';
|
||||||
|
import ArrowUpOnSquareIcon from '@heroicons/react/24/solid/ArrowUpOnSquareIcon';
|
||||||
|
import PlusIcon from '@heroicons/react/24/solid/PlusIcon';
|
||||||
|
import { Box, Button, CircularProgress, Container, Dialog, DialogContent, DialogTitle, Stack, SvgIcon, Typography,
|
||||||
|
DialogActions,
|
||||||
|
TextField,
|
||||||
|
Backdrop,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useSelection } from 'src/hooks/use-selection';
|
||||||
|
import { Layout as DashboardLayout } from 'src/layouts/dashboard/layout';
|
||||||
|
import { CustomersTable } from 'src/sections/customer/customers-table';
|
||||||
|
import { CustomersSearch } from 'src/sections/customer/customers-search';
|
||||||
|
import { applyPagination } from 'src/utils/apply-pagination';
|
||||||
|
import { useAuth } from 'src/hooks/use-auth';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { is } from 'date-fns/locale';
|
||||||
|
import { set } from 'nprogress';
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
|
||||||
|
const auth = useAuth();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const validateEmail = (email) => {
|
||||||
|
return email.match(
|
||||||
|
/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
//const [page, setPage] = useState(0);
|
||||||
|
//const [rowsPerPage, setRowsPerPage] = useState(5);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [creatingNewUser, setCreatingNewUser] = useState(false);
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
const [selected, setSelected] = useState([]);
|
||||||
|
const [addDeviceDialogOpen, setAddDeviceDialogOpen] = useState(false);
|
||||||
|
const [newUserData, setNewUserData] = useState({});
|
||||||
|
const [isPasswordEmpty, setIsPasswordEmpty] = useState(false);
|
||||||
|
const [isEmailEmpty, setIsEmailEmpty] = useState(false);
|
||||||
|
const [isEmailExistent, setIsEmailExistent] = useState(false);
|
||||||
|
|
||||||
|
const deleteUser = (id) => {
|
||||||
|
console.log("request to delete user: ", id)
|
||||||
|
|
||||||
|
var myHeaders = new Headers();
|
||||||
|
myHeaders.append("Content-Type", "application/json");
|
||||||
|
myHeaders.append("Authorization", auth.user.token);
|
||||||
|
|
||||||
|
var requestOptions = {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: myHeaders,
|
||||||
|
redirect: 'follow'
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(process.env.NEXT_PUBLIC_REST_ENDPOINT + '/auth/delete/' + id, requestOptions)
|
||||||
|
.then(response => {
|
||||||
|
if (response.status === 401) {
|
||||||
|
router.push("/auth/login")
|
||||||
|
} else if (response.status === 403) {
|
||||||
|
return router.push("/403")
|
||||||
|
}
|
||||||
|
setUsers(users.filter(user => user.email !== id))
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
return console.error('Error:', error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
console.log("fetching users data...")
|
||||||
|
var myHeaders = new Headers();
|
||||||
|
myHeaders.append("Content-Type", "application/json");
|
||||||
|
myHeaders.append("Authorization", auth.user.token);
|
||||||
|
|
||||||
|
var requestOptions = {
|
||||||
|
method: 'GET',
|
||||||
|
headers: myHeaders,
|
||||||
|
redirect: 'follow'
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(process.env.NEXT_PUBLIC_REST_ENDPOINT + '/users', requestOptions)
|
||||||
|
.then(response => {
|
||||||
|
if (response.status === 401) {
|
||||||
|
router.push("/auth/login")
|
||||||
|
} else if (response.status === 403) {
|
||||||
|
return router.push("/403")
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
})
|
||||||
|
.then(json => {
|
||||||
|
console.log("users: ", json)
|
||||||
|
setUsers(json)
|
||||||
|
// setPages(json.pages + 1)
|
||||||
|
// setPage(json.page +1)
|
||||||
|
// setDevices(json.devices)
|
||||||
|
setLoading(false)
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
return console.error('Error:', error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// if (auth.user.token) {
|
||||||
|
// console.log("auth.user.token =", auth.user.token)
|
||||||
|
// }else{
|
||||||
|
// auth.user.token = localStorage.getItem("token")
|
||||||
|
// }
|
||||||
|
//console.log("auth.user.token =", auth.user.token)
|
||||||
|
fetchUsers()
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// const handlePageChange = useCallback(
|
||||||
|
// (event, value) => {
|
||||||
|
// setPage(value);
|
||||||
|
// },
|
||||||
|
// []
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const handleRowsPerPageChange = useCallback(
|
||||||
|
// (event) => {
|
||||||
|
// setRowsPerPage(event.target.value);
|
||||||
|
// },
|
||||||
|
// []
|
||||||
|
// );
|
||||||
|
|
||||||
|
const createUser = async (data) => {
|
||||||
|
var myHeaders = new Headers();
|
||||||
|
myHeaders.append("Content-Type", "application/json");
|
||||||
|
myHeaders.append("Authorization", auth.user.token);
|
||||||
|
|
||||||
|
var raw = JSON.stringify(data);
|
||||||
|
|
||||||
|
var requestOptions = {
|
||||||
|
method: 'POST',
|
||||||
|
headers: myHeaders,
|
||||||
|
body: raw,
|
||||||
|
redirect: 'follow'
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = await fetch(process.env.NEXT_PUBLIC_REST_ENDPOINT+"/auth/register", requestOptions)
|
||||||
|
|
||||||
|
if (result.status == 200) {
|
||||||
|
console.log("user created: deu boa raça !!")
|
||||||
|
}else if (result.status == 403) {
|
||||||
|
console.log("num tenx permissão, seu boca de sandália")
|
||||||
|
setCreatingNewUser(false)
|
||||||
|
return router.push("/403")
|
||||||
|
}else if (result.status == 401){
|
||||||
|
console.log("taix nem autenticado, sai fora oh")
|
||||||
|
setCreatingNewUser(false)
|
||||||
|
return router.push("/auth/login")
|
||||||
|
}else if (result.status == 409){
|
||||||
|
console.log("usuário já existe, seu boca de bagre")
|
||||||
|
setIsEmailExistent(true)
|
||||||
|
setCreatingNewUser(false)
|
||||||
|
return
|
||||||
|
}else if (result.status == 400){
|
||||||
|
console.log("faltou mandar dados jow")
|
||||||
|
setAddDeviceDialogOpen(false)
|
||||||
|
setNewUserData({})
|
||||||
|
setIsPasswordEmpty(false)
|
||||||
|
setIsEmailEmpty(false)
|
||||||
|
setIsEmailExistent(false)
|
||||||
|
setCreatingNewUser(false)
|
||||||
|
return
|
||||||
|
}else {
|
||||||
|
console.log("agora quebrasse ux córno mô quiridu")
|
||||||
|
const content = await result.json()
|
||||||
|
setCreatingNewUser(false)
|
||||||
|
throw new Error(content);
|
||||||
|
}
|
||||||
|
setAddDeviceDialogOpen(false)
|
||||||
|
data["_id"] = data.email
|
||||||
|
data["createdAt"] = new Date().toLocaleDateString('es-pa')
|
||||||
|
data["level"] = 0
|
||||||
|
|
||||||
|
setUsers([...users, data])
|
||||||
|
setNewUserData({})
|
||||||
|
setIsPasswordEmpty(false)
|
||||||
|
setIsEmailEmpty(false)
|
||||||
|
setIsEmailExistent(false)
|
||||||
|
setCreatingNewUser(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>
|
||||||
|
Oktopus | Users
|
||||||
|
</title>
|
||||||
|
</Head>
|
||||||
|
<Box
|
||||||
|
component="main"
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
py: 8
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container maxWidth="xl">
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
justifyContent="space-between"
|
||||||
|
spacing={4}
|
||||||
|
>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Typography variant="h4">
|
||||||
|
Users
|
||||||
|
</Typography>
|
||||||
|
<Stack
|
||||||
|
alignItems="center"
|
||||||
|
direction="row"
|
||||||
|
spacing={1}
|
||||||
|
>
|
||||||
|
{/* <Button
|
||||||
|
color="inherit"
|
||||||
|
startIcon={(
|
||||||
|
<SvgIcon fontSize="small">
|
||||||
|
<ArrowUpOnSquareIcon />
|
||||||
|
</SvgIcon>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Import
|
||||||
|
</Button> */}
|
||||||
|
{/* <Button
|
||||||
|
color="inherit"
|
||||||
|
startIcon={(
|
||||||
|
<SvgIcon fontSize="small">
|
||||||
|
<ArrowDownOnSquareIcon />
|
||||||
|
</SvgIcon>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</Button> */}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
startIcon={(
|
||||||
|
<SvgIcon fontSize="small">
|
||||||
|
<PlusIcon />
|
||||||
|
</SvgIcon>
|
||||||
|
)}
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
setAddDeviceDialogOpen(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
{/* <CustomersSearch /> */}
|
||||||
|
{users && !loading ?
|
||||||
|
<CustomersTable
|
||||||
|
count={users.length}
|
||||||
|
items={users}
|
||||||
|
//onDeselectAll={customersSelection.handleDeselectAll}
|
||||||
|
onDeselectOne={(id) => {
|
||||||
|
setSelected(selected.filter((item) => item !== id))
|
||||||
|
}}
|
||||||
|
//onPageChange={handlePageChange}
|
||||||
|
//onRowsPerPageChange={handleRowsPerPageChange}
|
||||||
|
//onSelectAll={customersSelection.handleSelectAll}
|
||||||
|
onSelectOne={(id) => {
|
||||||
|
setSelected([...selected, id])
|
||||||
|
console.log("added user " + id + " to selected array")
|
||||||
|
}}
|
||||||
|
//page={page}
|
||||||
|
//rowsPerPage={rowsPerPage}
|
||||||
|
deleteUser={deleteUser}
|
||||||
|
selected={selected}
|
||||||
|
/> :
|
||||||
|
<CircularProgress></CircularProgress>
|
||||||
|
}
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
<Dialog
|
||||||
|
open={addDeviceDialogOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setAddDeviceDialogOpen(false)
|
||||||
|
setIsEmailEmpty(false)
|
||||||
|
setIsEmailExistent(false)
|
||||||
|
setIsPasswordEmpty(false)
|
||||||
|
setNewUserData({})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle>Create User</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Stack
|
||||||
|
alignItems="center"
|
||||||
|
direction="row"
|
||||||
|
spacing={2}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
inputProps={{
|
||||||
|
form: {
|
||||||
|
autocomplete: 'new-password',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
// focused={isEmailEmpty}
|
||||||
|
// color={isEmailEmpty ? "error" : "primary"}
|
||||||
|
helperText={isEmailEmpty ? "Email error" : (isEmailExistent ? "Email already exists" : "")}
|
||||||
|
autoFocus
|
||||||
|
required
|
||||||
|
margin="dense"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
label="Email Address"
|
||||||
|
type="email"
|
||||||
|
fullWidth
|
||||||
|
onChange={
|
||||||
|
(event) => {
|
||||||
|
setNewUserData({...newUserData, email: event.target.value})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
variant="standard">
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
// focused={isPasswordEmpty}
|
||||||
|
//color={isPasswordEmpty ? "error" : "primary"}
|
||||||
|
helperText={isPasswordEmpty ? "Password cannot be empty" : ""}
|
||||||
|
autoFocus
|
||||||
|
required
|
||||||
|
margin="dense"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
autoComplete='new-password'
|
||||||
|
fullWidth
|
||||||
|
onChange={
|
||||||
|
(event) => {
|
||||||
|
setNewUserData({...newUserData, password: event.target.value})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
variant="standard">
|
||||||
|
</TextField>
|
||||||
|
</Stack>
|
||||||
|
<Stack
|
||||||
|
alignItems="center"
|
||||||
|
direction="row"
|
||||||
|
spacing={2}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
inputProps={{
|
||||||
|
form: {
|
||||||
|
autocomplete: 'off',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
label="Full Name"
|
||||||
|
type="name"
|
||||||
|
fullWidth
|
||||||
|
variant="standard"
|
||||||
|
onChange={
|
||||||
|
(event) => {
|
||||||
|
setNewUserData({...newUserData, name: event.target.value})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
inputProps={{
|
||||||
|
form: {
|
||||||
|
autocomplete: 'off',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
autoComplete="off"
|
||||||
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
id="phone"
|
||||||
|
name="phone"
|
||||||
|
label="Phone Number"
|
||||||
|
type="phone"
|
||||||
|
fullWidth
|
||||||
|
variant="standard"
|
||||||
|
onChange={
|
||||||
|
(event) => {
|
||||||
|
setNewUserData({...newUserData, phone: event.target.value})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
</TextField>
|
||||||
|
</Stack>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={
|
||||||
|
() => {
|
||||||
|
setAddDeviceDialogOpen(false)
|
||||||
|
setIsEmailEmpty(false)
|
||||||
|
setIsEmailExistent(false)
|
||||||
|
setIsPasswordEmpty(false)
|
||||||
|
setNewUserData({})
|
||||||
|
}
|
||||||
|
}>Cancel</Button>
|
||||||
|
<Button onClick={()=>{
|
||||||
|
console.log("new user data: ", newUserData)
|
||||||
|
if (newUserData.password === undefined || newUserData.password === "") {
|
||||||
|
setIsPasswordEmpty(true)
|
||||||
|
return
|
||||||
|
} else{
|
||||||
|
setIsPasswordEmpty(false)
|
||||||
|
}
|
||||||
|
if (newUserData.email === undefined || newUserData.email === "") {
|
||||||
|
setIsEmailEmpty(true)
|
||||||
|
return
|
||||||
|
} else if(!validateEmail(newUserData.email)){
|
||||||
|
setIsEmailEmpty(true)
|
||||||
|
return
|
||||||
|
}else{
|
||||||
|
setIsEmailEmpty(false)
|
||||||
|
}
|
||||||
|
setIsEmailExistent(false)
|
||||||
|
setCreatingNewUser(true)
|
||||||
|
createUser(newUserData)
|
||||||
|
}}>Confirm</Button>
|
||||||
|
</DialogActions>
|
||||||
|
{
|
||||||
|
<Backdrop
|
||||||
|
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||||
|
open={creatingNewUser}
|
||||||
|
>
|
||||||
|
<CircularProgress color="inherit" />
|
||||||
|
</Backdrop>
|
||||||
|
}
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Page.getLayout = (page) => (
|
||||||
|
<DashboardLayout>
|
||||||
|
{page}
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Page;
|
||||||
|
|
@ -1,21 +1,31 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { format } from 'date-fns';
|
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
Card,
|
Card,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
Icon,
|
||||||
Stack,
|
Stack,
|
||||||
|
Tab,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TablePagination,
|
//TablePagination,
|
||||||
TableRow,
|
TableRow,
|
||||||
Typography
|
Typography,
|
||||||
|
SvgIcon,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
Button
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Scrollbar } from 'src/components/scrollbar';
|
import { Scrollbar } from 'src/components/scrollbar';
|
||||||
import { getInitials } from 'src/utils/get-initials';
|
import { getInitials } from 'src/utils/get-initials';
|
||||||
|
import TrashIcon from '@heroicons/react/24/outline/TrashIcon';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
export const CustomersTable = (props) => {
|
export const CustomersTable = (props) => {
|
||||||
const {
|
const {
|
||||||
|
|
@ -27,14 +37,18 @@ export const CustomersTable = (props) => {
|
||||||
onRowsPerPageChange,
|
onRowsPerPageChange,
|
||||||
onSelectAll,
|
onSelectAll,
|
||||||
onSelectOne,
|
onSelectOne,
|
||||||
|
deleteUser,
|
||||||
page = 0,
|
page = 0,
|
||||||
rowsPerPage = 0,
|
rowsPerPage = 0,
|
||||||
selected = []
|
selected = []
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const selectedSome = (selected.length > 0) && (selected.length < items.length);
|
// const selectedSome = (selected.length > 0) && (selected.length < items.length);
|
||||||
const selectedAll = (items.length > 0) && (selected.length === items.length);
|
// const selectedAll = (items.length > 0) && (selected.length === items.length);
|
||||||
|
|
||||||
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
|
const [userToDelete, setUserToDelete] = useState("")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<Scrollbar>
|
<Scrollbar>
|
||||||
|
|
@ -42,8 +56,8 @@ export const CustomersTable = (props) => {
|
||||||
<Table>
|
<Table>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell padding="checkbox">
|
{/* <TableCell padding="checkbox"> */}
|
||||||
<Checkbox
|
{/* <Checkbox
|
||||||
checked={selectedAll}
|
checked={selectedAll}
|
||||||
indeterminate={selectedSome}
|
indeterminate={selectedSome}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
|
|
@ -53,55 +67,60 @@ export const CustomersTable = (props) => {
|
||||||
onDeselectAll?.();
|
onDeselectAll?.();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/> */}
|
||||||
</TableCell>
|
{/* </TableCell> */}
|
||||||
<TableCell>
|
<TableCell sx={{marginLeft:"30px"}}>
|
||||||
Name
|
Name
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
Email
|
Email
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
{/* <TableCell>
|
||||||
Location
|
Location
|
||||||
</TableCell>
|
</TableCell> */}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
Phone
|
Phone
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
Signed Up
|
Created At
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
Level
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
Actions
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{items.map((customer) => {
|
{items.map((customer) => {
|
||||||
const isSelected = selected.includes(customer.id);
|
const isSelected = selected.includes(customer._id);
|
||||||
const createdAt = format(customer.createdAt, 'dd/MM/yyyy');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
hover
|
hover
|
||||||
key={customer.id}
|
key={customer._id}
|
||||||
selected={isSelected}
|
selected={isSelected}
|
||||||
>
|
>
|
||||||
<TableCell padding="checkbox">
|
{/* <TableCell padding="checkbox"> */}
|
||||||
<Checkbox
|
{/* <Checkbox
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
if (event.target.checked) {
|
if (event.target.checked) {
|
||||||
onSelectOne?.(customer.id);
|
console.log(customer._id+" is selected");
|
||||||
|
onSelectOne(customer._id);
|
||||||
} else {
|
} else {
|
||||||
onDeselectOne?.(customer.id);
|
onDeselectOne(customer._id);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/> */}
|
||||||
</TableCell>
|
{/* </TableCell> */}
|
||||||
<TableCell>
|
<TableCell align="center" sx={{margin: 'auto', textAlign: 'center'}}>
|
||||||
<Stack
|
<Stack
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
direction="row"
|
direction="row"
|
||||||
spacing={2}
|
spacing={2}
|
||||||
>
|
>
|
||||||
<Avatar src={customer.avatar}>
|
<Avatar src={customer.avatar ? customer.avatar : "/assets/avatars/default-avatar.png"}>
|
||||||
{getInitials(customer.name)}
|
{getInitials(customer.name)}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Typography variant="subtitle2">
|
<Typography variant="subtitle2">
|
||||||
|
|
@ -112,14 +131,33 @@ export const CustomersTable = (props) => {
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{customer.email}
|
{customer.email}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
{/* <TableCell>
|
||||||
{customer.address.city}, {customer.address.state}, {customer.address.country}
|
{customer.address}
|
||||||
</TableCell>
|
</TableCell> */}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{customer.phone}
|
{customer.phone}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{createdAt}
|
{customer.createdAt}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{customer.level == 1 ? "Admin" : "User"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{ customer.level == 0 ? <Button
|
||||||
|
onClick={() => {
|
||||||
|
console.log("delete user: ", customer._id)
|
||||||
|
setUserToDelete(customer.email);
|
||||||
|
setShowDeleteDialog(true);
|
||||||
|
}}
|
||||||
|
><SvgIcon
|
||||||
|
color="action"
|
||||||
|
fontSize="small"
|
||||||
|
sx={{ cursor: 'pointer'}}
|
||||||
|
>
|
||||||
|
<TrashIcon
|
||||||
|
></TrashIcon>
|
||||||
|
</SvgIcon></Button>: <span></span>}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|
@ -128,15 +166,43 @@ export const CustomersTable = (props) => {
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
</Scrollbar>
|
</Scrollbar>
|
||||||
<TablePagination
|
{/* <TablePagination
|
||||||
component="div"
|
component="div"
|
||||||
count={count}
|
count={count}
|
||||||
onPageChange={onPageChange}
|
//onPageChange={onPageChange}
|
||||||
onRowsPerPageChange={onRowsPerPageChange}
|
//onRowsPerPageChange={onRowsPerPageChange}
|
||||||
page={page}
|
//page={page}
|
||||||
rowsPerPage={rowsPerPage}
|
//rowsPerPage={rowsPerPage}
|
||||||
rowsPerPageOptions={[5, 10, 25]}
|
//rowsPerPageOptions={[5, 10, 25]}
|
||||||
/>
|
/> */}
|
||||||
|
<Dialog
|
||||||
|
open={showDeleteDialog}
|
||||||
|
onClose={() => setShowDeleteDialog(false)}
|
||||||
|
aria-labelledby="alert-dialog-title"
|
||||||
|
aria-describedby="alert-dialog-description"
|
||||||
|
>
|
||||||
|
<DialogTitle id="alert-dialog-title">{"Delete User"}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText id="alert-dialog-description">
|
||||||
|
Are you sure you want to delete this user?
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => {
|
||||||
|
setShowDeleteDialog(false)
|
||||||
|
setUserToDelete("")
|
||||||
|
}} color="primary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => {
|
||||||
|
deleteUser(userToDelete);
|
||||||
|
setShowDeleteDialog(false);
|
||||||
|
setUserToDelete("")
|
||||||
|
}} color="primary" autoFocus>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -147,10 +213,11 @@ CustomersTable.propTypes = {
|
||||||
onDeselectAll: PropTypes.func,
|
onDeselectAll: PropTypes.func,
|
||||||
onDeselectOne: PropTypes.func,
|
onDeselectOne: PropTypes.func,
|
||||||
onPageChange: PropTypes.func,
|
onPageChange: PropTypes.func,
|
||||||
onRowsPerPageChange: PropTypes.func,
|
//onRowsPerPageChange: PropTypes.func,
|
||||||
onSelectAll: PropTypes.func,
|
onSelectAll: PropTypes.func,
|
||||||
onSelectOne: PropTypes.func,
|
onSelectOne: PropTypes.func,
|
||||||
page: PropTypes.number,
|
deleteUser: PropTypes.func,
|
||||||
rowsPerPage: PropTypes.number,
|
//page: PropTypes.number,
|
||||||
|
//rowsPerPage: PropTypes.number,
|
||||||
selected: PropTypes.array
|
selected: PropTypes.array
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ export const OverviewLatestOrders = (props) => {
|
||||||
hover
|
hover
|
||||||
key={order.SN}
|
key={order.SN}
|
||||||
>
|
>
|
||||||
<TableCell TableCell align="center">
|
<TableCell align="center">
|
||||||
{order.SN}
|
{order.SN}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|
@ -97,18 +97,17 @@ export const OverviewLatestOrders = (props) => {
|
||||||
</SeverityPill>
|
</SeverityPill>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{ order.Status == 2 && (order.Mqtt == 0 && order.Websockets == 0 && order.Stomp == 0) ? <span></span>: <SvgIcon
|
{ order.Mqtt == 0 && order.Websockets == 0 && order.Stomp == 0 ? <span></span>: <Button>
|
||||||
|
<SvgIcon
|
||||||
fontSize="small"
|
fontSize="small"
|
||||||
sx={{cursor: order.Status == 2 && 'pointer'}}
|
sx={{cursor: order.Status == 2 && 'pointer'}}
|
||||||
onClick={()=>{
|
onClick={()=>{
|
||||||
if (order.Status == 2){
|
|
||||||
router.push("devices/"+order.SN+"/discovery")
|
router.push("devices/"+order.SN+"/discovery")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ArrowTopRightOnSquareIcon />
|
<ArrowTopRightOnSquareIcon />
|
||||||
</SvgIcon>}
|
</SvgIcon></Button>}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user