mirror of
https://github.com/Telecominfraproject/wlan-cloud-owprov-ui.git
synced 2025-10-29 17:52:25 +00:00
[WIFI-11565] Added logs page
Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
99
package-lock.json
generated
99
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wlan-cloud-owprov-ui",
|
||||
"version": "2.8.0(11)",
|
||||
"version": "2.8.0(12)",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wlan-cloud-owprov-ui",
|
||||
"version": "2.8.0(11)",
|
||||
"version": "2.8.0(12)",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.11",
|
||||
@@ -51,6 +51,8 @@
|
||||
"react-router-dom": "^6.4.2",
|
||||
"react-table": "^7.8.0",
|
||||
"react-tooltip": "^4.4.2",
|
||||
"react-virtualized-auto-sizer": "^1.0.7",
|
||||
"react-window": "^1.8.8",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"typescript": "^4.8.4",
|
||||
"uuid": "^9.0.0",
|
||||
@@ -64,6 +66,8 @@
|
||||
"@types/react-csv": "^1.1.3",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-table": "^7.7.12",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@vitejs/plugin-react": "^2.1.0",
|
||||
"eslint": "8.25.0",
|
||||
@@ -4111,6 +4115,24 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-virtualized-auto-sizer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz",
|
||||
"integrity": "sha512-GH8sAnBEM5GV9LTeiz56r4ZhMOUSrP43tAQNSRVxNexDjcNKLCEtnxusAItg1owFUFE6k0NslV26gqVClVvong==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-window": {
|
||||
"version": "1.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz",
|
||||
"integrity": "sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resize-observer-browser": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz",
|
||||
@@ -9654,6 +9676,39 @@
|
||||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-virtualized-auto-sizer": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.7.tgz",
|
||||
"integrity": "sha512-Mxi6lwOmjwIjC1X4gABXMJcKHsOo0xWl3E3ugOgufB8GJU+MqrtY35aBuvCYv/razQ1Vbp7h1gWJjGjoNN5pmA==",
|
||||
"engines": {
|
||||
"node": ">8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc",
|
||||
"react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/react-window": {
|
||||
"version": "1.8.8",
|
||||
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.8.tgz",
|
||||
"integrity": "sha512-D4IiBeRtGXziZ1n0XklnFGu7h9gU684zepqyKzgPNzrsrk7xOCxni+TCckjg2Nr/DiaEEGVVmnhYSlT2rB47dQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"memoize-one": ">=3.1.1 <6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-window/node_modules/memoize-one": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
|
||||
},
|
||||
"node_modules/recrawl-sync": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/recrawl-sync/-/recrawl-sync-2.2.3.tgz",
|
||||
@@ -14580,6 +14635,24 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-virtualized-auto-sizer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz",
|
||||
"integrity": "sha512-GH8sAnBEM5GV9LTeiz56r4ZhMOUSrP43tAQNSRVxNexDjcNKLCEtnxusAItg1owFUFE6k0NslV26gqVClVvong==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-window": {
|
||||
"version": "1.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz",
|
||||
"integrity": "sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/resize-observer-browser": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.7.tgz",
|
||||
@@ -18432,6 +18505,28 @@
|
||||
"prop-types": "^15.6.2"
|
||||
}
|
||||
},
|
||||
"react-virtualized-auto-sizer": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.7.tgz",
|
||||
"integrity": "sha512-Mxi6lwOmjwIjC1X4gABXMJcKHsOo0xWl3E3ugOgufB8GJU+MqrtY35aBuvCYv/razQ1Vbp7h1gWJjGjoNN5pmA==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-window": {
|
||||
"version": "1.8.8",
|
||||
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.8.tgz",
|
||||
"integrity": "sha512-D4IiBeRtGXziZ1n0XklnFGu7h9gU684zepqyKzgPNzrsrk7xOCxni+TCckjg2Nr/DiaEEGVVmnhYSlT2rB47dQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"memoize-one": ">=3.1.1 <6"
|
||||
},
|
||||
"dependencies": {
|
||||
"memoize-one": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"recrawl-sync": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/recrawl-sync/-/recrawl-sync-2.2.3.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wlan-cloud-owprov-ui",
|
||||
"version": "2.8.0(11)",
|
||||
"version": "2.8.0(12)",
|
||||
"description": "",
|
||||
"main": "index.tsx",
|
||||
"scripts": {
|
||||
@@ -56,6 +56,8 @@
|
||||
"react-router-dom": "^6.4.2",
|
||||
"react-table": "^7.8.0",
|
||||
"react-tooltip": "^4.4.2",
|
||||
"react-virtualized-auto-sizer": "^1.0.7",
|
||||
"react-window": "^1.8.8",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"vite": "^3.1.8",
|
||||
"typescript": "^4.8.4",
|
||||
@@ -69,6 +71,8 @@
|
||||
"@types/react-csv": "^1.1.3",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-table": "^7.7.12",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"eslint": "8.25.0",
|
||||
"vite-tsconfig-paths": "^3.5.1",
|
||||
|
||||
BIN
public/public/favicon.ico
Normal file
BIN
public/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 202 KiB |
165
public/public/favicon.svg
Normal file
165
public/public/favicon.svg
Normal file
@@ -0,0 +1,165 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 141.5 185.6" style="enable-background:new 0 0 141.5 185.6;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#414141;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
.st2{fill:#FED206;}
|
||||
.st3{fill:#EB6F53;}
|
||||
.st4{fill:#3BA9B6;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M120.7,183.9H21.5c-10.8,0-19.5-8.7-19.5-19.5V20.5c0-10.8,8.7-19.5,19.5-19.5h99.2
|
||||
c10.8,0,19.5,8.7,19.5,19.5v143.9C140.2,175.2,131.5,183.9,120.7,183.9z"/>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st1" d="M46.3,166.2v-3.4h-1.2v-0.6h3.1v0.6H47v3.4H46.3z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M49,166.2v-4h2.7v0.6h-2v1h2v0.6h-2v1.1h2v0.6H49z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M52.6,166.2v-4h0.7v3.4h1.8v0.6H52.6z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M55.7,166.2v-4h2.7v0.6h-2v1h2v0.6h-2v1.1h2v0.6H55.7z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M59.1,164.2c0-1.2,0.9-2.1,2.1-2.1c0.8,0,1.3,0.4,1.6,0.9l-0.6,0.3c-0.2-0.3-0.6-0.6-1-0.6
|
||||
c-0.8,0-1.4,0.6-1.4,1.4c0,0.8,0.6,1.4,1.4,1.4c0.4,0,0.8-0.3,1-0.6l0.6,0.3c-0.3,0.5-0.8,0.9-1.6,0.9
|
||||
C60,166.3,59.1,165.5,59.1,164.2z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M63.2,164.2c0-1.2,0.8-2.1,2-2.1c1.2,0,2,0.9,2,2.1c0,1.2-0.8,2.1-2,2.1C64,166.3,63.2,165.4,63.2,164.2z
|
||||
M66.5,164.2c0-0.8-0.5-1.4-1.3-1.4c-0.8,0-1.3,0.6-1.3,1.4c0,0.8,0.5,1.4,1.3,1.4C66,165.7,66.5,165,66.5,164.2z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M71.3,166.2v-3.1l-1.2,3.1h-0.3l-1.2-3.1v3.1h-0.7v-4h1l1.1,2.7l1.1-2.7h1v4H71.3z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M75.7,166.2v-4h0.7v4H75.7z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M80.4,166.2l-2.1-2.8v2.8h-0.7v-4h0.7l2,2.8v-2.8h0.7v4H80.4z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M82.3,166.2v-4H85v0.6h-2v1h2v0.6h-2v1.7H82.3z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M87.9,166.2l-0.9-1.5h-0.7v1.5h-0.7v-4h1.7c0.8,0,1.3,0.5,1.3,1.2c0,0.7-0.5,1.1-0.9,1.2l1,1.6H87.9z
|
||||
M88,163.5c0-0.4-0.3-0.6-0.7-0.6h-1v1.3h1C87.7,164.1,88,163.9,88,163.5z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M92.4,166.2l-0.3-0.8h-1.8l-0.3,0.8h-0.8l1.6-4h0.9l1.6,4H92.4z M91.2,162.9l-0.7,1.9h1.4L91.2,162.9z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M95.8,166.2v-4h1.5c0.8,0,1.2,0.5,1.2,1.2c0,0.6-0.4,1.2-1.2,1.2h-1.2v1.7H95.8z M98.2,163.4
|
||||
c0-0.5-0.3-0.9-0.9-0.9h-1.1v1.7h1.1C97.8,164.3,98.2,163.9,98.2,163.4z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M101.5,166.2l-1.1-1.6h-0.9v1.6h-0.3v-4h1.5c0.7,0,1.2,0.4,1.2,1.2c0,0.7-0.5,1.1-1.1,1.1l1.2,1.7H101.5z
|
||||
M101.6,163.4c0-0.5-0.4-0.9-0.9-0.9h-1.1v1.7h1.1C101.2,164.3,101.6,163.9,101.6,163.4z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M102.8,164.2c0-1.2,0.8-2.1,1.9-2.1c1.2,0,1.9,0.9,1.9,2.1c0,1.2-0.8,2.1-1.9,2.1
|
||||
C103.6,166.3,102.8,165.4,102.8,164.2z M106.3,164.2c0-1-0.6-1.7-1.6-1.7c-1,0-1.6,0.7-1.6,1.7c0,1,0.6,1.7,1.6,1.7
|
||||
C105.7,166,106.3,165.2,106.3,164.2z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M106.9,165.8l0.2-0.3c0.2,0.2,0.4,0.4,0.8,0.4c0.5,0,0.9-0.4,0.9-0.9v-2.8h0.3v2.8c0,0.8-0.5,1.2-1.2,1.2
|
||||
C107.5,166.3,107.2,166.1,106.9,165.8z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M110.4,166.2v-4h2.5v0.3h-2.2v1.5h2.1v0.3h-2.1v1.6h2.2v0.3H110.4z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M113.5,164.2c0-1.2,0.9-2.1,2-2.1c0.6,0,1.1,0.3,1.5,0.7l-0.3,0.2c-0.3-0.3-0.7-0.6-1.2-0.6
|
||||
c-0.9,0-1.7,0.7-1.7,1.7c0,1,0.7,1.7,1.7,1.7c0.5,0,0.9-0.2,1.2-0.6l0.3,0.2c-0.4,0.4-0.8,0.7-1.5,0.7
|
||||
C114.4,166.3,113.5,165.5,113.5,164.2z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M118.7,166.2v-3.7h-1.3v-0.3h2.9v0.3H119v3.7H118.7z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st1" points="26.3,163.8 31.6,158.5 36.9,163.8 37.7,163.8 31.6,157.6 25.5,163.8 "/>
|
||||
<polygon class="st1" points="36.9,164.7 31.6,170 26.3,164.7 25.5,164.7 31.6,170.8 37.7,164.7 "/>
|
||||
<polygon class="st1" points="31,163.8 36.3,158.5 41.6,163.8 42.5,163.8 36.3,157.6 30.2,163.8 "/>
|
||||
<polygon class="st1" points="41.6,164.7 36.3,170 31,164.7 30.2,164.7 36.3,170.8 42.5,164.7 "/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M33.2,100.7c-4.6,0-8.3,3.7-8.3,8.3s3.7,8.3,8.3,8.3s8.3-3.7,8.3-8.3S37.8,100.7,33.2,100.7z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st2" d="M33.2,35.2c40.7,0,73.8,33.1,73.8,73.8c0,0.7,0,1.4,0,2.1c0,1.7,0.6,3.3,1.7,4.6c1.2,1.2,2.8,1.9,4.5,2
|
||||
l0.2,0c3.5,0,6.3-2.7,6.4-6.2c0-0.8,0-1.7,0-2.5c0-47.7-38.8-86.6-86.6-86.6c-0.8,0-1.7,0-2.5,0c-1.7,0-3.3,0.8-4.5,2
|
||||
c-1.2,1.2-1.8,2.9-1.7,4.6c0.1,3.5,3,6.3,6.6,6.2C31.8,35.2,32.5,35.2,33.2,35.2z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st3" d="M33.2,60.5c26.7,0,48.5,21.7,48.5,48.5c0,0.6,0,1.3,0,2c-0.1,1.7,0.5,3.3,1.7,4.6c1.2,1.3,2.7,2,4.4,2.1
|
||||
c1.7,0.1,3.3-0.5,4.6-1.7c1.2-1.2,2-2.7,2-4.4c0-0.9,0.1-1.8,0.1-2.6c0-33.8-27.5-61.2-61.2-61.2c-0.8,0-1.6,0-2.6,0.1
|
||||
c-1.7,0.1-3.3,0.8-4.4,2.1c-1.2,1.3-1.8,2.9-1.7,4.6s0.8,3.3,2.1,4.4c1.3,1.2,2.9,1.8,4.6,1.7C31.9,60.5,32.6,60.5,33.2,60.5z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st4" d="M33.2,86.7c12.3,0,22.3,10,22.3,22.3c0,0.5,0,1.1-0.1,1.8c-0.3,3.5,2.3,6.6,5.8,6.9
|
||||
c3.5,0.3,6.6-2.3,6.9-5.8c0.1-1,0.1-1.9,0.1-2.8c0-19.3-15.7-35.1-35.1-35.1c-0.9,0-1.8,0-2.8,0.1c-1.7,0.1-3.2,0.9-4.3,2.2
|
||||
c-1.1,1.3-1.6,2.9-1.5,4.6c0.1,1.7,0.9,3.2,2.2,4.3c1.3,1.1,2.9,1.6,4.6,1.5C32.1,86.7,32.7,86.7,33.2,86.7z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<path class="st1" d="M35.8,130.4c1.1,0.6,2.1,1.5,2.7,2.6c0.7,1.1,1,2.3,1,3.7s-0.3,2.6-1,3.7c-0.7,1.1-1.6,2-2.7,2.6
|
||||
c-1.1,0.6-2.4,1-3.8,1s-2.7-0.3-3.8-1c-1.1-0.6-2.1-1.5-2.7-2.6c-0.7-1.1-1-2.3-1-3.7c0-1.3,0.3-2.6,1-3.7c0.7-1.1,1.6-2,2.7-2.6
|
||||
c1.1-0.6,2.4-0.9,3.8-0.9C33.4,129.5,34.7,129.8,35.8,130.4z M29.9,132.9c-0.7,0.4-1.2,0.9-1.6,1.6s-0.6,1.4-0.6,2.2
|
||||
c0,0.8,0.2,1.6,0.6,2.3c0.4,0.7,0.9,1.2,1.6,1.6c0.7,0.4,1.4,0.6,2.1,0.6c0.8,0,1.5-0.2,2.1-0.6c0.6-0.4,1.2-0.9,1.5-1.6
|
||||
c0.4-0.7,0.6-1.4,0.6-2.3c0-0.8-0.2-1.6-0.6-2.2s-0.9-1.2-1.5-1.6c-0.6-0.4-1.4-0.6-2.1-0.6C31.3,132.3,30.6,132.5,29.9,132.9z"/>
|
||||
<path class="st1" d="M50.6,133.6c0.8,0.5,1.4,1.1,1.8,2c0.4,0.8,0.6,1.8,0.6,2.9c0,1.1-0.2,2-0.6,2.8c-0.4,0.8-1,1.5-1.8,1.9
|
||||
c-0.8,0.5-1.6,0.7-2.6,0.7c-0.7,0-1.4-0.1-2-0.4s-1.1-0.7-1.5-1.2v5.4h-3.1V133h3.1v1.6c0.4-0.5,0.9-1,1.4-1.2s1.2-0.4,2-0.4
|
||||
C48.9,132.9,49.8,133.1,50.6,133.6z M49.1,140.5c0.5-0.6,0.7-1.3,0.7-2.2c0-0.9-0.2-1.6-0.7-2.1c-0.5-0.6-1.1-0.8-1.9-0.8
|
||||
s-1.4,0.3-1.9,0.8c-0.5,0.6-0.8,1.3-0.8,2.1c0,0.9,0.2,1.6,0.8,2.2s1.1,0.8,1.9,0.8S48.6,141,49.1,140.5z"/>
|
||||
<path class="st1" d="M63.4,134.4c0.9,1,1.4,2.4,1.4,4.2c0,0.3,0,0.6,0,0.7H57c0.2,0.7,0.5,1.2,1,1.6c0.5,0.4,1.1,0.6,1.8,0.6
|
||||
c0.5,0,1-0.1,1.5-0.3s0.9-0.5,1.3-0.9l1.6,1.6c-0.5,0.6-1.2,1.1-2,1.4c-0.8,0.3-1.6,0.5-2.6,0.5c-1.1,0-2.1-0.2-3-0.7
|
||||
s-1.5-1.1-2-1.9c-0.5-0.8-0.7-1.8-0.7-2.9c0-1.1,0.2-2.1,0.7-2.9s1.1-1.5,2-1.9c0.8-0.5,1.8-0.7,2.9-0.7
|
||||
C61.2,132.9,62.5,133.4,63.4,134.4z M61.8,137.5c0-0.7-0.3-1.3-0.7-1.7s-1-0.6-1.7-0.6c-0.7,0-1.2,0.2-1.7,0.6
|
||||
c-0.4,0.4-0.7,1-0.9,1.7H61.8z"/>
|
||||
<path class="st1" d="M76.2,134c0.7,0.7,1.1,1.7,1.1,3v6.8h-3.1v-5.9c0-0.7-0.2-1.2-0.6-1.6s-0.9-0.6-1.5-0.6
|
||||
c-0.8,0-1.4,0.3-1.8,0.8c-0.4,0.5-0.7,1.2-0.7,2v5.3h-3.1V133h3.1v1.9c0.7-1.3,2-2,3.7-2C74.6,132.8,75.5,133.2,76.2,134z"/>
|
||||
<path class="st1" d="M96,129.7h3.3l-4.7,14h-3.3l-2.9-10.1l-3,10.1h-3.2l-4.7-14h3.4l3,10.7l3-10.7H90l3.1,10.7L96,129.7z"/>
|
||||
<path class="st1" d="M103.3,128.7c0.3,0.3,0.5,0.7,0.5,1.2s-0.2,0.9-0.5,1.2c-0.3,0.3-0.7,0.5-1.2,0.5c-0.5,0-0.9-0.2-1.2-0.5
|
||||
c-0.3-0.3-0.5-0.7-0.5-1.2c0-0.5,0.2-0.9,0.5-1.2c0.3-0.3,0.7-0.5,1.2-0.5C102.6,128.2,103,128.3,103.3,128.7z M100.6,133h3.1
|
||||
v10.8h-3.1V133z"/>
|
||||
<path class="st1" d="M106.5,129.7h10.1l0,2.6h-6.9v3.4h6.3v2.6h-6.3v5.3h-3.2V129.7z"/>
|
||||
<path class="st1" d="M120.9,128.7c0.3,0.3,0.5,0.7,0.5,1.2s-0.2,0.9-0.5,1.2c-0.3,0.3-0.7,0.5-1.2,0.5c-0.5,0-0.9-0.2-1.2-0.5
|
||||
c-0.3-0.3-0.5-0.7-0.5-1.2c0-0.5,0.2-0.9,0.5-1.2c0.3-0.3,0.7-0.5,1.2-0.5C120.1,128.2,120.5,128.3,120.9,128.7z M118.1,133h3.1
|
||||
v10.8h-3.1V133z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.0 KiB |
12
src/App.tsx
12
src/App.tsx
@@ -3,7 +3,9 @@ import { Spinner } from '@chakra-ui/react';
|
||||
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import { AuthProvider } from 'contexts/AuthProvider';
|
||||
import { FirmwareSocketProvider } from 'contexts/FirmwareSocketProvider';
|
||||
import { ProvisioningSocketProvider } from 'contexts/ProvisioningSocketProvider';
|
||||
import { SecuritySocketProvider } from 'contexts/SecuritySocketProvider';
|
||||
import Router from 'router';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
@@ -23,9 +25,13 @@ const App = () => {
|
||||
<HashRouter>
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<AuthProvider token={storageToken !== null ? storageToken : undefined}>
|
||||
<ProvisioningSocketProvider>
|
||||
<Router />
|
||||
</ProvisioningSocketProvider>
|
||||
<SecuritySocketProvider>
|
||||
<FirmwareSocketProvider>
|
||||
<ProvisioningSocketProvider>
|
||||
<Router />
|
||||
</ProvisioningSocketProvider>
|
||||
</FirmwareSocketProvider>
|
||||
</SecuritySocketProvider>
|
||||
</AuthProvider>
|
||||
</Suspense>
|
||||
</HashRouter>
|
||||
|
||||
51
src/components/ShownLogsDropdown/index.tsx
Normal file
51
src/components/ShownLogsDropdown/index.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import * as React from 'react';
|
||||
import { ChevronDownIcon } from '@chakra-ui/icons';
|
||||
import { Button, Checkbox, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export type ShownLogsDropdownProps = {
|
||||
availableLogTypes: { id: number; helper?: string }[];
|
||||
hiddenLogIds: number[];
|
||||
setHiddenLogIds: (ids: number[]) => void;
|
||||
helperLabels?: { [key: number]: string };
|
||||
};
|
||||
|
||||
export const ShownLogsDropdown = ({
|
||||
availableLogTypes,
|
||||
hiddenLogIds,
|
||||
setHiddenLogIds,
|
||||
helperLabels,
|
||||
}: ShownLogsDropdownProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isActive = (id: number) => !hiddenLogIds.includes(id);
|
||||
const onToggle = (id: number) => () => {
|
||||
if (isActive(id)) {
|
||||
setHiddenLogIds([...hiddenLogIds, id]);
|
||||
} else {
|
||||
setHiddenLogIds(hiddenLogIds.filter((hid) => hid !== id));
|
||||
}
|
||||
};
|
||||
const label = (id: number, helper?: string) => {
|
||||
if (helperLabels && helperLabels[id] !== undefined) {
|
||||
return helperLabels[id];
|
||||
}
|
||||
return helper ?? id;
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu closeOnSelect={false}>
|
||||
<MenuButton as={Button} rightIcon={<ChevronDownIcon />} isDisabled={availableLogTypes.length === 0}>
|
||||
{t('logs.receiving_types')} ({availableLogTypes.length - hiddenLogIds.length})
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
{availableLogTypes.map((logType) => (
|
||||
<MenuItem key={uuid()} onClick={onToggle(logType.id)} isFocusable={false}>
|
||||
<Checkbox isChecked={isActive(logType.id)}>{label(logType.id, logType.helper)}</Checkbox>
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
92
src/contexts/FirmwareSocketProvider/index.tsx
Normal file
92
src/contexts/FirmwareSocketProvider/index.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useFirmwareStore } from './useStore';
|
||||
import { FirmwareSocketRawMessage } from './utils';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { axiosFms, axiosSec } from 'utils/axiosInstances';
|
||||
|
||||
export type FirmwareSocketContextReturn = Record<string, unknown>;
|
||||
|
||||
const FirmwareSocketContext = React.createContext<FirmwareSocketContextReturn>({
|
||||
webSocket: undefined,
|
||||
isOpen: false,
|
||||
});
|
||||
|
||||
export const FirmwareSocketProvider = ({ children }: { children: React.ReactElement }) => {
|
||||
const { token, isUserLoaded } = useAuth();
|
||||
const { addMessage, isOpen, webSocket, onStartWebSocket } = useFirmwareStore((state) => ({
|
||||
addMessage: state.addMessage,
|
||||
isOpen: state.isWebSocketOpen,
|
||||
webSocket: state.webSocket,
|
||||
onStartWebSocket: state.startWebSocket,
|
||||
}));
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const onMessage = useCallback((message: MessageEvent<string>) => {
|
||||
try {
|
||||
const data = JSON.parse(message.data) as FirmwareSocketRawMessage | undefined;
|
||||
if (data) {
|
||||
addMessage(data, queryClient);
|
||||
}
|
||||
return undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// useEffect for created the WebSocket and 'storing' it in useRef
|
||||
useEffect(() => {
|
||||
if (isUserLoaded && axiosFms?.defaults?.baseURL !== axiosSec?.defaults?.baseURL) {
|
||||
onStartWebSocket(token ?? '');
|
||||
}
|
||||
|
||||
const wsCurrent = webSocket;
|
||||
return () => wsCurrent?.close();
|
||||
}, [isUserLoaded]);
|
||||
|
||||
// useEffect for generating global notifications
|
||||
useEffect(() => {
|
||||
if (webSocket) {
|
||||
webSocket.addEventListener('message', onMessage);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (webSocket) webSocket.removeEventListener('message', onMessage);
|
||||
};
|
||||
}, [webSocket]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
let timeoutId;
|
||||
|
||||
if (webSocket) {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
/* timeoutId = setTimeout(() => {
|
||||
if (webSocket) webSocket.onclose = () => {};
|
||||
webSocket?.close();
|
||||
setIsOpen(false);
|
||||
}, 5000); */
|
||||
} else {
|
||||
// If tab is active again, verify if browser killed the WS
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!isOpen && isUserLoaded && axiosFms?.defaults?.baseURL !== axiosSec?.defaults?.baseURL) {
|
||||
onStartWebSocket(token ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [webSocket, isOpen]);
|
||||
|
||||
const values: FirmwareSocketContextReturn = useMemo(() => ({}), []);
|
||||
|
||||
return <FirmwareSocketContext.Provider value={values}>{children}</FirmwareSocketContext.Provider>;
|
||||
};
|
||||
|
||||
export const useGlobalFirmwareSocket: () => FirmwareSocketContextReturn = () => React.useContext(FirmwareSocketContext);
|
||||
158
src/contexts/FirmwareSocketProvider/useStore.ts
Normal file
158
src/contexts/FirmwareSocketProvider/useStore.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import create from 'zustand';
|
||||
import { FirmwareSocketRawMessage, SocketEventCallback, SocketWebSocketNotificationData } from './utils';
|
||||
import { NotificationType } from 'models/Socket';
|
||||
import { axiosFms } from 'utils/axiosInstances';
|
||||
|
||||
export type FirmwareWebSocketMessage =
|
||||
| {
|
||||
type: 'NOTIFICATION';
|
||||
data: SocketWebSocketNotificationData;
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
}
|
||||
| {
|
||||
type: 'UNKNOWN';
|
||||
data: {
|
||||
type?: string;
|
||||
type_id?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
};
|
||||
|
||||
const parseRawWebSocketMessage = (message?: FirmwareSocketRawMessage): SocketWebSocketNotificationData | undefined => {
|
||||
if (message && message.notification) {
|
||||
if (message.notification.type_id === 1) {
|
||||
return {
|
||||
type: 'LOG',
|
||||
log: message.notification.content,
|
||||
};
|
||||
}
|
||||
} else if (message?.notificationTypes) {
|
||||
return {
|
||||
type: 'INITIAL_MESSAGE',
|
||||
message,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export type FirmwareStoreState = {
|
||||
availableLogTypes: NotificationType[];
|
||||
hiddenLogIds: number[];
|
||||
setHiddenLogIds: (logsToHide: number[]) => void;
|
||||
lastMessage?: FirmwareWebSocketMessage;
|
||||
allMessages: FirmwareWebSocketMessage[];
|
||||
addMessage: (rawMsg: FirmwareSocketRawMessage, queryClient: QueryClient) => void;
|
||||
eventListeners: SocketEventCallback[];
|
||||
addEventListeners: (callback: SocketEventCallback[]) => void;
|
||||
webSocket?: WebSocket;
|
||||
send: (str: string) => void;
|
||||
startWebSocket: (token: string, tries?: number) => void;
|
||||
isWebSocketOpen: boolean;
|
||||
setWebSocketOpen: (isOpen: boolean) => void;
|
||||
lastSearchResults: string[];
|
||||
setLastSearchResults: (result: string[]) => void;
|
||||
errors: { str: string; timestamp: Date }[];
|
||||
};
|
||||
|
||||
export const useFirmwareStore = create<FirmwareStoreState>((set, get) => ({
|
||||
availableLogTypes: [],
|
||||
hiddenLogIds: [],
|
||||
setHiddenLogIds: (logsToHide: number[]) => {
|
||||
get().send(JSON.stringify({ 'drop-notifications': logsToHide }));
|
||||
set(() => ({
|
||||
hiddenLogIds: logsToHide,
|
||||
}));
|
||||
},
|
||||
allMessages: [] as FirmwareWebSocketMessage[],
|
||||
addMessage: (rawMsg: FirmwareSocketRawMessage) => {
|
||||
try {
|
||||
const msg = parseRawWebSocketMessage(rawMsg);
|
||||
if (msg) {
|
||||
// Handle notification-specific logic
|
||||
if (msg.type === 'INITIAL_MESSAGE') {
|
||||
if (msg.message.notificationTypes) {
|
||||
set({ availableLogTypes: msg.message.notificationTypes });
|
||||
}
|
||||
}
|
||||
// General handling
|
||||
const obj: FirmwareWebSocketMessage = {
|
||||
type: 'NOTIFICATION',
|
||||
data: msg,
|
||||
timestamp: msg.log?.timestamp ? new Date(msg.log.timestamp * 1000) : new Date(),
|
||||
id: uuid(),
|
||||
};
|
||||
|
||||
const eventsToFire = get().eventListeners.filter(
|
||||
({ type, serialNumber }) => type === msg.type && serialNumber === msg.serialNumber,
|
||||
);
|
||||
|
||||
if (eventsToFire.length > 0) {
|
||||
for (const event of eventsToFire) {
|
||||
event.callback();
|
||||
}
|
||||
|
||||
return set((state) => ({
|
||||
allMessages:
|
||||
state.allMessages.length <= 1000 ? [...state.allMessages, obj] : [...state.allMessages.slice(1), obj],
|
||||
lastMessage: obj,
|
||||
eventListeners: get().eventListeners.filter(
|
||||
({ id }) => !eventsToFire.find(({ id: findId }) => findId === id),
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
return set((state) => ({
|
||||
allMessages:
|
||||
state.allMessages.length <= 1000 ? [...state.allMessages, obj] : [...state.allMessages.slice(1), obj],
|
||||
lastMessage: obj,
|
||||
}));
|
||||
}
|
||||
return undefined;
|
||||
} catch {
|
||||
// TODO - Add error message to socket logs
|
||||
return set((state) => ({
|
||||
errors: [...state.errors, { str: JSON.stringify(rawMsg), timestamp: new Date() }],
|
||||
}));
|
||||
}
|
||||
},
|
||||
eventListeners: [] as SocketEventCallback[],
|
||||
addEventListeners: (events: SocketEventCallback[]) =>
|
||||
set((state) => ({ eventListeners: [...state.eventListeners, ...events] })),
|
||||
isWebSocketOpen: false,
|
||||
setWebSocketOpen: (isOpen: boolean) => set({ isWebSocketOpen: isOpen }),
|
||||
send: (str: string) => {
|
||||
const ws = get().webSocket;
|
||||
if (ws) ws.send(str);
|
||||
},
|
||||
startWebSocket: (token: string, tries = 0) => {
|
||||
const newTries = tries + 1;
|
||||
if (tries <= 10) {
|
||||
set({
|
||||
webSocket: new WebSocket(
|
||||
`${
|
||||
axiosFms?.defaults?.baseURL ? axiosFms.defaults.baseURL.replace('https', 'wss').replace('http', 'ws') : ''
|
||||
}/ws`,
|
||||
),
|
||||
});
|
||||
const ws = get().webSocket;
|
||||
if (ws) {
|
||||
ws.onopen = () => {
|
||||
set({ isWebSocketOpen: true });
|
||||
ws.send(`token:${token}`);
|
||||
};
|
||||
ws.onclose = () => {
|
||||
set({ isWebSocketOpen: false });
|
||||
setTimeout(() => get().startWebSocket(token, newTries), 3000);
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
lastSearchResults: [] as string[],
|
||||
setLastSearchResults: (results: string[]) => set({ lastSearchResults: results }),
|
||||
errors: [],
|
||||
}));
|
||||
51
src/contexts/FirmwareSocketProvider/utils.ts
Normal file
51
src/contexts/FirmwareSocketProvider/utils.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { InitialSocketMessage } from 'models/Socket';
|
||||
|
||||
export type FirmwareSocketNotificationTypeId = 1 | 1000 | 2000 | 3000 | 4000 | 5000 | 6000;
|
||||
export const FirmwareSocketNotificationTypeMap = {
|
||||
1: 'logs',
|
||||
};
|
||||
|
||||
type LogMessage = {
|
||||
notification: {
|
||||
notificationId: number;
|
||||
type?: undefined;
|
||||
type_id: 1;
|
||||
content: {
|
||||
level: LogLevel;
|
||||
msg: string;
|
||||
source: string;
|
||||
thread_id: number;
|
||||
thread_name: string;
|
||||
timestamp: number;
|
||||
};
|
||||
};
|
||||
serialNumbers?: undefined;
|
||||
notificationTypes?: undefined;
|
||||
};
|
||||
|
||||
export type FirmwareSocketRawMessage = Partial<LogMessage> | InitialSocketMessage;
|
||||
|
||||
export type LogLevel = 'information' | 'critical' | 'debug' | 'error' | 'fatal' | 'notice' | 'trace' | 'warning';
|
||||
|
||||
export type SocketWebSocketNotificationData =
|
||||
| {
|
||||
type: 'LOG';
|
||||
serialNumber?: undefined;
|
||||
serialNumbers?: undefined;
|
||||
notificationTypes?: undefined;
|
||||
log: LogMessage['notification']['content'];
|
||||
}
|
||||
| {
|
||||
type: 'INITIAL_MESSAGE';
|
||||
serialNumber?: undefined;
|
||||
serialNumbers?: undefined;
|
||||
notificationTypes?: undefined;
|
||||
log?: undefined;
|
||||
message: InitialSocketMessage;
|
||||
};
|
||||
export type SocketEventCallback = {
|
||||
id: string;
|
||||
type: 'LOG';
|
||||
serialNumber: string;
|
||||
callback: () => void;
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import debounce from '../../../../utils/debounce';
|
||||
import useWebSocketCommand from './useWebSocketCommand';
|
||||
import { ProviderCommandResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
||||
import debounce from 'utils/debounce';
|
||||
import { ProvisioningCommandResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
||||
|
||||
export type UseDeviceSearchProps = {
|
||||
minLength?: number;
|
||||
@@ -14,7 +14,7 @@ export const useProviderDeviceSearch = ({ minLength = 4, operatorId }: UseDevice
|
||||
{ command: string; serial_prefix: string; operatorId?: string } | undefined
|
||||
>(undefined);
|
||||
const [results, setResults] = useState<string[]>([]);
|
||||
const onNewResult = (newResult: ProviderCommandResponse) => {
|
||||
const onNewResult = (newResult: ProvisioningCommandResponse) => {
|
||||
if (newResult.response.serialNumbers) setResults(newResult.response.serialNumbers as string[]);
|
||||
};
|
||||
const { isOpen, send } = useWebSocketCommand({ callback: onNewResult });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import useWebSocketCommand from './useWebSocketCommand';
|
||||
import { ProviderCommandResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
||||
import { ProvisioningCommandResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
||||
import debounce from 'utils/debounce';
|
||||
|
||||
export type UseLocationSearchProps = {
|
||||
@@ -11,7 +11,7 @@ export const useLocationSearch = ({ minLength = 8 }: UseLocationSearchProps) =>
|
||||
const [tempValue, setTempValue] = useState('');
|
||||
const [waitingSearch, setWaitingSearch] = useState<{ command: string; address: string } | undefined>(undefined);
|
||||
const [results, setResults] = useState<string[]>([]);
|
||||
const onNewResult = (newResult: ProviderCommandResponse) => {
|
||||
const onNewResult = (newResult: ProvisioningCommandResponse) => {
|
||||
if (newResult.response.results) setResults(newResult.response.results as string[]);
|
||||
};
|
||||
const { isOpen, send } = useWebSocketCommand({ callback: onNewResult });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import useWebSocketCommand from './useWebSocketCommand';
|
||||
import { ProviderCommandResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
||||
import { ProvisioningCommandResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
||||
import { Subscriber } from 'models/Subscriber';
|
||||
import debounce from 'utils/debounce';
|
||||
|
||||
@@ -15,7 +15,7 @@ export const useSubscriberSearch = ({ minLength = 4, operatorId, mode }: UseSubs
|
||||
{ command: string; emailSearch?: string; nameSearch?: string; operatorId?: string } | undefined
|
||||
>(undefined);
|
||||
const [results, setResults] = useState<Subscriber[]>([]);
|
||||
const onNewResult = (newResult: ProviderCommandResponse) => {
|
||||
const onNewResult = (newResult: ProvisioningCommandResponse) => {
|
||||
if (newResult.response.users) setResults(newResult.response.users as Subscriber[]);
|
||||
};
|
||||
const { isOpen, send } = useWebSocketCommand({ callback: onNewResult });
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useProviderStore } from 'contexts/ProvisioningSocketProvider/useStore';
|
||||
import { ProviderCommandResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
||||
import { useProvisioningStore } from '../../useStore';
|
||||
import { ProvisioningCommandResponse } from '../../utils';
|
||||
import { randomIntId } from 'utils/stringHelper';
|
||||
|
||||
const useProviderWebSocketCommand = ({ callback }: { callback: (command: ProviderCommandResponse) => void }) => {
|
||||
const { isOpen, webSocket, lastMessage } = useProviderStore((state) => ({
|
||||
const useProviderWebSocketCommand = ({ callback }: { callback: (command: ProvisioningCommandResponse) => void }) => {
|
||||
const { isOpen, webSocket, lastMessage } = useProvisioningStore((state) => ({
|
||||
isOpen: state.isWebSocketOpen,
|
||||
webSocket: state.webSocket,
|
||||
lastMessage: state.lastMessage,
|
||||
|
||||
@@ -2,10 +2,10 @@ import React from 'react';
|
||||
import { Box, Heading, ListItem, UnorderedList } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { ProviderWebSocketVenueUpdateResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
||||
import { ProvisioningVenueNotificationMessage } from 'contexts/ProvisioningSocketProvider/utils';
|
||||
|
||||
interface Props {
|
||||
notification: ProviderWebSocketVenueUpdateResponse;
|
||||
notification: ProvisioningVenueNotificationMessage['notification'];
|
||||
}
|
||||
|
||||
const ConfigurationPushesNotificationContent = ({ notification }: Props) => {
|
||||
|
||||
@@ -2,10 +2,10 @@ import React from 'react';
|
||||
import { Box, Heading, ListItem, UnorderedList } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { ProviderWebSocketVenueUpdateResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
||||
import { ProvisioningVenueNotificationMessage } from 'contexts/ProvisioningSocketProvider/utils';
|
||||
|
||||
interface Props {
|
||||
notification: ProviderWebSocketVenueUpdateResponse;
|
||||
notification: ProvisioningVenueNotificationMessage['notification'];
|
||||
}
|
||||
|
||||
const DeviceRebootNotificationContent = ({ notification }: Props) => {
|
||||
|
||||
@@ -2,10 +2,10 @@ import React from 'react';
|
||||
import { Box, Heading, ListItem, UnorderedList } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { ProviderWebSocketVenueUpdateResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
||||
import { ProvisioningVenueNotificationMessage } from 'contexts/ProvisioningSocketProvider/utils';
|
||||
|
||||
interface Props {
|
||||
notification: ProviderWebSocketVenueUpdateResponse;
|
||||
notification: ProvisioningVenueNotificationMessage['notification'];
|
||||
}
|
||||
|
||||
const DeviceUpgradeNotificationContent = ({ notification }: Props) => {
|
||||
|
||||
@@ -2,24 +2,24 @@ import React from 'react';
|
||||
import ConfigurationPushesNotificationContent from './ConfigurationPushes';
|
||||
import DeviceRebootNotificationContent from './DeviceReboot';
|
||||
import DeviceUpgradeNotificationContent from './DeviceUpgrade';
|
||||
import { ProviderWebSocketVenueUpdateResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
||||
import { ProvisioningVenueNotificationMessage } from 'contexts/ProvisioningSocketProvider/utils';
|
||||
|
||||
interface Props {
|
||||
notification?: ProviderWebSocketVenueUpdateResponse;
|
||||
notification?: ProvisioningVenueNotificationMessage['notification'];
|
||||
}
|
||||
|
||||
const NotificationContent = ({ notification }: Props) => {
|
||||
if (!notification) return null;
|
||||
|
||||
if (notification.type === 'entity_configuration_update' || notification.type === 'venue_configuration_update') {
|
||||
if (notification.type_id === 2000 || notification.type === 'venue_config_update') {
|
||||
return <ConfigurationPushesNotificationContent notification={notification} />;
|
||||
}
|
||||
|
||||
if (notification.type === 'venue_rebooter') {
|
||||
if (notification.type_id === 3000 || notification.type === 'venue_rebooter') {
|
||||
return <DeviceRebootNotificationContent notification={notification} />;
|
||||
}
|
||||
|
||||
if (notification.type === 'venue_upgrader') {
|
||||
if (notification.type_id === 1000 || notification.type === 'venue_fw_upgrade') {
|
||||
return <DeviceUpgradeNotificationContent notification={notification} />;
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -19,14 +19,14 @@ import {
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { StringMap, TOptions } from 'i18next';
|
||||
import { TOptions } from 'i18next';
|
||||
import { X } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { ProviderWebSocketVenueUpdateResponse } from '../../utils';
|
||||
import { ProvisioningVenueNotificationMessage } from '../../utils';
|
||||
import NotificationContent from '.';
|
||||
|
||||
const getStatusFromNotification = (notification: ProviderWebSocketVenueUpdateResponse) => {
|
||||
const getStatusFromNotification = (notification: ProvisioningVenueNotificationMessage['notification']) => {
|
||||
let status: 'success' | 'warning' | 'error' = 'success';
|
||||
if (notification.content.warning?.length > 0) status = 'warning';
|
||||
if (notification.content.error?.length > 0) status = 'error';
|
||||
@@ -35,15 +35,10 @@ const getStatusFromNotification = (notification: ProviderWebSocketVenueUpdateRes
|
||||
};
|
||||
|
||||
const getNotificationDescription = (
|
||||
t: (key: string, options?: string | TOptions<StringMap> | undefined) => string,
|
||||
notification: ProviderWebSocketVenueUpdateResponse,
|
||||
t: (key: string, options?: string | TOptions<Record<string, number>> | undefined) => string,
|
||||
notification: ProvisioningVenueNotificationMessage['notification'],
|
||||
) => {
|
||||
if (
|
||||
notification.content.type === 'venue_configuration_update' ||
|
||||
notification.content.type === 'entity_configuration_update' ||
|
||||
notification.content.type === 'venue_rebooter' ||
|
||||
notification.content.type === 'venue_upgrader'
|
||||
) {
|
||||
if (notification.type_id === 1000 || notification.type_id === 2000 || notification.type_id === 3000) {
|
||||
return t('configurations.notification_details', {
|
||||
success: notification.content.success?.length ?? 0,
|
||||
warning: notification.content.warning?.length ?? 0,
|
||||
@@ -56,16 +51,19 @@ const getNotificationDescription = (
|
||||
const useWebSocketNotification = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [notif, setNotif] = useState<ProviderWebSocketVenueUpdateResponse | undefined>(undefined);
|
||||
const [notif, setNotif] = useState<ProvisioningVenueNotificationMessage['notification'] | undefined>(undefined);
|
||||
const toast = useToast();
|
||||
|
||||
const openDetails = useCallback((newObj: ProviderWebSocketVenueUpdateResponse, closeToast?: () => void) => {
|
||||
setNotif(newObj);
|
||||
if (closeToast) closeToast();
|
||||
onOpen();
|
||||
}, []);
|
||||
const openDetails = useCallback(
|
||||
(newObj: ProvisioningVenueNotificationMessage['notification'], closeToast?: () => void) => {
|
||||
setNotif(newObj);
|
||||
if (closeToast) closeToast();
|
||||
onOpen();
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const pushNotification = useCallback((notification: ProviderWebSocketVenueUpdateResponse) => {
|
||||
const pushNotification = useCallback((notification: ProvisioningVenueNotificationMessage['notification']) => {
|
||||
toast({
|
||||
id: uuid(),
|
||||
duration: 5000,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import useWebSocketNotification from './hooks/NotificationContent/useWebSocketNotification';
|
||||
import { useProviderStore } from './useStore';
|
||||
import { extractProviderWebSocketResponse } from './utils';
|
||||
import { useProvisioningStore } from './useStore';
|
||||
import { ProvisioningSocketRawMessage } from './utils';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { axiosProv, axiosSec } from 'utils/axiosInstances';
|
||||
|
||||
export type ProviderSocketContextReturn = Record<string, unknown>;
|
||||
export type ProvisioningSocketContextReturn = Record<string, unknown>;
|
||||
|
||||
const ProviderSocketContext = React.createContext<ProviderSocketContextReturn>({
|
||||
const ProvisioningSocketContext = React.createContext<ProvisioningSocketContextReturn>({
|
||||
webSocket: undefined,
|
||||
isOpen: false,
|
||||
});
|
||||
@@ -15,22 +15,18 @@ const ProviderSocketContext = React.createContext<ProviderSocketContextReturn>({
|
||||
export const ProvisioningSocketProvider = ({ children }: { children: React.ReactElement }) => {
|
||||
const { token, isUserLoaded } = useAuth();
|
||||
const { pushNotification, modal } = useWebSocketNotification();
|
||||
const { addMessage, isOpen, setIsOpen, webSocket, onStartWebSocket } = useProviderStore((state) => ({
|
||||
const { addMessage, isOpen, webSocket, onStartWebSocket } = useProvisioningStore((state) => ({
|
||||
addMessage: state.addMessage,
|
||||
setIsOpen: state.setWebSocketOpen,
|
||||
isOpen: state.isWebSocketOpen,
|
||||
webSocket: state.webSocket,
|
||||
onStartWebSocket: state.startWebSocket,
|
||||
}));
|
||||
|
||||
const onMessage = useCallback((msg: MessageEvent<string>) => {
|
||||
const onMessage = useCallback((message: MessageEvent<string>) => {
|
||||
try {
|
||||
const extracted = extractProviderWebSocketResponse(msg);
|
||||
if (extracted) {
|
||||
addMessage(extracted);
|
||||
if (extracted.type === 'NOTIFICATION') {
|
||||
pushNotification(extracted.data);
|
||||
}
|
||||
const data = JSON.parse(message.data) as ProvisioningSocketRawMessage | undefined;
|
||||
if (data) {
|
||||
addMessage(data, pushNotification);
|
||||
}
|
||||
return undefined;
|
||||
} catch {
|
||||
@@ -65,12 +61,13 @@ export const ProvisioningSocketProvider = ({ children }: { children: React.React
|
||||
|
||||
if (webSocket) {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
timeoutId = setTimeout(() => {
|
||||
/* timeoutId = setTimeout(() => {
|
||||
if (webSocket) webSocket.onclose = () => {};
|
||||
webSocket?.close();
|
||||
setIsOpen(false);
|
||||
}, 5000);
|
||||
}, 5000); */
|
||||
} else {
|
||||
// If tab is active again, verify if browser killed the WS
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!isOpen && isUserLoaded && axiosProv?.defaults?.baseURL !== axiosSec?.defaults?.baseURL) {
|
||||
@@ -86,17 +83,17 @@ export const ProvisioningSocketProvider = ({ children }: { children: React.React
|
||||
};
|
||||
}, [webSocket, isOpen]);
|
||||
|
||||
const values: ProviderSocketContextReturn = useMemo(() => ({}), []);
|
||||
const values: ProvisioningSocketContextReturn = useMemo(() => ({}), []);
|
||||
|
||||
return (
|
||||
<ProviderSocketContext.Provider value={values}>
|
||||
<ProvisioningSocketContext.Provider value={values}>
|
||||
<>
|
||||
{children}
|
||||
{modal}
|
||||
</>
|
||||
</ProviderSocketContext.Provider>
|
||||
</ProvisioningSocketContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useGlobalProvisioningSocket: () => ProviderSocketContextReturn = () =>
|
||||
React.useContext(ProviderSocketContext);
|
||||
export const useGlobalProvisioningSocket: () => ProvisioningSocketContextReturn = () =>
|
||||
React.useContext(ProvisioningSocketContext);
|
||||
|
||||
@@ -1,41 +1,170 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import create from 'zustand';
|
||||
import { ProviderWebSocketMessage, ProviderWebSocketParsedMessage } from './utils';
|
||||
import {
|
||||
ACCEPTED_VENUE_NOTIFICATION_TYPES,
|
||||
ProvisioningCommandResponse,
|
||||
ProvisioningSocketRawMessage,
|
||||
ProvisioningVenueNotificationMessage,
|
||||
SocketEventCallback,
|
||||
SocketWebSocketNotificationData,
|
||||
} from './utils';
|
||||
import { NotificationType } from 'models/Socket';
|
||||
import { axiosProv } from 'utils/axiosInstances';
|
||||
|
||||
export type ProviderStoreState = {
|
||||
lastMessage?: ProviderWebSocketMessage;
|
||||
allMessages: ProviderWebSocketMessage[];
|
||||
addMessage: (message: ProviderWebSocketParsedMessage) => void;
|
||||
export type ProvisioningWebSocketMessage =
|
||||
| {
|
||||
type: 'NOTIFICATION';
|
||||
data: SocketWebSocketNotificationData;
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
}
|
||||
| {
|
||||
type: 'COMMAND';
|
||||
data: ProvisioningCommandResponse;
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
}
|
||||
| {
|
||||
type: 'UNKNOWN';
|
||||
data: {
|
||||
type?: string;
|
||||
type_id?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
};
|
||||
|
||||
const parseRawWebSocketMessage = (
|
||||
message?: ProvisioningSocketRawMessage,
|
||||
): SocketWebSocketNotificationData | undefined => {
|
||||
if (message && message.notification) {
|
||||
if (message.notification.type_id === 1) {
|
||||
return {
|
||||
type: 'LOG',
|
||||
log: message.notification.content,
|
||||
};
|
||||
}
|
||||
if (
|
||||
message.notification.type_id === 1000 ||
|
||||
message.notification.type_id === 2000 ||
|
||||
message.notification.type_id === 3000
|
||||
) {
|
||||
return {
|
||||
type: 'NOTIFICATION',
|
||||
data: message.notification,
|
||||
};
|
||||
}
|
||||
} else if (message?.notificationTypes) {
|
||||
return {
|
||||
type: 'INITIAL_MESSAGE',
|
||||
message,
|
||||
};
|
||||
} else if (message?.response && message.command_response_id) {
|
||||
return {
|
||||
type: 'COMMAND',
|
||||
data: message as ProvisioningCommandResponse,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export type ProvisioningStoreState = {
|
||||
availableLogTypes: NotificationType[];
|
||||
hiddenLogIds: number[];
|
||||
setHiddenLogIds: (logsToHide: number[]) => void;
|
||||
lastMessage?: ProvisioningWebSocketMessage;
|
||||
allMessages: ProvisioningWebSocketMessage[];
|
||||
addMessage: (
|
||||
rawMsg: ProvisioningSocketRawMessage,
|
||||
pushNotification: (notification: ProvisioningVenueNotificationMessage['notification']) => void,
|
||||
) => void;
|
||||
eventListeners: SocketEventCallback[];
|
||||
addEventListeners: (callback: SocketEventCallback[]) => void;
|
||||
webSocket?: WebSocket;
|
||||
send: (str: string) => void;
|
||||
startWebSocket: (token: string, tries?: number) => void;
|
||||
isWebSocketOpen: boolean;
|
||||
setWebSocketOpen: (isOpen: boolean) => void;
|
||||
lastSearchResults: string[];
|
||||
setLastSearchResults: (result: string[]) => void;
|
||||
errors: { str: string; timestamp: Date }[];
|
||||
};
|
||||
|
||||
export const useProviderStore = create<ProviderStoreState>((set, get) => ({
|
||||
allMessages: [] as ProviderWebSocketMessage[],
|
||||
addMessage: (msg: ProviderWebSocketParsedMessage) => {
|
||||
// @ts-ignore
|
||||
const obj: ProviderWebSocketMessage =
|
||||
msg.type === 'COMMAND'
|
||||
? {
|
||||
type: msg.type,
|
||||
data: msg.data,
|
||||
timestamp: new Date(),
|
||||
}
|
||||
: {
|
||||
type: msg.type,
|
||||
data: msg.data,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
return set((state) => ({
|
||||
allMessages:
|
||||
state.allMessages.length <= 1000 ? [...state.allMessages, obj] : [...state.allMessages.slice(1), obj],
|
||||
lastMessage: obj,
|
||||
export const useProvisioningStore = create<ProvisioningStoreState>((set, get) => ({
|
||||
availableLogTypes: [],
|
||||
hiddenLogIds: [],
|
||||
setHiddenLogIds: (logsToHide: number[]) => {
|
||||
get().send(JSON.stringify({ 'drop-notifications': logsToHide }));
|
||||
set(() => ({
|
||||
hiddenLogIds: logsToHide,
|
||||
}));
|
||||
},
|
||||
allMessages: [] as ProvisioningWebSocketMessage[],
|
||||
addMessage: (rawMsg, pushNotification) => {
|
||||
try {
|
||||
const msg = parseRawWebSocketMessage(rawMsg);
|
||||
if (msg) {
|
||||
// Handle notification-specific logic
|
||||
if (msg.type === 'INITIAL_MESSAGE') {
|
||||
if (msg.message.notificationTypes) {
|
||||
set({ availableLogTypes: msg.message.notificationTypes });
|
||||
}
|
||||
}
|
||||
// Handle venue notifications
|
||||
if (msg.type === 'NOTIFICATION' && ACCEPTED_VENUE_NOTIFICATION_TYPES.includes(msg.data.type_id)) {
|
||||
pushNotification(msg.data);
|
||||
}
|
||||
// General handling
|
||||
const obj: ProvisioningWebSocketMessage =
|
||||
msg.type === 'COMMAND'
|
||||
? {
|
||||
type: 'COMMAND',
|
||||
data: msg.data,
|
||||
timestamp: new Date(),
|
||||
id: uuid(),
|
||||
}
|
||||
: {
|
||||
type: 'NOTIFICATION',
|
||||
data: msg,
|
||||
timestamp: msg.log?.timestamp ? new Date(msg.log.timestamp * 1000) : new Date(),
|
||||
id: uuid(),
|
||||
};
|
||||
|
||||
const eventsToFire = get().eventListeners.filter(({ type }) => type === msg.type);
|
||||
|
||||
if (eventsToFire.length > 0) {
|
||||
for (const event of eventsToFire) {
|
||||
event.callback();
|
||||
}
|
||||
|
||||
return set((state) => ({
|
||||
allMessages:
|
||||
state.allMessages.length <= 1000 ? [...state.allMessages, obj] : [...state.allMessages.slice(1), obj],
|
||||
lastMessage: obj,
|
||||
eventListeners: get().eventListeners.filter(
|
||||
({ id }) => !eventsToFire.find(({ id: findId }) => findId === id),
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
return set((state) => ({
|
||||
allMessages:
|
||||
state.allMessages.length <= 1000 ? [...state.allMessages, obj] : [...state.allMessages.slice(1), obj],
|
||||
lastMessage: obj,
|
||||
}));
|
||||
}
|
||||
return undefined;
|
||||
} catch {
|
||||
// TODO - Add error message to socket logs
|
||||
return set((state) => ({
|
||||
errors: [...state.errors, { str: JSON.stringify(rawMsg), timestamp: new Date() }],
|
||||
}));
|
||||
}
|
||||
},
|
||||
eventListeners: [] as SocketEventCallback[],
|
||||
addEventListeners: (events: SocketEventCallback[]) =>
|
||||
set((state) => ({ eventListeners: [...state.eventListeners, ...events] })),
|
||||
isWebSocketOpen: false,
|
||||
setWebSocketOpen: (isOpen: boolean) => set({ isWebSocketOpen: isOpen }),
|
||||
send: (str: string) => {
|
||||
@@ -65,4 +194,7 @@ export const useProviderStore = create<ProviderStoreState>((set, get) => ({
|
||||
}
|
||||
}
|
||||
},
|
||||
lastSearchResults: [] as string[],
|
||||
setLastSearchResults: (results: string[]) => set({ lastSearchResults: results }),
|
||||
errors: [],
|
||||
}));
|
||||
|
||||
@@ -1,76 +1,99 @@
|
||||
import { InitialSocketMessage } from 'models/Socket';
|
||||
import { Subscriber } from 'models/Subscriber';
|
||||
|
||||
// Notifications we react to from the WS
|
||||
export const acceptedNotificationTypes = [
|
||||
'venue_configuration_update',
|
||||
'entity_configuration_update',
|
||||
'venue_rebooter',
|
||||
'venue_upgrader',
|
||||
];
|
||||
|
||||
// Data received from WS on Venue update notification
|
||||
export type ProviderWebSocketVenueUpdateResponse = {
|
||||
notification_id: number;
|
||||
type: 'venue_configuration_update' | 'entity_configuration_update' | 'venue_rebooter' | 'venue_upgrader';
|
||||
content: {
|
||||
type: 'venue_configuration_update' | 'entity_configuration_update' | 'venue_rebooter' | 'venue_upgrader';
|
||||
title: string;
|
||||
details: string;
|
||||
success: string[];
|
||||
noFirmware?: string[];
|
||||
notConnected?: string[];
|
||||
skipped?: string[];
|
||||
warning: string[];
|
||||
error: string[];
|
||||
timeStamp: number;
|
||||
};
|
||||
export type ProvisioningSocketNotificationTypeId = 1 | 1000 | 2000 | 3000 | 4000 | 5000 | 6000;
|
||||
export const ProvisioningSocketNotificationTypeMap = {
|
||||
1: 'logs',
|
||||
1000: 'venue_fw_upgrade',
|
||||
2000: 'venue_config_update',
|
||||
3000: 'venue_rebooter',
|
||||
};
|
||||
|
||||
export type ProviderCommandResponse = {
|
||||
export const ACCEPTED_VENUE_NOTIFICATION_TYPES = [1000, 2000, 3000];
|
||||
|
||||
export type ProvisioningCommandResponse = {
|
||||
command_response_id: number;
|
||||
response: { serialNumbers?: string[]; users?: Subscriber[]; results?: string[] };
|
||||
notification?: undefined;
|
||||
notificationTypes?: undefined;
|
||||
};
|
||||
|
||||
// Parsed WebSocket message
|
||||
export type ProviderWebSocketParsedMessage =
|
||||
type LogMessage = {
|
||||
notification: {
|
||||
notificationId: number;
|
||||
type?: undefined;
|
||||
type_id: 1;
|
||||
content: {
|
||||
level: LogLevel;
|
||||
msg: string;
|
||||
source: string;
|
||||
thread_id: number;
|
||||
thread_name: string;
|
||||
timestamp: number;
|
||||
};
|
||||
};
|
||||
command_response_id?: undefined;
|
||||
response?: undefined;
|
||||
notificationTypes?: undefined;
|
||||
};
|
||||
|
||||
export type ProvisioningVenueNotificationMessage = {
|
||||
notification: {
|
||||
notification_id: number;
|
||||
type?: 'venue_fw_upgrade' | 'venue_config_update' | 'venue_rebooter';
|
||||
type_id: 1000 | 2000 | 3000;
|
||||
content: {
|
||||
title: string;
|
||||
details: string;
|
||||
success: string[];
|
||||
noFirmware?: string[];
|
||||
notConnected?: string[];
|
||||
skipped?: string[];
|
||||
warning: string[];
|
||||
error: string[];
|
||||
timeStamp: number;
|
||||
};
|
||||
};
|
||||
command_response_id?: undefined;
|
||||
response?: undefined;
|
||||
notificationTypes?: undefined;
|
||||
};
|
||||
|
||||
export type ProvisioningSocketRawMessage =
|
||||
| Partial<LogMessage>
|
||||
| Partial<ProvisioningVenueNotificationMessage>
|
||||
| Partial<ProvisioningCommandResponse>
|
||||
| InitialSocketMessage;
|
||||
|
||||
export type LogLevel = 'information' | 'critical' | 'debug' | 'error' | 'fatal' | 'notice' | 'trace' | 'warning';
|
||||
|
||||
export type SocketWebSocketNotificationData =
|
||||
| {
|
||||
type: 'NOTIFICATION';
|
||||
data: ProviderWebSocketVenueUpdateResponse;
|
||||
data: ProvisioningVenueNotificationMessage['notification'];
|
||||
log?: undefined;
|
||||
notificationTypes?: undefined;
|
||||
}
|
||||
| {
|
||||
type: 'LOG';
|
||||
notificationTypes?: undefined;
|
||||
log: LogMessage['notification']['content'];
|
||||
}
|
||||
| {
|
||||
type: 'COMMAND';
|
||||
data: ProviderCommandResponse;
|
||||
data: ProvisioningCommandResponse;
|
||||
notificationTypes?: undefined;
|
||||
log?: undefined;
|
||||
}
|
||||
| {
|
||||
type: 'INITIAL_MESSAGE';
|
||||
notificationTypes?: undefined;
|
||||
log?: undefined;
|
||||
message: InitialSocketMessage;
|
||||
};
|
||||
|
||||
// Parsing raw WS messages into a more usable format
|
||||
export const extractProviderWebSocketResponse = (message: MessageEvent): ProviderWebSocketParsedMessage | undefined => {
|
||||
try {
|
||||
const data = JSON.parse(message.data);
|
||||
if (data.notification && acceptedNotificationTypes.includes(data.notification.type)) {
|
||||
const notification = data.notification as ProviderWebSocketVenueUpdateResponse;
|
||||
return { data: notification, type: 'NOTIFICATION' };
|
||||
}
|
||||
if (data.command_response_id) {
|
||||
return { data, type: 'COMMAND' } as {
|
||||
type: 'COMMAND';
|
||||
data: ProviderCommandResponse;
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
export type SocketEventCallback = {
|
||||
id: string;
|
||||
type: 'LOG';
|
||||
serialNumber: string;
|
||||
callback: () => void;
|
||||
};
|
||||
|
||||
// What we store in the store
|
||||
export type ProviderWebSocketMessage =
|
||||
| {
|
||||
type: 'NOTIFICATION';
|
||||
data: ProviderWebSocketParsedMessage;
|
||||
timestamp: Date;
|
||||
}
|
||||
| {
|
||||
type: 'COMMAND';
|
||||
data: ProviderCommandResponse;
|
||||
timestamp: Date;
|
||||
};
|
||||
|
||||
91
src/contexts/SecuritySocketProvider/index.tsx
Normal file
91
src/contexts/SecuritySocketProvider/index.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useSecurityStore } from './useStore';
|
||||
import { SecuritySocketRawMessage } from './utils';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
|
||||
export type SecuritySocketContextReturn = Record<string, unknown>;
|
||||
|
||||
const SecuritySocketContext = React.createContext<SecuritySocketContextReturn>({
|
||||
webSocket: undefined,
|
||||
isOpen: false,
|
||||
});
|
||||
|
||||
export const SecuritySocketProvider = ({ children }: { children: React.ReactElement }) => {
|
||||
const { token, isUserLoaded } = useAuth();
|
||||
const { addMessage, isOpen, webSocket, onStartWebSocket } = useSecurityStore((state) => ({
|
||||
addMessage: state.addMessage,
|
||||
isOpen: state.isWebSocketOpen,
|
||||
webSocket: state.webSocket,
|
||||
onStartWebSocket: state.startWebSocket,
|
||||
}));
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const onMessage = useCallback((message: MessageEvent<string>) => {
|
||||
try {
|
||||
const data = JSON.parse(message.data) as SecuritySocketRawMessage | undefined;
|
||||
if (data) {
|
||||
addMessage(data, queryClient);
|
||||
}
|
||||
return undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// useEffect for created the WebSocket and 'storing' it in useRef
|
||||
useEffect(() => {
|
||||
if (isUserLoaded) {
|
||||
onStartWebSocket(token ?? '');
|
||||
}
|
||||
|
||||
const wsCurrent = webSocket;
|
||||
return () => wsCurrent?.close();
|
||||
}, [isUserLoaded]);
|
||||
|
||||
// useEffect for generating global notifications
|
||||
useEffect(() => {
|
||||
if (webSocket) {
|
||||
webSocket.addEventListener('message', onMessage);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (webSocket) webSocket.removeEventListener('message', onMessage);
|
||||
};
|
||||
}, [webSocket]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
let timeoutId;
|
||||
|
||||
if (webSocket) {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
/* timeoutId = setTimeout(() => {
|
||||
if (webSocket) webSocket.onclose = () => {};
|
||||
webSocket?.close();
|
||||
setIsOpen(false);
|
||||
}, 5000); */
|
||||
} else {
|
||||
// If tab is active again, verify if browser killed the WS
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!isOpen && isUserLoaded) {
|
||||
onStartWebSocket(token ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [webSocket, isOpen]);
|
||||
|
||||
const values: SecuritySocketContextReturn = useMemo(() => ({}), []);
|
||||
|
||||
return <SecuritySocketContext.Provider value={values}>{children}</SecuritySocketContext.Provider>;
|
||||
};
|
||||
|
||||
export const useGlobalSecuritySocket: () => SecuritySocketContextReturn = () => React.useContext(SecuritySocketContext);
|
||||
158
src/contexts/SecuritySocketProvider/useStore.ts
Normal file
158
src/contexts/SecuritySocketProvider/useStore.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import create from 'zustand';
|
||||
import { SecuritySocketRawMessage, SocketEventCallback, SocketWebSocketNotificationData } from './utils';
|
||||
import { NotificationType } from 'models/Socket';
|
||||
import { axiosSec } from 'utils/axiosInstances';
|
||||
|
||||
export type SecurityWebSocketMessage =
|
||||
| {
|
||||
type: 'NOTIFICATION';
|
||||
data: SocketWebSocketNotificationData;
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
}
|
||||
| {
|
||||
type: 'UNKNOWN';
|
||||
data: {
|
||||
type?: string;
|
||||
type_id?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
};
|
||||
|
||||
const parseRawWebSocketMessage = (message?: SecuritySocketRawMessage): SocketWebSocketNotificationData | undefined => {
|
||||
if (message && message.notification) {
|
||||
if (message.notification.type_id === 1) {
|
||||
return {
|
||||
type: 'LOG',
|
||||
log: message.notification.content,
|
||||
};
|
||||
}
|
||||
} else if (message?.notificationTypes) {
|
||||
return {
|
||||
type: 'INITIAL_MESSAGE',
|
||||
message,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export type SecurityStoreState = {
|
||||
availableLogTypes: NotificationType[];
|
||||
hiddenLogIds: number[];
|
||||
setHiddenLogIds: (logsToHide: number[]) => void;
|
||||
lastMessage?: SecurityWebSocketMessage;
|
||||
allMessages: SecurityWebSocketMessage[];
|
||||
addMessage: (rawMsg: SecuritySocketRawMessage, queryClient: QueryClient) => void;
|
||||
eventListeners: SocketEventCallback[];
|
||||
addEventListeners: (callback: SocketEventCallback[]) => void;
|
||||
webSocket?: WebSocket;
|
||||
send: (str: string) => void;
|
||||
startWebSocket: (token: string, tries?: number) => void;
|
||||
isWebSocketOpen: boolean;
|
||||
setWebSocketOpen: (isOpen: boolean) => void;
|
||||
lastSearchResults: string[];
|
||||
setLastSearchResults: (result: string[]) => void;
|
||||
errors: { str: string; timestamp: Date }[];
|
||||
};
|
||||
|
||||
export const useSecurityStore = create<SecurityStoreState>((set, get) => ({
|
||||
availableLogTypes: [],
|
||||
hiddenLogIds: [],
|
||||
setHiddenLogIds: (logsToHide: number[]) => {
|
||||
get().send(JSON.stringify({ 'drop-notifications': logsToHide }));
|
||||
set(() => ({
|
||||
hiddenLogIds: logsToHide,
|
||||
}));
|
||||
},
|
||||
allMessages: [] as SecurityWebSocketMessage[],
|
||||
addMessage: (rawMsg: SecuritySocketRawMessage) => {
|
||||
try {
|
||||
const msg = parseRawWebSocketMessage(rawMsg);
|
||||
if (msg) {
|
||||
// Handle notification-specific logic
|
||||
if (msg.type === 'INITIAL_MESSAGE') {
|
||||
if (msg.message.notificationTypes) {
|
||||
set({ availableLogTypes: msg.message.notificationTypes });
|
||||
}
|
||||
}
|
||||
// General handling
|
||||
const obj: SecurityWebSocketMessage = {
|
||||
type: 'NOTIFICATION',
|
||||
data: msg,
|
||||
timestamp: msg.log?.timestamp ? new Date(msg.log.timestamp * 1000) : new Date(),
|
||||
id: uuid(),
|
||||
};
|
||||
|
||||
const eventsToFire = get().eventListeners.filter(
|
||||
({ type, serialNumber }) => type === msg.type && serialNumber === msg.serialNumber,
|
||||
);
|
||||
|
||||
if (eventsToFire.length > 0) {
|
||||
for (const event of eventsToFire) {
|
||||
event.callback();
|
||||
}
|
||||
|
||||
return set((state) => ({
|
||||
allMessages:
|
||||
state.allMessages.length <= 1000 ? [...state.allMessages, obj] : [...state.allMessages.slice(1), obj],
|
||||
lastMessage: obj,
|
||||
eventListeners: get().eventListeners.filter(
|
||||
({ id }) => !eventsToFire.find(({ id: findId }) => findId === id),
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
return set((state) => ({
|
||||
allMessages:
|
||||
state.allMessages.length <= 1000 ? [...state.allMessages, obj] : [...state.allMessages.slice(1), obj],
|
||||
lastMessage: obj,
|
||||
}));
|
||||
}
|
||||
return undefined;
|
||||
} catch {
|
||||
// TODO - Add error message to socket logs
|
||||
return set((state) => ({
|
||||
errors: [...state.errors, { str: JSON.stringify(rawMsg), timestamp: new Date() }],
|
||||
}));
|
||||
}
|
||||
},
|
||||
eventListeners: [] as SocketEventCallback[],
|
||||
addEventListeners: (events: SocketEventCallback[]) =>
|
||||
set((state) => ({ eventListeners: [...state.eventListeners, ...events] })),
|
||||
isWebSocketOpen: false,
|
||||
setWebSocketOpen: (isOpen: boolean) => set({ isWebSocketOpen: isOpen }),
|
||||
send: (str: string) => {
|
||||
const ws = get().webSocket;
|
||||
if (ws) ws.send(str);
|
||||
},
|
||||
startWebSocket: (token: string, tries = 0) => {
|
||||
const newTries = tries + 1;
|
||||
if (tries <= 10) {
|
||||
set({
|
||||
webSocket: new WebSocket(
|
||||
`${
|
||||
axiosSec?.defaults?.baseURL ? axiosSec.defaults.baseURL.replace('https', 'wss').replace('http', 'ws') : ''
|
||||
}/ws`,
|
||||
),
|
||||
});
|
||||
const ws = get().webSocket;
|
||||
if (ws) {
|
||||
ws.onopen = () => {
|
||||
set({ isWebSocketOpen: true });
|
||||
ws.send(`token:${token}`);
|
||||
};
|
||||
ws.onclose = () => {
|
||||
set({ isWebSocketOpen: false });
|
||||
setTimeout(() => get().startWebSocket(token, newTries), 3000);
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
lastSearchResults: [] as string[],
|
||||
setLastSearchResults: (results: string[]) => set({ lastSearchResults: results }),
|
||||
errors: [],
|
||||
}));
|
||||
51
src/contexts/SecuritySocketProvider/utils.ts
Normal file
51
src/contexts/SecuritySocketProvider/utils.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { InitialSocketMessage } from 'models/Socket';
|
||||
|
||||
export type SecuritySocketNotificationTypeId = 1 | 1000 | 2000 | 3000 | 4000 | 5000 | 6000;
|
||||
export const SecuritySocketNotificationTypeMap = {
|
||||
1: 'logs',
|
||||
};
|
||||
|
||||
type LogMessage = {
|
||||
notification: {
|
||||
notificationId: number;
|
||||
type?: undefined;
|
||||
type_id: 1;
|
||||
content: {
|
||||
level: LogLevel;
|
||||
msg: string;
|
||||
source: string;
|
||||
thread_id: number;
|
||||
thread_name: string;
|
||||
timestamp: number;
|
||||
};
|
||||
};
|
||||
serialNumbers?: undefined;
|
||||
notificationTypes?: undefined;
|
||||
};
|
||||
|
||||
export type SecuritySocketRawMessage = Partial<LogMessage> | InitialSocketMessage;
|
||||
|
||||
export type LogLevel = 'information' | 'critical' | 'debug' | 'error' | 'fatal' | 'notice' | 'trace' | 'warning';
|
||||
|
||||
export type SocketWebSocketNotificationData =
|
||||
| {
|
||||
type: 'LOG';
|
||||
serialNumber?: undefined;
|
||||
serialNumbers?: undefined;
|
||||
notificationTypes?: undefined;
|
||||
log: LogMessage['notification']['content'];
|
||||
}
|
||||
| {
|
||||
type: 'INITIAL_MESSAGE';
|
||||
serialNumber?: undefined;
|
||||
serialNumbers?: undefined;
|
||||
notificationTypes?: undefined;
|
||||
log?: undefined;
|
||||
message: InitialSocketMessage;
|
||||
};
|
||||
export type SocketEventCallback = {
|
||||
id: string;
|
||||
type: 'LOG';
|
||||
serialNumber: string;
|
||||
callback: () => void;
|
||||
};
|
||||
384
src/hooks/Network/Devices.ts
Normal file
384
src/hooks/Network/Devices.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
import { DeviceRttyApiResponse, GatewayDevice, WifiScanCommand, WifiScanResult } from 'models/Device';
|
||||
import { Note } from 'models/Note';
|
||||
import { PageInfo } from 'models/Table';
|
||||
import { axiosGw } from 'utils/axiosInstances';
|
||||
|
||||
const getDeviceCount = () =>
|
||||
axiosGw.get('devices?countOnly=true').then((response) => response.data) as Promise<{ count: number }>;
|
||||
|
||||
export const useGetDeviceCount = ({ enabled }: { enabled: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
return useQuery(['devices', 'count'], getDeviceCount, {
|
||||
enabled,
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('inventory-fetching-error'))
|
||||
toast({
|
||||
id: 'inventory-fetching-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_fetching_obj', {
|
||||
obj: t('devices.one'),
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export type DeviceWithStatus = {
|
||||
UUID: number;
|
||||
associations_2G: number;
|
||||
associations_5G: number;
|
||||
compatible: string;
|
||||
connected: boolean;
|
||||
certificateExpiryDate?: number;
|
||||
createdTimestamp: number;
|
||||
devicePassword: string;
|
||||
deviceType: 'AP' | 'SWITCH' | 'IOT' | 'MESH';
|
||||
entity: string;
|
||||
firmware: string;
|
||||
fwUpdatePolicy: string;
|
||||
ipAddress: string;
|
||||
lastConfigurationChange: number;
|
||||
lastConfigurationDownload: number;
|
||||
lastContact: number | string;
|
||||
lastFWUpdate: number;
|
||||
locale: string;
|
||||
location: string;
|
||||
macAddress: string;
|
||||
manufacturer: string;
|
||||
messageCount: number;
|
||||
modified: number;
|
||||
notes: Note[];
|
||||
owner: string;
|
||||
restrictedDevice: boolean;
|
||||
rxBytes: number;
|
||||
serialNumber: string;
|
||||
subscriber: string;
|
||||
txBytes: number;
|
||||
venue: string;
|
||||
verifiedCertificate: string;
|
||||
};
|
||||
|
||||
const getDevices = (limit: number, offset: number) =>
|
||||
axiosGw
|
||||
.get(`devices?deviceWithStatus=true&limit=${limit}&offset=${offset}`)
|
||||
.then((response) => response.data) as Promise<{ devicesWithStatus: DeviceWithStatus[] }>;
|
||||
|
||||
export const useGetDevices = ({
|
||||
pageInfo,
|
||||
enabled,
|
||||
onError,
|
||||
}: {
|
||||
pageInfo?: PageInfo;
|
||||
enabled: boolean;
|
||||
onError?: (e: AxiosError) => void;
|
||||
}) => {
|
||||
const offset = pageInfo?.limit !== undefined ? pageInfo.limit * pageInfo.index : 0;
|
||||
|
||||
return useQuery(
|
||||
['devices', 'all', { limit: pageInfo?.limit, offset }],
|
||||
() => getDevices(pageInfo?.limit || 0, offset),
|
||||
{
|
||||
keepPreviousData: true,
|
||||
enabled: enabled && pageInfo !== undefined,
|
||||
staleTime: 30000,
|
||||
onError,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export type DeviceStatus = {
|
||||
UUID: number;
|
||||
associations_2G: number;
|
||||
associations_5G: number;
|
||||
connected: boolean;
|
||||
certificateExpiryDate: number;
|
||||
connectionCompletionTime: number;
|
||||
firmware: string;
|
||||
ipAddress: string;
|
||||
kafkaClients: number;
|
||||
kafkaPackets: number;
|
||||
lastContact: number;
|
||||
locale: string;
|
||||
messageCount: number;
|
||||
rxBytes: number;
|
||||
sessionId: number;
|
||||
started: number;
|
||||
totalConnectionTime: number;
|
||||
txBytes: number;
|
||||
verifiedCertificate: string;
|
||||
webSocketClients: number;
|
||||
websocketPackets: number;
|
||||
};
|
||||
|
||||
const getDeviceStatus = (serialNumber?: string) =>
|
||||
axiosGw.get(`device/${serialNumber}/status`).then((response) => response.data) as Promise<DeviceStatus>;
|
||||
|
||||
export const useGetDeviceStatus = ({
|
||||
serialNumber,
|
||||
onError,
|
||||
}: {
|
||||
serialNumber?: string;
|
||||
onError?: (e: AxiosError) => void;
|
||||
}) =>
|
||||
useQuery(['device', serialNumber, 'status'], () => getDeviceStatus(serialNumber), {
|
||||
enabled: serialNumber !== undefined && serialNumber !== '',
|
||||
onError,
|
||||
});
|
||||
|
||||
export type DevicesStats = {
|
||||
averageConnectionTime: number;
|
||||
connectedDevices: number;
|
||||
connectingDevices: number;
|
||||
};
|
||||
const getInitialStats = async () =>
|
||||
axiosGw.get(`devices?connectionStatistics=true`).then(({ data }: { data: DevicesStats }) => data);
|
||||
export const useGetDevicesStats = ({ onError }: { onError?: (e: AxiosError) => void }) => {
|
||||
const { isUserLoaded } = useAuth();
|
||||
|
||||
return useQuery(['devices', 'all', 'connection-statistics'], () => getInitialStats(), {
|
||||
enabled: isUserLoaded,
|
||||
onError,
|
||||
});
|
||||
};
|
||||
|
||||
export type DeviceHealthCheck = {
|
||||
serialNumber: string;
|
||||
values: {
|
||||
UUID: number;
|
||||
recorded: number;
|
||||
sanity: number;
|
||||
values: {
|
||||
unit: {
|
||||
memory: number;
|
||||
};
|
||||
};
|
||||
}[];
|
||||
};
|
||||
const getDeviceHealthChecks = (serialNumber?: string, limit?: number) =>
|
||||
axiosGw
|
||||
.get(`device/${serialNumber}/healthchecks?newest=true&limit=${limit ?? 50}`)
|
||||
.then((response) => response.data) as Promise<DeviceHealthCheck>;
|
||||
|
||||
export const useGetDeviceHealthChecks = ({
|
||||
serialNumber,
|
||||
onError,
|
||||
limit,
|
||||
}: {
|
||||
serialNumber?: string;
|
||||
onError?: (e: AxiosError) => void;
|
||||
limit?: number;
|
||||
}) =>
|
||||
useQuery(['device', serialNumber, 'healthchecks', { limit }], () => getDeviceHealthChecks(serialNumber, limit), {
|
||||
enabled: serialNumber !== undefined && serialNumber !== '',
|
||||
keepPreviousData: true,
|
||||
onError,
|
||||
});
|
||||
|
||||
export const useGetDevice = ({ serialNumber, onClose }: { serialNumber?: string; onClose?: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
return useQuery(
|
||||
['device', serialNumber],
|
||||
() => axiosGw.get(`device/${serialNumber}`).then(({ data }: { data: GatewayDevice }) => data),
|
||||
{
|
||||
staleTime: 60 * 1000,
|
||||
enabled: serialNumber !== undefined && serialNumber !== '',
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('gateway-device-fetching-error'))
|
||||
toast({
|
||||
id: 'gateway-device-error',
|
||||
title: t('common.error'),
|
||||
description:
|
||||
e?.response?.status === 404
|
||||
? t('devices.not_found_gateway')
|
||||
: t('crud.error_fetching_obj', {
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
obj: t('devices.one'),
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
if (onClose) onClose();
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const deleteDevice = async (serialNumber: string) => axiosGw.delete(`device/${serialNumber}`);
|
||||
export const useDeleteDevice = ({ serialNumber }: { serialNumber: string }) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(deleteDevice, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['devices']);
|
||||
queryClient.invalidateQueries(['device', serialNumber]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useRebootDevice = ({ serialNumber }: { serialNumber: string }) =>
|
||||
useMutation(() => axiosGw.post(`device/${serialNumber}/reboot`, { serialNumber, when: 0 }));
|
||||
|
||||
export const useBlinkDevice = ({ serialNumber }: { serialNumber: string }) =>
|
||||
useMutation(() =>
|
||||
axiosGw.post(`device/${serialNumber}/leds`, { serialNumber, when: 0, pattern: 'blink', duration: 30 }),
|
||||
);
|
||||
export const useFactoryReset = ({
|
||||
serialNumber,
|
||||
keepRedirector,
|
||||
onClose,
|
||||
}: {
|
||||
serialNumber: string;
|
||||
keepRedirector: boolean;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
return useMutation(() => axiosGw.post(`device/${serialNumber}/factory`, { serialNumber, keepRedirector }), {
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
id: `factory-reset-success-${uuid()}`,
|
||||
title: t('common.success'),
|
||||
description: t('commands.factory_reset_success'),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
onError: (e: AxiosError) => {
|
||||
toast({
|
||||
id: uuid(),
|
||||
title: t('common.error'),
|
||||
description: t('commands.factory_reset_error', {
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useWifiScanDevice = ({ serialNumber }: { serialNumber: string }) => {
|
||||
const toast = useToast();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation(
|
||||
({ dfs, bandwidth, activeScan }: WifiScanCommand): Promise<WifiScanResult | undefined> =>
|
||||
axiosGw
|
||||
.post<WifiScanResult>(`device/${serialNumber}/wifiscan`, {
|
||||
serialNumber,
|
||||
override_dfs: dfs,
|
||||
bandwidth: bandwidth !== '' ? bandwidth : undefined,
|
||||
activeScan,
|
||||
})
|
||||
.then(({ data }: { data: WifiScanResult }) => data),
|
||||
{
|
||||
onSuccess: () => {},
|
||||
onError: (e: AxiosError) => {
|
||||
toast({
|
||||
id: uuid(),
|
||||
title: t('common.error'),
|
||||
description: t('commands.wifiscan_error', {
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const useGetDeviceRtty = ({ serialNumber, extraId }: { serialNumber: string; extraId: string | number }) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
return useQuery(
|
||||
['get-gateway-device-rtty', serialNumber, extraId],
|
||||
() => axiosGw.get(`device/${serialNumber}/rtty`).then(({ data }: { data: DeviceRttyApiResponse }) => data),
|
||||
{
|
||||
enabled: false,
|
||||
onSuccess: ({ server, viewport, connectionId }) => {
|
||||
const url = `https://${server}:${viewport}/connect/${connectionId}`;
|
||||
window.open(url, '_blank')?.focus();
|
||||
},
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('get-gateway-device-rtty-error'))
|
||||
toast({
|
||||
id: 'get-gateway-device-rtty',
|
||||
title: t('common.error'),
|
||||
description:
|
||||
e?.response?.status === 404
|
||||
? t('devices.not_found_gateway')
|
||||
: t('devices.error_rtty', {
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export type DeviceCapabilities = {
|
||||
capabilities: { [key: string]: unknown }[];
|
||||
serialNumber: string;
|
||||
firstUpdate: number;
|
||||
lastUpdate: number;
|
||||
};
|
||||
const getCapabilities = async (serialNumber: string) =>
|
||||
axiosGw.get(`device/${serialNumber}/capabilities`).then(({ data }: { data: DeviceCapabilities }) => data);
|
||||
export const useGetDeviceCapabilities = ({
|
||||
serialNumber,
|
||||
onError,
|
||||
}: {
|
||||
serialNumber: string;
|
||||
onError?: (e: AxiosError) => void;
|
||||
}) => {
|
||||
const { isUserLoaded } = useAuth();
|
||||
|
||||
return useQuery(['deviceCapabilities', serialNumber], () => getCapabilities(serialNumber), {
|
||||
enabled: isUserLoaded,
|
||||
onError,
|
||||
});
|
||||
};
|
||||
|
||||
const modifyDevice = async ({ serialNumber, notes }: { serialNumber: string; notes?: Note[] }) =>
|
||||
axiosGw.put(`device/${serialNumber}`, { notes });
|
||||
export const useUpdateDevice = ({ serialNumber }: { serialNumber: string }) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(modifyDevice, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['devices']);
|
||||
queryClient.invalidateQueries(['device', serialNumber]);
|
||||
},
|
||||
});
|
||||
};
|
||||
14
src/models/Socket.ts
Normal file
14
src/models/Socket.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export type NotificationType = {
|
||||
helper?: string;
|
||||
id: number;
|
||||
};
|
||||
|
||||
export type InitialSocketMessage = {
|
||||
notification: undefined;
|
||||
serialNumbers: undefined;
|
||||
command_response_id: undefined;
|
||||
response: undefined;
|
||||
success?: string;
|
||||
error?: string;
|
||||
notificationTypes?: NotificationType[];
|
||||
};
|
||||
178
src/pages/Notifications/FmsLogs/index.tsx
Normal file
178
src/pages/Notifications/FmsLogs/index.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import * as React from 'react';
|
||||
import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import { Download } from 'phosphor-react';
|
||||
import { CSVLink } from 'react-csv';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import CardBody from 'components/Card/CardBody';
|
||||
import CardHeader from 'components/Card/CardHeader';
|
||||
import { ShownLogsDropdown } from 'components/ShownLogsDropdown';
|
||||
import { useFirmwareStore } from 'contexts/FirmwareSocketProvider/useStore';
|
||||
import { LogLevel } from 'contexts/FirmwareSocketProvider/utils';
|
||||
import { dateForFilename } from 'utils/dateFormatting';
|
||||
import { uppercaseFirstLetter } from 'utils/stringHelper';
|
||||
|
||||
const FmsLogsCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { availableLogTypes, hiddenLogIds, setHiddenLogIds, logs } = useFirmwareStore((state) => ({
|
||||
logs: state.allMessages,
|
||||
availableLogTypes: state.availableLogTypes,
|
||||
hiddenLogIds: state.hiddenLogIds,
|
||||
setHiddenLogIds: state.setHiddenLogIds,
|
||||
}));
|
||||
const [level, setLevel] = React.useState<'' | LogLevel>('');
|
||||
|
||||
const data = React.useMemo(() => {
|
||||
const arr = logs.filter(
|
||||
(d) => d.type === 'NOTIFICATION' && d.data.type === 'LOG' && (level === '' || d.data.log.level === level),
|
||||
);
|
||||
return arr.reverse();
|
||||
}, [logs, level]);
|
||||
|
||||
const colorSchemeMap: Record<LogLevel, string> = {
|
||||
information: 'blue',
|
||||
critical: 'red',
|
||||
debug: 'teal',
|
||||
error: 'red',
|
||||
fatal: 'purple',
|
||||
notice: 'blue',
|
||||
trace: 'blue',
|
||||
warning: 'yellow',
|
||||
};
|
||||
|
||||
type RowProps = { index: number; style: React.CSSProperties };
|
||||
const Row = React.useCallback(
|
||||
({ index, style }: RowProps) => {
|
||||
const msg = data[index];
|
||||
if (msg) {
|
||||
if (msg.type === 'NOTIFICATION' && msg.data.type === 'LOG') {
|
||||
return (
|
||||
<Box style={style}>
|
||||
<Flex w="100%">
|
||||
<Box flex="0 1 110px">
|
||||
<Text>{msg.timestamp.toLocaleTimeString()}</Text>
|
||||
</Box>
|
||||
<Box flex="0 1 200px">
|
||||
<Text w="200px" textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||
{msg.data.log.source}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flex="0 1 140px">
|
||||
<Text w="140px" textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||
{msg.data.log.thread_id}-{msg.data.log.thread_name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flex="0 1 110px">
|
||||
<Badge
|
||||
ml={1}
|
||||
size="lg"
|
||||
fontSize="0.9em"
|
||||
variant="solid"
|
||||
colorScheme={colorSchemeMap[msg.data.log.level]}
|
||||
>
|
||||
{msg.data.log.level}
|
||||
</Badge>
|
||||
</Box>
|
||||
<Box textAlign="left" w="calc(100% - 180px - 210px - 120px - 60px)">
|
||||
<Text textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||
{JSON.stringify(msg.data.log.msg).replace(/"/g, '')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[t, data],
|
||||
);
|
||||
|
||||
const downloadableLogs = React.useMemo(
|
||||
() =>
|
||||
data.map((msg) =>
|
||||
msg.type === 'NOTIFICATION' && msg.data.type === 'LOG'
|
||||
? {
|
||||
timestamp: msg.timestamp.toLocaleString(),
|
||||
thread: `${msg.data.log.thread_id}-${msg.data.log.thread_name}`,
|
||||
source: msg.data?.log?.source ?? '-',
|
||||
level: msg.data?.log?.level,
|
||||
message: JSON.stringify(msg.data?.log?.msg),
|
||||
}
|
||||
: {},
|
||||
),
|
||||
[data],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader px={4} pt={4}>
|
||||
<Spacer />
|
||||
<HStack spacing={2}>
|
||||
<ShownLogsDropdown
|
||||
availableLogTypes={availableLogTypes}
|
||||
setHiddenLogIds={setHiddenLogIds}
|
||||
hiddenLogIds={hiddenLogIds}
|
||||
helperLabels={{
|
||||
1: t('logs.one'),
|
||||
}}
|
||||
/>
|
||||
<Select size="md" value={level} onChange={(e) => setLevel(e.target.value as '' | LogLevel)} w="130px">
|
||||
<option value="">{t('common.select_all')}</option>
|
||||
{Object.keys(colorSchemeMap).map((key) => (
|
||||
<option key={uuid()} value={key}>
|
||||
{uppercaseFirstLetter(key)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<CSVLink
|
||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||
data={downloadableLogs as object[]}
|
||||
>
|
||||
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
||||
{t('logs.export')}
|
||||
</Button>
|
||||
</CSVLink>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody p={4}>
|
||||
<Box overflowX="auto" w="100%">
|
||||
<Table size="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th w="110px">{t('common.time')}</Th>
|
||||
<Th w="200px">{t('logs.source')}</Th>
|
||||
<Th w="160px">
|
||||
{t('logs.thread')} ID-{t('common.name')}
|
||||
</Th>
|
||||
<Th w="90px" pl={0}>
|
||||
{t('logs.level')}
|
||||
</Th>
|
||||
<Th>{t('logs.message')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
</Table>
|
||||
<Box ml={4} h="calc(70vh)">
|
||||
<ReactVirtualizedAutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
height={height}
|
||||
width={width}
|
||||
itemCount={data.length}
|
||||
itemSize={35}
|
||||
itemKey={(index) => data[index]?.id ?? uuid()}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
)}
|
||||
</ReactVirtualizedAutoSizer>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FmsLogsCard;
|
||||
181
src/pages/Notifications/GeneralLogs/index.tsx
Normal file
181
src/pages/Notifications/GeneralLogs/index.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import * as React from 'react';
|
||||
import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import { Download } from 'phosphor-react';
|
||||
import { CSVLink } from 'react-csv';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import CardBody from 'components/Card/CardBody';
|
||||
import CardHeader from 'components/Card/CardHeader';
|
||||
import { ShownLogsDropdown } from 'components/ShownLogsDropdown';
|
||||
import { useProvisioningStore } from 'contexts/ProvisioningSocketProvider/useStore';
|
||||
import { LogLevel } from 'contexts/ProvisioningSocketProvider/utils';
|
||||
import { dateForFilename } from 'utils/dateFormatting';
|
||||
import { uppercaseFirstLetter } from 'utils/stringHelper';
|
||||
|
||||
const GeneralLogsCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { availableLogTypes, hiddenLogIds, setHiddenLogIds, logs } = useProvisioningStore((state) => ({
|
||||
logs: state.allMessages,
|
||||
availableLogTypes: state.availableLogTypes,
|
||||
hiddenLogIds: state.hiddenLogIds,
|
||||
setHiddenLogIds: state.setHiddenLogIds,
|
||||
}));
|
||||
const [level, setLevel] = React.useState<'' | LogLevel>('');
|
||||
|
||||
const data = React.useMemo(() => {
|
||||
const arr = logs.filter(
|
||||
(d) => d.type === 'NOTIFICATION' && d.data.type === 'LOG' && (level === '' || d.data.log.level === level),
|
||||
);
|
||||
return arr.reverse();
|
||||
}, [logs, level]);
|
||||
|
||||
const colorSchemeMap: Record<LogLevel, string> = {
|
||||
information: 'blue',
|
||||
critical: 'red',
|
||||
debug: 'teal',
|
||||
error: 'red',
|
||||
fatal: 'purple',
|
||||
notice: 'blue',
|
||||
trace: 'blue',
|
||||
warning: 'yellow',
|
||||
};
|
||||
|
||||
type RowProps = { index: number; style: React.CSSProperties };
|
||||
const Row = React.useCallback(
|
||||
({ index, style }: RowProps) => {
|
||||
const msg = data[index];
|
||||
if (msg) {
|
||||
if (msg.type === 'NOTIFICATION' && msg.data.type === 'LOG') {
|
||||
return (
|
||||
<Box style={style}>
|
||||
<Flex w="100%">
|
||||
<Box flex="0 1 110px">
|
||||
<Text>{msg.timestamp.toLocaleTimeString()}</Text>
|
||||
</Box>
|
||||
<Box flex="0 1 200px">
|
||||
<Text w="200px" textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||
{msg.data.log.source}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flex="0 1 140px">
|
||||
<Text w="140px" textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||
{msg.data.log.thread_id}-{msg.data.log.thread_name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flex="0 1 110px">
|
||||
<Badge
|
||||
ml={1}
|
||||
size="lg"
|
||||
fontSize="0.9em"
|
||||
variant="solid"
|
||||
colorScheme={colorSchemeMap[msg.data.log.level]}
|
||||
>
|
||||
{msg.data.log.level}
|
||||
</Badge>
|
||||
</Box>
|
||||
<Box textAlign="left" w="calc(100% - 180px - 210px - 120px - 60px)">
|
||||
<Text textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||
{JSON.stringify(msg.data.log.msg).replace(/"/g, '')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[t, data],
|
||||
);
|
||||
|
||||
const downloadableLogs = React.useMemo(
|
||||
() =>
|
||||
data.map((msg) =>
|
||||
msg.type === 'NOTIFICATION' && msg.data.type === 'LOG'
|
||||
? {
|
||||
timestamp: msg.timestamp.toLocaleString(),
|
||||
thread: `${msg.data.log.thread_id}-${msg.data.log.thread_name}`,
|
||||
source: msg.data?.log?.source ?? '-',
|
||||
level: msg.data?.log?.level,
|
||||
message: JSON.stringify(msg.data?.log?.msg),
|
||||
}
|
||||
: {},
|
||||
),
|
||||
[data],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader px={4} pt={4}>
|
||||
<Spacer />
|
||||
<HStack spacing={2}>
|
||||
<ShownLogsDropdown
|
||||
availableLogTypes={availableLogTypes}
|
||||
setHiddenLogIds={setHiddenLogIds}
|
||||
hiddenLogIds={hiddenLogIds}
|
||||
helperLabels={{
|
||||
1: t('logs.one'),
|
||||
1000: t('logs.venue_upgrade'),
|
||||
2000: t('logs.venue_config'),
|
||||
3000: t('logs.venue_reboot'),
|
||||
}}
|
||||
/>
|
||||
<Select size="md" value={level} onChange={(e) => setLevel(e.target.value as '' | LogLevel)} w="130px">
|
||||
<option value="">{t('common.select_all')}</option>
|
||||
{Object.keys(colorSchemeMap).map((key) => (
|
||||
<option key={uuid()} value={key}>
|
||||
{uppercaseFirstLetter(key)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<CSVLink
|
||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||
data={downloadableLogs as object[]}
|
||||
>
|
||||
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
||||
{t('logs.export')}
|
||||
</Button>
|
||||
</CSVLink>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody p={4}>
|
||||
<Box overflowX="auto" w="100%">
|
||||
<Table size="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th w="110px">{t('common.time')}</Th>
|
||||
<Th w="200px">{t('logs.source')}</Th>
|
||||
<Th w="160px">
|
||||
{t('logs.thread')} ID-{t('common.name')}
|
||||
</Th>
|
||||
<Th w="90px" pl={0}>
|
||||
{t('logs.level')}
|
||||
</Th>
|
||||
<Th>{t('logs.message')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
</Table>
|
||||
<Box ml={4} h="calc(70vh)">
|
||||
<ReactVirtualizedAutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
height={height}
|
||||
width={width}
|
||||
itemCount={data.length}
|
||||
itemSize={35}
|
||||
itemKey={(index) => data[index]?.id ?? uuid()}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
)}
|
||||
</ReactVirtualizedAutoSizer>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralLogsCard;
|
||||
162
src/pages/Notifications/Notifications/index.tsx
Normal file
162
src/pages/Notifications/Notifications/index.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Button, Flex, HStack, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import { Download } from 'phosphor-react';
|
||||
import { CSVLink } from 'react-csv';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import CardBody from 'components/Card/CardBody';
|
||||
import CardHeader from 'components/Card/CardHeader';
|
||||
import { ShownLogsDropdown } from 'components/ShownLogsDropdown';
|
||||
import { useProvisioningStore } from 'contexts/ProvisioningSocketProvider/useStore';
|
||||
import { dateForFilename } from 'utils/dateFormatting';
|
||||
|
||||
const NotificationsCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { availableLogTypes, hiddenLogIds, setHiddenLogIds, logs } = useProvisioningStore((state) => ({
|
||||
logs: state.allMessages,
|
||||
availableLogTypes: state.availableLogTypes,
|
||||
hiddenLogIds: state.hiddenLogIds,
|
||||
setHiddenLogIds: state.setHiddenLogIds,
|
||||
}));
|
||||
|
||||
const labels = {
|
||||
1: t('logs.one'),
|
||||
1000: t('logs.venue_upgrade'),
|
||||
2000: t('logs.venue_config'),
|
||||
3000: t('logs.venue_reboot'),
|
||||
};
|
||||
|
||||
const data = React.useMemo(() => {
|
||||
const arr = logs.filter(
|
||||
(log) =>
|
||||
log.type === 'NOTIFICATION' &&
|
||||
log.data.type === 'NOTIFICATION' &&
|
||||
(log.data.data.type_id === 1000 || log.data.data.type_id === 2000 || log.data.data.type_id === 3000),
|
||||
);
|
||||
|
||||
return arr.reverse() as {
|
||||
type: 'NOTIFICATION';
|
||||
data: {
|
||||
type: 'NOTIFICATION';
|
||||
data: {
|
||||
notification_id: number;
|
||||
type?: 'venue_fw_upgrade' | 'venue_config_update' | 'venue_rebooter';
|
||||
type_id: 1000 | 2000 | 3000;
|
||||
content: {
|
||||
title: string;
|
||||
details: string;
|
||||
success: string[];
|
||||
noFirmware?: string[];
|
||||
notConnected?: string[];
|
||||
skipped?: string[];
|
||||
warning: string[];
|
||||
error: string[];
|
||||
timeStamp: number;
|
||||
};
|
||||
};
|
||||
log?: undefined;
|
||||
notificationTypes?: undefined;
|
||||
};
|
||||
timestamp: Date;
|
||||
id: string;
|
||||
}[];
|
||||
}, [logs]);
|
||||
|
||||
type RowProps = { index: number; style: React.CSSProperties };
|
||||
const Row = React.useCallback(
|
||||
({ index, style }: RowProps) => {
|
||||
const msg = data[index];
|
||||
if (msg) {
|
||||
if (msg.type === 'NOTIFICATION' && msg.data.type === 'NOTIFICATION') {
|
||||
return (
|
||||
<Box style={style}>
|
||||
<Flex w="100%">
|
||||
<Box flex="0 1 110px">
|
||||
<Text>{msg.timestamp.toLocaleTimeString()}</Text>
|
||||
</Box>
|
||||
<Box flex="0 1 230px" textAlign="left">
|
||||
<Text textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap" w="230px">
|
||||
{msg.data.data.content.title ?? '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flex="0 1 140px">
|
||||
<Text>{labels[msg.data.data.type_id]}</Text>
|
||||
</Box>
|
||||
<Box textAlign="left" w="calc(100% - 80px - 220px - 140px - 60px)">
|
||||
<Text textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||
{JSON.stringify(msg.data.data.content)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[t, data],
|
||||
);
|
||||
|
||||
const downloadableLogs = React.useMemo(
|
||||
() =>
|
||||
data.map((msg) => ({
|
||||
timestamp: msg.timestamp.toLocaleString(),
|
||||
serialNumber: labels[msg.data.data.type_id] ?? '-',
|
||||
type: labels[msg.data.data.type_id] ?? '-',
|
||||
data: JSON.stringify(msg.data.data),
|
||||
})),
|
||||
[data],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader px={4} pt={4}>
|
||||
<Spacer />
|
||||
<HStack spacing={2}>
|
||||
<ShownLogsDropdown
|
||||
availableLogTypes={availableLogTypes}
|
||||
setHiddenLogIds={setHiddenLogIds}
|
||||
hiddenLogIds={hiddenLogIds}
|
||||
helperLabels={labels}
|
||||
/>
|
||||
<CSVLink
|
||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||
data={downloadableLogs as object[]}
|
||||
>
|
||||
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
||||
{t('logs.export')}
|
||||
</Button>
|
||||
</CSVLink>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody p={4}>
|
||||
<Box overflowX="auto" w="100%">
|
||||
<Table size="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th w="110px">{t('common.time')}</Th>
|
||||
<Th w="250px">{t('contacts.title')}</Th>
|
||||
<Th w="120px" pl={0}>
|
||||
{t('common.type')}
|
||||
</Th>
|
||||
<Th>{t('analytics.raw_data')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
</Table>
|
||||
<Box ml={4} h="calc(70vh)">
|
||||
<ReactVirtualizedAutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List height={height} width={width} itemCount={data.length} itemSize={35}>
|
||||
{Row}
|
||||
</List>
|
||||
)}
|
||||
</ReactVirtualizedAutoSizer>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationsCard;
|
||||
178
src/pages/Notifications/SecLogs/index.tsx
Normal file
178
src/pages/Notifications/SecLogs/index.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import * as React from 'react';
|
||||
import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import { Download } from 'phosphor-react';
|
||||
import { CSVLink } from 'react-csv';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import CardBody from 'components/Card/CardBody';
|
||||
import CardHeader from 'components/Card/CardHeader';
|
||||
import { ShownLogsDropdown } from 'components/ShownLogsDropdown';
|
||||
import { useSecurityStore } from 'contexts/SecuritySocketProvider/useStore';
|
||||
import { LogLevel } from 'contexts/SecuritySocketProvider/utils';
|
||||
import { dateForFilename } from 'utils/dateFormatting';
|
||||
import { uppercaseFirstLetter } from 'utils/stringHelper';
|
||||
|
||||
const SecLogsCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { availableLogTypes, hiddenLogIds, setHiddenLogIds, logs } = useSecurityStore((state) => ({
|
||||
logs: state.allMessages,
|
||||
availableLogTypes: state.availableLogTypes,
|
||||
hiddenLogIds: state.hiddenLogIds,
|
||||
setHiddenLogIds: state.setHiddenLogIds,
|
||||
}));
|
||||
const [level, setLevel] = React.useState<'' | LogLevel>('');
|
||||
|
||||
const data = React.useMemo(() => {
|
||||
const arr = logs.filter(
|
||||
(d) => d.type === 'NOTIFICATION' && d.data.type === 'LOG' && (level === '' || d.data.log.level === level),
|
||||
);
|
||||
return arr.reverse();
|
||||
}, [logs, level]);
|
||||
|
||||
const colorSchemeMap: Record<LogLevel, string> = {
|
||||
information: 'blue',
|
||||
critical: 'red',
|
||||
debug: 'teal',
|
||||
error: 'red',
|
||||
fatal: 'purple',
|
||||
notice: 'blue',
|
||||
trace: 'blue',
|
||||
warning: 'yellow',
|
||||
};
|
||||
|
||||
type RowProps = { index: number; style: React.CSSProperties };
|
||||
const Row = React.useCallback(
|
||||
({ index, style }: RowProps) => {
|
||||
const msg = data[index];
|
||||
if (msg) {
|
||||
if (msg.type === 'NOTIFICATION' && msg.data.type === 'LOG') {
|
||||
return (
|
||||
<Box style={style}>
|
||||
<Flex w="100%">
|
||||
<Box flex="0 1 110px">
|
||||
<Text>{msg.timestamp.toLocaleTimeString()}</Text>
|
||||
</Box>
|
||||
<Box flex="0 1 200px">
|
||||
<Text w="200px" textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||
{msg.data.log.source}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flex="0 1 140px">
|
||||
<Text w="140px" textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||
{msg.data.log.thread_id}-{msg.data.log.thread_name}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flex="0 1 110px">
|
||||
<Badge
|
||||
ml={1}
|
||||
size="lg"
|
||||
fontSize="0.9em"
|
||||
variant="solid"
|
||||
colorScheme={colorSchemeMap[msg.data.log.level]}
|
||||
>
|
||||
{msg.data.log.level}
|
||||
</Badge>
|
||||
</Box>
|
||||
<Box textAlign="left" w="calc(100% - 180px - 210px - 120px - 60px)">
|
||||
<Text textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||
{JSON.stringify(msg.data.log.msg).replace(/"/g, '')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[t, data],
|
||||
);
|
||||
|
||||
const downloadableLogs = React.useMemo(
|
||||
() =>
|
||||
data.map((msg) =>
|
||||
msg.type === 'NOTIFICATION' && msg.data.type === 'LOG'
|
||||
? {
|
||||
timestamp: msg.timestamp.toLocaleString(),
|
||||
thread: `${msg.data.log.thread_id}-${msg.data.log.thread_name}`,
|
||||
source: msg.data?.log?.source ?? '-',
|
||||
level: msg.data?.log?.level,
|
||||
message: JSON.stringify(msg.data?.log?.msg),
|
||||
}
|
||||
: {},
|
||||
),
|
||||
[data],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader px={4} pt={4}>
|
||||
<Spacer />
|
||||
<HStack spacing={2}>
|
||||
<ShownLogsDropdown
|
||||
availableLogTypes={availableLogTypes}
|
||||
setHiddenLogIds={setHiddenLogIds}
|
||||
hiddenLogIds={hiddenLogIds}
|
||||
helperLabels={{
|
||||
1: t('logs.one'),
|
||||
}}
|
||||
/>
|
||||
<Select size="md" value={level} onChange={(e) => setLevel(e.target.value as '' | LogLevel)} w="130px">
|
||||
<option value="">{t('common.select_all')}</option>
|
||||
{Object.keys(colorSchemeMap).map((key) => (
|
||||
<option key={uuid()} value={key}>
|
||||
{uppercaseFirstLetter(key)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<CSVLink
|
||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||
data={downloadableLogs as object[]}
|
||||
>
|
||||
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
||||
{t('logs.export')}
|
||||
</Button>
|
||||
</CSVLink>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody p={4}>
|
||||
<Box overflowX="auto" w="100%">
|
||||
<Table size="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th w="110px">{t('common.time')}</Th>
|
||||
<Th w="200px">{t('logs.source')}</Th>
|
||||
<Th w="160px">
|
||||
{t('logs.thread')} ID-{t('common.name')}
|
||||
</Th>
|
||||
<Th w="90px" pl={0}>
|
||||
{t('logs.level')}
|
||||
</Th>
|
||||
<Th>{t('logs.message')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
</Table>
|
||||
<Box ml={4} h="calc(70vh)">
|
||||
<ReactVirtualizedAutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
height={height}
|
||||
width={width}
|
||||
itemCount={data.length}
|
||||
itemSize={35}
|
||||
itemKey={(index) => data[index]?.id ?? uuid()}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
)}
|
||||
</ReactVirtualizedAutoSizer>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecLogsCard;
|
||||
105
src/pages/Notifications/index.tsx
Normal file
105
src/pages/Notifications/index.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React from 'react';
|
||||
import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FmsLogsCard from './FmsLogs';
|
||||
import GeneralLogsCard from './GeneralLogs';
|
||||
import LogsCard from './Notifications';
|
||||
import SecLogsCard from './SecLogs';
|
||||
import Card from 'components/Card';
|
||||
import CardHeader from 'components/Card/CardHeader';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
|
||||
const INDEX_PARAM = 'notifications-tab-index';
|
||||
|
||||
const getDefaultTabIndex = () => {
|
||||
const index = localStorage.getItem(INDEX_PARAM) || '0';
|
||||
try {
|
||||
return parseInt(index, 10);
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const NotificationsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isUserLoaded } = useAuth();
|
||||
const [tabIndex, setTabIndex] = React.useState(getDefaultTabIndex());
|
||||
|
||||
const handleTabChange = (index: number) => {
|
||||
setTabIndex(index);
|
||||
localStorage.setItem(INDEX_PARAM, index.toString());
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column" pt="75px">
|
||||
{isUserLoaded && (
|
||||
<Card p={0}>
|
||||
<Tabs index={tabIndex} onChange={handleTabChange} variant="enclosed" isLazy>
|
||||
<TabList>
|
||||
<CardHeader>
|
||||
<Tab>
|
||||
{t('venues.one')} {t('notification.other')}
|
||||
</Tab>
|
||||
<Tab>Provisioning</Tab>
|
||||
<Tab>{t('logs.security')}</Tab>
|
||||
<Tab>{t('logs.firmware')}</Tab>
|
||||
</CardHeader>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel p={0}>
|
||||
<Box
|
||||
borderLeft="1px solid"
|
||||
borderRight="1px solid"
|
||||
borderBottom="1px solid"
|
||||
borderColor="var(--chakra-colors-chakra-border-color)"
|
||||
borderBottomLeftRadius="15px"
|
||||
borderBottomRightRadius="15px"
|
||||
>
|
||||
<LogsCard />
|
||||
</Box>
|
||||
</TabPanel>
|
||||
<TabPanel p={0}>
|
||||
<Box
|
||||
borderLeft="1px solid"
|
||||
borderRight="1px solid"
|
||||
borderBottom="1px solid"
|
||||
borderColor="var(--chakra-colors-chakra-border-color)"
|
||||
borderBottomLeftRadius="15px"
|
||||
borderBottomRightRadius="15px"
|
||||
>
|
||||
<GeneralLogsCard />
|
||||
</Box>
|
||||
</TabPanel>
|
||||
<TabPanel p={0}>
|
||||
<Box
|
||||
borderLeft="1px solid"
|
||||
borderRight="1px solid"
|
||||
borderBottom="1px solid"
|
||||
borderColor="var(--chakra-colors-chakra-border-color)"
|
||||
borderBottomLeftRadius="15px"
|
||||
borderBottomRightRadius="15px"
|
||||
>
|
||||
<SecLogsCard />
|
||||
</Box>
|
||||
</TabPanel>
|
||||
<TabPanel p={0}>
|
||||
<Box
|
||||
borderLeft="1px solid"
|
||||
borderRight="1px solid"
|
||||
borderBottom="1px solid"
|
||||
borderColor="var(--chakra-colors-chakra-border-color)"
|
||||
borderBottomLeftRadius="15px"
|
||||
borderBottomRightRadius="15px"
|
||||
>
|
||||
<FmsLogsCard />
|
||||
</Box>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Card>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationsPage;
|
||||
@@ -43,7 +43,7 @@ const SystemPage = () => {
|
||||
<Card mb={4} py={2} px={4}>
|
||||
<CardHeader>
|
||||
<Heading size="md" my="auto">
|
||||
{t('controller.firmware.endpoints')} ({endpoints?.length ?? 0})
|
||||
{t('controller.firmware.endpoints')}
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<RefreshButton onClick={refetch} isFetching={isFetching} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Icon } from '@chakra-ui/react';
|
||||
import { Info, Storefront, Tag, TreeStructure, UsersThree } from 'phosphor-react';
|
||||
import { Info, ListBullets, Storefront, Tag, TreeStructure, UsersThree } from 'phosphor-react';
|
||||
import { Route } from 'models/Routes';
|
||||
|
||||
const AccountPage = React.lazy(() => import('pages/Profile'));
|
||||
@@ -8,6 +8,7 @@ const ConfigurationPage = React.lazy(() => import('pages/ConfigurationPage'));
|
||||
const EntityPage = React.lazy(() => import('pages/EntityPage'));
|
||||
const InventoryPage = React.lazy(() => import('pages/InventoryPage'));
|
||||
const MapPage = React.lazy(() => import('pages/MapPage'));
|
||||
const NotificationsPage = React.lazy(() => import('pages/Notifications'));
|
||||
const OperatorPage = React.lazy(() => import('pages/OperatorPage'));
|
||||
const OperatorsPage = React.lazy(() => import('pages/OperatorsPage'));
|
||||
const SubscriberPage = React.lazy(() => import('pages/SubscriberPage'));
|
||||
@@ -45,6 +46,15 @@ const routes: Route[] = [
|
||||
),
|
||||
component: OperatorsPage,
|
||||
},
|
||||
{
|
||||
authorized: ['root', 'partner', 'admin', 'csr', 'system'],
|
||||
path: '/logs',
|
||||
name: 'controller.devices.logs',
|
||||
icon: (active: boolean) => (
|
||||
<Icon as={ListBullets} color="inherit" h={active ? '32px' : '24px'} w={active ? '32px' : '24px'} />
|
||||
),
|
||||
component: NotificationsPage,
|
||||
},
|
||||
{
|
||||
authorized: ['root', 'partner', 'admin', 'csr', 'system'],
|
||||
path: '/users',
|
||||
|
||||
Reference in New Issue
Block a user