Compare commits
68 Commits
feature/Co
...
master
Author | SHA1 | Date | |
---|---|---|---|
324d2ac7f4 | |||
8a0212a139 | |||
802806b4c2 | |||
1d4bf2d0b6 | |||
79dcb4d75a | |||
7c67fa7de0 | |||
89a416ac38 | |||
e9ffe514dd | |||
6a579772df | |||
20a4b4d349 | |||
0085b95198 | |||
579481ce16 | |||
645f2bb44b | |||
c3bbbd3d13 | |||
0df5b350d9 | |||
265a59d4c3 | |||
e9fcfd4ffa | |||
3b5b544a3e | |||
57998e3bc5 | |||
9cae969803 | |||
37ea775d59 | |||
c9c631b947 | |||
433b235929 | |||
7588b58453 | |||
251a00eaa6 | |||
5768b78619 | |||
08df5a624b | |||
c0ae0a30fe | |||
8970d4fec3 | |||
dbad96be95 | |||
8764e1b45a | |||
6e61fc7756 | |||
f0af8f08e3 | |||
76c3787107 | |||
544df78ac8 | |||
02df37692f | |||
ed4e207ff0 | |||
a5a37fdd1b | |||
1b52e16db5 | |||
c9550ea5b8 | |||
3de33014e5 | |||
a458ad1712 | |||
030b259d32 | |||
022852b163 | |||
2d5eca233e | |||
9d6e0cd453 | |||
a2c3112f81 | |||
c7e3fcabcf | |||
70c4c91035 | |||
aa39597541 | |||
bcbab1383a | |||
bab0984c30 | |||
0667c6ec39 | |||
0d71899ce1 | |||
966d8bd79e | |||
5b42ce9f43 | |||
85320d3cf3 | |||
9d55c39e33 | |||
d75e9d378d | |||
3cdaa2dc35 | |||
9af0ba1bb8 | |||
04c27560ea | |||
73157520ab | |||
f862e4b8da | |||
f85346aea9 | |||
1d438bc349 | |||
86999cd646 | |||
8979ad6db3 |
25
.dockerignore
Normal file
@ -0,0 +1,25 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/docs
|
||||
**/bin
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
45
.drone.yml
Normal file
@ -0,0 +1,45 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: code-analysis
|
||||
image: aosapps/drone-sonar-plugin
|
||||
settings:
|
||||
sonar_host:
|
||||
from_secret: SONAR_HOST
|
||||
sonar_token:
|
||||
from_secret: SONAR_CODE
|
||||
|
||||
- name: kaniko
|
||||
image: banzaicloud/drone-kaniko
|
||||
settings:
|
||||
registry: registry.kmlabz.com
|
||||
repo: birbnetes/${DRONE_REPO_NAME}
|
||||
username:
|
||||
from_secret: DOCKER_USERNAME
|
||||
password:
|
||||
from_secret: DOCKER_PASSWORD
|
||||
tags:
|
||||
- latest
|
||||
- ${DRONE_BUILD_NUMBER}
|
||||
|
||||
- name: dockerhub
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: birbnetes/${DRONE_REPO_NAME}
|
||||
username:
|
||||
from_secret: DOCKERHUB_USER
|
||||
password:
|
||||
from_secret: DOCKERHUB_PASSWORD
|
||||
tags:
|
||||
- latest
|
||||
- ${DRONE_BUILD_NUMBER}
|
||||
|
||||
- name: ms-teams
|
||||
image: kuperiu/drone-teams
|
||||
settings:
|
||||
webhook:
|
||||
from_secret: TEAMS_WEBHOOK
|
||||
when:
|
||||
status: [ failure ]
|
@ -8,6 +8,9 @@
|
||||
<SpaRoot>ClientApp\</SpaRoot>
|
||||
<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
|
||||
<AssemblyName>Birdmap.API</AssemblyName>
|
||||
<UserSecretsId>a919c854-b332-49ee-8e38-96549f828836</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
@ -31,8 +34,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MQTTnet" Version="3.0.13" />
|
||||
<PackageReference Include="MQTTnet.AspNetCore" Version="3.0.13" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.10.9" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="NLog" Version="4.7.5" />
|
||||
<PackageReference Include="NLog.Web" Version="4.9.3" />
|
||||
@ -52,6 +54,7 @@
|
||||
<None Remove="ClientApp\src\components\auth\Auth.tsx" />
|
||||
<None Remove="ClientApp\src\components\auth\AuthClient.ts" />
|
||||
<None Remove="ClientApp\src\components\auth\AuthService.ts" />
|
||||
<None Remove="ClientApp\src\components\dashboard\ServiceInfoService.ts" />
|
||||
<None Remove="ClientApp\src\components\devices\DeviceService.ts" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -61,7 +64,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="ClientApp\src\common\components\" />
|
||||
<Folder Include="ClientApp\src\components\dashboard\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
115
Birdmap.API/ClientApp/package-lock.json
generated
@ -1610,6 +1610,25 @@
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
|
||||
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
|
||||
},
|
||||
"@rollup/plugin-babel": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.2.1.tgz",
|
||||
"integrity": "sha512-Jd7oqFR2dzZJ3NWANDyBjwTtX/lYbZpVcmkHrfQcpvawHs9E4c0nYk5U2mfZ6I/DZcIvy506KZJi54XK/jxH7A==",
|
||||
"requires": {
|
||||
"@babel/helper-module-imports": "^7.10.4",
|
||||
"@rollup/pluginutils": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"@rollup/pluginutils": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
|
||||
"integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==",
|
||||
"requires": {
|
||||
"@types/estree": "0.0.39",
|
||||
"estree-walker": "^1.0.1",
|
||||
"picomatch": "^2.2.2"
|
||||
}
|
||||
},
|
||||
"@svgr/babel-plugin-add-jsx-attribute": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz",
|
||||
@ -1761,6 +1780,11 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
|
||||
"integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag=="
|
||||
},
|
||||
"@types/estree": {
|
||||
"version": "0.0.39",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
|
||||
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="
|
||||
},
|
||||
"@types/glob": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz",
|
||||
@ -2356,6 +2380,20 @@
|
||||
"normalize-path": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"apexcharts": {
|
||||
"version": "3.22.2",
|
||||
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.22.2.tgz",
|
||||
"integrity": "sha512-pR+cmApk7dhfYILBpe8RVb+FdLfVCt/RDWvAJO1F5feeSQ8lKDgFkRuVu9KOeEarHVXjUpnhLqHNMx7YaprK8A==",
|
||||
"requires": {
|
||||
"@rollup/plugin-babel": "^5.2.1",
|
||||
"svg.draggable.js": "^2.2.2",
|
||||
"svg.easing.js": "^2.0.0",
|
||||
"svg.filter.js": "^2.0.2",
|
||||
"svg.pathmorphing.js": "^0.1.3",
|
||||
"svg.resize.js": "^1.4.3",
|
||||
"svg.select.js": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
|
||||
@ -5566,6 +5604,11 @@
|
||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
|
||||
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="
|
||||
},
|
||||
"estree-walker": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
|
||||
"integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg=="
|
||||
},
|
||||
"esutils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
@ -10799,6 +10842,14 @@
|
||||
"prop-types": "^15.6.2"
|
||||
}
|
||||
},
|
||||
"react-apexcharts": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.3.7.tgz",
|
||||
"integrity": "sha512-2OFhEHd70/WHN0kmrJtVx37UfaL71ZogVkwezmDqwQWgwhK6upuhlnEEX7tEq4xvjA+RFDn6hiUTNIuC/Q7Zqw==",
|
||||
"requires": {
|
||||
"prop-types": "^15.5.7"
|
||||
}
|
||||
},
|
||||
"react-app-polyfill": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-1.0.6.tgz",
|
||||
@ -13066,6 +13117,70 @@
|
||||
"resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz",
|
||||
"integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ=="
|
||||
},
|
||||
"svg.draggable.js": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
|
||||
"integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
|
||||
"requires": {
|
||||
"svg.js": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"svg.easing.js": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz",
|
||||
"integrity": "sha1-iqmUawqOJ4V6XEChDrpAkeVpHxI=",
|
||||
"requires": {
|
||||
"svg.js": ">=2.3.x"
|
||||
}
|
||||
},
|
||||
"svg.filter.js": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz",
|
||||
"integrity": "sha1-kQCOFROJ3ZIwd5/L5uLJo2LRwgM=",
|
||||
"requires": {
|
||||
"svg.js": "^2.2.5"
|
||||
}
|
||||
},
|
||||
"svg.js": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
|
||||
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA=="
|
||||
},
|
||||
"svg.pathmorphing.js": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz",
|
||||
"integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==",
|
||||
"requires": {
|
||||
"svg.js": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"svg.resize.js": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
|
||||
"integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
|
||||
"requires": {
|
||||
"svg.js": "^2.6.5",
|
||||
"svg.select.js": "^2.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"svg.select.js": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
|
||||
"integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
|
||||
"requires": {
|
||||
"svg.js": "^2.2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"svg.select.js": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
|
||||
"integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
|
||||
"requires": {
|
||||
"svg.js": "^2.6.5"
|
||||
}
|
||||
},
|
||||
"svgo": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.0.tgz",
|
||||
|
@ -7,6 +7,7 @@
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.56",
|
||||
"@microsoft/signalr": "^5.0.0",
|
||||
"apexcharts": "^3.22.2",
|
||||
"bootstrap": "^4.3.1",
|
||||
"connected-react-router": "6.5.2",
|
||||
"google-map-react": "^2.1.9",
|
||||
@ -17,6 +18,7 @@
|
||||
"merge": "1.2.1",
|
||||
"popper.js": "^1.16.0",
|
||||
"react": "^16.11.0",
|
||||
"react-apexcharts": "^1.3.7",
|
||||
"react-dom": "16.11.0",
|
||||
"react-google-maps": "^9.4.5",
|
||||
"react-redux": "7.1.1",
|
||||
|
@ -22,7 +22,17 @@
|
||||
-->
|
||||
<title>Birdmap</title>
|
||||
</head>
|
||||
<body style="height: 100vh;">
|
||||
<body>
|
||||
<style>
|
||||
body {
|
||||
height: 100vh;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar {
|
||||
display:none;
|
||||
}
|
||||
</style>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
|
@ -1,22 +1,17 @@
|
||||
import { Box, Container, IconButton, Menu, MenuItem, MenuList, Paper, Grow, Popper } from '@material-ui/core';
|
||||
import AccountCircle from '@material-ui/icons/AccountCircle';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import { positions } from '@material-ui/system';
|
||||
import { Box, Paper } from '@material-ui/core';
|
||||
import { blueGrey, grey, orange } from '@material-ui/core/colors';
|
||||
import { createMuiTheme, createStyles, makeStyles, Theme } from '@material-ui/core/styles';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { ThemeProvider } from '@material-ui/styles';
|
||||
import React, { useState, } from 'react';
|
||||
import { BrowserRouter, NavLink, Redirect, Route, Switch, Link } from 'react-router-dom';
|
||||
import BirdmapTitle from './components/appBar/BirdmapTitle';
|
||||
import React, { useState } from 'react';
|
||||
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom';
|
||||
import BirdmapBar from './components/appBar/BirdmapBar';
|
||||
import Auth from './components/auth/Auth';
|
||||
import AuthService from './components/auth/AuthService';
|
||||
import { ClickAwayListener } from '@material-ui/core';
|
||||
import MapContainer from './components/heatmap/Heatmap';
|
||||
import Dashboard from './components/dashboard/Dashboard';
|
||||
import Devices from './components/devices/Devices';
|
||||
import { blueGrey, blue, orange, grey } from '@material-ui/core/colors';
|
||||
import DevicesContextProvider from './contexts/DevicesContextProvider'
|
||||
|
||||
import MapContainer from './components/heatmap/Heatmap';
|
||||
import Logs from './components/logs/Logs';
|
||||
import DevicesContextProvider from './contexts/DevicesContextProvider';
|
||||
|
||||
const theme = createMuiTheme({
|
||||
palette: {
|
||||
@ -25,14 +20,13 @@ const theme = createMuiTheme({
|
||||
dark: grey[400],
|
||||
},
|
||||
secondary: {
|
||||
main: orange[200],
|
||||
main: blueGrey[700],
|
||||
dark: blueGrey[50],
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function App() {
|
||||
|
||||
const [authenticated, setAuthenticated] = useState(AuthService.isAuthenticated());
|
||||
const [isAdmin, setIsAdmin] = useState(AuthService.isAdmin());
|
||||
|
||||
@ -47,14 +41,19 @@ function App() {
|
||||
);
|
||||
}
|
||||
|
||||
const LogsComponent = () => {
|
||||
return <Logs/>
|
||||
}
|
||||
|
||||
const DashboardComponent = () => {
|
||||
return <Link to="/devices/5">This is a link</Link>;
|
||||
return <Dashboard isAdmin={isAdmin}/>;
|
||||
};
|
||||
|
||||
const DevicesComponent = () => {
|
||||
return <Devices isAdmin={isAdmin}/>;
|
||||
|
||||
};
|
||||
|
||||
const HeatmapComponent = () => {
|
||||
return (
|
||||
<Paper elevation={0}>
|
||||
@ -63,15 +62,46 @@ function App() {
|
||||
);
|
||||
};
|
||||
|
||||
const HeaderComponent = () => {
|
||||
return (
|
||||
<BirdmapBar onLogout={AuthService.logout} isAdmin={isAdmin} isAuthenticated={authenticated}/>
|
||||
);
|
||||
}
|
||||
|
||||
const PredicateRoute = ({ component: Component, predicate: Predicate, ...rest }: { [x: string]: any, component: any, predicate: any }) => {
|
||||
return (
|
||||
<PredicateRouteInternal {...rest} header={HeaderComponent} body={Component} predicate={Predicate}/>
|
||||
);
|
||||
}
|
||||
|
||||
const PublicRoute = ({ component: Component, ...rest }: { [x: string]: any, component: any }) => {
|
||||
return (
|
||||
<PredicateRoute {...rest} component={Component} predicate={true}/>
|
||||
);
|
||||
}
|
||||
|
||||
const PrivateRoute = ({ component: Component, ...rest }: { [x: string]: any, component: any }) => {
|
||||
return (
|
||||
<PredicateRoute {...rest} component={Component} predicate={authenticated}/>
|
||||
);
|
||||
}
|
||||
|
||||
const AdminRoute = ({ component: Component, ...rest }: { [x: string]: any, component: any }) => {
|
||||
return (
|
||||
<PredicateRoute {...rest} component={Component} predicate={authenticated && isAdmin}/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<BrowserRouter>
|
||||
<Switch>
|
||||
<PublicRoute path="/login" component={AuthComponent} />
|
||||
<PublicRoute exact path="/login" component={AuthComponent} />
|
||||
<AdminRoute exact path="/logs" component={LogsComponent} />
|
||||
<DevicesContextProvider>
|
||||
<PrivateRoute path="/" exact authenticated={authenticated} component={DashboardComponent} />
|
||||
<PrivateRoute path="/devices/:id?" exact authenticated={authenticated} component={DevicesComponent} />
|
||||
<PrivateRoute path="/heatmap" exact authenticated={authenticated} component={HeatmapComponent} />
|
||||
<PrivateRoute exact path="/" component={DashboardComponent} />
|
||||
<PrivateRoute exact path="/devices/:id?" component={DevicesComponent} />
|
||||
<PrivateRoute exact path="/heatmap" component={HeatmapComponent} />
|
||||
</DevicesContextProvider>
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
@ -81,112 +111,26 @@ function App() {
|
||||
|
||||
export default App;
|
||||
|
||||
const PublicRoute = ({ component: Component, ...rest }: { [x: string]: any, component: any}) => {
|
||||
const PredicateRouteInternal = ({ header: HeaderComponent, body: BodyComponent, predicate: Predicate, ...rest }: { [x: string]: any, header: any, body: any, predicate: any }) => {
|
||||
return (
|
||||
<Route {...rest} render={matchProps => (
|
||||
<DefaultLayout component={Component} authenticated={false} isAdmin={false} {...matchProps} />
|
||||
)} />
|
||||
);
|
||||
}
|
||||
|
||||
const PrivateRoute = ({ component: Component, authenticated: Authenticated, ...rest }: { [x: string]: any, component: any, authenticated: any }) => {
|
||||
return (
|
||||
<Route {...rest} render={matchProps => (
|
||||
Authenticated
|
||||
? <DefaultLayout component={Component} authenticated={Authenticated} {...matchProps} />
|
||||
Predicate
|
||||
? <DefaultLayoutInternal header={HeaderComponent} body={BodyComponent} {...matchProps} />
|
||||
: <Redirect to='/login' />
|
||||
)} />
|
||||
);
|
||||
};
|
||||
|
||||
const DefaultLayout = ({ component: Component, authenticated: Authenticated, ...rest }: { [x: string]: any, component: any, authenticated: any }) => {
|
||||
const DefaultLayoutInternal = ({ header: HeaderComponent, body: BodyComponent, ...rest }: { [x: string]: any, header: any, body: any }) => {
|
||||
const classes = useDefaultLayoutStyles();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const anchorRef = React.useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleToggle = () => {
|
||||
setOpen((prevOpen) => !prevOpen);
|
||||
};
|
||||
|
||||
const handleClose = (event: React.MouseEvent<EventTarget>) => {
|
||||
if (anchorRef.current && anchorRef.current.contains(event.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleLogout = (event: React.MouseEvent<EventTarget>) => {
|
||||
if (anchorRef.current && anchorRef.current.contains(event.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AuthService.logout();
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
function handleListKeyDown(event: React.KeyboardEvent) {
|
||||
if (event.key === 'Tab') {
|
||||
event.preventDefault();
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
const prevOpen = React.useRef(open);
|
||||
React.useEffect(() => {
|
||||
if (prevOpen.current === true && open === false) {
|
||||
anchorRef.current!.focus();
|
||||
}
|
||||
|
||||
prevOpen.current = open;
|
||||
}, [open]);
|
||||
|
||||
|
||||
const renderNavLinks = () => {
|
||||
return Authenticated
|
||||
? <Container className={classes.nav_menu}>
|
||||
<NavLink exact to="/" className={classes.nav_menu_item} activeClassName={classes.nav_menu_item_active}>Dashboard</NavLink>
|
||||
<NavLink to="/devices" className={classes.nav_menu_item} activeClassName={classes.nav_menu_item_active}>Devices</NavLink>
|
||||
<NavLink exact to="/heatmap" className={classes.nav_menu_item} activeClassName={classes.nav_menu_item_active}>Heatmap</NavLink>
|
||||
<IconButton className={classes.nav_menu_icon}
|
||||
ref={anchorRef}
|
||||
aria-haspopup="true"
|
||||
aria-controls={open ? 'menu-list-grow' : undefined}
|
||||
aria-label="account of current user"
|
||||
onClick={handleToggle}>
|
||||
<AccountCircle/>
|
||||
</IconButton>
|
||||
<Popper open={open} anchorEl={anchorRef.current} role={undefined} transition disablePortal>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{ transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom' }}>
|
||||
<Paper>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MenuList autoFocusItem={open} id="menu-list-grow" onKeyDown={handleListKeyDown}>
|
||||
<MenuItem onClick={handleLogout} component={Link} {...{ to: '/login' }}>Logout</MenuItem>
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
</Container>
|
||||
: null;
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<AppBar position="static" className={classes.bar_root}>
|
||||
<Toolbar>
|
||||
<BirdmapTitle />
|
||||
<Typography component={'span'} className={classes.typo}>
|
||||
{renderNavLinks()}
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Box zIndex="modal" className={classes.box_root}>
|
||||
<Component {...rest} />
|
||||
<Box className={classes.header}>
|
||||
<HeaderComponent />
|
||||
</Box>
|
||||
<Box className={classes.body}>
|
||||
<BodyComponent {...rest} />
|
||||
</Box>
|
||||
</React.Fragment>
|
||||
);
|
||||
@ -194,46 +138,12 @@ const DefaultLayout = ({ component: Component, authenticated: Authenticated, ...
|
||||
|
||||
const useDefaultLayoutStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
bar_root: {
|
||||
header: {
|
||||
height: '7%',
|
||||
},
|
||||
box_root: {
|
||||
body: {
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
height: '93%',
|
||||
},
|
||||
typo: {
|
||||
marginLeft: 'auto',
|
||||
color: 'white',
|
||||
},
|
||||
nav_menu: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
nav_menu_icon: {
|
||||
color: 'inherit',
|
||||
marginLeft: '24px',
|
||||
'&:hover': {
|
||||
color: 'inherit',
|
||||
}
|
||||
},
|
||||
nav_menu_item: {
|
||||
textDecoration: 'none',
|
||||
fontWeight: 'normal',
|
||||
color: 'inherit',
|
||||
marginLeft: '24px',
|
||||
'&:hover': {
|
||||
color: 'inherit',
|
||||
}
|
||||
},
|
||||
nav_menu_item_active: {
|
||||
textDecoration: 'underline',
|
||||
fontWeight: 'bold',
|
||||
color: 'inherit',
|
||||
marginLeft: '24px',
|
||||
'&:hover': {
|
||||
color: 'inherit',
|
||||
}
|
||||
},
|
||||
}
|
||||
}),
|
||||
);
|
@ -1,5 +1,5 @@
|
||||
export default {
|
||||
probability_method_name: 'NotifyDeviceAsync',
|
||||
probability_method_name: 'NotifyMessagesAsync',
|
||||
update_method_name: 'NotifyDeviceUpdatedAsync',
|
||||
update_all_method_name: 'NotifyAllUpdatedAsync',
|
||||
};
|
136
Birdmap.API/ClientApp/src/components/appBar/BirdmapBar.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import { ClickAwayListener, Container, createStyles, Grow, IconButton, makeStyles, MenuItem, MenuList, Paper, Popper, Theme } from '@material-ui/core';
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import AccountCircle from '@material-ui/icons/AccountCircle';
|
||||
import React from 'react';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
import BirdmapTitle from './BirdmapTitle';
|
||||
|
||||
export default function BirdmapBar(props: { onLogout: () => void; isAuthenticated: any; isAdmin: any; }) {
|
||||
const classes = useAppbarStyles();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const anchorRef = React.useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleToggle = () => {
|
||||
setOpen((prevOpen) => !prevOpen);
|
||||
};
|
||||
|
||||
const handleClose = (event: React.MouseEvent<EventTarget>) => {
|
||||
if (anchorRef.current && anchorRef.current.contains(event.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleLogout = (event: React.MouseEvent<EventTarget>) => {
|
||||
if (anchorRef.current && anchorRef.current.contains(event.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
props.onLogout();
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
function handleListKeyDown(event: React.KeyboardEvent) {
|
||||
if (event.key === 'Tab') {
|
||||
event.preventDefault();
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
const prevOpen = React.useRef(open);
|
||||
React.useEffect(() => {
|
||||
if (prevOpen.current === true && open === false) {
|
||||
anchorRef.current!.focus();
|
||||
}
|
||||
|
||||
prevOpen.current = open;
|
||||
}, [open]);
|
||||
|
||||
const renderNavLinks = () => {
|
||||
return props.isAuthenticated
|
||||
? <Container className={classes.nav_menu}>
|
||||
<NavLink exact to="/" className={classes.nav_menu_item} activeClassName={classes.nav_menu_item_active}>Dashboard</NavLink>
|
||||
{props.isAdmin ? <NavLink exact to="/logs" className={classes.nav_menu_item} activeClassName={classes.nav_menu_item_active}>Logs</NavLink> : null}
|
||||
<NavLink to="/devices" className={classes.nav_menu_item} activeClassName={classes.nav_menu_item_active}>Devices</NavLink>
|
||||
<NavLink exact to="/heatmap" className={classes.nav_menu_item} activeClassName={classes.nav_menu_item_active}>Heatmap</NavLink>
|
||||
<IconButton className={classes.nav_menu_icon}
|
||||
ref={anchorRef}
|
||||
aria-haspopup="true"
|
||||
aria-controls={open ? 'menu-list-grow' : undefined}
|
||||
aria-label="account of current user"
|
||||
onClick={handleToggle}>
|
||||
<AccountCircle />
|
||||
</IconButton>
|
||||
<Popper open={open} anchorEl={anchorRef.current} role={undefined} transition disablePortal>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{ transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom' }}>
|
||||
<Paper>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MenuList autoFocusItem={open} id="menu-list-grow" onKeyDown={handleListKeyDown}>
|
||||
<MenuItem onClick={handleLogout} component={Link} {...{ to: '/login' }}>Logout</MenuItem>
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
</Container>
|
||||
: null;
|
||||
};
|
||||
|
||||
return (
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<BirdmapTitle />
|
||||
<Typography component={'span'} className={classes.typo}>
|
||||
{renderNavLinks()}
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
const useAppbarStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
typo: {
|
||||
marginLeft: 'auto',
|
||||
color: 'white',
|
||||
},
|
||||
nav_menu: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
nav_menu_icon: {
|
||||
color: 'inherit',
|
||||
marginLeft: '24px',
|
||||
'&:hover': {
|
||||
color: 'inherit',
|
||||
}
|
||||
},
|
||||
nav_menu_item: {
|
||||
textDecoration: 'none',
|
||||
fontWeight: 'normal',
|
||||
color: 'inherit',
|
||||
marginLeft: '24px',
|
||||
'&:hover': {
|
||||
color: 'inherit',
|
||||
}
|
||||
},
|
||||
nav_menu_item_active: {
|
||||
textDecoration: 'underline',
|
||||
fontWeight: 'bold',
|
||||
color: 'inherit',
|
||||
marginLeft: '24px',
|
||||
'&:hover': {
|
||||
color: 'inherit',
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
328
Birdmap.API/ClientApp/src/components/dashboard/Dashboard.jsx
Normal file
@ -0,0 +1,328 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withStyles } from '@material-ui/styles';
|
||||
import Services from './services/Services';
|
||||
import { blueGrey } from '@material-ui/core/colors';
|
||||
import { Box, Grid, IconButton, Paper, Typography } from '@material-ui/core';
|
||||
import DonutChart from './charts/DonutChart';
|
||||
import HeatmapChart from './charts/HeatmapChart';
|
||||
import BarChart from './charts/BarChart';
|
||||
import LineChart from './charts/LineChart';
|
||||
import DevicesContext from '../../contexts/DevicesContext';
|
||||
import C from '../../common/Constants';
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
padding: '64px',
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
},
|
||||
typo: {
|
||||
fontSize: theme.typography.pxToRem(20),
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
},
|
||||
paper: {
|
||||
backgroundColor: blueGrey[50],
|
||||
padding: '16px',
|
||||
}
|
||||
});
|
||||
|
||||
class Dashboard extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
deviceSeries: [],
|
||||
sensorSeries: [],
|
||||
heatmapSecondsSeries: [],
|
||||
heatmapMinutesSeries: [],
|
||||
barSeries: [],
|
||||
barCategories: [],
|
||||
lineSeries: [],
|
||||
};
|
||||
|
||||
this.updateSeries = this.updateSeries.bind(this);
|
||||
this.updateDynamic = this.updateDynamic.bind(this);
|
||||
this.performTask = this.performTask.bind(this);
|
||||
}
|
||||
|
||||
static contextType = DevicesContext;
|
||||
|
||||
componentDidMount() {
|
||||
this.context.addHandler(C.update_all_method_name, this.updateSeries);
|
||||
this.context.addHandler(C.update_method_name, this.updateSeries);
|
||||
this.updateSeries();
|
||||
this.updateDynamic();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.context.removeHandler(C.update_all_method_name, this.updateSeries);
|
||||
this.context.removeHandler(C.update_method_name, this.updateSeries);
|
||||
if (this.updateTimer) {
|
||||
clearTimeout(this.updateTimer);
|
||||
}
|
||||
}
|
||||
|
||||
getItemsWithStatus(iterate, status) {
|
||||
const items = [];
|
||||
|
||||
for (var d of iterate) {
|
||||
if (d.status == status) {
|
||||
items.push(d);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
getDevicesWithStatus(status) {
|
||||
return this.getItemsWithStatus(this.context.devices, status);
|
||||
}
|
||||
|
||||
getSensorsWithStatus(status) {
|
||||
const sensors = [];
|
||||
|
||||
for (var d of this.context.devices) {
|
||||
sensors.push(...d.sensors)
|
||||
}
|
||||
|
||||
return this.getItemsWithStatus(sensors, status);
|
||||
}
|
||||
|
||||
getDeviceSeries() {
|
||||
var online = this.getDevicesWithStatus("Online").length;
|
||||
var offline = this.getDevicesWithStatus("Offline").length;
|
||||
var error = this.getDevicesWithStatus("Error").length;
|
||||
|
||||
return [online, offline, error]
|
||||
}
|
||||
|
||||
getSensorSeries() {
|
||||
var online = this.getSensorsWithStatus("Online").length;
|
||||
var offline = this.getSensorsWithStatus("Offline").length;
|
||||
var unknown = this.getSensorsWithStatus("Unknown").length;
|
||||
|
||||
return [online, offline, unknown]
|
||||
}
|
||||
|
||||
updateSeries() {
|
||||
this.setState({
|
||||
deviceSeries: this.getDeviceSeries(),
|
||||
sensorSeries: this.getSensorSeries()
|
||||
});
|
||||
}
|
||||
|
||||
updateDynamic = () => {
|
||||
const secondAgo = new Date();
|
||||
secondAgo.setMilliseconds(0);
|
||||
const minuteAgo = new Date(Date.now() - 1000 * 60);
|
||||
const hourAgo = new Date(Date.now() - 1000 * 60 * 60);
|
||||
|
||||
const minuteDevicePoints = {};
|
||||
const hourDevicePoints = {};
|
||||
const barDevicePoints = {};
|
||||
const linePoints = {};
|
||||
|
||||
for (var d of this.context.devices) {
|
||||
minuteDevicePoints[d.id] = Array(60).fill(0);
|
||||
hourDevicePoints[d.id] = Array(60).fill(0);
|
||||
barDevicePoints[d.id] = Array(3).fill(0);
|
||||
}
|
||||
|
||||
const processHeatmapItem = (items, index) => {
|
||||
const p = items[index];
|
||||
if (p.date > minuteAgo) {
|
||||
var seconds = Math.floor((p.date.getTime() - minuteAgo.getTime()) / 1000);
|
||||
var oldProb = minuteDevicePoints[p.deviceId][seconds];
|
||||
if (oldProb < p.prob) {
|
||||
minuteDevicePoints[p.deviceId][seconds] = p.prob;
|
||||
}
|
||||
}
|
||||
|
||||
if (p.date > hourAgo) {
|
||||
var minutes = Math.floor((p.date.getTime() - hourAgo.getTime()) / (1000 * 60));
|
||||
var oldProb = hourDevicePoints[p.deviceId][minutes];
|
||||
if (oldProb < p.prob) {
|
||||
hourDevicePoints[p.deviceId][minutes] = p.prob;
|
||||
}
|
||||
}
|
||||
|
||||
if (p.prob > 0.5 && p.prob <= 0.7) {
|
||||
barDevicePoints[p.deviceId][0] += 1;
|
||||
}
|
||||
if (p.prob > 0.7 && p.prob <= 0.9) {
|
||||
barDevicePoints[p.deviceId][1] += 1;
|
||||
}
|
||||
if (p.prob > 0.9) {
|
||||
barDevicePoints[p.deviceId][2] += 1;
|
||||
}
|
||||
|
||||
if (p.date < secondAgo) {
|
||||
var shortDate = p.date.toUTCString();
|
||||
var point = linePoints[shortDate];
|
||||
if (point === undefined) {
|
||||
linePoints[shortDate] = 1;
|
||||
} else {
|
||||
linePoints[shortDate] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onFinished = () => {
|
||||
const minuteHeatmapSeries = [];
|
||||
|
||||
var i = 0;
|
||||
for (var p in minuteDevicePoints) {
|
||||
minuteHeatmapSeries.push({
|
||||
name: "Device " + i,
|
||||
data: minuteDevicePoints[p].map((value, index) => ({
|
||||
x: new Date(Date.now() - (60 - index) * 1000).toLocaleTimeString('hu-HU'),
|
||||
y: value
|
||||
})),
|
||||
});
|
||||
i++;
|
||||
};
|
||||
|
||||
const hourHeatmapSeries = [];
|
||||
|
||||
var i = 0;
|
||||
for (var p in hourDevicePoints) {
|
||||
hourHeatmapSeries.push({
|
||||
name: "Device " + i,
|
||||
data: hourDevicePoints[p].map((value, index) => ({
|
||||
x: new Date(Date.now() - (60 - index) * 1000 * 60).toLocaleTimeString('hu-HU').substring(0, 5),
|
||||
y: value
|
||||
})),
|
||||
});
|
||||
i++;
|
||||
};
|
||||
|
||||
const barSeries = [];
|
||||
|
||||
const getCount = column => {
|
||||
var counts = [];
|
||||
|
||||
for (var p in barDevicePoints) {
|
||||
counts.unshift(barDevicePoints[p][column]);
|
||||
}
|
||||
|
||||
return counts;
|
||||
};
|
||||
|
||||
barSeries.push({
|
||||
name: "Prob > 0.5",
|
||||
data: getCount(0),
|
||||
});
|
||||
barSeries.push({
|
||||
name: "Prob > 0.7",
|
||||
data: getCount(1),
|
||||
});
|
||||
barSeries.push({
|
||||
name: "Prob > 0.9",
|
||||
data: getCount(2),
|
||||
});
|
||||
|
||||
const lineSeries = [{ name: "message/sec", data: [] }];
|
||||
for (var m in linePoints) {
|
||||
lineSeries[0].data.push({
|
||||
x: new Date(m).getTime(),
|
||||
y: linePoints[m],
|
||||
})
|
||||
}
|
||||
|
||||
const getBarCategories = () => {
|
||||
const categories = [];
|
||||
|
||||
for (var i = this.context.devices.length - 1; i >= 0; i--) {
|
||||
categories.push("Device " + i)
|
||||
}
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
const toUpdate = [
|
||||
{ heatmapSecondsSeries: minuteHeatmapSeries },
|
||||
{ heatmapMinutesSeries: hourHeatmapSeries },
|
||||
{ barSeries: barSeries },
|
||||
{ barCategories: getBarCategories() },
|
||||
{ lineSeries: lineSeries }
|
||||
];
|
||||
|
||||
//Set states must be done separately otherwise ApexChart's UI update freezes the page.
|
||||
this.performTask(toUpdate, 2, 300, (list, index) => {
|
||||
this.setState(list[index]);
|
||||
},
|
||||
() => {
|
||||
this.updateTimer = setTimeout(this.updateDynamic, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
this.performTask(this.context.heatmapPoints, Math.ceil(this.context.heatmapPoints.length / 50), 20,
|
||||
processHeatmapItem, onFinished);
|
||||
}
|
||||
|
||||
performTask(items, numToProcess, wait, processItem, onFinished) {
|
||||
var pos = 0;
|
||||
// This is run once for every numToProcess items.
|
||||
function iteration() {
|
||||
// Calculate last position.
|
||||
var j = Math.min(pos + numToProcess, items.length);
|
||||
// Start at current position and loop to last position.
|
||||
for (var i = pos; i < j; i++) {
|
||||
processItem(items, i);
|
||||
}
|
||||
// Increment current position.
|
||||
pos += numToProcess;
|
||||
// Only continue if there are more items to process.
|
||||
if (pos < items.length)
|
||||
setTimeout(iteration, wait); // Wait 10 ms to let the UI update.
|
||||
else
|
||||
onFinished();
|
||||
}
|
||||
iteration();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<Box className={classes.root}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12}>
|
||||
<Services isAdmin={this.props.isAdmin} />
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Paper className={classes.paper}>
|
||||
<DonutChart totalLabel="Devices" series={this.state.deviceSeries} />
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Paper className={classes.paper}>
|
||||
<DonutChart totalLabel="Sensors" series={this.state.sensorSeries} />
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Paper className={classes.paper}>
|
||||
<HeatmapChart label="Highest probability per second by devices" series={this.state.heatmapSecondsSeries} />
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Paper className={classes.paper}>
|
||||
<HeatmapChart label="Highest probability per minute by devices" series={this.state.heatmapMinutesSeries} />
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Paper className={classes.paper}>
|
||||
<BarChart label="# of messages by devices" series={this.state.barSeries} categories={this.state.barCategories} />
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Paper className={classes.paper}>
|
||||
<LineChart label="# of messages per second" series={this.state.lineSeries} />
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Dashboard);
|
@ -0,0 +1,92 @@
|
||||
import React, { Component } from 'react';
|
||||
import Chart from 'react-apexcharts';
|
||||
import { blueGrey, green, red, orange, amber } from '@material-ui/core/colors';
|
||||
|
||||
export class BarChart extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
options: {},
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.categories !== this.props.categories) {
|
||||
this.setState({options: {
|
||||
chart: {
|
||||
stacked: true,
|
||||
animations: {
|
||||
enabled: true,
|
||||
easing: 'linear',
|
||||
speed: 250,
|
||||
animateGradually: {
|
||||
enabled: false,
|
||||
},
|
||||
dynamicAnimation: {
|
||||
enabled: true,
|
||||
speed: 250
|
||||
}
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: true,
|
||||
},
|
||||
},
|
||||
colors: [blueGrey[500], blueGrey[700], blueGrey[900]],
|
||||
stroke: {
|
||||
width: 1,
|
||||
colors: ['#fff']
|
||||
},
|
||||
title: {
|
||||
text: this.props.label,
|
||||
style: {
|
||||
fontSize: '22px',
|
||||
fontWeight: 600,
|
||||
fontFamily: 'Helvetica, Arial, sans-serif',
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
categories: this.props.categories,
|
||||
labels: {
|
||||
formatter: function (val) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
title: {
|
||||
text: undefined
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
y: {
|
||||
formatter: function (val) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
},
|
||||
fill: {
|
||||
opacity: 1
|
||||
},
|
||||
legend: {
|
||||
position: 'top',
|
||||
}
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Chart
|
||||
options={this.state.options}
|
||||
series={this.props.series}
|
||||
type="bar"
|
||||
height={600}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default BarChart;
|
@ -0,0 +1,67 @@
|
||||
import React, { Component } from 'react';
|
||||
import Chart from 'react-apexcharts';
|
||||
import { blueGrey, green, red } from '@material-ui/core/colors';
|
||||
|
||||
|
||||
export class DonutChart extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
options: {
|
||||
legend: {
|
||||
fontSize: '18px',
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
startAngle: 0,
|
||||
expandOnClick: false,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
customScale: 1,
|
||||
dataLabels: {
|
||||
offset: 0,
|
||||
minAngleToShowLabel: 10
|
||||
},
|
||||
donut: {
|
||||
size: '65%',
|
||||
background: 'transparent',
|
||||
labels: {
|
||||
show: true,
|
||||
total: {
|
||||
show: true,
|
||||
showAlways: true,
|
||||
label: props.totalLabel,
|
||||
fontSize: '22px',
|
||||
fontFamily: 'Helvetica, Arial, sans-serif',
|
||||
fontWeight: 600,
|
||||
color: '#373d3f',
|
||||
formatter: function (w) {
|
||||
return w.globals.seriesTotals.reduce((a, b) => {
|
||||
return a + b
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
colors: [green[500], blueGrey[500], red[500]],
|
||||
labels: ['Online', 'Offline', 'Error / Unknown']},
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Chart
|
||||
options={this.state.options}
|
||||
series={this.props.series}
|
||||
type="donut"/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default DonutChart;
|
@ -0,0 +1,55 @@
|
||||
import React, { Component } from 'react';
|
||||
import Chart from 'react-apexcharts';
|
||||
import { blueGrey, green, red } from '@material-ui/core/colors';
|
||||
|
||||
export class HeatmapChart extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
options: {
|
||||
chart: {
|
||||
animations: {
|
||||
enabled: true,
|
||||
easing: 'linear',
|
||||
speed: 250,
|
||||
animateGradually: {
|
||||
enabled: false,
|
||||
speed: 250,
|
||||
},
|
||||
dynamicAnimation: {
|
||||
enabled: true,
|
||||
speed: 250
|
||||
}
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
colors: [blueGrey[900]],
|
||||
title: {
|
||||
text: props.label,
|
||||
style: {
|
||||
fontSize: '22px',
|
||||
fontWeight: 600,
|
||||
fontFamily: 'Helvetica, Arial, sans-serif',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Chart
|
||||
options={this.state.options}
|
||||
series={this.props.series}
|
||||
type="heatmap"
|
||||
height={600}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default HeatmapChart
|
@ -0,0 +1,74 @@
|
||||
import React, { Component } from 'react';
|
||||
import Chart from 'react-apexcharts';
|
||||
import { blueGrey, green, red } from '@material-ui/core/colors';
|
||||
|
||||
export class LineChart extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
options: {
|
||||
chart: {
|
||||
animations: {
|
||||
enabled: true,
|
||||
easing: 'linear',
|
||||
speed: 250,
|
||||
animateGradually: {
|
||||
enabled: false,
|
||||
},
|
||||
dynamicAnimation: {
|
||||
enabled: true,
|
||||
speed: 250
|
||||
}
|
||||
},
|
||||
zoom: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
colors: [blueGrey[900]],
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
stroke: {
|
||||
curve: 'straight'
|
||||
},
|
||||
title: {
|
||||
text: this.props.label,
|
||||
align: 'left',
|
||||
style: {
|
||||
fontSize: '22px',
|
||||
fontWeight: 600,
|
||||
fontFamily: 'Helvetica, Arial, sans-serif',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
row: {
|
||||
colors: ['#f3f3f3', 'transparent'], // takes an array which will be repeated on columns
|
||||
opacity: 0.5
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
formatter: function (val) {
|
||||
return new Date(val).toLocaleTimeString('hu-HU');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Chart
|
||||
options={this.state.options}
|
||||
series={this.props.series}
|
||||
type="line"
|
||||
height={600}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default LineChart
|
@ -0,0 +1,61 @@
|
||||
import { TextField } from '@material-ui/core';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export default function AddNewDialog(props) {
|
||||
const [name, setName] = useState("");
|
||||
const [url, setUrl] = useState("");
|
||||
|
||||
const onNameChange = (event) => {
|
||||
setName(event.target.value);
|
||||
}
|
||||
|
||||
const onUrlChange = (event) => {
|
||||
setUrl(event.target.value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
open={props.open}
|
||||
onClose={props.handleClose}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">{"Add new service."}</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
id="name"
|
||||
label="Name"
|
||||
type="text"
|
||||
onChange={onNameChange}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
id="url"
|
||||
label="Url"
|
||||
type="text"
|
||||
onChange={onUrlChange}
|
||||
fullWidth
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.handleClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => props.handleAdd(name, url)} color="primary" autoFocus>
|
||||
Add
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogContentText from '@material-ui/core/DialogContentText';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import React from 'react';
|
||||
|
||||
export default function DeleteDialog(props) {
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
open={props.open}
|
||||
onClose={() => props.handleClose(false)}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">{"Are you sure?"}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
Deleting is permament.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => props.handleClose(false)} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => props.handleClose(true)} color="primary" autoFocus>
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,234 @@
|
||||
import { Box, FormControlLabel, Grid, IconButton, Paper, TextField, Typography } from '@material-ui/core';
|
||||
import Accordion from '@material-ui/core/Accordion';
|
||||
import AccordionDetails from '@material-ui/core/AccordionDetails';
|
||||
import AccordionSummary from '@material-ui/core/AccordionSummary';
|
||||
import { blueGrey, green, red } from '@material-ui/core/colors';
|
||||
import { CancelRounded, CheckCircleRounded, Delete, Edit } from '@material-ui/icons/';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { withStyles } from '@material-ui/styles';
|
||||
import React, { Component } from 'react';
|
||||
import DeleteDialog from './DeleteDialog';
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
padding: '64px',
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
},
|
||||
acc_summary: {
|
||||
backgroundColor: blueGrey[50],
|
||||
padding: theme.spacing(2),
|
||||
textAlign: 'center',
|
||||
height: '75px',
|
||||
},
|
||||
acc_details: {
|
||||
backgroundColor: blueGrey[100],
|
||||
},
|
||||
grid_typo: {
|
||||
fontSize: theme.typography.pxToRem(20),
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
},
|
||||
grid_typo_2: {
|
||||
marginLeft: '5px',
|
||||
fontSize: theme.typography.pxToRem(15),
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
typo: {
|
||||
fontSize: theme.typography.pxToRem(20),
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
},
|
||||
icon_box: {
|
||||
marginLeft: '30px',
|
||||
},
|
||||
paper: {
|
||||
backgroundColor: blueGrey[50],
|
||||
padding: theme.spacing(2),
|
||||
textAlign: 'center',
|
||||
height: '75px',
|
||||
}
|
||||
});
|
||||
|
||||
class ServiceInfoComponent extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isDialogOpen: false,
|
||||
isEditing: false,
|
||||
name: "",
|
||||
url: "",
|
||||
}
|
||||
|
||||
this.handleDialogClose = this.handleDialogClose.bind(this);
|
||||
this.onNameChange = this.onNameChange.bind(this);
|
||||
this.onUrlChange = this.onUrlChange.bind(this);
|
||||
this.handleSaveCancel = this.handleSaveCancel.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({ name: this.props.info.service.name, url: this.props.info.service.uri });
|
||||
|
||||
if (this.props.isEditing !== undefined) {
|
||||
this.setState({ isEditing: this.props.isEditing });
|
||||
}
|
||||
}
|
||||
|
||||
onNameChange(event) {
|
||||
this.setState({ name: event.target.value });
|
||||
}
|
||||
|
||||
onUrlChange(event) {
|
||||
this.setState({ url: event.target.value });
|
||||
}
|
||||
|
||||
getColor(status) {
|
||||
if (status === "OK")
|
||||
return { color: green[600] };
|
||||
else
|
||||
return { color: red[600] };
|
||||
}
|
||||
|
||||
handleDialogClose(result) {
|
||||
this.setState({ isDialogOpen: false });
|
||||
if (result === true) {
|
||||
this.props.service.delete(this.props.info.service.id);
|
||||
}
|
||||
}
|
||||
|
||||
handleEditCancel(value) {
|
||||
this.setState({ isEditing: value });
|
||||
}
|
||||
|
||||
handleSaveCancel() {
|
||||
let request = {
|
||||
...this.props.info.service
|
||||
};
|
||||
|
||||
request.name = this.state.name;
|
||||
request.uri = this.state.url;
|
||||
|
||||
if (request.id > 0) {
|
||||
this.props.service.put(request).catch(ex => {
|
||||
console.log(ex);
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.props.service.post(request).catch(ex => {
|
||||
console.log(ex);
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({ isEditing: false });
|
||||
}
|
||||
|
||||
renderSaveCancel() {
|
||||
return (
|
||||
<Box styles={{ marginLeft: 'auto' }}>
|
||||
<IconButton color="primary" onClick={this.handleSaveCancel}>
|
||||
<CheckCircleRounded fontSize="large" />
|
||||
</IconButton>
|
||||
<IconButton color="primary" onClick={() => this.setState({ isEditing: false })}>
|
||||
<CancelRounded fontSize="large" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
renderButtons() {
|
||||
const renderEditDelete = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DeleteDialog open={this.state.isDialogOpen} handleClose={this.handleDialogClose}/>
|
||||
<IconButton color="primary" onClick={() => this.handleEditCancel(true)}>
|
||||
<Edit fontSize="large"/>
|
||||
</IconButton>
|
||||
<IconButton style={{color: red[600]}} onClick={() => this.setState({ isDialogOpen: true })}>
|
||||
<Delete fontSize="large"/>
|
||||
</IconButton>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<Box className={classes.icon_box}>
|
||||
{this.props.isAdmin && this.props.info.service.name !== "Mqtt Client Service" ? renderEditDelete() : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
|
||||
const renderAccordion = () => {
|
||||
return (
|
||||
<Accordion>
|
||||
<AccordionSummary className={classes.acc_summary}
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls={"device-panel-/" + this.props.info.service.name}
|
||||
id={"device-panel-/" + this.props.info.service.name}>
|
||||
<Grid container
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justify="flex-start"
|
||||
alignItems="center">
|
||||
<Grid item>
|
||||
<Typography className={classes.grid_typo}>{this.props.info.service.name}</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography className={classes.grid_typo_2}>{this.props.info.service.uri}</Typography>
|
||||
</Grid>
|
||||
<Grid item style={{ marginLeft: 'auto' }}>
|
||||
<Grid container
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justify="flex-start"
|
||||
alignItems="center">
|
||||
<Grid item>
|
||||
<FormControlLabel
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onFocus={(event) => event.stopPropagation()}
|
||||
control={this.renderButtons()} />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography style={this.getColor(this.props.info.statusCode)}>Status: <b>{this.props.info.statusCode}</b></Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails className={classes.acc_details}>
|
||||
{this.props.info.response}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTextFields = () => {
|
||||
return (
|
||||
<Paper className={classes.acc_summary}>
|
||||
<Grid container
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justify="flex-start"
|
||||
alignItems="center">
|
||||
<Grid item xs>
|
||||
<TextField label="Name" type="text" defaultValue={this.props.info.service.name} onChange={this.onNameChange} />
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<TextField label="Url" type="text" fullWidth defaultValue={this.props.info.service.uri} onChange={this.onUrlChange}/>
|
||||
</Grid>
|
||||
<Grid item xs>
|
||||
{this.renderSaveCancel()}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
return this.state.isEditing ? renderTextFields() : renderAccordion();
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(ServiceInfoComponent);
|
@ -0,0 +1,386 @@
|
||||
"use strict";
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
//----------------------
|
||||
// <auto-generated>
|
||||
// Generated using the NSwag toolchain v13.8.2.0 (NJsonSchema v10.2.1.0 (Newtonsoft.Json v12.0.0.0)) (http://NSwag.org)
|
||||
// </auto-generated>
|
||||
//----------------------
|
||||
// ReSharper disable InconsistentNaming
|
||||
var __extends = (this && this.__extends) || (function () {
|
||||
var extendStatics = function (d, b) {
|
||||
extendStatics = Object.setPrototypeOf ||
|
||||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
||||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
||||
return extendStatics(d, b);
|
||||
};
|
||||
return function (d, b) {
|
||||
extendStatics(d, b);
|
||||
function __() { this.constructor = d; }
|
||||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ApiException = exports.HttpStatusCode = exports.ServiceRequest = exports.ServiceInfo = void 0;
|
||||
var ServiceInfoService = /** @class */ (function () {
|
||||
function ServiceInfoService(baseUrl, http) {
|
||||
this.jsonParseReviver = undefined;
|
||||
this.http = http ? http : window;
|
||||
this.baseUrl = baseUrl !== undefined && baseUrl !== null ? baseUrl : "";
|
||||
}
|
||||
ServiceInfoService.prototype.getCount = function () {
|
||||
var _this = this;
|
||||
var url_ = this.baseUrl + "/api/Services/count";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
var options_ = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
'Authorization': sessionStorage.getItem('user')
|
||||
}
|
||||
};
|
||||
return this.http.fetch(url_, options_).then(function (_response) {
|
||||
return _this.processGetCount(_response);
|
||||
});
|
||||
};
|
||||
ServiceInfoService.prototype.processGetCount = function (response) {
|
||||
var _this = this;
|
||||
var status = response.status;
|
||||
var _headers = {};
|
||||
if (response.headers && response.headers.forEach) {
|
||||
response.headers.forEach(function (v, k) { return _headers[k] = v; });
|
||||
}
|
||||
;
|
||||
if (status === 200) {
|
||||
return response.text().then(function (_responseText) {
|
||||
var result200 = null;
|
||||
var resultData200 = _responseText === "" ? null : JSON.parse(_responseText, _this.jsonParseReviver);
|
||||
result200 = resultData200 !== undefined ? resultData200 : null;
|
||||
return result200;
|
||||
});
|
||||
}
|
||||
else if (status !== 200 && status !== 204) {
|
||||
return response.text().then(function (_responseText) {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
};
|
||||
ServiceInfoService.prototype.get = function () {
|
||||
var _this = this;
|
||||
var url_ = this.baseUrl + "/api/Services";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
var options_ = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
'Authorization': sessionStorage.getItem('user')
|
||||
}
|
||||
};
|
||||
return this.http.fetch(url_, options_).then(function (_response) {
|
||||
return _this.processGet(_response);
|
||||
});
|
||||
};
|
||||
ServiceInfoService.prototype.processGet = function (response) {
|
||||
var _this = this;
|
||||
var status = response.status;
|
||||
var _headers = {};
|
||||
if (response.headers && response.headers.forEach) {
|
||||
response.headers.forEach(function (v, k) { return _headers[k] = v; });
|
||||
}
|
||||
;
|
||||
if (status === 200) {
|
||||
return response.text().then(function (_responseText) {
|
||||
var result200 = null;
|
||||
var resultData200 = _responseText === "" ? null : JSON.parse(_responseText, _this.jsonParseReviver);
|
||||
if (Array.isArray(resultData200)) {
|
||||
result200 = [];
|
||||
for (var _i = 0, resultData200_1 = resultData200; _i < resultData200_1.length; _i++) {
|
||||
var item = resultData200_1[_i];
|
||||
result200.push(ServiceInfo.fromJS(item));
|
||||
}
|
||||
}
|
||||
return result200;
|
||||
});
|
||||
}
|
||||
else if (status !== 200 && status !== 204) {
|
||||
return response.text().then(function (_responseText) {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
};
|
||||
ServiceInfoService.prototype.post = function (request) {
|
||||
var _this = this;
|
||||
var url_ = this.baseUrl + "/api/Services";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
var content_ = JSON.stringify(request);
|
||||
var options_ = {
|
||||
body: content_,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
'Authorization': sessionStorage.getItem('user')
|
||||
}
|
||||
};
|
||||
return this.http.fetch(url_, options_).then(function (_response) {
|
||||
return _this.processPost(_response);
|
||||
});
|
||||
};
|
||||
ServiceInfoService.prototype.processPost = function (response) {
|
||||
var _this = this;
|
||||
var status = response.status;
|
||||
var _headers = {};
|
||||
if (response.headers && response.headers.forEach) {
|
||||
response.headers.forEach(function (v, k) { return _headers[k] = v; });
|
||||
}
|
||||
;
|
||||
if (status === 201) {
|
||||
return response.text().then(function (_responseText) {
|
||||
var result201 = null;
|
||||
var resultData201 = _responseText === "" ? null : JSON.parse(_responseText, _this.jsonParseReviver);
|
||||
result201 = ServiceRequest.fromJS(resultData201);
|
||||
return result201;
|
||||
});
|
||||
}
|
||||
else if (status !== 200 && status !== 204) {
|
||||
return response.text().then(function (_responseText) {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
};
|
||||
ServiceInfoService.prototype.put = function (request) {
|
||||
var _this = this;
|
||||
var url_ = this.baseUrl + "/api/Services";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
var content_ = JSON.stringify(request);
|
||||
var options_ = {
|
||||
body: content_,
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Authorization': sessionStorage.getItem('user')
|
||||
}
|
||||
};
|
||||
return this.http.fetch(url_, options_).then(function (_response) {
|
||||
return _this.processPut(_response);
|
||||
});
|
||||
};
|
||||
ServiceInfoService.prototype.processPut = function (response) {
|
||||
var status = response.status;
|
||||
var _headers = {};
|
||||
if (response.headers && response.headers.forEach) {
|
||||
response.headers.forEach(function (v, k) { return _headers[k] = v; });
|
||||
}
|
||||
;
|
||||
if (status === 204) {
|
||||
return response.text().then(function (_responseText) {
|
||||
return;
|
||||
});
|
||||
}
|
||||
else if (status !== 200 && status !== 204) {
|
||||
return response.text().then(function (_responseText) {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
};
|
||||
ServiceInfoService.prototype.delete = function (id) {
|
||||
var _this = this;
|
||||
var url_ = this.baseUrl + "/api/Services/{id}";
|
||||
if (id === undefined || id === null)
|
||||
throw new Error("The parameter 'id' must be defined.");
|
||||
url_ = url_.replace("{id}", encodeURIComponent("" + id));
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
var options_ = {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
'Authorization': sessionStorage.getItem('user')
|
||||
}
|
||||
};
|
||||
return this.http.fetch(url_, options_).then(function (_response) {
|
||||
return _this.processDelete(_response);
|
||||
});
|
||||
};
|
||||
ServiceInfoService.prototype.processDelete = function (response) {
|
||||
var status = response.status;
|
||||
var _headers = {};
|
||||
if (response.headers && response.headers.forEach) {
|
||||
response.headers.forEach(function (v, k) { return _headers[k] = v; });
|
||||
}
|
||||
;
|
||||
if (status === 204) {
|
||||
return response.text().then(function (_responseText) {
|
||||
return;
|
||||
});
|
||||
}
|
||||
else if (status !== 200 && status !== 204) {
|
||||
return response.text().then(function (_responseText) {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
};
|
||||
return ServiceInfoService;
|
||||
}());
|
||||
exports.default = ServiceInfoService;
|
||||
var ServiceInfo = /** @class */ (function () {
|
||||
function ServiceInfo(data) {
|
||||
if (data) {
|
||||
for (var property in data) {
|
||||
if (data.hasOwnProperty(property))
|
||||
this[property] = data[property];
|
||||
}
|
||||
}
|
||||
}
|
||||
ServiceInfo.prototype.init = function (_data) {
|
||||
if (_data) {
|
||||
this.service = _data["service"] ? ServiceRequest.fromJS(_data["service"]) : undefined;
|
||||
this.statusCode = _data["statusCode"];
|
||||
this.response = _data["response"];
|
||||
}
|
||||
};
|
||||
ServiceInfo.fromJS = function (data) {
|
||||
data = typeof data === 'object' ? data : {};
|
||||
var result = new ServiceInfo();
|
||||
result.init(data);
|
||||
return result;
|
||||
};
|
||||
ServiceInfo.prototype.toJSON = function (data) {
|
||||
data = typeof data === 'object' ? data : {};
|
||||
data["service"] = this.service ? this.service.toJSON() : undefined;
|
||||
data["statusCode"] = this.statusCode;
|
||||
data["response"] = this.response;
|
||||
return data;
|
||||
};
|
||||
return ServiceInfo;
|
||||
}());
|
||||
exports.ServiceInfo = ServiceInfo;
|
||||
var ServiceRequest = /** @class */ (function () {
|
||||
function ServiceRequest(data) {
|
||||
if (data) {
|
||||
for (var property in data) {
|
||||
if (data.hasOwnProperty(property))
|
||||
this[property] = data[property];
|
||||
}
|
||||
}
|
||||
}
|
||||
ServiceRequest.prototype.init = function (_data) {
|
||||
if (_data) {
|
||||
this.id = _data["id"];
|
||||
this.name = _data["name"];
|
||||
this.uri = _data["uri"];
|
||||
}
|
||||
};
|
||||
ServiceRequest.fromJS = function (data) {
|
||||
data = typeof data === 'object' ? data : {};
|
||||
var result = new ServiceRequest();
|
||||
result.init(data);
|
||||
return result;
|
||||
};
|
||||
ServiceRequest.prototype.toJSON = function (data) {
|
||||
data = typeof data === 'object' ? data : {};
|
||||
data["id"] = this.id;
|
||||
data["name"] = this.name;
|
||||
data["uri"] = this.uri;
|
||||
return data;
|
||||
};
|
||||
return ServiceRequest;
|
||||
}());
|
||||
exports.ServiceRequest = ServiceRequest;
|
||||
var HttpStatusCode;
|
||||
(function (HttpStatusCode) {
|
||||
HttpStatusCode["Continue"] = "Continue";
|
||||
HttpStatusCode["SwitchingProtocols"] = "SwitchingProtocols";
|
||||
HttpStatusCode["Processing"] = "Processing";
|
||||
HttpStatusCode["EarlyHints"] = "EarlyHints";
|
||||
HttpStatusCode["OK"] = "OK";
|
||||
HttpStatusCode["Created"] = "Created";
|
||||
HttpStatusCode["Accepted"] = "Accepted";
|
||||
HttpStatusCode["NonAuthoritativeInformation"] = "NonAuthoritativeInformation";
|
||||
HttpStatusCode["NoContent"] = "NoContent";
|
||||
HttpStatusCode["ResetContent"] = "ResetContent";
|
||||
HttpStatusCode["PartialContent"] = "PartialContent";
|
||||
HttpStatusCode["MultiStatus"] = "MultiStatus";
|
||||
HttpStatusCode["AlreadyReported"] = "AlreadyReported";
|
||||
HttpStatusCode["IMUsed"] = "IMUsed";
|
||||
HttpStatusCode["MultipleChoices"] = "Ambiguous";
|
||||
HttpStatusCode["Ambiguous"] = "Ambiguous";
|
||||
HttpStatusCode["MovedPermanently"] = "Moved";
|
||||
HttpStatusCode["Moved"] = "Moved";
|
||||
HttpStatusCode["Found"] = "Redirect";
|
||||
HttpStatusCode["Redirect"] = "Redirect";
|
||||
HttpStatusCode["SeeOther"] = "RedirectMethod";
|
||||
HttpStatusCode["RedirectMethod"] = "RedirectMethod";
|
||||
HttpStatusCode["NotModified"] = "NotModified";
|
||||
HttpStatusCode["UseProxy"] = "UseProxy";
|
||||
HttpStatusCode["Unused"] = "Unused";
|
||||
HttpStatusCode["TemporaryRedirect"] = "TemporaryRedirect";
|
||||
HttpStatusCode["RedirectKeepVerb"] = "TemporaryRedirect";
|
||||
HttpStatusCode["PermanentRedirect"] = "PermanentRedirect";
|
||||
HttpStatusCode["BadRequest"] = "BadRequest";
|
||||
HttpStatusCode["Unauthorized"] = "Unauthorized";
|
||||
HttpStatusCode["PaymentRequired"] = "PaymentRequired";
|
||||
HttpStatusCode["Forbidden"] = "Forbidden";
|
||||
HttpStatusCode["NotFound"] = "NotFound";
|
||||
HttpStatusCode["MethodNotAllowed"] = "MethodNotAllowed";
|
||||
HttpStatusCode["NotAcceptable"] = "NotAcceptable";
|
||||
HttpStatusCode["ProxyAuthenticationRequired"] = "ProxyAuthenticationRequired";
|
||||
HttpStatusCode["RequestTimeout"] = "RequestTimeout";
|
||||
HttpStatusCode["Conflict"] = "Conflict";
|
||||
HttpStatusCode["Gone"] = "Gone";
|
||||
HttpStatusCode["LengthRequired"] = "LengthRequired";
|
||||
HttpStatusCode["PreconditionFailed"] = "PreconditionFailed";
|
||||
HttpStatusCode["RequestEntityTooLarge"] = "RequestEntityTooLarge";
|
||||
HttpStatusCode["RequestUriTooLong"] = "RequestUriTooLong";
|
||||
HttpStatusCode["UnsupportedMediaType"] = "UnsupportedMediaType";
|
||||
HttpStatusCode["RequestedRangeNotSatisfiable"] = "RequestedRangeNotSatisfiable";
|
||||
HttpStatusCode["ExpectationFailed"] = "ExpectationFailed";
|
||||
HttpStatusCode["MisdirectedRequest"] = "MisdirectedRequest";
|
||||
HttpStatusCode["UnprocessableEntity"] = "UnprocessableEntity";
|
||||
HttpStatusCode["Locked"] = "Locked";
|
||||
HttpStatusCode["FailedDependency"] = "FailedDependency";
|
||||
HttpStatusCode["UpgradeRequired"] = "UpgradeRequired";
|
||||
HttpStatusCode["PreconditionRequired"] = "PreconditionRequired";
|
||||
HttpStatusCode["TooManyRequests"] = "TooManyRequests";
|
||||
HttpStatusCode["RequestHeaderFieldsTooLarge"] = "RequestHeaderFieldsTooLarge";
|
||||
HttpStatusCode["UnavailableForLegalReasons"] = "UnavailableForLegalReasons";
|
||||
HttpStatusCode["InternalServerError"] = "InternalServerError";
|
||||
HttpStatusCode["NotImplemented"] = "NotImplemented";
|
||||
HttpStatusCode["BadGateway"] = "BadGateway";
|
||||
HttpStatusCode["ServiceUnavailable"] = "ServiceUnavailable";
|
||||
HttpStatusCode["GatewayTimeout"] = "GatewayTimeout";
|
||||
HttpStatusCode["HttpVersionNotSupported"] = "HttpVersionNotSupported";
|
||||
HttpStatusCode["VariantAlsoNegotiates"] = "VariantAlsoNegotiates";
|
||||
HttpStatusCode["InsufficientStorage"] = "InsufficientStorage";
|
||||
HttpStatusCode["LoopDetected"] = "LoopDetected";
|
||||
HttpStatusCode["NotExtended"] = "NotExtended";
|
||||
HttpStatusCode["NetworkAuthenticationRequired"] = "NetworkAuthenticationRequired";
|
||||
})(HttpStatusCode = exports.HttpStatusCode || (exports.HttpStatusCode = {}));
|
||||
var ApiException = /** @class */ (function (_super) {
|
||||
__extends(ApiException, _super);
|
||||
function ApiException(message, status, response, headers, result) {
|
||||
var _this = _super.call(this) || this;
|
||||
_this.isApiException = true;
|
||||
_this.message = message;
|
||||
_this.status = status;
|
||||
_this.response = response;
|
||||
_this.headers = headers;
|
||||
_this.result = result;
|
||||
return _this;
|
||||
}
|
||||
ApiException.isApiException = function (obj) {
|
||||
return obj.isApiException === true;
|
||||
};
|
||||
return ApiException;
|
||||
}(Error));
|
||||
exports.ApiException = ApiException;
|
||||
function throwException(message, status, response, headers, result) {
|
||||
if (result !== null && result !== undefined)
|
||||
throw result;
|
||||
else
|
||||
throw new ApiException(message, status, response, headers, null);
|
||||
}
|
||||
//# sourceMappingURL=SystemInfoService.js.map
|
@ -0,0 +1,389 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
//----------------------
|
||||
// <auto-generated>
|
||||
// Generated using the NSwag toolchain v13.8.2.0 (NJsonSchema v10.2.1.0 (Newtonsoft.Json v12.0.0.0)) (http://NSwag.org)
|
||||
// </auto-generated>
|
||||
//----------------------
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
export default class ServiceInfoService {
|
||||
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
|
||||
private baseUrl: string;
|
||||
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
|
||||
|
||||
constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> }) {
|
||||
this.http = http ? http : <any>window;
|
||||
this.baseUrl = baseUrl !== undefined && baseUrl !== null ? baseUrl : "";
|
||||
}
|
||||
|
||||
getCount(): Promise<number> {
|
||||
let url_ = this.baseUrl + "/api/Services/count";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
let options_ = <RequestInit>{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
'Authorization': sessionStorage.getItem('user')
|
||||
}
|
||||
};
|
||||
|
||||
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||
return this.processGetCount(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processGetCount(response: Response): Promise<number> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
|
||||
result200 = resultData200 !== undefined ? resultData200 : <any>null;
|
||||
return result200;
|
||||
});
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<number>(<any>null);
|
||||
}
|
||||
|
||||
get(): Promise<ServiceInfo[]> {
|
||||
let url_ = this.baseUrl + "/api/Services";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
let options_ = <RequestInit>{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
'Authorization': sessionStorage.getItem('user')
|
||||
}
|
||||
};
|
||||
|
||||
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||
return this.processGet(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processGet(response: Response): Promise<ServiceInfo[]> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
|
||||
if (Array.isArray(resultData200)) {
|
||||
result200 = [] as any;
|
||||
for (let item of resultData200)
|
||||
result200!.push(ServiceInfo.fromJS(item));
|
||||
}
|
||||
return result200;
|
||||
});
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<ServiceInfo[]>(<any>null);
|
||||
}
|
||||
|
||||
post(request: ServiceRequest): Promise<ServiceRequest> {
|
||||
let url_ = this.baseUrl + "/api/Services";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
const content_ = JSON.stringify(request);
|
||||
|
||||
let options_ = <RequestInit>{
|
||||
body: content_,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
'Authorization': sessionStorage.getItem('user')
|
||||
}
|
||||
};
|
||||
|
||||
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||
return this.processPost(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processPost(response: Response): Promise<ServiceRequest> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 201) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result201: any = null;
|
||||
let resultData201 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
|
||||
result201 = ServiceRequest.fromJS(resultData201);
|
||||
return result201;
|
||||
});
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<ServiceRequest>(<any>null);
|
||||
}
|
||||
|
||||
put(request: ServiceRequest): Promise<void> {
|
||||
let url_ = this.baseUrl + "/api/Services";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
const content_ = JSON.stringify(request);
|
||||
|
||||
let options_ = <RequestInit>{
|
||||
body: content_,
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Authorization': sessionStorage.getItem('user')
|
||||
}
|
||||
};
|
||||
|
||||
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||
return this.processPut(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processPut(response: Response): Promise<void> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return;
|
||||
});
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<void>(<any>null);
|
||||
}
|
||||
|
||||
delete(id: number): Promise<void> {
|
||||
let url_ = this.baseUrl + "/api/Services/{id}";
|
||||
if (id === undefined || id === null)
|
||||
throw new Error("The parameter 'id' must be defined.");
|
||||
url_ = url_.replace("{id}", encodeURIComponent("" + id));
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
let options_ = <RequestInit>{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
'Authorization': sessionStorage.getItem('user')
|
||||
}
|
||||
};
|
||||
|
||||
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||
return this.processDelete(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processDelete(response: Response): Promise<void> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return;
|
||||
});
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<void>(<any>null);
|
||||
}
|
||||
}
|
||||
|
||||
export class ServiceInfo implements IServiceInfo {
|
||||
service?: ServiceRequest | undefined;
|
||||
statusCode!: HttpStatusCode;
|
||||
response?: string | undefined;
|
||||
|
||||
constructor(data?: IServiceInfo) {
|
||||
if (data) {
|
||||
for (var property in data) {
|
||||
if (data.hasOwnProperty(property))
|
||||
(<any>this)[property] = (<any>data)[property];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(_data?: any) {
|
||||
if (_data) {
|
||||
this.service = _data["service"] ? ServiceRequest.fromJS(_data["service"]) : <any>undefined;
|
||||
this.statusCode = _data["statusCode"];
|
||||
this.response = _data["response"];
|
||||
}
|
||||
}
|
||||
|
||||
static fromJS(data: any): ServiceInfo {
|
||||
data = typeof data === 'object' ? data : {};
|
||||
let result = new ServiceInfo();
|
||||
result.init(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
toJSON(data?: any) {
|
||||
data = typeof data === 'object' ? data : {};
|
||||
data["service"] = this.service ? this.service.toJSON() : <any>undefined;
|
||||
data["statusCode"] = this.statusCode;
|
||||
data["response"] = this.response;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IServiceInfo {
|
||||
service?: ServiceRequest | undefined;
|
||||
statusCode: HttpStatusCode;
|
||||
response?: string | undefined;
|
||||
}
|
||||
|
||||
export class ServiceRequest implements IServiceRequest {
|
||||
id!: number;
|
||||
name?: string | undefined;
|
||||
uri?: string | undefined;
|
||||
|
||||
constructor(data?: IServiceRequest) {
|
||||
if (data) {
|
||||
for (var property in data) {
|
||||
if (data.hasOwnProperty(property))
|
||||
(<any>this)[property] = (<any>data)[property];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(_data?: any) {
|
||||
if (_data) {
|
||||
this.id = _data["id"];
|
||||
this.name = _data["name"];
|
||||
this.uri = _data["uri"];
|
||||
}
|
||||
}
|
||||
|
||||
static fromJS(data: any): ServiceRequest {
|
||||
data = typeof data === 'object' ? data : {};
|
||||
let result = new ServiceRequest();
|
||||
result.init(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
toJSON(data?: any) {
|
||||
data = typeof data === 'object' ? data : {};
|
||||
data["id"] = this.id;
|
||||
data["name"] = this.name;
|
||||
data["uri"] = this.uri;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IServiceRequest {
|
||||
id: number;
|
||||
name?: string | undefined;
|
||||
uri?: string | undefined;
|
||||
}
|
||||
|
||||
export enum HttpStatusCode {
|
||||
Continue = "Continue",
|
||||
SwitchingProtocols = "SwitchingProtocols",
|
||||
Processing = "Processing",
|
||||
EarlyHints = "EarlyHints",
|
||||
OK = "OK",
|
||||
Created = "Created",
|
||||
Accepted = "Accepted",
|
||||
NonAuthoritativeInformation = "NonAuthoritativeInformation",
|
||||
NoContent = "NoContent",
|
||||
ResetContent = "ResetContent",
|
||||
PartialContent = "PartialContent",
|
||||
MultiStatus = "MultiStatus",
|
||||
AlreadyReported = "AlreadyReported",
|
||||
IMUsed = "IMUsed",
|
||||
MultipleChoices = "Ambiguous",
|
||||
Ambiguous = "Ambiguous",
|
||||
MovedPermanently = "Moved",
|
||||
Moved = "Moved",
|
||||
Found = "Redirect",
|
||||
Redirect = "Redirect",
|
||||
SeeOther = "RedirectMethod",
|
||||
RedirectMethod = "RedirectMethod",
|
||||
NotModified = "NotModified",
|
||||
UseProxy = "UseProxy",
|
||||
Unused = "Unused",
|
||||
TemporaryRedirect = "TemporaryRedirect",
|
||||
RedirectKeepVerb = "TemporaryRedirect",
|
||||
PermanentRedirect = "PermanentRedirect",
|
||||
BadRequest = "BadRequest",
|
||||
Unauthorized = "Unauthorized",
|
||||
PaymentRequired = "PaymentRequired",
|
||||
Forbidden = "Forbidden",
|
||||
NotFound = "NotFound",
|
||||
MethodNotAllowed = "MethodNotAllowed",
|
||||
NotAcceptable = "NotAcceptable",
|
||||
ProxyAuthenticationRequired = "ProxyAuthenticationRequired",
|
||||
RequestTimeout = "RequestTimeout",
|
||||
Conflict = "Conflict",
|
||||
Gone = "Gone",
|
||||
LengthRequired = "LengthRequired",
|
||||
PreconditionFailed = "PreconditionFailed",
|
||||
RequestEntityTooLarge = "RequestEntityTooLarge",
|
||||
RequestUriTooLong = "RequestUriTooLong",
|
||||
UnsupportedMediaType = "UnsupportedMediaType",
|
||||
RequestedRangeNotSatisfiable = "RequestedRangeNotSatisfiable",
|
||||
ExpectationFailed = "ExpectationFailed",
|
||||
MisdirectedRequest = "MisdirectedRequest",
|
||||
UnprocessableEntity = "UnprocessableEntity",
|
||||
Locked = "Locked",
|
||||
FailedDependency = "FailedDependency",
|
||||
UpgradeRequired = "UpgradeRequired",
|
||||
PreconditionRequired = "PreconditionRequired",
|
||||
TooManyRequests = "TooManyRequests",
|
||||
RequestHeaderFieldsTooLarge = "RequestHeaderFieldsTooLarge",
|
||||
UnavailableForLegalReasons = "UnavailableForLegalReasons",
|
||||
InternalServerError = "InternalServerError",
|
||||
NotImplemented = "NotImplemented",
|
||||
BadGateway = "BadGateway",
|
||||
ServiceUnavailable = "ServiceUnavailable",
|
||||
GatewayTimeout = "GatewayTimeout",
|
||||
HttpVersionNotSupported = "HttpVersionNotSupported",
|
||||
VariantAlsoNegotiates = "VariantAlsoNegotiates",
|
||||
InsufficientStorage = "InsufficientStorage",
|
||||
LoopDetected = "LoopDetected",
|
||||
NotExtended = "NotExtended",
|
||||
NetworkAuthenticationRequired = "NetworkAuthenticationRequired",
|
||||
}
|
||||
|
||||
export class ApiException extends Error {
|
||||
message: string;
|
||||
status: number;
|
||||
response: string;
|
||||
headers: { [key: string]: any; };
|
||||
result: any;
|
||||
|
||||
constructor(message: string, status: number, response: string, headers: { [key: string]: any; }, result: any) {
|
||||
super();
|
||||
|
||||
this.message = message;
|
||||
this.status = status;
|
||||
this.response = response;
|
||||
this.headers = headers;
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
protected isApiException = true;
|
||||
|
||||
static isApiException(obj: any): obj is ApiException {
|
||||
return obj.isApiException === true;
|
||||
}
|
||||
}
|
||||
|
||||
function throwException(message: string, status: number, response: string, headers: { [key: string]: any; }, result?: any): any {
|
||||
if (result !== null && result !== undefined)
|
||||
throw result;
|
||||
else
|
||||
throw new ApiException(message, status, response, headers, null);
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
import { Grid, Typography } from '@material-ui/core';
|
||||
import Accordion from '@material-ui/core/Accordion';
|
||||
import AccordionDetails from '@material-ui/core/AccordionDetails';
|
||||
import AccordionSummary from '@material-ui/core/AccordionSummary';
|
||||
import { blueGrey } from '@material-ui/core/colors';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { Skeleton } from '@material-ui/lab';
|
||||
import { withStyles } from '@material-ui/styles';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
const styles = theme => ({
|
||||
acc_summary: {
|
||||
backgroundColor: blueGrey[50],
|
||||
padding: theme.spacing(2),
|
||||
textAlign: 'center',
|
||||
height: '75px',
|
||||
},
|
||||
acc_details: {
|
||||
backgroundColor: blueGrey[100],
|
||||
},
|
||||
grid_typo: {
|
||||
fontSize: theme.typography.pxToRem(20),
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
},
|
||||
grid_typo_2: {
|
||||
marginLeft: '5px',
|
||||
fontSize: theme.typography.pxToRem(15),
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
});
|
||||
|
||||
class ServiceInfoSkeleton extends Component {
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
|
||||
return (
|
||||
<Accordion>
|
||||
<AccordionSummary className={classes.acc_summary}
|
||||
expandIcon={<ExpandMoreIcon />}>
|
||||
<Grid container
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justify="flex-start"
|
||||
alignItems="center">
|
||||
<Grid item>
|
||||
<Typography className={classes.grid_typo}><Skeleton width={200} /></Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography className={classes.grid_typo_2}><Skeleton width={300} /></Typography>
|
||||
</Grid>
|
||||
<Grid item style={{ marginLeft: 'auto' }}>
|
||||
<Grid container
|
||||
spacing={1}
|
||||
direction="row"
|
||||
justify="flex-start"
|
||||
alignItems="center">
|
||||
<Grid item>
|
||||
<Typography><Skeleton width={150} /></Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails className={classes.acc_details}>
|
||||
<Grid container
|
||||
spacing={1}
|
||||
direction="column"
|
||||
justify="flex-start"
|
||||
alignItems="flex-start">
|
||||
<Grid item>
|
||||
<Typography><Skeleton width={800} /></Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography><Skeleton width={300} /></Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography><Skeleton width={500} /></Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(ServiceInfoSkeleton);
|
@ -0,0 +1,146 @@
|
||||
import { Grid, IconButton, Paper, Typography } from '@material-ui/core';
|
||||
import { blueGrey } from '@material-ui/core/colors';
|
||||
import { AddBox, Refresh } from '@material-ui/icons/';
|
||||
import { withStyles } from '@material-ui/styles';
|
||||
import { HubConnectionBuilder } from '@microsoft/signalr';
|
||||
import React, { Component } from 'react';
|
||||
import AddNewDialog from './AddNewDialog';
|
||||
import ServiceInfoService, { ServiceRequest } from './ServiceInfoService';
|
||||
import ServiceInfoComponent from './ServiceInfoComponent';
|
||||
import ServiceInfoSkeleton from './ServiceInfoSkeleton';
|
||||
|
||||
const styles = theme => ({
|
||||
typo: {
|
||||
fontSize: theme.typography.pxToRem(20),
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
},
|
||||
paper: {
|
||||
backgroundColor: blueGrey[50],
|
||||
height: '60px',
|
||||
}
|
||||
});
|
||||
|
||||
const hub_url = "/hubs/services";
|
||||
const notify_method_name = "NotifyUpdatedAsync";
|
||||
|
||||
class Services extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hubConnection: null,
|
||||
isDialogOpen: false,
|
||||
isLoading: false,
|
||||
service: new ServiceInfoService(),
|
||||
services: [],
|
||||
serviceCount: [1, 2, 3],
|
||||
}
|
||||
|
||||
this.handleDevicesUpdated = this.handleDevicesUpdated.bind(this);
|
||||
this.addDevice = this.addDevice.bind(this);
|
||||
}
|
||||
|
||||
handleDevicesUpdated() {
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
this.state.service.getCount().then(result => {
|
||||
const updatedCount = [];
|
||||
for (var i = 0; i < result; i++) {
|
||||
updatedCount.push(i);
|
||||
}
|
||||
this.setState({ serviceCount: updatedCount });
|
||||
}).catch(ex => {
|
||||
console.log(ex);
|
||||
});
|
||||
|
||||
this.state.service.get().then(result => {
|
||||
const updatedServices = [];
|
||||
for (var s of result) {
|
||||
updatedServices.push(s);
|
||||
}
|
||||
this.setState({ services: updatedServices });
|
||||
}).catch(ex => {
|
||||
console.log(ex);
|
||||
}).finally(() => this.setState({ isLoading: false }));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.handleDevicesUpdated();
|
||||
const newConnection = new HubConnectionBuilder()
|
||||
.withUrl(hub_url)
|
||||
.withAutomaticReconnect()
|
||||
.build();
|
||||
|
||||
this.setState({ hubConnection: newConnection });
|
||||
|
||||
newConnection.start()
|
||||
.then(_ => {
|
||||
console.log('Services hub Connected!');
|
||||
newConnection.on(notify_method_name, () => this.handleDevicesUpdated());
|
||||
}).catch(e => console.log('Services hub Connection failed: ', e));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.state.hubConnection != null) {
|
||||
this.state.hubConnection.off(notify_method_name);
|
||||
console.log('Services hub Disconnected!');
|
||||
}
|
||||
}
|
||||
|
||||
addDevice(name, url) {
|
||||
this.setState({ isDialogOpen: false });
|
||||
let request = new ServiceRequest();
|
||||
request.id = 0;
|
||||
request.name = name;
|
||||
request.uri = url;
|
||||
|
||||
this.state.service.post(request).catch(ex => {
|
||||
console.log(ex);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
|
||||
const ServiceComponents = this.state.services.map((info, index) => (
|
||||
<ServiceInfoComponent key={index} isAdmin={this.props.isAdmin} info={info} service={this.state.service} />
|
||||
));
|
||||
|
||||
const Skeletons = this.state.serviceCount.map((i, index) => (
|
||||
<ServiceInfoSkeleton key={index} />
|
||||
));
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Paper className={classes.paper} square>
|
||||
<Grid container
|
||||
spacing={0}
|
||||
direction="row"
|
||||
justify="center"
|
||||
alignItems="center">
|
||||
<Grid item>
|
||||
<Typography className={classes.typo}>Services</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
{this.props.isAdmin ?
|
||||
<IconButton color="primary" onClick={() => this.setState({ isDialogOpen: true })}>
|
||||
<AddBox fontSize="large" />
|
||||
</IconButton>
|
||||
: null
|
||||
}
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<IconButton color="primary" onClick={this.handleDevicesUpdated}>
|
||||
<Refresh fontSize="large" />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<AddNewDialog open={this.state.isDialogOpen} handleClose={() => this.setState({ isDialogOpen: false })} handleAdd={this.addDevice} />
|
||||
</Grid>
|
||||
</Paper>
|
||||
{this.state.isLoading ? Skeletons : ServiceComponents}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Services);
|
@ -1,14 +1,14 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Box, FormControlLabel, Grid, IconButton, Typography } from '@material-ui/core';
|
||||
import Accordion from '@material-ui/core/Accordion';
|
||||
import { blue, blueGrey, green, orange, red, yellow } from '@material-ui/core/colors';
|
||||
import AccordionSummary from '@material-ui/core/AccordionSummary';
|
||||
import AccordionDetails from '@material-ui/core/AccordionDetails';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { Grid, Typography, Paper, IconButton, Box, FormControlLabel } from '@material-ui/core';
|
||||
import { withStyles } from '@material-ui/styles';
|
||||
import { withRouter } from "react-router";
|
||||
import AccordionSummary from '@material-ui/core/AccordionSummary';
|
||||
import { blueGrey, green, orange, red } from '@material-ui/core/colors';
|
||||
import { Power, PowerOff, Refresh } from '@material-ui/icons/';
|
||||
import DeviceService from '../../common/DeviceService'
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import { withStyles } from '@material-ui/styles';
|
||||
import React, { Component } from 'react';
|
||||
import { withRouter } from "react-router";
|
||||
import DeviceService from '../../common/DeviceService';
|
||||
import DevicesContext from '../../contexts/DevicesContext';
|
||||
|
||||
const styles = theme => ({
|
||||
@ -31,6 +31,7 @@ const styles = theme => ({
|
||||
grid_item: {
|
||||
width: '100%',
|
||||
marginLeft: '5px',
|
||||
marginRight: '30px',
|
||||
},
|
||||
grid_item_typo: {
|
||||
fontSize: theme.typography.pxToRem(15),
|
||||
@ -62,8 +63,8 @@ class DeviceComponent extends Component {
|
||||
if (status == "Online") {
|
||||
return { color: green[600] };
|
||||
} else if (status == "Offline") {
|
||||
return { color: orange[900] };
|
||||
} else /* if (device.status == "unknown") */ {
|
||||
return { color: blueGrey[500] };
|
||||
} else /* if (device.status == "Unknown" || device.status == "Error") */ {
|
||||
return { color: red[800] };
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Box, Paper, Typography, IconButton, Grid } from '@material-ui/core';
|
||||
import { blue, blueGrey, green, orange, red, yellow } from '@material-ui/core/colors';
|
||||
import { Box, Grid, IconButton, Paper, Typography } from '@material-ui/core';
|
||||
import { blueGrey } from '@material-ui/core/colors';
|
||||
import { Power, PowerOff, Refresh } from '@material-ui/icons/';
|
||||
import { withStyles } from '@material-ui/styles';
|
||||
import React from 'react';
|
||||
import DeviceService from '../../common/DeviceService';
|
||||
import DevicesContext from '../../contexts/DevicesContext';
|
||||
import DeviceComponent from './DeviceComponent';
|
||||
import { Power, PowerOff, Refresh } from '@material-ui/icons/';
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
@ -15,7 +15,6 @@ const styles = theme => ({
|
||||
paper: {
|
||||
backgroundColor: blueGrey[50],
|
||||
height: '60px',
|
||||
margin: 'auto',
|
||||
},
|
||||
typo: {
|
||||
fontSize: theme.typography.pxToRem(20),
|
||||
@ -69,15 +68,15 @@ class Devices extends React.Component {
|
||||
<Box className={classes.root}>
|
||||
<Paper className={classes.paper} square>
|
||||
<Grid container
|
||||
spacing={3}
|
||||
spacing={0}
|
||||
direction="row"
|
||||
justify="center"
|
||||
alignItems="center">
|
||||
<Grid item>
|
||||
<Typography className={classes.typo}>All Devices</Typography>
|
||||
</Grid>
|
||||
{this.renderButtons()}
|
||||
<Grid item>
|
||||
{this.renderButtons()}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
|
@ -1,11 +1,9 @@
|
||||
import RadioButtonCheckedIcon from '@material-ui/icons/RadioButtonChecked';
|
||||
import PlayCircleFilledIcon from '@material-ui/icons/PlayCircleFilled';
|
||||
import { shadows } from '@material-ui/system';
|
||||
import { Box, Popover, Typography, Tooltip, Grid } from '@material-ui/core';
|
||||
import { Box, Tooltip } from '@material-ui/core';
|
||||
import { blue, red, yellow } from '@material-ui/core/colors';
|
||||
import React, { Component } from 'react';
|
||||
import { useHistory, withRouter } from 'react-router-dom';
|
||||
import RadioButtonCheckedIcon from '@material-ui/icons/RadioButtonChecked';
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
import React, { Component } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
class DeviceMarker extends Component {
|
||||
constructor(props) {
|
||||
@ -18,9 +16,9 @@ class DeviceMarker extends Component {
|
||||
|
||||
getColor() {
|
||||
const { device } = this.props;
|
||||
if (device.status == "Online") {
|
||||
if (device.status === "Online") {
|
||||
return { color: blue[800] };
|
||||
} else if (device.status == "Offline") {
|
||||
} else if (device.status === "Offline") {
|
||||
return { color: yellow[800] };
|
||||
} else /* if (device.status == "unknown") */ {
|
||||
return { color: red[800] };
|
||||
|
@ -1,14 +1,22 @@
|
||||
/*global google*/
|
||||
import { Box, withStyles } from '@material-ui/core';
|
||||
import GoogleMapReact from 'google-map-react';
|
||||
import React, { Component } from 'react';
|
||||
import DeviceMarker from './DeviceMarker'
|
||||
import C from '../../common/Constants'
|
||||
import C from '../../common/Constants';
|
||||
import DevicesContext from '../../contexts/DevicesContext';
|
||||
import DeviceMarker from './DeviceMarker';
|
||||
|
||||
const lat_offset = 0.000038;
|
||||
const lng_offset = -0.000058;
|
||||
|
||||
export default class MapContainer extends Component {
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
height: '93vh',
|
||||
width: '100%',
|
||||
}
|
||||
});
|
||||
|
||||
class MapContainer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@ -20,11 +28,18 @@ export default class MapContainer extends Component {
|
||||
};
|
||||
|
||||
this.probabilityHandler = this.probabilityHandler.bind(this);
|
||||
this.handlePoint = this.handlePoint.bind(this);
|
||||
}
|
||||
|
||||
static contextType = DevicesContext;
|
||||
|
||||
probabilityHandler(point) {
|
||||
probabilityHandler(points) {
|
||||
for (var point of points) {
|
||||
this.handlePoint(point);
|
||||
}
|
||||
}
|
||||
|
||||
handlePoint(point) {
|
||||
if (point.prob > 0.5) {
|
||||
|
||||
this.setState({
|
||||
@ -57,6 +72,8 @@ export default class MapContainer extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classes} = this.props;
|
||||
|
||||
const heatMapData = {
|
||||
positions: this.state.heatmapPoints,
|
||||
options: {
|
||||
@ -85,7 +102,7 @@ export default class MapContainer extends Component {
|
||||
));
|
||||
|
||||
return (
|
||||
<div style={{ height: '93vh', width: '100%' }}>
|
||||
<Box className={classes.root}>
|
||||
<GoogleMapReact
|
||||
bootstrapURLKeys={{
|
||||
key: ["AIzaSyCZ51VFfxqZ2GkCmVrcNZdUKsM0fuBQUCY"],
|
||||
@ -99,7 +116,9 @@ export default class MapContainer extends Component {
|
||||
defaultCenter={this.state.center}>
|
||||
{Markers}
|
||||
</GoogleMapReact>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(MapContainer);
|
208
Birdmap.API/ClientApp/src/components/logs/LogService.js
Normal file
@ -0,0 +1,208 @@
|
||||
"use strict";
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
//----------------------
|
||||
// <auto-generated>
|
||||
// Generated using the NSwag toolchain v13.8.2.0 (NJsonSchema v10.2.1.0 (Newtonsoft.Json v12.0.0.0)) (http://NSwag.org)
|
||||
// </auto-generated>
|
||||
//----------------------
|
||||
// ReSharper disable InconsistentNaming
|
||||
var __extends = (this && this.__extends) || (function () {
|
||||
var extendStatics = function (d, b) {
|
||||
extendStatics = Object.setPrototypeOf ||
|
||||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
||||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
||||
return extendStatics(d, b);
|
||||
};
|
||||
return function (d, b) {
|
||||
extendStatics(d, b);
|
||||
function __() { this.constructor = d; }
|
||||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ApiException = exports.HttpStatusCode = void 0;
|
||||
var LogService = /** @class */ (function () {
|
||||
function LogService(baseUrl, http) {
|
||||
this.jsonParseReviver = undefined;
|
||||
this.http = http ? http : window;
|
||||
this.baseUrl = baseUrl !== undefined && baseUrl !== null ? baseUrl : "api/logs";
|
||||
}
|
||||
LogService.prototype.getAll = function () {
|
||||
var _this = this;
|
||||
var url_ = this.baseUrl + "/all";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
var options_ = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
'Authorization': sessionStorage.getItem('user')
|
||||
}
|
||||
};
|
||||
return this.http.fetch(url_, options_).then(function (_response) {
|
||||
return _this.processGetAll(_response);
|
||||
});
|
||||
};
|
||||
LogService.prototype.processGetAll = function (response) {
|
||||
var _this = this;
|
||||
var status = response.status;
|
||||
var _headers = {};
|
||||
if (response.headers && response.headers.forEach) {
|
||||
response.headers.forEach(function (v, k) { return _headers[k] = v; });
|
||||
}
|
||||
;
|
||||
if (status === 200) {
|
||||
return response.text().then(function (_responseText) {
|
||||
var result200 = null;
|
||||
var resultData200 = _responseText === "" ? null : JSON.parse(_responseText, _this.jsonParseReviver);
|
||||
if (Array.isArray(resultData200)) {
|
||||
result200 = [];
|
||||
for (var _i = 0, resultData200_1 = resultData200; _i < resultData200_1.length; _i++) {
|
||||
var item = resultData200_1[_i];
|
||||
result200.push(item);
|
||||
}
|
||||
}
|
||||
return result200;
|
||||
});
|
||||
}
|
||||
else if (status !== 200 && status !== 204) {
|
||||
return response.text().then(function (_responseText) {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
};
|
||||
LogService.prototype.getFiles = function (filenames) {
|
||||
var _this = this;
|
||||
var url_ = this.baseUrl + "?";
|
||||
if (filenames !== undefined && filenames !== null)
|
||||
filenames && filenames.forEach(function (item) { url_ += "filenames=" + encodeURIComponent("" + item) + "&"; });
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
var options_ = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Accept": "application/octet-stream",
|
||||
'Authorization': sessionStorage.getItem('user')
|
||||
}
|
||||
};
|
||||
return this.http.fetch(url_, options_).then(function (_response) {
|
||||
return _this.processGetFiles(_response);
|
||||
});
|
||||
};
|
||||
LogService.prototype.processGetFiles = function (response) {
|
||||
var status = response.status;
|
||||
var _headers = {};
|
||||
if (response.headers && response.headers.forEach) {
|
||||
response.headers.forEach(function (v, k) { return _headers[k] = v; });
|
||||
}
|
||||
;
|
||||
if (status === 200 || status === 206) {
|
||||
var contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined;
|
||||
var fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined;
|
||||
var fileName_1 = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined;
|
||||
return response.blob().then(function (blob) { return { fileName: fileName_1, data: blob, status: status, headers: _headers }; });
|
||||
}
|
||||
else if (status !== 200 && status !== 204) {
|
||||
return response.text().then(function (_responseText) {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
};
|
||||
return LogService;
|
||||
}());
|
||||
exports.default = LogService;
|
||||
var HttpStatusCode;
|
||||
(function (HttpStatusCode) {
|
||||
HttpStatusCode["Continue"] = "Continue";
|
||||
HttpStatusCode["SwitchingProtocols"] = "SwitchingProtocols";
|
||||
HttpStatusCode["Processing"] = "Processing";
|
||||
HttpStatusCode["EarlyHints"] = "EarlyHints";
|
||||
HttpStatusCode["OK"] = "OK";
|
||||
HttpStatusCode["Created"] = "Created";
|
||||
HttpStatusCode["Accepted"] = "Accepted";
|
||||
HttpStatusCode["NonAuthoritativeInformation"] = "NonAuthoritativeInformation";
|
||||
HttpStatusCode["NoContent"] = "NoContent";
|
||||
HttpStatusCode["ResetContent"] = "ResetContent";
|
||||
HttpStatusCode["PartialContent"] = "PartialContent";
|
||||
HttpStatusCode["MultiStatus"] = "MultiStatus";
|
||||
HttpStatusCode["AlreadyReported"] = "AlreadyReported";
|
||||
HttpStatusCode["IMUsed"] = "IMUsed";
|
||||
HttpStatusCode["MultipleChoices"] = "Ambiguous";
|
||||
HttpStatusCode["Ambiguous"] = "Ambiguous";
|
||||
HttpStatusCode["MovedPermanently"] = "Moved";
|
||||
HttpStatusCode["Moved"] = "Moved";
|
||||
HttpStatusCode["Found"] = "Redirect";
|
||||
HttpStatusCode["Redirect"] = "Redirect";
|
||||
HttpStatusCode["SeeOther"] = "RedirectMethod";
|
||||
HttpStatusCode["RedirectMethod"] = "RedirectMethod";
|
||||
HttpStatusCode["NotModified"] = "NotModified";
|
||||
HttpStatusCode["UseProxy"] = "UseProxy";
|
||||
HttpStatusCode["Unused"] = "Unused";
|
||||
HttpStatusCode["TemporaryRedirect"] = "TemporaryRedirect";
|
||||
HttpStatusCode["RedirectKeepVerb"] = "TemporaryRedirect";
|
||||
HttpStatusCode["PermanentRedirect"] = "PermanentRedirect";
|
||||
HttpStatusCode["BadRequest"] = "BadRequest";
|
||||
HttpStatusCode["Unauthorized"] = "Unauthorized";
|
||||
HttpStatusCode["PaymentRequired"] = "PaymentRequired";
|
||||
HttpStatusCode["Forbidden"] = "Forbidden";
|
||||
HttpStatusCode["NotFound"] = "NotFound";
|
||||
HttpStatusCode["MethodNotAllowed"] = "MethodNotAllowed";
|
||||
HttpStatusCode["NotAcceptable"] = "NotAcceptable";
|
||||
HttpStatusCode["ProxyAuthenticationRequired"] = "ProxyAuthenticationRequired";
|
||||
HttpStatusCode["RequestTimeout"] = "RequestTimeout";
|
||||
HttpStatusCode["Conflict"] = "Conflict";
|
||||
HttpStatusCode["Gone"] = "Gone";
|
||||
HttpStatusCode["LengthRequired"] = "LengthRequired";
|
||||
HttpStatusCode["PreconditionFailed"] = "PreconditionFailed";
|
||||
HttpStatusCode["RequestEntityTooLarge"] = "RequestEntityTooLarge";
|
||||
HttpStatusCode["RequestUriTooLong"] = "RequestUriTooLong";
|
||||
HttpStatusCode["UnsupportedMediaType"] = "UnsupportedMediaType";
|
||||
HttpStatusCode["RequestedRangeNotSatisfiable"] = "RequestedRangeNotSatisfiable";
|
||||
HttpStatusCode["ExpectationFailed"] = "ExpectationFailed";
|
||||
HttpStatusCode["MisdirectedRequest"] = "MisdirectedRequest";
|
||||
HttpStatusCode["UnprocessableEntity"] = "UnprocessableEntity";
|
||||
HttpStatusCode["Locked"] = "Locked";
|
||||
HttpStatusCode["FailedDependency"] = "FailedDependency";
|
||||
HttpStatusCode["UpgradeRequired"] = "UpgradeRequired";
|
||||
HttpStatusCode["PreconditionRequired"] = "PreconditionRequired";
|
||||
HttpStatusCode["TooManyRequests"] = "TooManyRequests";
|
||||
HttpStatusCode["RequestHeaderFieldsTooLarge"] = "RequestHeaderFieldsTooLarge";
|
||||
HttpStatusCode["UnavailableForLegalReasons"] = "UnavailableForLegalReasons";
|
||||
HttpStatusCode["InternalServerError"] = "InternalServerError";
|
||||
HttpStatusCode["NotImplemented"] = "NotImplemented";
|
||||
HttpStatusCode["BadGateway"] = "BadGateway";
|
||||
HttpStatusCode["ServiceUnavailable"] = "ServiceUnavailable";
|
||||
HttpStatusCode["GatewayTimeout"] = "GatewayTimeout";
|
||||
HttpStatusCode["HttpVersionNotSupported"] = "HttpVersionNotSupported";
|
||||
HttpStatusCode["VariantAlsoNegotiates"] = "VariantAlsoNegotiates";
|
||||
HttpStatusCode["InsufficientStorage"] = "InsufficientStorage";
|
||||
HttpStatusCode["LoopDetected"] = "LoopDetected";
|
||||
HttpStatusCode["NotExtended"] = "NotExtended";
|
||||
HttpStatusCode["NetworkAuthenticationRequired"] = "NetworkAuthenticationRequired";
|
||||
})(HttpStatusCode = exports.HttpStatusCode || (exports.HttpStatusCode = {}));
|
||||
var ApiException = /** @class */ (function (_super) {
|
||||
__extends(ApiException, _super);
|
||||
function ApiException(message, status, response, headers, result) {
|
||||
var _this = _super.call(this) || this;
|
||||
_this.isApiException = true;
|
||||
_this.message = message;
|
||||
_this.status = status;
|
||||
_this.response = response;
|
||||
_this.headers = headers;
|
||||
_this.result = result;
|
||||
return _this;
|
||||
}
|
||||
ApiException.isApiException = function (obj) {
|
||||
return obj.isApiException === true;
|
||||
};
|
||||
return ApiException;
|
||||
}(Error));
|
||||
exports.ApiException = ApiException;
|
||||
function throwException(message, status, response, headers, result) {
|
||||
if (result !== null && result !== undefined)
|
||||
throw result;
|
||||
else
|
||||
throw new ApiException(message, status, response, headers, null);
|
||||
}
|
||||
//# sourceMappingURL=LogService.js.map
|
201
Birdmap.API/ClientApp/src/components/logs/LogService.ts
Normal file
@ -0,0 +1,201 @@
|
||||
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
//----------------------
|
||||
// <auto-generated>
|
||||
// Generated using the NSwag toolchain v13.8.2.0 (NJsonSchema v10.2.1.0 (Newtonsoft.Json v12.0.0.0)) (http://NSwag.org)
|
||||
// </auto-generated>
|
||||
//----------------------
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
export default class LogService {
|
||||
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
|
||||
private baseUrl: string;
|
||||
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
|
||||
|
||||
constructor(baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> }) {
|
||||
this.http = http ? http : <any>window;
|
||||
this.baseUrl = baseUrl !== undefined && baseUrl !== null ? baseUrl : "api/logs";
|
||||
}
|
||||
|
||||
getAll(): Promise<string[]> {
|
||||
let url_ = this.baseUrl + "/all";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
let options_ = <RequestInit>{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
'Authorization': sessionStorage.getItem('user')
|
||||
}
|
||||
};
|
||||
|
||||
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||
return this.processGetAll(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processGetAll(response: Response): Promise<string[]> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
|
||||
if (Array.isArray(resultData200)) {
|
||||
result200 = [] as any;
|
||||
for (let item of resultData200)
|
||||
result200!.push(item);
|
||||
}
|
||||
return result200;
|
||||
});
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<string[]>(<any>null);
|
||||
}
|
||||
|
||||
getFiles(filenames: string[] | null | undefined): Promise<FileResponse | null> {
|
||||
let url_ = this.baseUrl + "?";
|
||||
if (filenames !== undefined && filenames !== null)
|
||||
filenames && filenames.forEach(item => { url_ += "filenames=" + encodeURIComponent("" + item) + "&"; });
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
let options_ = <RequestInit>{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Accept": "application/octet-stream",
|
||||
'Authorization': sessionStorage.getItem('user')
|
||||
}
|
||||
};
|
||||
|
||||
return this.http.fetch(url_, options_).then((_response: Response) => {
|
||||
return this.processGetFiles(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processGetFiles(response: Response): Promise<FileResponse | null> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200 || status === 206) {
|
||||
const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined;
|
||||
const fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined;
|
||||
const fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined;
|
||||
return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; });
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<FileResponse | null>(<any>null);
|
||||
}
|
||||
}
|
||||
|
||||
export interface FileResponse {
|
||||
data: Blob;
|
||||
status: number;
|
||||
fileName?: string;
|
||||
headers?: { [name: string]: any };
|
||||
}
|
||||
|
||||
export enum HttpStatusCode {
|
||||
Continue = "Continue",
|
||||
SwitchingProtocols = "SwitchingProtocols",
|
||||
Processing = "Processing",
|
||||
EarlyHints = "EarlyHints",
|
||||
OK = "OK",
|
||||
Created = "Created",
|
||||
Accepted = "Accepted",
|
||||
NonAuthoritativeInformation = "NonAuthoritativeInformation",
|
||||
NoContent = "NoContent",
|
||||
ResetContent = "ResetContent",
|
||||
PartialContent = "PartialContent",
|
||||
MultiStatus = "MultiStatus",
|
||||
AlreadyReported = "AlreadyReported",
|
||||
IMUsed = "IMUsed",
|
||||
MultipleChoices = "Ambiguous",
|
||||
Ambiguous = "Ambiguous",
|
||||
MovedPermanently = "Moved",
|
||||
Moved = "Moved",
|
||||
Found = "Redirect",
|
||||
Redirect = "Redirect",
|
||||
SeeOther = "RedirectMethod",
|
||||
RedirectMethod = "RedirectMethod",
|
||||
NotModified = "NotModified",
|
||||
UseProxy = "UseProxy",
|
||||
Unused = "Unused",
|
||||
TemporaryRedirect = "TemporaryRedirect",
|
||||
RedirectKeepVerb = "TemporaryRedirect",
|
||||
PermanentRedirect = "PermanentRedirect",
|
||||
BadRequest = "BadRequest",
|
||||
Unauthorized = "Unauthorized",
|
||||
PaymentRequired = "PaymentRequired",
|
||||
Forbidden = "Forbidden",
|
||||
NotFound = "NotFound",
|
||||
MethodNotAllowed = "MethodNotAllowed",
|
||||
NotAcceptable = "NotAcceptable",
|
||||
ProxyAuthenticationRequired = "ProxyAuthenticationRequired",
|
||||
RequestTimeout = "RequestTimeout",
|
||||
Conflict = "Conflict",
|
||||
Gone = "Gone",
|
||||
LengthRequired = "LengthRequired",
|
||||
PreconditionFailed = "PreconditionFailed",
|
||||
RequestEntityTooLarge = "RequestEntityTooLarge",
|
||||
RequestUriTooLong = "RequestUriTooLong",
|
||||
UnsupportedMediaType = "UnsupportedMediaType",
|
||||
RequestedRangeNotSatisfiable = "RequestedRangeNotSatisfiable",
|
||||
ExpectationFailed = "ExpectationFailed",
|
||||
MisdirectedRequest = "MisdirectedRequest",
|
||||
UnprocessableEntity = "UnprocessableEntity",
|
||||
Locked = "Locked",
|
||||
FailedDependency = "FailedDependency",
|
||||
UpgradeRequired = "UpgradeRequired",
|
||||
PreconditionRequired = "PreconditionRequired",
|
||||
TooManyRequests = "TooManyRequests",
|
||||
RequestHeaderFieldsTooLarge = "RequestHeaderFieldsTooLarge",
|
||||
UnavailableForLegalReasons = "UnavailableForLegalReasons",
|
||||
InternalServerError = "InternalServerError",
|
||||
NotImplemented = "NotImplemented",
|
||||
BadGateway = "BadGateway",
|
||||
ServiceUnavailable = "ServiceUnavailable",
|
||||
GatewayTimeout = "GatewayTimeout",
|
||||
HttpVersionNotSupported = "HttpVersionNotSupported",
|
||||
VariantAlsoNegotiates = "VariantAlsoNegotiates",
|
||||
InsufficientStorage = "InsufficientStorage",
|
||||
LoopDetected = "LoopDetected",
|
||||
NotExtended = "NotExtended",
|
||||
NetworkAuthenticationRequired = "NetworkAuthenticationRequired",
|
||||
}
|
||||
|
||||
export class ApiException extends Error {
|
||||
message: string;
|
||||
status: number;
|
||||
response: string;
|
||||
headers: { [key: string]: any; };
|
||||
result: any;
|
||||
|
||||
constructor(message: string, status: number, response: string, headers: { [key: string]: any; }, result: any) {
|
||||
super();
|
||||
|
||||
this.message = message;
|
||||
this.status = status;
|
||||
this.response = response;
|
||||
this.headers = headers;
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
protected isApiException = true;
|
||||
|
||||
static isApiException(obj: any): obj is ApiException {
|
||||
return obj.isApiException === true;
|
||||
}
|
||||
}
|
||||
|
||||
function throwException(message: string, status: number, response: string, headers: { [key: string]: any; }, result?: any): any {
|
||||
if (result !== null && result !== undefined)
|
||||
throw result;
|
||||
else
|
||||
throw new ApiException(message, status, response, headers, null);
|
||||
}
|
128
Birdmap.API/ClientApp/src/components/logs/Logs.jsx
Normal file
@ -0,0 +1,128 @@
|
||||
import { Box, Button, Checkbox, List, ListItem, ListItemIcon, ListItemText, Paper, withStyles } from '@material-ui/core';
|
||||
import { blueGrey } from '@material-ui/core/colors';
|
||||
import React, { Component } from 'react';
|
||||
import LogService from './LogService';
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
padding: '64px',
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
},
|
||||
paper: {
|
||||
backgroundColor: blueGrey[50],
|
||||
},
|
||||
});
|
||||
|
||||
class Logs extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
service: null,
|
||||
files: [],
|
||||
checked: [],
|
||||
selectAllChecked: false,
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
var service = new LogService();
|
||||
this.setState({service: service});
|
||||
|
||||
service.getAll().then(result => {
|
||||
this.setState({files: result});
|
||||
}).catch(ex => console.log(ex));
|
||||
}
|
||||
|
||||
handleToggle = (value) => {
|
||||
const currentIndex = this.state.checked.indexOf(value);
|
||||
const newChecked = [...this.state.checked];
|
||||
|
||||
if (currentIndex === -1) {
|
||||
newChecked.push(value);
|
||||
} else {
|
||||
newChecked.splice(currentIndex, 1);
|
||||
}
|
||||
|
||||
this.setState({checked: newChecked});
|
||||
}
|
||||
|
||||
handleSelectAllToggle = () => {
|
||||
this.setState({selectAllChecked: !this.state.selectAllChecked});
|
||||
if (this.state.selectAllChecked) {
|
||||
this.setState({checked: []});
|
||||
} else {
|
||||
const newChecked = [...this.state.files];
|
||||
this.setState({checked: newChecked});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onDownload = () => {
|
||||
this.state.service.getFiles(this.state.checked)
|
||||
.then(result => {
|
||||
const filename = `Logs-${new Date().toISOString()}.zip`;
|
||||
const textUrl = URL.createObjectURL(result.data);
|
||||
const element = document.createElement('a');
|
||||
element.setAttribute('href', textUrl);
|
||||
element.setAttribute('download', filename);
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
this.setState({checked: []});
|
||||
this.setState({selectAllChecked: false});
|
||||
})
|
||||
.catch(ex => console.log(ex));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
|
||||
const Files = this.state.files.map((value) => {
|
||||
const labelId = `checkbox-list-label-${value}`;
|
||||
|
||||
return (
|
||||
<ListItem key={value} role={undefined} dense button onClick={() => this.handleToggle(value)}>
|
||||
<ListItemIcon>
|
||||
<Checkbox
|
||||
edge="start"
|
||||
checked={this.state.checked.indexOf(value) !== -1}
|
||||
tabIndex={-1}
|
||||
disableRipple
|
||||
inputProps={{ 'aria-labelledby': labelId }}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText id={labelId} primary={`${value}`} />
|
||||
</ListItem>
|
||||
);
|
||||
})
|
||||
|
||||
return (
|
||||
<Box className={classes.root}>
|
||||
<Paper className={classes.paper}>
|
||||
<List className={classes.paper}>
|
||||
<ListItem key="Select-all" role={undefined} dense button onClick={this.handleSelectAllToggle}>
|
||||
<ListItemIcon>
|
||||
<Checkbox
|
||||
edge="start"
|
||||
checked={this.state.selectAllChecked}
|
||||
tabIndex={-1}
|
||||
disableRipple
|
||||
inputProps={{ 'aria-labelledby': "Select-all" }}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<ListItemText id="checkbox-list-label-Select-all" primary={(this.state.selectAllChecked ? "Uns" : "S") + "elect all"} />
|
||||
</ListItem>
|
||||
{Files}
|
||||
</List>
|
||||
<Button onClick={this.onDownload}>
|
||||
Download
|
||||
</Button>
|
||||
</Paper>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Logs);
|
@ -60,6 +60,7 @@ export default class DevicesContextProvider extends Component {
|
||||
}
|
||||
service.getall().then(result => {
|
||||
this.setState({ devices: result });
|
||||
this.invokeHandlers(C.update_all_method_name, null);
|
||||
}).catch(ex => {
|
||||
console.log(ex);
|
||||
});
|
||||
@ -96,26 +97,29 @@ export default class DevicesContextProvider extends Component {
|
||||
|
||||
newConnection.start()
|
||||
.then(_ => {
|
||||
console.log('Hub Connected!');
|
||||
console.log('Devices hub Connected!');
|
||||
|
||||
newConnection.on(C.probability_method_name, (id, date, prob) => {
|
||||
newConnection.on(C.probability_method_name, (messages) => {
|
||||
//console.log(method_name + " recieved: [id: " + id + ", date: " + date + ", prob: " + prob + "]");
|
||||
var device = this.state.devices.filter(function (x) { return x.id === id })[0]
|
||||
var newPoint = { lat: device.coordinates.latitude, lng: device.coordinates.longitude, prob: prob, date: date };
|
||||
const newPoints = [];
|
||||
for (var message of messages) {
|
||||
var device = this.state.devices.filter(function (x) { return x.id === message.deviceId })[0]
|
||||
var newPoint = { deviceId: device.id, lat: device.coordinates.latitude, lng: device.coordinates.longitude, prob: message.probability, date: new Date(message.date) };
|
||||
newPoints.push(newPoint);
|
||||
}
|
||||
this.setState({
|
||||
heatmapPoints: [...this.state.heatmapPoints, newPoint]
|
||||
heatmapPoints: this.state.heatmapPoints.concat(newPoints)
|
||||
});
|
||||
|
||||
this.invokeHandlers(C.probability_method_name, newPoint);
|
||||
this.invokeHandlers(C.probability_method_name, newPoints);
|
||||
});
|
||||
|
||||
newConnection.on(C.update_all_method_name, () => {
|
||||
this.updateAllDevicesInternal(service);
|
||||
this.invokeHandlers(C.update_all_method_name, null);
|
||||
});
|
||||
|
||||
newConnection.on(C.update_method_name, (id) => this.updateDeviceInternal(id, service));
|
||||
}).catch(e => console.log('Hub Connection failed: ', e));
|
||||
}).catch(e => console.log('Devices hub Connection failed: ', e));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -123,7 +127,7 @@ export default class DevicesContextProvider extends Component {
|
||||
this.state.hubConnection.off(C.probability_method_name);
|
||||
this.state.hubConnection.off(C.update_all_method_name);
|
||||
this.state.hubConnection.off(C.update_method_name);
|
||||
console.log('Hub Disconnected!');
|
||||
console.log('Devices hub Disconnected!');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,7 @@ namespace Birdmap.Controllers
|
||||
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||
var tokenString = tokenHandler.WriteToken(token);
|
||||
|
||||
var response = _mapper.Map<AuthenticateResponse>(user);
|
||||
AuthenticateResponse response = _mapper.Map<AuthenticateResponse>(user);
|
||||
response.AccessToken = tokenString;
|
||||
response.TokenType = "Bearer";
|
||||
response.ExpiresIn = expiresInSeconds;
|
||||
|
@ -1,15 +1,14 @@
|
||||
using Birdmap.BLL.Interfaces;
|
||||
using Birdmap.BLL.Services.CommunicationServices.Hubs;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Birdmap.API.Services.Hubs;
|
||||
using Birdmap.API.Services;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Birdmap.API.Controllers
|
||||
{
|
||||
|
66
Birdmap.API/Controllers/LogsController.cs
Normal file
@ -0,0 +1,66 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Birdmap.API.Controllers
|
||||
{
|
||||
[Authorize(Roles = "Admin")]
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class LogsController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<LogsController> _logger;
|
||||
private readonly string _logFolderPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Logs");
|
||||
|
||||
public LogsController(ILogger<LogsController> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet("all")]
|
||||
public ActionResult<List<string>> GetAll()
|
||||
{
|
||||
_logger.LogInformation($"Getting all log filenames from folder: '{_logFolderPath}'...");
|
||||
|
||||
return Directory.EnumerateFiles(_logFolderPath, "*.log")
|
||||
.Select(f => Path.GetFileName(f))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetFiles([FromQuery] params string[] filenames)
|
||||
{
|
||||
if (!filenames.Any())
|
||||
return null;
|
||||
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
var zipStream = new MemoryStream();
|
||||
|
||||
using (var zip = new ZipArchive(zipStream, ZipArchiveMode.Create, true))
|
||||
{
|
||||
foreach (var file in Directory.GetFiles(_logFolderPath, "*.log"))
|
||||
{
|
||||
var filename = Path.GetFileName(file);
|
||||
|
||||
if (filenames.Contains(filename))
|
||||
{
|
||||
zip.CreateEntryFromFile(file, filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
zipStream.Position = 0;
|
||||
|
||||
return File(zipStream, "application/octet-stream");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,17 @@
|
||||
using AutoMapper;
|
||||
using Birdmap.API.DTOs;
|
||||
using Birdmap.BLL.Interfaces;
|
||||
using Birdmap.BLL.Services.CommunicationServices.Hubs;
|
||||
using Birdmap.DAL.Entities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@ -21,40 +24,71 @@ namespace Birdmap.API.Controllers
|
||||
{
|
||||
private readonly IServiceService _service;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly ICommunicationService _communicationService;
|
||||
private readonly IHubContext<ServicesHub, IServicesHubClient> _hubContext;
|
||||
private readonly ILogger<ServicesController> _logger;
|
||||
|
||||
public ServicesController(IServiceService service, IMapper mapper, ILogger<ServicesController> logger)
|
||||
public ServicesController(IServiceService service, IMapper mapper, ICommunicationServiceProvider communicationServiceProvider,
|
||||
IHubContext<ServicesHub, IServicesHubClient> hubContext, ILogger<ServicesController> logger)
|
||||
{
|
||||
_service = service;
|
||||
_mapper = mapper;
|
||||
_communicationService = communicationServiceProvider.Service;
|
||||
_hubContext = hubContext;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet("count"), ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<int>> GetCountAsync()
|
||||
{
|
||||
_logger.LogInformation($"Getting service count from db...");
|
||||
|
||||
return await _service.GetServiceCountAsync() + 1;
|
||||
}
|
||||
|
||||
[HttpGet, ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<List<ServiceInfo>>> GetAsync()
|
||||
{
|
||||
_logger.LogInformation($"Getting all services from db...");
|
||||
var serviceInfos = (await _service.GetAllServicesAsync())
|
||||
.Select(s => new ServiceInfo { Service = _mapper.Map<ServiceRequest>(s) });
|
||||
.Select(s => new ServiceInfo { Service = _mapper.Map<ServiceRequest>(s) }).ToList();
|
||||
|
||||
var client = new HttpClient();
|
||||
var tasks = new List<Task>();
|
||||
foreach (var si in serviceInfos)
|
||||
{
|
||||
try
|
||||
tasks.Add(Task.Run(async () =>
|
||||
{
|
||||
_logger.LogInformation($"Sending a request to service [{si.Service.Name}] with url [{si.Service.Uri}]...");
|
||||
var response = await client.GetAsync(si.Service.Uri);
|
||||
si.StatusCode = response.StatusCode;
|
||||
si.Response = await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning($"Requesting service [{si.Service.Name}] faulted.");
|
||||
si.StatusCode = System.Net.HttpStatusCode.ServiceUnavailable;
|
||||
si.Response = ex.ToString();
|
||||
}
|
||||
var client = new HttpClient();
|
||||
try
|
||||
{
|
||||
_logger.LogInformation($"Sending a request to service [{si.Service.Name}] with url [{si.Service.Uri}]...");
|
||||
var response = await client.GetAsync(si.Service.Uri);
|
||||
si.StatusCode = response.StatusCode;
|
||||
si.Response = await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning($"Requesting service [{si.Service.Name}] faulted.");
|
||||
si.StatusCode = HttpStatusCode.ServiceUnavailable;
|
||||
si.Response = ex.ToString();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
serviceInfos.Add(new()
|
||||
{
|
||||
Service = new()
|
||||
{
|
||||
Id = 0,
|
||||
Name = "Message Queue Service",
|
||||
Uri = "localhost",
|
||||
},
|
||||
Response = $"IsConnected: {_communicationService.IsConnected}",
|
||||
StatusCode = _communicationService.IsConnected ? HttpStatusCode.OK : HttpStatusCode.ServiceUnavailable,
|
||||
});
|
||||
|
||||
return serviceInfos.ToList();
|
||||
}
|
||||
|
||||
@ -67,6 +101,7 @@ namespace Birdmap.API.Controllers
|
||||
_mapper.Map<Service>(request));
|
||||
|
||||
_logger.LogInformation($"Created service [{created.Id}].");
|
||||
await _hubContext.Clients.All.NotifyUpdatedAsync();
|
||||
|
||||
return CreatedAtAction(
|
||||
nameof(GetAsync),
|
||||
@ -82,6 +117,7 @@ namespace Birdmap.API.Controllers
|
||||
service.IsFromConfig = false;
|
||||
|
||||
await _service.UpdateServiceAsync(service);
|
||||
await _hubContext.Clients.All.NotifyUpdatedAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
@ -93,6 +129,7 @@ namespace Birdmap.API.Controllers
|
||||
_logger.LogInformation($"Deleting service [{id}]...");
|
||||
|
||||
await _service.DeleteServiceAsync(id);
|
||||
await _hubContext.Clients.All.NotifyUpdatedAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
@ -1,33 +0,0 @@
|
||||
using Birdmap.API.Options;
|
||||
using Birdmap.API.Services.Mqtt;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System;
|
||||
|
||||
namespace Birdmap.API.Extensions
|
||||
{
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddMqttClientServiceWithConfig(this IServiceCollection services, Action<AspCoreMqttClientOptions> configureOptions)
|
||||
{
|
||||
services.AddSingleton(serviceProvider =>
|
||||
{
|
||||
var optionBuilder = new AspCoreMqttClientOptions(serviceProvider);
|
||||
configureOptions(optionBuilder);
|
||||
return optionBuilder.Build();
|
||||
});
|
||||
services.AddSingleton<MqttClientService>();
|
||||
services.AddSingleton<IHostedService>(serviceProvider =>
|
||||
{
|
||||
return serviceProvider.GetService<MqttClientService>();
|
||||
});
|
||||
services.AddSingleton(serviceProvider =>
|
||||
{
|
||||
var mqttClientService = serviceProvider.GetService<MqttClientService>();
|
||||
var mqttClientServiceProvider = new MqttClientServiceProvider(mqttClientService);
|
||||
return mqttClientServiceProvider;
|
||||
});
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
using Birdmap.DAL;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -39,6 +40,10 @@ namespace Birdmap.API
|
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureAppConfiguration((hostingContext, config) =>
|
||||
{
|
||||
config.AddEnvironmentVariables(prefix: "Birdmap_");
|
||||
})
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
@ -53,8 +58,8 @@ namespace Birdmap.API
|
||||
private static void SeedDatabase(IHost host)
|
||||
{
|
||||
using var scope = host.Services.CreateScope();
|
||||
var dbInitializer = scope.ServiceProvider.GetRequiredService<DbInitializer>();
|
||||
|
||||
var dbInitializer = scope.ServiceProvider.GetRequiredService<DbInitializer>();
|
||||
dbInitializer.Initialize();
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,11 @@
|
||||
{
|
||||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iis": {
|
||||
"applicationUrl": "http://localhost/Birdmap.API",
|
||||
"sslPort": 0
|
||||
},
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:63288",
|
||||
"sslPort": 44331
|
||||
@ -12,16 +16,24 @@
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"Birdmap_LocalDbConnectionString": "Data Source=DESKTOP-3600;Initial Catalog=birdmap2;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"Birdmap": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000"
|
||||
},
|
||||
"Docker": {
|
||||
"commandName": "Docker",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
|
||||
"publishAllPorts": true,
|
||||
"useSSL": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Birdmap.API.Services.Hubs
|
||||
{
|
||||
public class DevicesHub : Hub<IDevicesHubClient>
|
||||
{
|
||||
private readonly ILogger<DevicesHub> _logger;
|
||||
|
||||
public DevicesHub(ILogger<DevicesHub> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override Task OnConnectedAsync()
|
||||
{
|
||||
_logger.LogInformation("Hub Client connected.");
|
||||
|
||||
return base.OnConnectedAsync();
|
||||
}
|
||||
|
||||
public override Task OnDisconnectedAsync(Exception exception)
|
||||
{
|
||||
_logger.LogInformation("Hub Client disconnected.");
|
||||
|
||||
return base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
|
||||
public Task SendProbabilityAsync(Guid deviceId, DateTime date, double probability)
|
||||
{
|
||||
return Clients.All.NotifyDeviceAsync(deviceId, date, probability);
|
||||
}
|
||||
|
||||
public Task SendDeviceUpdateAsync(Guid deviceId)
|
||||
{
|
||||
return Clients.All.NotifyDeviceUpdatedAsync(deviceId);
|
||||
}
|
||||
|
||||
public Task SendAllUpdatedAsync()
|
||||
{
|
||||
return Clients.All.NotifyAllUpdatedAsync();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Birdmap.API.Services
|
||||
{
|
||||
public interface IDevicesHubClient
|
||||
{
|
||||
Task NotifyDeviceAsync(Guid deviceId, DateTime date, double probability);
|
||||
Task NotifyDeviceUpdatedAsync(Guid deviceId);
|
||||
Task NotifyAllUpdatedAsync();
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
namespace Birdmap.API.Services.Mqtt
|
||||
{
|
||||
public class MqttClientServiceProvider
|
||||
{
|
||||
public IMqttClientService MqttClientService { get; }
|
||||
|
||||
public MqttClientServiceProvider(IMqttClientService mqttClientService)
|
||||
{
|
||||
MqttClientService = mqttClientService;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
using AutoMapper;
|
||||
using Birdmap.API.Extensions;
|
||||
using Birdmap.API.Middlewares;
|
||||
using Birdmap.API.Services.Hubs;
|
||||
using Birdmap.BLL;
|
||||
using Birdmap.BLL.Services.CommunicationServices.Hubs;
|
||||
using Birdmap.DAL;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
@ -13,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using NSwag.Generation.Processors.Security;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Birdmap.API
|
||||
@ -42,8 +42,6 @@ namespace Birdmap.API
|
||||
|
||||
services.AddAutoMapper(typeof(Startup));
|
||||
|
||||
services.AddSignalR();
|
||||
|
||||
var key = Encoding.ASCII.GetBytes(Configuration["Secret"]);
|
||||
services.AddAuthentication(opt =>
|
||||
{
|
||||
@ -64,33 +62,6 @@ namespace Birdmap.API
|
||||
};
|
||||
});
|
||||
|
||||
services.AddMqttClientServiceWithConfig(opt =>
|
||||
{
|
||||
var mqtt = Configuration.GetSection("Mqtt");
|
||||
|
||||
var mqttClient = mqtt.GetSection("ClientSettings");
|
||||
var clientSettings = new
|
||||
{
|
||||
Id = mqttClient.GetValue<string>("Id"),
|
||||
Username = mqttClient.GetValue<string>("Username"),
|
||||
Password = mqttClient.GetValue<string>("Password"),
|
||||
Topic = mqttClient.GetValue<string>("Topic"),
|
||||
};
|
||||
|
||||
var mqttBrokerHost = mqtt.GetSection("BrokerHostSettings");
|
||||
var brokerHostSettings = new
|
||||
{
|
||||
Host = mqttBrokerHost.GetValue<string>("Host"),
|
||||
Port = mqttBrokerHost.GetValue<int>("Port"),
|
||||
};
|
||||
|
||||
opt
|
||||
.WithTopic(clientSettings.Topic)
|
||||
.WithCredentials(clientSettings.Username, clientSettings.Password)
|
||||
.WithClientId(clientSettings.Id)
|
||||
.WithTcpServer(brokerHostSettings.Host, brokerHostSettings.Port);
|
||||
});
|
||||
|
||||
// In production, the React files will be served from this directory
|
||||
services.AddSpaStaticFiles(configuration =>
|
||||
{
|
||||
@ -101,7 +72,7 @@ namespace Birdmap.API
|
||||
{
|
||||
opt.Title = "Birdmap";
|
||||
opt.OperationProcessors.Add(new OperationSecurityScopeProcessor("Jwt Token"));
|
||||
opt.AddSecurity("Jwt Token", new string[] { },
|
||||
opt.AddSecurity("Jwt Token", Array.Empty<string>(),
|
||||
new NSwag.OpenApiSecurityScheme
|
||||
{
|
||||
Type = NSwag.OpenApiSecuritySchemeType.ApiKey,
|
||||
@ -125,11 +96,9 @@ namespace Birdmap.API
|
||||
app.UseOpenApi();
|
||||
app.UseSwaggerUi3();
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseStaticFiles();
|
||||
app.UseSpaStaticFiles();
|
||||
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseRouting();
|
||||
app.UseAuthorization();
|
||||
@ -139,6 +108,7 @@ namespace Birdmap.API
|
||||
endpoints.MapHealthChecks("/health");
|
||||
endpoints.MapControllers();
|
||||
endpoints.MapHub<DevicesHub>("/hubs/devices");
|
||||
endpoints.MapHub<ServicesHub>("/hubs/services");
|
||||
});
|
||||
|
||||
app.UseSpa(spa =>
|
||||
|
@ -5,5 +5,51 @@
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"Kestrel": {
|
||||
"Certificates": {
|
||||
"Default": {
|
||||
"Password": "certpass123",
|
||||
"Path": "C:\\Users\\Ricsi\\AppData\\Roaming\\ASP.NET\\Https\\aspnetapp.pfx"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Secret": "7vj.3KW.hYE!}4u6",
|
||||
// "LocalDbConnectionString": "Data Source=DESKTOP-3600\\SQLEXPRESS;Initial Catalog=birdmap;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False",
|
||||
"LocalDbConnectionString": "Data Source=DESKTOP-3600;Initial Catalog=birdmap2;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False",
|
||||
"Defaults": {
|
||||
"Services": {
|
||||
"Local Database": "https://localhost:44331/health",
|
||||
"KMLabz Services": "https://birb.k8s.kmlabz.com/devices"
|
||||
},
|
||||
"Users": [
|
||||
{
|
||||
"Name": "admin",
|
||||
"Password": "pass",
|
||||
"Role": "Admin"
|
||||
},
|
||||
{
|
||||
"Name": "user",
|
||||
"Password": "pass",
|
||||
"Role": "User"
|
||||
}
|
||||
]
|
||||
},
|
||||
"UseDummyServices": true,
|
||||
"ServicesBaseUrl": "https://birb.k8s.kmlabz.com/",
|
||||
"UseRabbitMq": false,
|
||||
"Mqtt": {
|
||||
"BrokerHostSettings": {
|
||||
"Host": "localhost",
|
||||
"Port": 1883
|
||||
},
|
||||
|
||||
"ClientSettings": {
|
||||
"Id": "ASP.NET Core client",
|
||||
"Username": "username",
|
||||
"Password": "password",
|
||||
"Topic": "devices/output"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,40 +6,51 @@
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"Kestrel": {
|
||||
"Certificates": {
|
||||
"Default": {
|
||||
"Password": "",
|
||||
"Path": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"Secret": "7vj.3KW.hYE!}4u6",
|
||||
// "LocalDbConnectionString": "Data Source=DESKTOP-3600\\SQLEXPRESS;Initial Catalog=birdmap;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False",
|
||||
"LocalDbConnectionString": "Data Source=DESKTOP-3600;Initial Catalog=birdmap;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False",
|
||||
"Secret": "",
|
||||
"LocalDbConnectionString": "",
|
||||
"Defaults": {
|
||||
"Services": {
|
||||
"Local Database": "https://localhost:44331/health",
|
||||
"KMLabz Services": "https://birb.k8s.kmlabz.com/devices"
|
||||
},
|
||||
"Users": [
|
||||
{
|
||||
"Name": "admin",
|
||||
"Password": "pass",
|
||||
"Role": "Admin"
|
||||
},
|
||||
{
|
||||
"Name": "user",
|
||||
"Password": "pass",
|
||||
"Role": "User"
|
||||
}
|
||||
]
|
||||
"Users": []
|
||||
},
|
||||
"UseDummyServices": true,
|
||||
"UseDummyServices": false,
|
||||
"ServicesBaseUrl": "https://birb.k8s.kmlabz.com/",
|
||||
"UseRabbitMq": false,
|
||||
"Mqtt": {
|
||||
"BrokerHostSettings": {
|
||||
"Host": "localhost",
|
||||
"VirtualHost": "",
|
||||
"Host": "",
|
||||
"Port": 1883
|
||||
},
|
||||
|
||||
"ExchangeSettings": {
|
||||
"Name": "",
|
||||
"Type": "",
|
||||
"Durable": false,
|
||||
"AutoDelete": false
|
||||
},
|
||||
|
||||
"QueueSettings": {
|
||||
"Name": "",
|
||||
"Durable": false,
|
||||
"Exclusive": false,
|
||||
"AutoDelete": false
|
||||
},
|
||||
|
||||
"ClientSettings": {
|
||||
"Id": "ASP.NET Core client",
|
||||
"Username": "username",
|
||||
"Password": "password",
|
||||
"Topic": "devices/output"
|
||||
"Username": "",
|
||||
"Password": "",
|
||||
"Topic": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
5
Birdmap.API/libman.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"defaultProvider": "cdnjs",
|
||||
"libraries": []
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
autoReload="true"
|
||||
internalLogLevel="Info"
|
||||
internalLogFile="${basedir}Log/internal-nlog.txt"
|
||||
internalLogFile="${basedir}Logs/internal-nlog.txt"
|
||||
throwConfigExceptions="true">
|
||||
|
||||
<!-- enable asp.net core layout renderers -->
|
||||
@ -14,17 +14,17 @@
|
||||
<!-- the targets to write to -->
|
||||
<targets async="true">
|
||||
<default-target-parameters xsi:type="File" keepFileOpen="false" maxArchiveFiles="10" archiveAboveSize="1048576"/>
|
||||
<target xsi:type="File" name="allFile" fileName="${basedir}Log/birdmap-all-${shortdate}.log"
|
||||
<target xsi:type="File" name="allFile" fileName="${basedir}Logs/birdmap-all-${shortdate}.log"
|
||||
layout="${longdate} [${threadname:whenEmpty=${threadid}}] ${uppercase:${level}} ${logger} - ${message} ${exception:format=tostring}" />
|
||||
|
||||
<target xsi:type="File" name="mqttFile" fileName="${basedir}Log/birdmap-mqtt-${shortdate}.log"
|
||||
<target xsi:type="File" name="mqttFile" fileName="${basedir}Logs/birdmap-mqtt-${shortdate}.log"
|
||||
layout="${longdate} [${threadname:whenEmpty=${threadid}}] ${uppercase:${level}} ${logger} - ${message} ${exception:format=tostring}" />
|
||||
|
||||
<target xsi:type="File" name="hubsFile" fileName="${basedir}Log/birdmap-hubs-${shortdate}.log"
|
||||
<target xsi:type="File" name="hubsFile" fileName="${basedir}Logs/birdmap-hubs-${shortdate}.log"
|
||||
layout="${longdate} [${threadname:whenEmpty=${threadid}}] ${uppercase:${level}} ${logger} - ${message} ${exception:format=tostring}" />
|
||||
|
||||
<!-- another file log, only own logs. Uses some ASP.NET core renderers -->
|
||||
<target xsi:type="File" name="ownFile" fileName="${basedir}Log/birdmap-own-${shortdate}.log"
|
||||
<target xsi:type="File" name="ownFile" fileName="${basedir}Logs/birdmap-own-${shortdate}.log"
|
||||
layout="${longdate} [${threadname:whenEmpty=${threadid}}] ${uppercase:${level}} ${callsite} - ${message} ${exception:format=tostring} (url: ${aspnet-request-url})(action: ${aspnet-mvc-action})" />
|
||||
</targets>
|
||||
|
||||
@ -35,6 +35,8 @@
|
||||
|
||||
<!--Skip non-critical Mqtt logs-->
|
||||
<logger name="*.*Mqtt*.*" minlevel="Trace" maxlevel="Warning" writeTo="mqttFile" final="true"/>
|
||||
<logger name="*.*RabbitMq*.*" minlevel="Trace" maxlevel="Warning" writeTo="mqttFile" final="true"/>
|
||||
<logger name="*.*CommunicationServiceBase*.*" minlevel="Trace" maxlevel="Warning" writeTo="mqttFile" final="true"/>
|
||||
|
||||
<!--Skip non-critical Hub logs-->
|
||||
<logger name="*.*Hubs*.*" minlevel="Trace" maxlevel="Warning" writeTo="hubsFile" final="true"/>
|
||||
|
@ -5,7 +5,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Core" Version="1.1.0" />
|
||||
<PackageReference Include="MQTTnet" Version="3.0.13" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="RabbitMQ.Client" Version="6.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
9
Birdmap.BLL/Interfaces/ICommunicationService.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Birdmap.BLL.Interfaces
|
||||
{
|
||||
public interface ICommunicationService : IHostedService
|
||||
{
|
||||
public bool IsConnected { get; }
|
||||
}
|
||||
}
|
7
Birdmap.BLL/Interfaces/ICommunicationServiceProvider.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Birdmap.BLL.Interfaces
|
||||
{
|
||||
public interface ICommunicationServiceProvider
|
||||
{
|
||||
public ICommunicationService Service { get; }
|
||||
}
|
||||
}
|
@ -3,13 +3,11 @@ using MQTTnet.Client.Connecting;
|
||||
using MQTTnet.Client.Disconnecting;
|
||||
using MQTTnet.Client.Receiving;
|
||||
|
||||
namespace Birdmap.API.Services
|
||||
namespace Birdmap.BLL.Interfaces
|
||||
{
|
||||
public interface IMqttClientService : IHostedService,
|
||||
IMqttClientConnectedHandler,
|
||||
public interface IMqttClientService : IMqttClientConnectedHandler,
|
||||
IMqttClientDisconnectedHandler,
|
||||
IMqttApplicationMessageReceivedHandler
|
||||
{
|
||||
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ namespace Birdmap.BLL.Interfaces
|
||||
{
|
||||
public interface IServiceService
|
||||
{
|
||||
Task<int> GetServiceCountAsync();
|
||||
Task<List<Service>> GetAllServicesAsync();
|
||||
Task<Service> GetServiceAsync(int id);
|
||||
Task<Service> CreateServiceAsync(Service service);
|
||||
|
@ -1,18 +1,18 @@
|
||||
using MQTTnet.Client.Options;
|
||||
using System;
|
||||
|
||||
namespace Birdmap.API.Options
|
||||
namespace Birdmap.BLL.Options
|
||||
{
|
||||
public class AspCoreMqttClientOptions : MqttClientOptionsBuilder
|
||||
public class MqttClientOptions : MqttClientOptionsBuilder
|
||||
{
|
||||
public IServiceProvider ServiceProvider { get; }
|
||||
|
||||
public AspCoreMqttClientOptions(IServiceProvider serviceProvider)
|
||||
public MqttClientOptions(IServiceProvider serviceProvider)
|
||||
{
|
||||
ServiceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public AspCoreMqttClientOptions WithTopic(string topic)
|
||||
public MqttClientOptions WithTopic(string topic)
|
||||
{
|
||||
WithUserProperty("Topic", topic);
|
||||
|
11
Birdmap.BLL/Options/RabbitMqClientOptions.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Birdmap.BLL.Options
|
||||
{
|
||||
public record RabbitMqClientOptions(
|
||||
string Hostname, int Port, string VirtualHost,
|
||||
string Username, string Password,
|
||||
string ExchangeName, string ExchangeType,
|
||||
bool ExchangeDurable, bool ExchangeAutoDelete,
|
||||
string QueueName,
|
||||
bool QueueDurable, bool QueueAutoDelete, bool QueueExclusive,
|
||||
string Topic);
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
using Birdmap.BLL.Interfaces;
|
||||
using Birdmap.BLL.Services.CommunicationServices.Hubs;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace Birdmap.BLL.Services.CommunationServices
|
||||
{
|
||||
internal class Payload
|
||||
{
|
||||
[JsonProperty("tag")]
|
||||
public Guid TagID { get; set; }
|
||||
|
||||
[JsonProperty("probability")]
|
||||
public double Probability { get; set; }
|
||||
}
|
||||
|
||||
internal abstract class CommunicationServiceBase : ICommunicationService
|
||||
{
|
||||
protected readonly ILogger _logger;
|
||||
protected readonly IInputService _inputService;
|
||||
protected readonly IHubContext<DevicesHub, IDevicesHubClient> _hubContext;
|
||||
private readonly Timer _hubTimer;
|
||||
private readonly List<Message> _messages = new();
|
||||
private readonly object _messageLock = new();
|
||||
|
||||
public abstract bool IsConnected { get; }
|
||||
|
||||
public CommunicationServiceBase(ILogger logger, IInputService inputService, IHubContext<DevicesHub, IDevicesHubClient> hubContext)
|
||||
{
|
||||
_logger = logger;
|
||||
_inputService = inputService;
|
||||
_hubContext = hubContext;
|
||||
_hubTimer = new Timer()
|
||||
{
|
||||
AutoReset = true,
|
||||
Interval = 1000,
|
||||
};
|
||||
_hubTimer.Elapsed += SendMqttMessagesWithSignalR;
|
||||
}
|
||||
|
||||
protected async Task ProcessJsonMessageAsync(string json)
|
||||
{
|
||||
try
|
||||
{
|
||||
var payload = JsonConvert.DeserializeObject<Payload>(json);
|
||||
var inputResponse = await _inputService.GetInputAsync(payload.TagID);
|
||||
|
||||
lock (_messageLock)
|
||||
{
|
||||
_messages.Add(new Message(inputResponse.Message.Device_id, inputResponse.Message.Date.UtcDateTime, payload.Probability));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Could not handle application message.");
|
||||
}
|
||||
}
|
||||
|
||||
protected void StartMessageTimer()
|
||||
{
|
||||
_hubTimer.Start();
|
||||
}
|
||||
protected void StopMessageTimer()
|
||||
{
|
||||
_hubTimer.Stop();
|
||||
}
|
||||
|
||||
private void SendMqttMessagesWithSignalR(object sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
lock (_messageLock)
|
||||
{
|
||||
if (_messages.Any())
|
||||
{
|
||||
_logger.LogInformation($"Sending ({_messages.Count}) messages: {string.Join(" | ", _messages)}");
|
||||
_hubContext.Clients.All.NotifyMessagesAsync(_messages);
|
||||
_messages.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract Task StartAsync(CancellationToken cancellationToken);
|
||||
|
||||
public abstract Task StopAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
using Birdmap.BLL.Interfaces;
|
||||
|
||||
namespace Birdmap.BLL.Services.CommunicationServices
|
||||
{
|
||||
internal class CommunicationServiceProvider : ICommunicationServiceProvider
|
||||
{
|
||||
public ICommunicationService Service { get; }
|
||||
|
||||
public CommunicationServiceProvider(ICommunicationService service)
|
||||
{
|
||||
Service = service;
|
||||
}
|
||||
}
|
||||
}
|
31
Birdmap.BLL/Services/CommunationServices/Hubs/DevicesHub.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Birdmap.BLL.Services.CommunicationServices.Hubs
|
||||
{
|
||||
public class DevicesHub : Hub<IDevicesHubClient>
|
||||
{
|
||||
private readonly ILogger<DevicesHub> _logger;
|
||||
|
||||
public DevicesHub(ILogger<DevicesHub> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override Task OnConnectedAsync()
|
||||
{
|
||||
_logger.LogInformation("Devices Hub Client connected.");
|
||||
|
||||
return base.OnConnectedAsync();
|
||||
}
|
||||
|
||||
public override Task OnDisconnectedAsync(Exception exception)
|
||||
{
|
||||
_logger.LogInformation("Devices Hub Client disconnected.");
|
||||
|
||||
return base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Birdmap.BLL.Services.CommunicationServices.Hubs
|
||||
{
|
||||
public record Message(Guid DeviceId, DateTime Date, double Probability);
|
||||
|
||||
public interface IDevicesHubClient
|
||||
{
|
||||
Task NotifyMessagesAsync(IEnumerable<Message> messages);
|
||||
Task NotifyDeviceUpdatedAsync(Guid deviceId);
|
||||
Task NotifyAllUpdatedAsync();
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Birdmap.BLL.Services.CommunicationServices.Hubs
|
||||
{
|
||||
public interface IServicesHubClient
|
||||
{
|
||||
Task NotifyUpdatedAsync();
|
||||
}
|
||||
}
|
31
Birdmap.BLL/Services/CommunationServices/Hubs/ServicesHub.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Birdmap.BLL.Services.CommunicationServices.Hubs
|
||||
{
|
||||
public class ServicesHub : Hub<IServicesHubClient>
|
||||
{
|
||||
private readonly ILogger<ServicesHub> _logger;
|
||||
|
||||
public ServicesHub(ILogger<ServicesHub> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override Task OnConnectedAsync()
|
||||
{
|
||||
_logger.LogInformation("Services Hub Client connected.");
|
||||
|
||||
return base.OnConnectedAsync();
|
||||
}
|
||||
|
||||
public override Task OnDisconnectedAsync(Exception exception)
|
||||
{
|
||||
_logger.LogInformation("Services Hub Client disconnected.");
|
||||
|
||||
return base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
using Birdmap.API.Services.Hubs;
|
||||
using Birdmap.BLL.Interfaces;
|
||||
using Birdmap.BLL.Interfaces;
|
||||
using Birdmap.BLL.Services.CommunationServices;
|
||||
using Birdmap.BLL.Services.CommunicationServices.Hubs;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MQTTnet;
|
||||
@ -7,29 +8,25 @@ using MQTTnet.Client;
|
||||
using MQTTnet.Client.Connecting;
|
||||
using MQTTnet.Client.Disconnecting;
|
||||
using MQTTnet.Client.Options;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Birdmap.API.Services.Mqtt
|
||||
namespace Birdmap.BLL.Services.CommunicationServices.Mqtt
|
||||
{
|
||||
public class MqttClientService : IMqttClientService
|
||||
internal class MqttClientService : CommunicationServiceBase, IMqttClientService
|
||||
{
|
||||
private readonly IMqttClient _mqttClient;
|
||||
private readonly IMqttClientOptions _options;
|
||||
private readonly ILogger<MqttClientService> _logger;
|
||||
private readonly IInputService _inputService;
|
||||
private readonly IHubContext<DevicesHub, IDevicesHubClient> _hubContext;
|
||||
|
||||
public override bool IsConnected => _mqttClient.IsConnected;
|
||||
|
||||
public MqttClientService(IMqttClientOptions options, ILogger<MqttClientService> logger, IInputService inputService, IHubContext<DevicesHub, IDevicesHubClient> hubContext)
|
||||
: base(logger, inputService, hubContext)
|
||||
{
|
||||
_options = options;
|
||||
_logger = logger;
|
||||
_inputService = inputService;
|
||||
_hubContext = hubContext;
|
||||
|
||||
_mqttClient = new MqttFactory().CreateMqttClient();
|
||||
ConfigureMqttClient();
|
||||
}
|
||||
@ -41,33 +38,14 @@ namespace Birdmap.API.Services.Mqtt
|
||||
_mqttClient.ApplicationMessageReceivedHandler = this;
|
||||
}
|
||||
|
||||
private class Payload
|
||||
{
|
||||
[JsonProperty("tag")]
|
||||
public Guid TagID { get; set; }
|
||||
|
||||
[JsonProperty("probability")]
|
||||
public double Probability { get; set; }
|
||||
}
|
||||
|
||||
public async Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs)
|
||||
public Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs)
|
||||
{
|
||||
var message = eventArgs.ApplicationMessage.ConvertPayloadToString();
|
||||
|
||||
_logger.LogInformation($"Recieved [{eventArgs.ClientId}] " +
|
||||
_logger.LogDebug($"Recieved [{eventArgs.ClientId}] " +
|
||||
$"Topic: {eventArgs.ApplicationMessage.Topic} | Payload: {message} | QoS: {eventArgs.ApplicationMessage.QualityOfServiceLevel} | Retain: {eventArgs.ApplicationMessage.Retain}");
|
||||
|
||||
try
|
||||
{
|
||||
var payload = JsonConvert.DeserializeObject<Payload>(message);
|
||||
var inputResponse = await _inputService.GetInputAsync(payload.TagID);
|
||||
|
||||
await _hubContext.Clients.All.NotifyDeviceAsync(inputResponse.Message.Device_id, inputResponse.Message.Date.UtcDateTime, payload.Probability);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Could not handle application message.");
|
||||
}
|
||||
return ProcessJsonMessageAsync(message);
|
||||
}
|
||||
|
||||
public async Task HandleConnectedAsync(MqttClientConnectedEventArgs eventArgs)
|
||||
@ -78,6 +56,7 @@ namespace Birdmap.API.Services.Mqtt
|
||||
_logger.LogInformation($"Connected. Auth result: {eventArgs.AuthenticateResult}. Subscribing to topic: {topic}");
|
||||
|
||||
await _mqttClient.SubscribeAsync(topic);
|
||||
StartMessageTimer();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -87,21 +66,22 @@ namespace Birdmap.API.Services.Mqtt
|
||||
|
||||
public async Task HandleDisconnectedAsync(MqttClientDisconnectedEventArgs eventArgs)
|
||||
{
|
||||
_logger.LogWarning(eventArgs.Exception, $"Disconnected. Reason {eventArgs.ReasonCode}. Auth result: {eventArgs.AuthenticateResult}. Reconnecting...");
|
||||
_logger.LogDebug(eventArgs.Exception, $"Disconnected. Reason {eventArgs.ReasonCode}. Auth result: {eventArgs.AuthenticateResult}. Reconnecting...");
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||
|
||||
try
|
||||
{
|
||||
StopMessageTimer();
|
||||
await _mqttClient.ConnectAsync(_options, CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Reconnect failed...");
|
||||
_logger.LogDebug(ex, $"Reconnect failed...");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -117,7 +97,7 @@ namespace Birdmap.API.Services.Mqtt
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
@ -0,0 +1,114 @@
|
||||
using Birdmap.BLL.Interfaces;
|
||||
using Birdmap.BLL.Options;
|
||||
using Birdmap.BLL.Services.CommunicationServices.Hubs;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RabbitMQ.Client;
|
||||
using RabbitMQ.Client.Events;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Birdmap.BLL.Services.CommunationServices.RabbitMq
|
||||
{
|
||||
internal class RabbitMqClientService : CommunicationServiceBase
|
||||
{
|
||||
private IConnection _connection;
|
||||
private IModel _channel;
|
||||
private readonly IConnectionFactory _factory;
|
||||
private readonly RabbitMqClientOptions _options;
|
||||
|
||||
public override bool IsConnected => _connection.IsOpen;
|
||||
|
||||
public RabbitMqClientService(RabbitMqClientOptions options, ILogger<RabbitMqClientService> logger, IInputService inputService, IHubContext<DevicesHub, IDevicesHubClient> hubContext)
|
||||
: base(logger, inputService, hubContext)
|
||||
{
|
||||
_options = options;
|
||||
_factory = new ConnectionFactory()
|
||||
{
|
||||
HostName = options.Hostname,
|
||||
Port = options.Port,
|
||||
UserName = options.Username,
|
||||
Password = options.Password,
|
||||
|
||||
AutomaticRecoveryEnabled = true,
|
||||
};
|
||||
}
|
||||
|
||||
private Task OnRecieved(object sender, BasicDeliverEventArgs eventArgs)
|
||||
{
|
||||
var props = eventArgs.BasicProperties;
|
||||
var body = Encoding.UTF8.GetString(eventArgs.Body.ToArray());
|
||||
|
||||
_logger.LogDebug($"Recieved [{props.UserId}] " +
|
||||
$"ConsumerTag: {eventArgs.ConsumerTag} | DeliveryTag: {eventArgs.DeliveryTag} | Payload: {body} | Priority: {props.Priority}");
|
||||
|
||||
return ProcessJsonMessageAsync(body);
|
||||
}
|
||||
|
||||
public override async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
Connect();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Cannot connect. Reconnecting...");
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await StartAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public override Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
StopMessageTimer();
|
||||
_channel?.Close();
|
||||
_connection?.Close();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Cannot disconnect...");
|
||||
return Task.FromException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void Connect()
|
||||
{
|
||||
|
||||
_connection = _factory.CreateConnection();
|
||||
_channel = _connection.CreateModel();
|
||||
|
||||
_channel.ExchangeDeclare(
|
||||
exchange: _options.ExchangeName,
|
||||
type: _options.ExchangeType,
|
||||
durable: _options.ExchangeDurable,
|
||||
autoDelete: _options.ExchangeAutoDelete);
|
||||
|
||||
_channel.QueueDeclare(
|
||||
queue: _options.QueueName,
|
||||
durable: _options.QueueDurable,
|
||||
exclusive: _options.QueueExclusive,
|
||||
autoDelete: _options.QueueAutoDelete);
|
||||
|
||||
_channel.QueueBind(queue: _options.QueueName,
|
||||
exchange: _options.ExchangeName,
|
||||
routingKey: _options.Topic);
|
||||
|
||||
var consumer = new AsyncEventingBasicConsumer(_channel);
|
||||
consumer.Received += OnRecieved;
|
||||
|
||||
_channel.BasicConsume(queue: _options.QueueName,
|
||||
autoAck: true,
|
||||
consumer: consumer);
|
||||
|
||||
StartMessageTimer();
|
||||
}
|
||||
}
|
||||
}
|
@ -16,12 +16,12 @@ namespace Birdmap.BLL.Services
|
||||
private const double centerLat = 48.275939;
|
||||
private const double radius = 0.001;
|
||||
|
||||
private static readonly Random Rand = new Random();
|
||||
private static readonly Random Rand = new();
|
||||
|
||||
private static readonly Lazy<ICollection<Device>> Devices = new Lazy<ICollection<Device>>(GenerateDevices);
|
||||
private static readonly Lazy<ICollection<Device>> Devices = new(GenerateDevices);
|
||||
|
||||
private static readonly Dictionary<Guid, InputSingeResponse> TagToInput = new Dictionary<Guid, InputSingeResponse>();
|
||||
private static readonly object InputLock = new object();
|
||||
private static readonly Dictionary<Guid, InputSingeResponse> TagToInput = new();
|
||||
private static readonly object InputLock = new();
|
||||
|
||||
private static ListOfDevices GenerateDevices()
|
||||
{
|
||||
@ -43,20 +43,20 @@ namespace Birdmap.BLL.Services
|
||||
var sensors = new ArrayofSensors();
|
||||
for (int s = 0; s < Rand.Next(1, 6); s++)
|
||||
{
|
||||
sensors.Add(new Sensor
|
||||
sensors.Add(new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Status = GetRandomEnum<SensorStatus>(),
|
||||
});
|
||||
}
|
||||
|
||||
devices.Add(new Device
|
||||
devices.Add(new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Sensors = sensors,
|
||||
Status = GetRandomEnum<DeviceStatus>(),
|
||||
Url = "dummyservice.device.url",
|
||||
Coordinates = new Coordinates
|
||||
Coordinates = new()
|
||||
{
|
||||
Latitude = GetPlusMinus(centerLat, radius),
|
||||
Longitude = GetPlusMinus(centerLong, radius),
|
||||
@ -150,10 +150,10 @@ namespace Birdmap.BLL.Services
|
||||
{
|
||||
if (!TagToInput.TryGetValue(tagID, out var value))
|
||||
{
|
||||
value = new InputSingeResponse
|
||||
value = new()
|
||||
{
|
||||
Status = "Dummy_OK",
|
||||
Message = new InputObject
|
||||
Message = new()
|
||||
{
|
||||
Tag = tagID,
|
||||
Date = DateTime.Now,
|
||||
|
@ -17,14 +17,15 @@ namespace Birdmap.BLL.Services
|
||||
using System = global::System;
|
||||
|
||||
[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.8.2.0 (NJsonSchema v10.2.1.0 (Newtonsoft.Json v12.0.0.0))")]
|
||||
public partial class LiveDummyService : IDeviceService
|
||||
public partial class LiveDeviceService : IDeviceService
|
||||
{
|
||||
private string _baseUrl = "https://birb.k8s.kmlabz.com";
|
||||
private System.Net.Http.HttpClient _httpClient;
|
||||
private System.Lazy<Newtonsoft.Json.JsonSerializerSettings> _settings;
|
||||
|
||||
public LiveDummyService(System.Net.Http.HttpClient httpClient)
|
||||
public LiveDeviceService(string baseUrl, System.Net.Http.HttpClient httpClient)
|
||||
{
|
||||
_baseUrl = baseUrl;
|
||||
_httpClient = httpClient;
|
||||
_settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(CreateSerializerSettings);
|
||||
}
|
||||
|
@ -23,8 +23,9 @@ namespace Birdmap.BLL.Services
|
||||
private System.Net.Http.HttpClient _httpClient;
|
||||
private System.Lazy<Newtonsoft.Json.JsonSerializerSettings> _settings;
|
||||
|
||||
public LiveInputService(System.Net.Http.HttpClient httpClient)
|
||||
public LiveInputService(string baseUrl, System.Net.Http.HttpClient httpClient)
|
||||
{
|
||||
_baseUrl = baseUrl;
|
||||
_httpClient = httpClient;
|
||||
_settings = new System.Lazy<Newtonsoft.Json.JsonSerializerSettings>(CreateSerializerSettings);
|
||||
}
|
||||
|
@ -17,6 +17,11 @@ namespace Birdmap.BLL.Services
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public Task<int> GetServiceCountAsync()
|
||||
{
|
||||
return _context.Services.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<Service> CreateServiceAsync(Service service)
|
||||
{
|
||||
_context.Services.Add(service);
|
||||
|
@ -1,7 +1,14 @@
|
||||
using Birdmap.BLL.Interfaces;
|
||||
using Birdmap.BLL.Options;
|
||||
using Birdmap.BLL.Services;
|
||||
using Birdmap.BLL.Services.CommunationServices.RabbitMq;
|
||||
using Birdmap.BLL.Services.CommunicationServices;
|
||||
using Birdmap.BLL.Services.CommunicationServices.Mqtt;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Birdmap.BLL
|
||||
{
|
||||
@ -20,11 +27,129 @@ namespace Birdmap.BLL
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddTransient<IInputService, LiveInputService>();
|
||||
services.AddTransient<IDeviceService, LiveDummyService>();
|
||||
var baseUrl = configuration.GetValue<string>("ServicesBaseUrl");
|
||||
|
||||
services.AddTransient<IInputService, LiveInputService>(serviceProvider =>
|
||||
{
|
||||
var httpClient = serviceProvider.GetService<HttpClient>();
|
||||
var service = new LiveInputService(baseUrl, httpClient);
|
||||
return service;
|
||||
});
|
||||
services.AddTransient<IDeviceService, LiveDeviceService>(serviceProvider =>
|
||||
{
|
||||
var httpClient = serviceProvider.GetService<HttpClient>();
|
||||
var service = new LiveDeviceService(baseUrl, httpClient);
|
||||
return service;
|
||||
});
|
||||
}
|
||||
|
||||
services.AddSignalR();
|
||||
|
||||
var mqtt = configuration.GetSection("Mqtt");
|
||||
|
||||
var client = mqtt.GetSection("ClientSettings");
|
||||
var clientSettings = new
|
||||
{
|
||||
Id = client.GetValue<string>("Id"),
|
||||
Username = client.GetValue<string>("Username"),
|
||||
Password = client.GetValue<string>("Password"),
|
||||
Topic = client.GetValue<string>("Topic"),
|
||||
};
|
||||
|
||||
var brokerHost = mqtt.GetSection("BrokerHostSettings");
|
||||
var brokerHostSettings = new
|
||||
{
|
||||
Host = brokerHost.GetValue<string>("Host"),
|
||||
Port = brokerHost.GetValue<int>("Port"),
|
||||
VirtualHost = brokerHost.GetValue<string>("VirtualHost"),
|
||||
};
|
||||
|
||||
var exchange = mqtt.GetSection("ExchangeSettings");
|
||||
var exchangeSettings = new
|
||||
{
|
||||
Name = exchange.GetValue<string>("Name"),
|
||||
Type = exchange.GetValue<string>("Type"),
|
||||
Durable = exchange.GetValue<bool>("Durable"),
|
||||
AutoDelete = exchange.GetValue<bool>("AutoDelete"),
|
||||
};
|
||||
|
||||
var queue = mqtt.GetSection("QueueSettings");
|
||||
var queueSettings = new
|
||||
{
|
||||
Name = queue.GetValue<string>("Name"),
|
||||
Durable = exchange.GetValue<bool>("Durable"),
|
||||
Exclusive = exchange.GetValue<bool>("Exclusive"),
|
||||
AutoDelete = exchange.GetValue<bool>("AutoDelete"),
|
||||
};
|
||||
|
||||
if (configuration.GetValue<bool>("UseRabbitMq"))
|
||||
{
|
||||
services.AddRabbitMqClientServiceWithConfig(new RabbitMqClientOptions(
|
||||
Hostname: brokerHostSettings.Host,
|
||||
Port: brokerHostSettings.Port,
|
||||
VirtualHost: brokerHostSettings.VirtualHost,
|
||||
Username: clientSettings.Username,
|
||||
Password: clientSettings.Password,
|
||||
ExchangeName: exchangeSettings.Name,
|
||||
ExchangeType: exchangeSettings.Type,
|
||||
ExchangeDurable: exchangeSettings.Durable,
|
||||
ExchangeAutoDelete: exchangeSettings.AutoDelete,
|
||||
QueueName: queueSettings.Name,
|
||||
QueueDurable: queueSettings.Durable,
|
||||
QueueExclusive: queueSettings.Exclusive,
|
||||
QueueAutoDelete: queueSettings.AutoDelete,
|
||||
Topic: clientSettings.Topic));
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddMqttClientServiceWithConfig(opt =>
|
||||
{
|
||||
opt
|
||||
.WithTopic(clientSettings.Topic)
|
||||
.WithCredentials(clientSettings.Username, clientSettings.Password)
|
||||
.WithClientId(clientSettings.Id)
|
||||
.WithTcpServer(brokerHostSettings.Host, brokerHostSettings.Port);
|
||||
});
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddMqttClientServiceWithConfig(this IServiceCollection services, Action<MqttClientOptions> configureOptions)
|
||||
{
|
||||
services.AddSingleton(serviceProvider =>
|
||||
{
|
||||
var optionBuilder = new MqttClientOptions(serviceProvider);
|
||||
configureOptions(optionBuilder);
|
||||
return optionBuilder.Build();
|
||||
});
|
||||
|
||||
services.AddClientServiceWithProvider<MqttClientService>();
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddRabbitMqClientServiceWithConfig(this IServiceCollection services, RabbitMqClientOptions options)
|
||||
{
|
||||
services.AddSingleton(options);
|
||||
|
||||
services.AddClientServiceWithProvider<RabbitMqClientService>();
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddClientServiceWithProvider<T>(this IServiceCollection services) where T : class, ICommunicationService
|
||||
{
|
||||
services.AddSingleton<T>();
|
||||
services.AddSingleton<IHostedService>(serviceProvider =>
|
||||
{
|
||||
return serviceProvider.GetService<T>();
|
||||
});
|
||||
services.AddSingleton<ICommunicationServiceProvider>(serviceProvider =>
|
||||
{
|
||||
var clientService = serviceProvider.GetService<T>();
|
||||
var clientServiceProvider = new CommunicationServiceProvider(clientService);
|
||||
return clientServiceProvider;
|
||||
});
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,10 +22,17 @@ namespace Birdmap.DAL
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
EnsureCreated();
|
||||
AddDefaultUsers();
|
||||
AddDefaultServices();
|
||||
}
|
||||
|
||||
private void EnsureCreated()
|
||||
{
|
||||
_logger.LogInformation("Ensuring database is created...");
|
||||
_context.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
private void AddDefaultServices()
|
||||
{
|
||||
_logger.LogInformation("Removing previously added default services...");
|
||||
|
@ -13,6 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Birdmap.Common", "Birdmap.C
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MQTTnet.TestApp.WinForm", "MQTTnet.TestApp.WinForm\MQTTnet.TestApp.WinForm.csproj", "{E1707FE7-4A65-42AC-B71C-6CC1A55FC42A}"
|
||||
EndProject
|
||||
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{9443433B-1D13-41F0-B345-B36ACD15EF81}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -39,6 +41,10 @@ Global
|
||||
{E1707FE7-4A65-42AC-B71C-6CC1A55FC42A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E1707FE7-4A65-42AC-B71C-6CC1A55FC42A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E1707FE7-4A65-42AC-B71C-6CC1A55FC42A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9443433B-1D13-41F0-B345-B36ACD15EF81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9443433B-1D13-41F0-B345-B36ACD15EF81}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9443433B-1D13-41F0-B345-B36ACD15EF81}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9443433B-1D13-41F0-B345-B36ACD15EF81}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
27
Dockerfile
Normal file
@ -0,0 +1,27 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
|
||||
WORKDIR /app
|
||||
RUN apt-get update && apt-get install -y curl
|
||||
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash -
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
|
||||
RUN apt-get update && apt-get install -y curl
|
||||
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash -
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
WORKDIR /src
|
||||
COPY ["Birdmap.API/Birdmap.API.csproj", "Birdmap.API/"]
|
||||
COPY ["Birdmap.BLL/Birdmap.BLL.csproj", "Birdmap.BLL/"]
|
||||
COPY ["Birdmap.Common/Birdmap.Common.csproj", "Birdmap.Common/"]
|
||||
COPY ["Birdmap.DAL/Birdmap.DAL.csproj", "Birdmap.DAL/"]
|
||||
RUN dotnet restore "Birdmap.API/Birdmap.API.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Birdmap.API"
|
||||
RUN dotnet build "Birdmap.API.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "Birdmap.API.csproj" -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Birdmap.API.dll"]
|
2
MQTTnet.TestApp.WinForm/Form1.Designer.cs
generated
@ -267,7 +267,7 @@
|
||||
this.trackBar1.LargeChange = 500;
|
||||
this.trackBar1.Location = new System.Drawing.Point(180, 162);
|
||||
this.trackBar1.Maximum = 5050;
|
||||
this.trackBar1.Minimum = 50;
|
||||
this.trackBar1.Minimum = 1;
|
||||
this.trackBar1.Name = "trackBar1";
|
||||
this.trackBar1.Size = new System.Drawing.Size(247, 45);
|
||||
this.trackBar1.SmallChange = 100;
|
||||
|
1
MQTTnet.TestApp.WinForm/Retained.json
Normal file
@ -0,0 +1 @@
|
||||
[{"Topic":"devices/output","Payload":"eyJ0YWciOiJkNDhhODAwOC0wYjU2LTRkYzAtODYxMy0zMWY0MDY4OTk0ZDciLCJwcm9iYWJpbGl0eSI6MC4yNTMxNDI4NDgyNjMwOTU1fQ==","QualityOfServiceLevel":1,"Retain":true,"UserProperties":null,"ContentType":null,"ResponseTopic":null,"PayloadFormatIndicator":null,"MessageExpiryInterval":null,"TopicAlias":null,"CorrelationData":null,"SubscriptionIdentifiers":null}]
|
15
docker-compose.dcproj
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" Sdk="Microsoft.Docker.Sdk">
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectVersion>2.1</ProjectVersion>
|
||||
<DockerTargetOS>Linux</DockerTargetOS>
|
||||
<ProjectGuid>9443433b-1d13-41f0-b345-b36acd15ef81</ProjectGuid>
|
||||
<DockerLaunchAction>LaunchBrowser</DockerLaunchAction>
|
||||
<DockerServiceUrl>{Scheme}://localhost:{ServicePort}</DockerServiceUrl>
|
||||
<DockerServiceName>birdmap.api</DockerServiceName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Include="docker-compose.yml" />
|
||||
<None Include=".dockerignore" />
|
||||
</ItemGroup>
|
||||
</Project>
|
56
docker-compose.yml
Normal file
@ -0,0 +1,56 @@
|
||||
version: '3.4'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: "mcr.microsoft.com/mssql/server:2019-latest"
|
||||
environment:
|
||||
- ACCEPT_EULA=Y
|
||||
- SA_PASSWORD=RPSsql12345
|
||||
|
||||
birdmap.api:
|
||||
image: ${DOCKER_REGISTRY-}birdmapapi
|
||||
ports:
|
||||
- "8000:80"
|
||||
- "8001:443"
|
||||
volumes:
|
||||
- ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro
|
||||
- ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Docker
|
||||
- ASPNETCORE_URLS=https://+;http://+
|
||||
- ASPNETCORE_HTTPS_PORT=8001
|
||||
- Birdmap_Kestrel__Certificates__Default__Password=certpass123
|
||||
- Birdmap_Kestrel__Certificates__Default__Path=/root/.aspnet/https/aspnetapp.pfx
|
||||
- Birdmap_Secret=7gz;]=bQe}n#3~RwC+Y<SrjoE:sHwO
|
||||
- Birdmap_LocalDbConnectionString=Data Source=db;Initial Catalog=birdmap;User=sa;Password=RPSsql12345
|
||||
- Birdmap_Defaults__Users__0__Name=admin
|
||||
- Birdmap_Defaults__Users__0__Password=pass
|
||||
- Birdmap_Defaults__Users__0__Role=Admin
|
||||
- Birdmap_Defaults__Users__1__Name=user
|
||||
- Birdmap_Defaults__Users__1__Password=pass
|
||||
- Birdmap_Defaults__Users__1__Role=User
|
||||
- Birdmap_Defaults__Services__Local-Database=https://localhost:8001/health
|
||||
- Birdmap_Defaults__Services__KMLabz-Service=https://birb.k8s.kmlabz.com/devices
|
||||
- Birdmap_UseDummyServices=true
|
||||
- Birdmap_ServicesBaseUrl=https://birb.k8s.kmlabz.com/
|
||||
- Birdmap_UseRabbitMq=false
|
||||
- Birdmap_Mqtt__BrokerHostSettings__Host=localhost
|
||||
- Birdmap_Mqtt__BrokerHostSettings__Port=1883
|
||||
- Birdmap_Mqtt__BrokerHostSettings__VirtualHost=/
|
||||
- Birdmap_Mqtt__ExchangeSettings__Name=birbmapexchange
|
||||
- Birdmap_Mqtt__ExchangeSettings__Type=fanout
|
||||
- Birdmap_Mqtt__ExchangeSettings__Durable=true
|
||||
- Birdmap_Mqtt__ExchangeSettings__AutoDelete=true
|
||||
- Birdmap_Mqtt__QueueSettings__Name=birbmapqueue
|
||||
- Birdmap_Mqtt__QueueSettings__Durable=true
|
||||
- Birdmap_Mqtt__QueueSettings__Exclusive=true
|
||||
- Birdmap_Mqtt__QueueSettings__AutoDelete=true
|
||||
- Birdmap_Mqtt__ClientSettings__Id=ASP.NET Core client
|
||||
- Birdmap_Mqtt__ClientSettings__Username=username
|
||||
- Birdmap_Mqtt__ClientSettings__Password=password
|
||||
- Birdmap_Mqtt__ClientSettings__Topic=devices/output
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 78 KiB |
1
docs/birdmap-frontend-routes.drawio
Normal file
@ -0,0 +1 @@
|
||||
<mxfile host="app.diagrams.net" modified="2020-12-04T17:21:22.434Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36" etag="puyzN88MKD48mWwX1Ew6" version="13.10.9" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">5Vxbd5s4EP41fjQHJG5+TOKm7W52T7ZNN7uPspENG4xckJ24v34FiIskfEkKmDo+PU0YhITmm/k0M1I8gjerl48xWvt/EA+HI6B7LyM4HQFgmACM0n+6t8sljmnlgmUceLxRJfga/MBcqHPpJvBwIjSkhIQ0WIvCOYkiPKeCDMUxeRabLUgojrpGS6wIvs5RqEofA4/6udQFTiX/hIOlX4xs2JP8zgoVjflMEh955Lkmgh9G8CYmhOa/rV5ucJgqr9DL4+fdY3j3ZH/87a/kO/p2/fvDn3+P885uX/NIOYUYR/TNXS+nK7i9T5bjHfCmD+G37+vZj7HJp0Z3hb6wx9THL0lMfbIkEQo/VNLrmGwiD6e96uyqanNHyJoJDSb8D1O647aANpQwkU9XIb+bj5kOJEF0ZH68XUI28RwfaAe5maF4iQ/2V6HIzB+TFabxjj0Y4xDRYCu+HeJ2uCzbVbpmv3B1vwJV/pZbFG74SPcx9oI5ovgL2VCsIFPpPVXisx9Q/HWNMk08M98VdbwgEeUAGGyO18sQJQmHLKExeSq9IW1dmrZewnMqGlscU/xSU5WqT34XQu5InEksmzPJc+WXRuFsfs0nTf3nIWi0fl3R8QVYf0G8x81/D1ytm3+j7sE5dM80HO/+4c9nF/+mF5pVXE5f6jenO8ElhoAZOCdlFa/ZxFkjeKsylk9Ws01ynK0E/kmp6xatgjDV/iccbjFlIzRwGgqDZcQu5gwEHDcTGxsyiJbsyq6uHjJrYWtfh1xniVxnWyrXuQ1U57ZAdQecXYDuC0MuTkOuNCIbpcRrh5SrOQvIQhJnLe3vmzTGYcqCi4Wr63pdVMO7EKYdjJMMqCvWwDDXL/Un7CX/WUpuQ7IMIqFNyO/kL1M8cmEromFLVuL0uCIe4BfBTKZ4gTYhvUM7FpVcHASWfkYIGhdG1VEXKEzUcJBNkIrqVfgwVQOjzvCK31gFnpcvpJi5J5plXaXaXZMgotlErOuRNU37YmtnwqEq1a+o9VRE9vPkCeo3G7QPutK+GpXTeHOhyjfNgSnfUsOLzSwM5kPIh9pXv+GK6i/hOBv12A3hXbAdSELaAfnAoQHgKABceasgukz1m4Oz/4mifkXpOPKu0qpkyvSpCoN5Z9n9IYo8mijCZtXXVGs1aLaQnZxO8hHu0yWs5lgSsnZRMy66yKfJn6rXLKWOTLlkNJE6yvWgdJShX077J2KxE8OBFO87NMNhdzFB3YHNQ17JC+m8s1G5WtdNcL/17/XWsa4B6LgCHKAVa7E0yxS6dTV3InZCFosEd4OwWtIYnM/bv4LPA3kxlV31VJ+HUlRqOaY2qX96ZQBHTYg/J1cb6jNos7qXd5lksG9vpGIDSy92yjhQ43boAAidGowM7N7IQM29B0cGzi9BBparwdrHaokaHEOzax9HXDfOyxSGeZwpmjeceiaLMB3uGs2fllkGcVPWeeEi+3TDJ4dzAcYnhltEWxxO2IopQm3i1G3CETnLsDXX0WsfyTS7IxtHjS1HwEarNHHL/1cuz287vRsGizotx+rALhjwhmwLluYYvcHfSBdptv9OcYamK8YTLYUTbMkw+wIVNGUTwq7aq3fLTt2Ha29XrWZiskXNCKVklZWMUEyLOIiw6Ray2yCsNtvLSGkWkvlT0YTbmfvzwVI9CGrOnI5EsYZEAGoBym4nIDJ0kcOUGtbJFRGxH0fqZk+Mw3BAu1ozTgCHBjL0Zt304ERqynVGJ/LwNpjjJJvlVfret+/bo8BAPApO2vIo2dKhe4E+1RhsnsunfIzoCq3ftyPtSdCbHak7P7Kg3Y4fWZJ1F0dJ2nejsVR36tGN1KD9vPFd8r59yBzIYmQ7LTmRI52Hkc8DXsJaZLpDcqL37UBHqnKyA4GuHAhI52WBKa0epzpQmRGV0Vx3HgSatdO9B1nqOZEvzJpwzGQ3ZLUmEY7OfVSz/fMitgTt+Y9qWk1MJmm9x/2igyd6j3JBblSD3THS6kV6qfvW9o/6PV1ScGf9uNeG+pfrwdLOPgRqQNbzgUd1w3eKEn9GUOxdLgzywdNGHGCvOAAVh7zudrkoyKdPB+ANapHoU16puVwUTGdwKKg1hjuW6F8uBLYxOAjUP0RQtH7uc0BF8HA8rmtWfl9hnaOBhuMVZVz3xjoJdIAmHOsQ++09klMTsuEZzMlFgXMajAFNzdgfokN5w/PkuoCha24tvwBiwc60pH67NphhZY4Hg7JhGwxwoZA4SsC+2WCgYx/KFXs3mF/gb1Pshi9eGZ7BmHo3DGPqg2KYYhqDNhjzRIM5a2nKAiKw0inVN9sLOGiHvdvL686+ZzsWD34QMTPxUOKX+UfNfFL5PaIUx1EmATpUsgyQb4TUtkVaNLpXfe1NX+wjoGy9daNwIvaj5EKv3ubYs7pKJXceZ++NsprfqjLVfPwTDZddVt+uljevvqMOfvgf</diagram></mxfile>
|
1
docs/mqtt-communication-sequence.drawio
Normal file
@ -0,0 +1 @@
|
||||
<mxfile host="app.diagrams.net" modified="2020-12-03T11:07:06.295Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36" etag="ikHYmKMKhruEys4yzSz3" version="13.10.6" type="device"><diagram id="kgpKYQtTHZ0yAKxKKP6v" name="Page-1">5Vpbb6M4FP41SLMPjcBcUh6TtJOt1Gpmt5U62zcnOIk1DkbGNGF+/RpwAGNI6DZ0205f6nNsbHy+79xQDHu23c8ZjDZ3NEDEAGawN+wrAwDLunTFv0yTFhrPsQvFmuFALqoU9/gXkkpTahMcoFhZyCklHEeqcknDEC25ooOM0Z26bEWJemoE10hT3C8h0bWPOOCbQnvpmpX+T4TXm8PJlilntvCwWCriDQzorqayrw17xijlxWi7nyGSGe9gl+K5rx2z5YsxFPI+D9hhMv26Z/4yvTWj8NvjA4DzC7nLMySJvPAUsyBEXNgceAawBQ72dHIj1twj9oyXSF6GpwcLiXtF2TDZklu8QgSHQppGiOGt2IaJGSLV3yvdVODFodBl81YuEwKjGC/ybbNDGVomLMbP6G8UF7TItTQJAxRIqbRpLnBGf5YoZZvqJjrcFzGO9jWVNNkcUfGCLBVL5OylRC9VxV3FhRLxTY0H5UIo+bcud64gEgOJ0gsQAxpiGiIRxSHPz3WnhnvVQIMyvqFrGkJSx+OVdj1Krt7GdlzF2OM2Y7fYeihTu63OsYVRzTXu/np4+G2dwwYqYP+/d3gfxzvcl1rb6WFs3daWdwZb36Br/rR6fCK/5k/2+JuNJ3N4YTmnjZ3dDotsOiF4HQrVgnJOt8JIKAwmWXrOdIQuf542K9pj/iMbjyzHkfI/2eqRK6WrvXw4F9KaUMMy1xXviQIt9/fDLKYJW6LTNOSQrRE/ZsJxO+IMEciFd6vlTAuA8tHvGacrpgAHjDLPK/8chTlialSbNBv7F7eTW9YLisYplq3mRtBkWnF9bSOBO0xry6RDdt6mZHCqyp3v1VgPTGW9GBRvUDG/tPwrnEFPFiJLEAH+dMHEaM1L2tXcI2PgLVyImtmebviWSKpD6S1LwcY8/jTdaIuDINtD5AER/mGVF/Tg1hFlyopZPmyUabTuBEf8vjNMXZiCe56nQlBI/5XWhyV0tYoRb4B5Hvj0xAGD4IFOk9UKMR3JLwHK8v1NYIBZ1t1AjopRxOgCLjDBPP3jd4HbdsBZ4S7jy8j16nEKqIcAdcMByTF+N1VFC7D9klYnfm57sDxVVthDVXCXmq2jZEFwvDFmwJiIU833P/iCgx7R4Iy1kc4Eq2elAvRK5Vjl0yiZzl+5jNUiV6te+xYnJWm7NuooTs4WMzQat31YuQmjhH/k9vFEPDre0LhAxehN28dW1N7Px5UThu3mXN/20e8X552hTG1rpi4+pTQrrVi4RlaBNUspQnAUo8KcuY8sCU3EgdPdBnN0H8E87O0YjNRi6wy8tRofBS3QYkvQYszBeNvjs8cgnbjpXyqduOkd5s/ai/dNZr3b7kFyl9NIOWUp9dLc5XbVZG+Uu/wWNn3OVtY76ucfs5M1NfQEV+4Qh6Iq/SQt6UnYBupIgTMCvrLvoV15A1z1inLQGB+K1/2RB3XfP8hFkLetckEV5XMprUvNOH/IGgIe0/KVtOH7zhuljWNV38keqIN2rySV20wbzSqhb9rwrPForNJTlHmjselXfw26Dt0EtdXTnzOTWMdLxo+ZSvQanSGesDD7xKF/AP0kyeU0kgNllwvbG5ljNb1Y1sgFr08xQqx+aFIsr36uY1//Cw==</diagram></mxfile>
|
BIN
docs/thesis.pdf
Normal file
68
docs/thesis/Makefile
Normal file
@ -0,0 +1,68 @@
|
||||
DOCUMENT=thesis
|
||||
#MODE=-interaction=batchmode
|
||||
|
||||
all: clean xelatex
|
||||
echo
|
||||
|
||||
xelatex: compile_xelatex
|
||||
mv $(DOCUMENT)-xelatex.pdf ../pdf/$(DOCUMENT).pdf
|
||||
|
||||
compile_xelatex:
|
||||
xelatex $(MODE) $(DOCUMENT)
|
||||
bibtex $(DOCUMENT)
|
||||
xelatex $(MODE) $(DOCUMENT)
|
||||
xelatex $(MODE) $(DOCUMENT)
|
||||
mv $(DOCUMENT).pdf $(DOCUMENT)-xelatex.pdf
|
||||
|
||||
pdflatex: compile_pdflatex
|
||||
mv $(DOCUMENT)-pdflatex.pdf ../pdf/$(DOCUMENT).pdf
|
||||
|
||||
compile_pdflatex:
|
||||
pdflatex $(MODE) $(DOCUMENT)
|
||||
bibtex $(DOCUMENT)
|
||||
pdflatex $(MODE) $(DOCUMENT)
|
||||
pdflatex $(MODE) $(DOCUMENT)
|
||||
mv $(DOCUMENT).pdf $(DOCUMENT)-pdflatex.pdf
|
||||
|
||||
lualatex: compile_lualatex
|
||||
mv $(DOCUMENT)-lualatex.pdf ../pdf/$(DOCUMENT).pdf
|
||||
|
||||
compile_lualatex:
|
||||
lualatex $(MODE) $(DOCUMENT)
|
||||
bibtex $(DOCUMENT)
|
||||
lualatex $(MODE) $(DOCUMENT)
|
||||
lualatex $(MODE) $(DOCUMENT)
|
||||
mv $(DOCUMENT).pdf $(DOCUMENT)-lualatex.pdf
|
||||
|
||||
switch_to_hungarian:
|
||||
sed -i "s|^\\\input{include/thesis-en}|%\\\input{include/thesis-en}|" $(DOCUMENT).tex
|
||||
sed -i "s|^%\\\input{include/thesis-hu}|\\\input{include/thesis-hu}|" $(DOCUMENT).tex
|
||||
|
||||
test_hu:
|
||||
${MAKE} clean compile_xelatex
|
||||
${MAKE} clean compile_pdflatex
|
||||
${MAKE} clean compile_lualatex
|
||||
mv $(DOCUMENT)-xelatex.pdf ../pdf/$(DOCUMENT)-xelatex-hu.pdf
|
||||
mv $(DOCUMENT)-pdflatex.pdf ../pdf/$(DOCUMENT)-pdflatex-hu.pdf
|
||||
mv $(DOCUMENT)-lualatex.pdf ../pdf/$(DOCUMENT)-lualatex-hu.pdf
|
||||
|
||||
switch_to_english:
|
||||
sed -i "s|^\\\input{include/thesis-hu}|%\\\input{include/thesis-hu}|" $(DOCUMENT).tex
|
||||
sed -i "s|^%\\\input{include/thesis-en}|\\\input{include/thesis-en}|" $(DOCUMENT).tex
|
||||
|
||||
test_en:
|
||||
${MAKE} switch_to_english
|
||||
${MAKE} clean compile_xelatex
|
||||
${MAKE} clean compile_pdflatex
|
||||
${MAKE} clean compile_lualatex
|
||||
mv $(DOCUMENT)-xelatex.pdf ../pdf/$(DOCUMENT)-xelatex-en.pdf
|
||||
mv $(DOCUMENT)-pdflatex.pdf ../pdf/$(DOCUMENT)-pdflatex-en.pdf
|
||||
mv $(DOCUMENT)-lualatex.pdf ../pdf/$(DOCUMENT)-lualatex-en.pdf
|
||||
${MAKE} switch_to_hungarian
|
||||
|
||||
test: test_hu test_en
|
||||
echo
|
||||
|
||||
clean:
|
||||
echo Cleaning temporary files...
|
||||
rm -f *.aux *.dvi *.thm *.lof *.log *.lot *.fls *.out *.toc *.bbl *.blg
|
190
docs/thesis/bib/mybib.bib
Normal file
@ -0,0 +1,190 @@
|
||||
@misc{kubernetes,
|
||||
title = {What is Kubernetes?},
|
||||
url = {https://kubernetes.io/docs/concepts/overview/what-is-kubernetes/},
|
||||
note = {Megtekintve: 2020.11.28},
|
||||
}
|
||||
|
||||
@misc{kubernetes-dashboard,
|
||||
title = {Kubernetes Web UI (Dashboard)},
|
||||
url = {https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/},
|
||||
note = {Megtekintve: 2020.11.28},
|
||||
}
|
||||
|
||||
@misc{docker,
|
||||
title = {Docker overview},
|
||||
url = {https://docs.docker.com/get-started/overview/},
|
||||
note = {Megtekintve: 2020.11.28},
|
||||
}
|
||||
|
||||
@misc{grafana,
|
||||
title = {What is Grafana?},
|
||||
url = {https://grafana.com/docs/grafana/latest/getting-started/},
|
||||
note = {Megtekintve: 2020.11.29},
|
||||
}
|
||||
|
||||
@misc{kibana,
|
||||
title = {What is Kibana?},
|
||||
url = {https://www.elastic.co/what-is/kibana},
|
||||
note = {Megtekintve: 2020.11.29},
|
||||
}
|
||||
|
||||
@misc{git,
|
||||
title = {A Git hivatalos oldalának About szekciója},
|
||||
url = {https://git-scm.com/about/branching-and-merging},
|
||||
note = {Megtekintve: 2020.11.30},
|
||||
}
|
||||
|
||||
@misc{trello,
|
||||
title = {What is Trello?},
|
||||
url = {https://trello.com/en/about},
|
||||
note = {Megtekintve: 2020.11.30},
|
||||
}
|
||||
|
||||
@misc{vs,
|
||||
title = {Welcome to the Visual Studio IDE},
|
||||
url = {https://docs.microsoft.com/en-us/visualstudio/get-started/visual-studio-ide?view=vs-2019},
|
||||
note = {Megtekintve: 2020.11.30},
|
||||
}
|
||||
|
||||
@misc{vs-code,
|
||||
title = {User Interface},
|
||||
url = {https://code.visualstudio.com/docs/getstarted/userinterface},
|
||||
note = {Megtekintve: 2020.11.30},
|
||||
}
|
||||
|
||||
@misc{nlog,
|
||||
title = {Welcome to NLog!},
|
||||
url = {https://nlog-project.org/},
|
||||
note = {Megtekintve: 2020.11.30},
|
||||
}
|
||||
|
||||
@misc{jwt,
|
||||
title = {Introduction to JSON Web Tokens},
|
||||
url = {https://jwt.io/introduction/},
|
||||
note = {Megtekintve: 2020.12.02},
|
||||
}
|
||||
|
||||
@misc{automapper,
|
||||
title = {What is AutoMapper?},
|
||||
url = {https://automapper.org/},
|
||||
note = {Megtekintve: 2020.11.30},
|
||||
}
|
||||
|
||||
@misc{react,
|
||||
title = {Getting Started with React.js},
|
||||
url = {https://reactjs.org/docs/getting-started.html},
|
||||
note = {Megtekintve: 2020.11.31},
|
||||
}
|
||||
|
||||
@misc{react-context,
|
||||
title = {When to Use Context},
|
||||
url = {https://reactjs.org/docs/context.html#when-to-use-context},
|
||||
note = {Megtekintve: 2020.11.31},
|
||||
}
|
||||
|
||||
@misc{material,
|
||||
title = {Design Guidelines},
|
||||
url = {https://material.io/resources/get-started#design},
|
||||
note = {Megtekintve: 2020.11.31},
|
||||
}
|
||||
|
||||
@misc{material-ui,
|
||||
title = {Quick start},
|
||||
url = {https://material-ui.com/getting-started/usage/#quick-start},
|
||||
note = {Megtekintve: 2020.11.31},
|
||||
}
|
||||
|
||||
@misc{apexcharts,
|
||||
title = {What is ApexCharts?},
|
||||
url = {https://apexcharts.com/},
|
||||
note = {Megtekintve: 2020.11.31},
|
||||
}
|
||||
|
||||
@misc{google-map-react,
|
||||
title = {Google Map React},
|
||||
url = {https://www.npmjs.com/package/google-map-react},
|
||||
note = {Megtekintve: 2020.11.31},
|
||||
}
|
||||
|
||||
@misc{nswag,
|
||||
title = {NSwag: The Swagger/OpenAPI toolchain for .NET, ASP.NET Core and TypeScript},
|
||||
url = {https://github.com/RicoSuter/NSwag#nswag-the-swaggeropenapi-toolchain-for-net-aspnet-core-and-typescript},
|
||||
note = {Megtekintve: 2020.12.01},
|
||||
}
|
||||
|
||||
@misc{nswag-studio,
|
||||
title = {NSwagStudio},
|
||||
url = {https://github.com/RicoSuter/NSwag/wiki/NSwagStudio},
|
||||
note = {Megtekintve: 2020.12.01},
|
||||
}
|
||||
|
||||
@misc{swagger-ui,
|
||||
title = {Swagger UI},
|
||||
url = {https://swagger.io/tools/swagger-ui/},
|
||||
note = {Megtekintve: 2020.12.01},
|
||||
}
|
||||
|
||||
@misc{hmacsha512,
|
||||
title = {Az HMACSHA512 dokumentációja},
|
||||
url = {https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.hmacsha512},
|
||||
note = {Megtekintve: 2020.12.02},
|
||||
}
|
||||
|
||||
@misc{nuget,
|
||||
title = {An introduction to NuGet},
|
||||
url = {https://docs.microsoft.com/en-us/nuget/what-is-nuget},
|
||||
note = {Megtekintve: 2020.12.04},
|
||||
}
|
||||
|
||||
@misc{signalr,
|
||||
title = {Introduction to ASP.NET Core SignalR},
|
||||
url = {https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction?view=aspnetcore-5.0},
|
||||
note = {Megtekintve: 2020.12.04},
|
||||
}
|
||||
|
||||
@misc{mqttnet-github,
|
||||
title = {MQTTnet},
|
||||
url = {https://github.com/chkr1011/MQTTnet#mqttnet},
|
||||
note = {Megtekintve: 2020.12.04},
|
||||
}
|
||||
|
||||
@misc{mqttnet-winforms,
|
||||
title = {MQTTnet.TestApp.WinForm},
|
||||
url = {https://github.com/SeppPenner/MQTTnet.TestApp.WinForm#mqttnettestappwinform},
|
||||
note = {Megtekintve: 2020.12.07},
|
||||
}
|
||||
|
||||
@misc{mqttnet-examples,
|
||||
title = {Az MQTT.NET github oldalán található példák},
|
||||
url = {https://github.com/chkr1011/MQTTnet/wiki/Examples},
|
||||
note = {Megtekintve: 2020.12.07},
|
||||
}
|
||||
|
||||
@misc{dockerfile,
|
||||
title = {Dockerfile reference},
|
||||
url = {https://docs.docker.com/engine/reference/builder/},
|
||||
note = {Megtekintve: 2020.12.08},
|
||||
}
|
||||
|
||||
@misc{docker-compose,
|
||||
title = {Overview of Docker Compose},
|
||||
url = {https://docs.docker.com/compose/},
|
||||
note = {Megtekintve: 2020.12.08},
|
||||
}
|
||||
|
||||
|
||||
@thesis{birdnetes-tdk,
|
||||
author = {Torma Kristóf és Pünkösdi Marcell},
|
||||
institution = {Budapesti Műszaki és Gazdaságtudományi Egyetem},
|
||||
title = {Madárhang azonosító és riasztó felhő-natív rendszer},
|
||||
type = {tdk},
|
||||
year = {2020},
|
||||
}
|
||||
|
||||
@thesis{birdnetes-thesis,
|
||||
author = {Nagy Kristóf},
|
||||
institution = {Budapesti Műszaki és Gazdaságtudományi Egyetem},
|
||||
title = {Tömeges gép-gép kommunikáció mezőgazdasági alkalmazása},
|
||||
type = {thesis},
|
||||
year = {2020},
|
||||
}
|
63
docs/thesis/content/abstract.tex
Normal file
@ -0,0 +1,63 @@
|
||||
\pagenumbering{roman}
|
||||
\setcounter{page}{1}
|
||||
|
||||
\selecthungarian
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
% Abstract in Hungarian
|
||||
%----------------------------------------------------------------------------
|
||||
\chapter*{Kivonat}\addcontentsline{toc}{chapter}{Kivonat}
|
||||
|
||||
Adott egy tanszéken fejlesztett felhőalapú elosztott rendszer, melynek eszközei madárhangok azonosítására képesek.
|
||||
Ha a rendszer úgy észleli, hogy az egyik általa vezérelt eszköz mikrofonja felvételén madárhang található,
|
||||
akkor riasztást kezdeményez az eszközön ezzel elijesztve a madarat ezáltal megóvva a növényzetet.
|
||||
|
||||
A rendszernek több kisebb komponense van, amelyek rengeteg adatot dolgoznak fel és nincs jelenleg egy olyan egységes grafikus felület, ahol a rendszer teljes állapotát
|
||||
át lehetne tekinteni, ahol a feldolgozott adatokat vizualizálni lehetne.
|
||||
|
||||
A piacon létezik már több olyan szoftver csomag, amely hasonló problémákra próbál megoldást nyújtani, de ezek sem mindig
|
||||
tudják kielégíteni azokat a speciális igényeket, amelyek egy ilyen rendszernél felmerülnek.
|
||||
|
||||
Jelen szakdolgozat célja egy olyan vizualizációs megoldás bemutatása, amelynek segítségével a rendszer könnyedén áttekinthető
|
||||
és kezelhető. A tanszéki rendszer által kezelt eszközök a felületen is vezérelhetők
|
||||
és azok működéséről különböző statisztikákat felhasználva egyszerűen értelmezhető diagramok generálódnak.
|
||||
|
||||
A backend megvalósítására az ASP.NET Core-t választottam, mely platformfüggetlen megoldást nyújt a web kérések kiszolgálására.
|
||||
A frontend-et a React.js használatával készítettem, mely segítségével egyszerűen és gyorsan lehet reszponzív felhasználói felületeket készíteni.
|
||||
Dolgozatomban bemutatom a tanszéken fejlesztett rendszert, a mikroszolgáltatások vizualizálásának alternatíváit,
|
||||
ismertetem az általam választott technológiákat és a készített alkalmazás felépítését.
|
||||
|
||||
\vfill
|
||||
\selectenglish
|
||||
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
% Abstract in English
|
||||
%----------------------------------------------------------------------------
|
||||
\chapter*{Abstract}\addcontentsline{toc}{chapter}{Abstract}
|
||||
|
||||
There is a department developed cloud-based distributed system whose devices are capable of identifying bird sounds.
|
||||
If the system detects a bird's voice on the recording of a microphone on one of the devices, it will trigger
|
||||
an alarm on the device scaring the bird away thereby protecting the vegetation.
|
||||
|
||||
The system has several smaller components that process a lot of data and currently there is no unified graphical user interface where the overall state of the system
|
||||
could be reviewed, where the processed data could be visualized.
|
||||
|
||||
There are already several software packages on the market that try to solve similar problems,
|
||||
however they aren't always able to meet the special needs that arise with such a system.
|
||||
|
||||
The purpose of this thesis is to present a visualization solution that allows the users to easily review
|
||||
and manage the system. The devices maintained by the department developed system can be controlled on the interface
|
||||
and easy-to-understand diagrams are generated using statistics about their operation.
|
||||
|
||||
I chose ASP.NET Core as the backend framework, which provides a platform-independent solution for serving web requests.
|
||||
The frontend was created using React.js, which allows for an easy and quick way to create responsive user interfaces.
|
||||
In my thesis I present the system developed at the department, the alternatives of visualization of microservices,
|
||||
I describe the technologies I have chosen and the structure of the application I have created.
|
||||
|
||||
\vfill
|
||||
\selectthesislanguage
|
||||
|
||||
\newcounter{romanPage}
|
||||
\setcounter{romanPage}{\value{page}}
|
||||
\stepcounter{romanPage}
|
87
docs/thesis/content/appendices.tex
Normal file
@ -0,0 +1,87 @@
|
||||
%----------------------------------------------------------------------------
|
||||
\appendix
|
||||
%----------------------------------------------------------------------------
|
||||
\chapter*{\fuggelek}\addcontentsline{toc}{chapter}{\fuggelek}
|
||||
\setcounter{chapter}{\appendixnumber}
|
||||
%\setcounter{equation}{0} % a fofejezet-szamlalo az angol ABC 6. betuje (F) lesz
|
||||
\numberwithin{equation}{section}
|
||||
\numberwithin{figure}{section}
|
||||
\numberwithin{lstlisting}{section}
|
||||
%\numberwithin{tabular}{section}
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\section{A Docker image készítéséhez használt fájlok}
|
||||
%----------------------------------------------------------------------------
|
||||
\begin{lstlisting}[style=dockerfile, caption=A Dockerfile tartalma, label=lst:dockerfile]
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
|
||||
WORKDIR /app
|
||||
RUN apt-get update && apt-get install -y curl
|
||||
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash -
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
|
||||
RUN apt-get update && apt-get install -y curl
|
||||
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash -
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
WORKDIR /src
|
||||
COPY ["Birdmap.API/Birdmap.API.csproj", "Birdmap.API/"]
|
||||
COPY ["Birdmap.BLL/Birdmap.BLL.csproj", "Birdmap.BLL/"]
|
||||
COPY ["Birdmap.Common/Birdmap.Common.csproj", "Birdmap.Common/"]
|
||||
COPY ["Birdmap.DAL/Birdmap.DAL.csproj", "Birdmap.DAL/"]
|
||||
RUN dotnet restore "Birdmap.API/Birdmap.API.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/Birdmap.API"
|
||||
RUN dotnet build "Birdmap.API.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "Birdmap.API.csproj" -c Release -o /app/publish
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Birdmap.API.dll"]
|
||||
\end{lstlisting}
|
||||
|
||||
\begin{lstlisting}[style=docker-compose, caption=A docker-compose.yml fájl tartalma, label=lst:docker-compose]
|
||||
version: '3.4'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: "mcr.microsoft.com/mssql/server:2019-latest"
|
||||
environment:
|
||||
- ACCEPT_EULA=Y
|
||||
- SA_PASSWORD=RPSsql12345
|
||||
|
||||
birdmap.api:
|
||||
image: ${DOCKER_REGISTRY-}birdmapapi
|
||||
ports:
|
||||
- "8000:80"
|
||||
- "8001:443"
|
||||
volumes:
|
||||
- ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro
|
||||
- ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Birdmap.API/Dockerfile
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
...
|
||||
- Birdmap_LocalDbConnectionString=Data Source=db;Initial Catalog=birdmap;User=sa;Password=RPSsql12345
|
||||
- Birdmap_Defaults__Users__0__Name=admin
|
||||
- Birdmap_Defaults__Users__0__Password=pass
|
||||
- Birdmap_Defaults__Users__0__Role=Admin
|
||||
- Birdmap_Defaults__Users__1__Name=user
|
||||
- Birdmap_Defaults__Users__1__Password=pass
|
||||
- Birdmap_Defaults__Users__1__Role=User
|
||||
- Birdmap_Defaults__Services__Local-Database=https://localhost:8001/health
|
||||
- Birdmap_Defaults__Services__KMLabz-Service=https://birb.k8s.kmlabz.com/devices
|
||||
- Birdmap_UseDummyServices=true
|
||||
- Birdmap_ServicesBaseUrl=https://birb.k8s.kmlabz.com/
|
||||
- Birdmap_Mqtt__BrokerHostSettings__Host=localhost
|
||||
- Birdmap_Mqtt__BrokerHostSettings__Port=1883
|
||||
- Birdmap_Mqtt__ClientSettings__Id=ASP.NET Core client
|
||||
- Birdmap_Mqtt__ClientSettings__Username=username
|
||||
- Birdmap_Mqtt__ClientSettings__Password=password
|
||||
- Birdmap_Mqtt__ClientSettings__Topic=devices/output
|
||||
\end{lstlisting}
|
225
docs/thesis/content/birdmap-backend.tex
Normal file
@ -0,0 +1,225 @@
|
||||
%----------------------------------------------------------------------------
|
||||
\chapter{Szerver oldal}
|
||||
\label{chapt:birdmap-backend}
|
||||
%----------------------------------------------------------------------------
|
||||
Ebben a fejezetben bemutatom a szerveroldal architektúráját, felépítését. Ismertetem a különböző szoftver komponensek feladatát.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\section{Architektúra}
|
||||
%----------------------------------------------------------------------------
|
||||
A szerveroldal fejlesztésénél a három rétegú architektúrát alkalmaztam, melynek lényege, hogy az alkalmazást logikailag három elkülönülő részre bontjuk:
|
||||
\begin{itemize}
|
||||
\item \textbf{Adatelérési réteg}. Ez a rész felel a tárolt entitások modell definícióiért, illetve azoknak a kiolvasásáért, tárolásáért egy adatbázisból vagy fájlrendszerből.
|
||||
\item \textbf{Megjelenítési réteg}. Ezen réteg feladata a kliensoldal közvetlen kiszolgálása. Bármilyen irányú kommunikáció a kliensek felé ezen a rétegen keresztül történik.
|
||||
\item \textbf{Üzleti logikai réteg}. Minden, ami nem a közvetlen kommunikációért, megjelenítésért vagy adat elérésért, tárolásért felel, az ide kerül.
|
||||
A fenti két réteg között helyezkedik el és feladata a különböző folyamatok értékelése és futtatása, valamint az adatok feldolgozása.
|
||||
\end{itemize}
|
||||
|
||||
Az ASP.NET Core beépítetten támogatja a dependency injection-t, mely a \verb+Startup+ osztály \verb+ConfigureServices+ metódusával konfigurálható.
|
||||
Én minden rétegbe tettem egy ilyen \verb+Startup+ osztályt, hogy azok feleljenek a saját szolgáltatásaik konfigurálásáért és regisztrálásáért.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\section{Adatelérési réteg}
|
||||
%----------------------------------------------------------------------------
|
||||
Az adatelérést az Entity Framework Core segítségével oldottam meg. Telepítettem egy MSSQL adatbázis szervert a számítógépemre, melynek csatlakozási paramétereivel
|
||||
a \verb+Startup+ osztályban felkonfigurálom az EF Core által nyújtott \verb+DbContext+ saját leszármazott változatát.
|
||||
Így csak az entitások elkészítése és azok alapértelmezett értékeinek az adatbázisba való feltöltése marad hátra.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Entitások}
|
||||
%----------------------------------------------------------------------------
|
||||
Mivel az adatok nagy részét külső szolgáltatások fogják nyújtani, így lokálisan összesen két entitás létrehozására volt szükség.
|
||||
Az egyik a \verb+User+, mely az alkalmazás felhasználóinak adatait tárolja.
|
||||
A másik a \verb+Service+, mely a külső szolgáltatások adatainak tárolását szolgálja, amelyeket azért tárolok az adatbázisban és nem mondjuk a konfigurációs fájlban,
|
||||
mert szerettem volna, hogyha a kezelőfelületen lehetne őket szerkeszteni, törölni.
|
||||
|
||||
\begin{lstlisting}[style=csharp, caption=A User és a Service modell]
|
||||
public record User
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public byte[] PasswordHash { get; set; }
|
||||
public byte[] PasswordSalt { get; set; }
|
||||
|
||||
public Roles Role { get; set; }
|
||||
|
||||
public bool IsFromConfig { get; set; }
|
||||
}
|
||||
|
||||
public record Service
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public Uri Url { get; set; }
|
||||
|
||||
public bool IsFromConfig { get; set; }
|
||||
}
|
||||
\end{lstlisting}
|
||||
|
||||
Az alkalmazás használata szempontjából a felhasználók két csoportba oszlanak.
|
||||
Vannak adminisztrátor és sima felhasználók, utóbbi csak az adatok olvasására, míg előbb azok módosítására is jogosult.
|
||||
A \verb+Role+ mező ennek a megkülönböztetsnek a jelzője.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Seedelés}
|
||||
\label{subsect:seeding}
|
||||
%----------------------------------------------------------------------------
|
||||
Az alkalmazás konfigurációs fájljából meg lehet adni alapértelmezett felhasználókat és szolgáltatásokat.
|
||||
Ezeknek megkülönböztetésére szolgál az entitások \verb+IsFromConfig+ mezője.
|
||||
A szerver indítása legelején, megvizsgálja, hogy létezik-e az adatbázis és ha igen kitöröl minden olyan entitást ahol az \verb+IsFromConfig+ mező igaz.
|
||||
Majd hozzáadja az újonnan beolvasott értékeket.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\section{Üzleti logikai réteg}
|
||||
%----------------------------------------------------------------------------
|
||||
Ebben a rétegben található meg a szerver legtöbb szolgáltatása. It vannak implementálva a Birbnetes Command and Control és Input komponensekkel kommunikáló szolgáltatások is,
|
||||
melyeket azok OpenAPI leírói alapján az NSwag Studio \cite{nswag-studio} alkalmazással generáltam. Az OpenAPI a klienseken kívül definiálja még az azok által használt modelleket is.
|
||||
A Command and Control által használt \verb+Device+ modell tartalmazza annak egyedi azonosítóját, státuszát, koordinátáit és a használt szenzorok listáját,
|
||||
melyeknek szintén van egy modellje \verb+Sensor+ néven. Ennek szintén van azonosítója és státusza. Az Input szolgáltatásnak is van saját modellje,
|
||||
amely a hangüzenetek metaadatait reprezentálja. Többek között tartalmazza a kihelyezett eszköz egyedi azonosítóját és a hangüzenet keltének dátumát.
|
||||
|
||||
Ugyan itt található meg a \verb+User+ és \verb+Service+ entitások létrehozásáért, olvasásáért, szerkesztéséért és törléséért felelős szolgáltatások is.
|
||||
Valamint itt található még az autentikációért felelős szolgáltatás is. A felhasználók jelszavainak tárolására a HMAC (Hash-based Message Authentication Code) algoritmust,
|
||||
pontosabban annak a \verb+HMACSHA512+ \cite{hmacsha512} C\# implementációját használtam.
|
||||
|
||||
Minden jelszóhoz generálok egy egyedi kulcsot és azzal egy hash-t, majd ezeket tárolom a \verb+User+ modell \verb+PasswordSalt+ és \verb+PasswordHash+ mezőiben.
|
||||
Amikor egy felhasználó be akar jelentkezni először megvizsgálom, hogy egyáltalán létezik-e az adatbázisban az adott nevű felhasználó,
|
||||
ha igen, akkor a megadott jelszóból az imént említett folyamattal generált kulcsot és hash-t összehasonlítom az adatbázisban tárolttal.
|
||||
|
||||
Azért hasznos ily módon, és nem mondjuk egyszerű szöveges formában tárolni a felhasználók jelszavát, mert így a felhasználón kívül senki sem tudja, hogy mi volt az eredeti jelszava,
|
||||
az algoritmus egyirányú volta miatt\footnotemark. Ha véletlenül rossz kezekbe kerülne az adatbázis tartalma, akkor sem fognak tudni bejeletkezni a felhasználók adataival.
|
||||
|
||||
\footnotetext{Generálni egyszerű és gyors. Visszafejteni közel lehetetlen.}
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Kommunikációs Szolgáltatások}
|
||||
%----------------------------------------------------------------------------
|
||||
A kliensoldal frissítésére több megoldás is létezik. Például bizonyos időközönként lehetne kéréseket indítani a szerver felé a friss adatok megszerzéséért.
|
||||
Egy másik megoldás a SignalR használata, amellyel a klienseket eseményvezérelten lehet értesíteni, megvalósítja a kétoldalú kommunikációt.
|
||||
Így a kliensek csak akkor indítanak kéréseket amikor az adat tényleg változott. Ezzel a technológiával oldottam meg például, hogy az eszközök állapotainak változására
|
||||
frissüljön a felület.
|
||||
|
||||
Egy másik szerveroldalon használt szolgáltatás a Birbnetes MQTT kommunikációért felelős szolgáltatás,
|
||||
mely felregisztrál a \ref{subsect:birdnetes-ai-service}-as alfejezetben bemutatott AI Service által publikált üzenetekre.
|
||||
Ezekben az üzenetekben található a hanganyagok egyedi azonosítója, illetve azok seregélytől való származásának valószínűsége.
|
||||
Ha a szolgáltatás kap egy ilyen üzenetet akkor lekérdezi a \ref{subsect:birdnetes-input-service}-es alfejezetben bemutatott Input Service-től
|
||||
a hanganyag azonosítójához tartozó metaadatokat.
|
||||
Ezekből felhasználva a kihelyezett eszköz azonosítóját, a hanganyag beérkezésének dátumát és az említett valószínűséget új üzenetek készülnek, melyeket egy pufferben tárolódnak.
|
||||
Ezt a folyamatot a \ref{fig:birdmap-mqtt-service}-es ábra szemlélteti.
|
||||
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=150mm, keepaspectratio]{figures/mqtt-communication-sequence.png}
|
||||
\caption{A Birdmap MQTT szolgáltatásának szekvenciája}
|
||||
\label{fig:birdmap-mqtt-service}
|
||||
\end{figure}
|
||||
|
||||
A puffer tartalmát másodperces gyakorisággal elküldöm a klienseknek a SignalR segítségével.
|
||||
Azért van szükség a puffer használatára, mert az MQTT-n érkezett üzenetek gyakorisága akár milliszekundum nagyságrendű is lehet.
|
||||
Míg a szerver képes is az üzeneteket feldolgozni, ha ezeket rögtön tovább küldeném a kliensek felé, azok nem biztos, hogy képesek lennének rá.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\section{Megjelenítési réteg}
|
||||
%----------------------------------------------------------------------------
|
||||
A fejezet elején említett \verb+Startup+ osztály ebben a rétegben található, itt kerülnek az egyes szolgáltatások regisztrálásra.
|
||||
Itt történik a \ref{subsect:seeding} fejezetben leírt adatbázis seedelése is.
|
||||
|
||||
Többek között a naplózás is itt kerül inicializálásra, mely az NLog saját konfigurációs fájljával történik.
|
||||
Meg lehet adni különböző szűrőket és kimeneteket, amellyel szelektálni lehet, hogy az egyes naplózott események hova kerüljenek.
|
||||
Például az MQTT szolgáltatás napló bejegyzéseit a \ref{lst:nlog-config} lista alapján szűrtem.
|
||||
Minden \verb+Debug+ szinttől nagyobb és \verb+Error+ szinttől kisebb bejegyzés, mely tartalmazza az \verb+Mqtt+ kulcsszót az \verb+mqttFile+ azonosítójú fájlba kerül.
|
||||
|
||||
\begin{lstlisting}[style=xml, caption=Az NLog.config fájl egy részlete, label=lst:nlog-config]
|
||||
<targets>
|
||||
...
|
||||
<target xsi:type="File" name="mqttFile" fileName="..." layout="..." />
|
||||
...
|
||||
</targets>
|
||||
|
||||
<rules>
|
||||
...
|
||||
<logger name="*.*Mqtt*.*" minlevel="Trace" maxlevel="Warning" writeTo="mqttFile"
|
||||
final="true"/>
|
||||
...
|
||||
</rules>
|
||||
\end{lstlisting}
|
||||
|
||||
A \verb+Startup+ osztály másik metódusa a \verb+Configure+, mellyel a HTTP kérések csővezetéke konfigurálható.
|
||||
Azaz, hogy egy kérés-t milyen sorrendben dolgozzák fel a regisztrált szolgáltatások.
|
||||
A szerveroldali kivételkezelésre szánt szolgáltatás, az \verb+ExceptionHandlerMiddleware+ is itt van használva,
|
||||
amely elkap minden kivételt, amit a csővezeték további részei dobtak és JSON formátumban visszaadja azokat a kliensnek.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Swagger}
|
||||
\label{subsect:backend-swagger}
|
||||
%----------------------------------------------------------------------------
|
||||
Az NSwag \cite{nswag} szoftvercsomag segítségével regisztrálok egy szolgáltatást,
|
||||
mely a szerveroldalon található kontrollereket felhasználva generál egy OpenAPI specifikációt és annak egy Swagger UI \cite{swagger-ui} felületet,
|
||||
ahol a végpontok kipróbálhatóak, tesztelhetőek kliensoldal nélkül is.
|
||||
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=150mm, keepaspectratio]{figures/swagger-ui.png}
|
||||
\caption{Az alkalmazásom Swagger felülete}
|
||||
\label{fig:swagger-ui}
|
||||
\end{figure}
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Kontrollerek}
|
||||
%----------------------------------------------------------------------------
|
||||
A kontrollerek határozzák meg, hogy a szerveroldalon milyen végpontokat, milyen paraméterekkel lehet meghívni, ahhoz milyen jogosultságok kellenek.
|
||||
|
||||
\begin{lstlisting}[style=csharp, caption=Az eszköz kontroller és annak "online" végpontja, label=lst:devices-controller]
|
||||
[Authorize(Roles = "User, Admin")]
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class DevicesController : ControllerBase
|
||||
{
|
||||
[Authorize(Roles = "Admin")]
|
||||
[HttpPost, Route("online")]
|
||||
public async Task<IActionResult> Onlineall()
|
||||
{
|
||||
...
|
||||
}
|
||||
...
|
||||
}
|
||||
\end{lstlisting}
|
||||
|
||||
A jogosultságok kezelését a JSON Web Token-ekkel oldottam meg. A felhasználó bejelentkezéskor kap egy ilyen token-t,
|
||||
amelyben tárolom a hozzá tartozó szerepet. A \ref{lst:devices-controller}-as listában látszik, hogy hogyan használom ezeket a szerepeket.
|
||||
A \verb+DevicesController+ végpontjait alapértelmezetten \verb+User+ és \verb+Admin+ jogosultságú felhasználó hívhatja, az "api/devices/online" végpontot azonban csak \verb+Admin+ jogosultságú.
|
||||
Hasonlóképpen oldottam meg ezt a többi kontrollernél is. A \verb+User+ felhasználók csak olyan végpontokat hívhat, mely kizárólag az állapotok olvasásával jár.
|
||||
Az \verb+Admin+ felhasználók hívhatnak bármilyen végpontot.
|
||||
|
||||
A szerveroldalon négy különböző kontroller található, melyek mindegyikének alapvető feladata az üzleti logikát megvalósító szolgáltatások használata, a működés naplózás,
|
||||
illetve az imént említett végpontok autorizálása és kiszolgálása. Ezeken kívül a kontrollerek speciális feladata a következő:
|
||||
\begin{itemize}
|
||||
\item Az \textbf{AuthController} felel a felhasználók bejelentkezésének lebonyolításáért, a JSON Web Token elkészítéséért. Az \verb+[Authorize]+ helyett itt az \verb+[AllowAnonymous]+ attribútum van használva, mellyel azt lehet jelezni, hogy a végpont bejelentkezés nélkül is hívható.
|
||||
\item A \textbf{ServiceController} felel az alkalmazás által használt külső szolgáltatások állapotának lekérdezhetőségéért. Ilyenek például a Birbnetes rendszer vagy az MQTT szolgáltatás állapota.
|
||||
\item A \textbf{DevicesController} felel a Command and Control mikroszolgáltatással való kommunikáció megvalósításáért, illetve a SignalR használatáért. Ha egy felhasználó valamelyik végpontot használva változtat valamelyik eszköz állapotán, akkor a kontroller jelez erről a klienseknek.
|
||||
\item A \textbf{LogController} felel azért, hogy az \verb+Admin+ jogosultságú felhasználók letölthessék a szerveroldalon készült naplófájlokat.
|
||||
\end{itemize}
|
||||
|
||||
Az adatbázisból érkező adatok gyakran túl sok vagy túl kevés információt tartalmaznak ahhoz, hogy kiolvasás után rögtön elküldjem a kliensoldalnak.
|
||||
Például amikor a felhasználó bejelentkezik a kiolvasott \verb+User+ objektum tartalmazza annak jelszavát (hash-elt formában), viszont nem tartalmazza az autorizációhoz használt token adatait.
|
||||
Ennek a megoldására adatátviteli objektumokat hoztam létre, melyek csak azokat a mezőket tartalmazzák amelyekre a felhasználónak szüksége van.
|
||||
Az adatbázisból kiolvasott objektum hasznos részeit és egyéb használni kívánt információt átmásolom az átviteli objektumba. Majd ezt küldöm el a kliensoldal felé.
|
||||
|
||||
Hogy az adatok másolását ne kézzel kelljen csinálnom, az AutoMapper \cite{automapper} szoftvercsomagot alkalmaztam, melynek használata rendkívül egyszerű.
|
||||
Meg lehet adni profilokat, ahol két objektum közötti leképzéseket lehet felvenni. A szoftvercsomag automatikusan átmásolja az azonos nevű mezőket az egyik objektumból a másikba,
|
||||
de meg lehet adni egyedi leképzéseket is.
|
||||
\pagebreak
|
||||
\begin{lstlisting}[style=csharp, caption=Egy példa az AutoMapper használatára.]
|
||||
// Creating maps.
|
||||
CreateMap<User, AuthenticateResponse>()
|
||||
.ForMember(m => m.Username, opt => opt.MapFrom(m => m.Name))
|
||||
.ForMember(m => m.UserRole, opt => opt.MapFrom(m => m.Role))
|
||||
.ReverseMap();
|
||||
|
||||
CreateMap<Service, ServiceRequest>()
|
||||
.ReverseMap();
|
||||
|
||||
// Using maps.
|
||||
IMapper mapper = GetMapper();
|
||||
User user = GetUserFromDb();
|
||||
AuthenticateResponse response = mapper.Map<AuthenticateResponse>(user);
|
||||
\end{lstlisting}
|
291
docs/thesis/content/birdmap-frontend.tex
Normal file
@ -0,0 +1,291 @@
|
||||
%----------------------------------------------------------------------------
|
||||
\chapter{Kliens oldal}
|
||||
\label{chapt:birdmap-frontend}
|
||||
%----------------------------------------------------------------------------
|
||||
Ebben a fejezetben bemutatom a kliensoldal architektúráját. Ismertetem a különböző komponensek felépítését.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\section{Architektúra}
|
||||
%----------------------------------------------------------------------------
|
||||
Az alkalmazásnak minden oldala egy külön React komponens, mely mindegyikének saját mappája van a főkönyvtár alatt,
|
||||
ahol az egyes oldalak által használt szolgáltatások és egyéb komponensek találhatóak.
|
||||
A közösen használt szolgáltatások és komponensek a common mappába kerültek.
|
||||
|
||||
A kliensoldal belépési pontja az \verb+App.js+ fájlban található \verb+App+ komponens.
|
||||
Itt egy React \verb+Switch+-ben fel van sorolva az összes oldal komponense azok elérési útvonalai szerint.
|
||||
Ezt szemlélteti a \ref{lst:react-switch}-es lista.
|
||||
Az a komponens jelenik meg a felületen, amelyiknek \verb+path+ mező értéke megegyezik az URL-ben találhatóval.
|
||||
|
||||
\begin{lstlisting}[style=jsx, caption=Az App.js Switch tartalma., label=lst:react-switch]
|
||||
<Switch>
|
||||
<PublicRoute exact path="/login" component={AuthComponent} />
|
||||
<AdminRoute exact path="/logs" component={LogsComponent} />
|
||||
<DevicesContextProvider>
|
||||
<PrivateRoute exact path="/" component={DashboardComponent} />
|
||||
<PrivateRoute exact path="/devices/:id?" component={DevicesComponent} />
|
||||
<PrivateRoute exact path="/heatmap" component={HeatmapComponent} />
|
||||
</DevicesContextProvider>
|
||||
</Switch>
|
||||
\end{lstlisting}
|
||||
|
||||
Hozzáférés szempontjából háromfajta oldalt különböztetünk meg:
|
||||
\begin{itemize}
|
||||
\item \textbf{Publikus oldal}. Az oldal bejelentkezés nélkül is látogatható.
|
||||
\item \textbf{Privát oldal}. Az oldal csak bejelentkezés után látogatható.
|
||||
\item \textbf{Admin oldal}. Az oldalt csak bejelentkezett admin felhasználók látogathatják.
|
||||
\end{itemize}
|
||||
|
||||
Ezek alapján készítettem két generikus komponenst. Az egyik a \verb+DefaultLayout+ komponens, mely az oldal alapértelmezett elrendezéséért felel.
|
||||
Paraméterében át lehet adni egy másik megjeleníteni kívánt komponenst, melyet a fejléc alatt jelenít meg.
|
||||
Mivel minden komponens ebbe az bázis komponensbe van csomagolva, így akárhova navigálunk az oldalon a felület mindig egységes marad.
|
||||
|
||||
A másik komponens a \verb+PredicateRoute+, melynek paraméterében meg lehet adni egy feltételt, illetve egy másik komponenst.
|
||||
Ha a feltétel hamis akkor átirányítja a felhasználót a bejelentkező oldalra, ha igaz akkor megjeleníti a \verb+DefaultLayout+-ba csomagolt komponenst.
|
||||
Publikus oldalnál a feltétel mindig igaz.
|
||||
Privátnál a feltétel a bejelentkezéshez van kötve.
|
||||
Az admin oldal feltétele egyrészt szintén a bejelentkezés, másrészt a felhasználó \verb+Admin+ jogosultsága.
|
||||
Ezt a folyamatot próbálja szemléltetni a \ref{fig:birdmap-frontend-architecture}-es ábra.
|
||||
Legfelül sárgával vannak feltüntetve a hívható végpontok, alattuk a hozzájuk kapcsolt megjelenítendő komponensek, azok alatt pedig a hozzáférést szabályozó komponensek.
|
||||
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=150mm, keepaspectratio]{figures/birdmap-frontend-routes.png}
|
||||
\caption{A Birdmap kliensoldalának architektúrája}
|
||||
\label{fig:birdmap-frontend-architecture}
|
||||
\end{figure}
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\section{Kommunikáció a szerveroldallal}
|
||||
%----------------------------------------------------------------------------
|
||||
A szerveroldallal való kommunikációt rendkívül egyszerűen tudtam implementálni köszönhetően a \ref{subsect:backend-swagger}-as fejezetben bemutatott Swagger oldalnak
|
||||
és annak, hogy az NSwag Studio-val \cite{nswag-studio} a C\#-on kívül lehet TypeScript\footnotemark klienseket is generálni a leíró fájlból.
|
||||
Így készültek el a komponensek kommunikációért felelős szolgáltatásai.
|
||||
|
||||
\footnotetext{JavaScript-re épített statikus típusdefiníciókat tartalmazó nyelv. JavaScript és TypeScript együtt is használható.}
|
||||
%----------------------------------------------------------------------------
|
||||
\section{Komponensek}
|
||||
%----------------------------------------------------------------------------
|
||||
Ebben a szakaszban ismertetem az egyes oldalak komponenseit és azok alkomponenseit,
|
||||
illetve a navigációért felelős fejlécet.
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Navigáció}
|
||||
%----------------------------------------------------------------------------
|
||||
A fejléc két komponensből áll. Az egyik az oldal címe a másik az oldalak linkjeit tartalmazó komponens.
|
||||
Utóbbit a React \verb+NavLink+ komponenseivel készítettem, melyeknek meg lehet adni, hogy kattintásra hova irányítsa a felhasználót.
|
||||
Ha a jelenlegi webcím tartalmazza a linknek megadott címet, akkor az aktív státuszba kerül, melyre külön stílus osztályok vonatkoznak.
|
||||
Ezt használva, az aktív linkeket egy aláhúzással jelölöm.
|
||||
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=150mm, keepaspectratio]{figures/appbar-user-admin.png}
|
||||
\caption{A Birdmap fejléce. Felül a User, alul az Admin felhasználóké}
|
||||
\label{fig:birdmap-appbar}
|
||||
\end{figure}
|
||||
|
||||
A fejléc alapértelmezetten része a \verb+DefaultLayout+ komponensnek, így minden oldalon megjelenítésre kerül.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Login}
|
||||
%----------------------------------------------------------------------------
|
||||
A bejelentkező oldal viszonylag egyszerű. Két szövegdobozt és egy bejelentkező gombot tartalmaz, ahogy az a \ref{fig:birdmap-login}-as ábrán is látszik.
|
||||
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=60mm, keepaspectratio]{figures/birdmap-login.png}
|
||||
\caption{A Birdmap bejelentkező felülete}
|
||||
\label{fig:birdmap-login}
|
||||
\end{figure}
|
||||
|
||||
A generált szerverrel kommunikáló szolgáltatás be van csomagolva egy közösen használt másik szolgáltatásba.
|
||||
Ennek célja, hogy a bejelentkezés eredményét több komponens is olvashassa, hiszen az alkalmazás felületét alapvetően megkülönbözteti,
|
||||
egyrészt a bejelentkezés sikeressége, másrészt a bejelentkezett felhasználó jogosultsági köre.
|
||||
|
||||
Sikeres bejelentkezés után a szerver elküldi a felhasználó szerepét, illetve a hozzáférési token-t, amelyre a kliens többi szolgáltatásának is szüksége lesz a kommunikációhoz.
|
||||
Ezeket az oldal \verb+sessionStorage+-ában\footnotemark tárolom és a becsomagolt szolgáltatáson keresztül elérhetőek.
|
||||
|
||||
Kijelentkezni a navigációs fejlécben található profil ikonra való kattintással lehet.
|
||||
|
||||
\footnotetext{Webtárhely objektum. Lehetővé teszi a kulcs-érték párok tárolását a böngészőben.}
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Logs}
|
||||
%----------------------------------------------------------------------------
|
||||
Ez az oldal az \verb+Admin+ felhasználó számára lehetővé teszi a szerveren található naplófájlok letöltését \verb+zip+ fájlformátumú archív fájlokban.
|
||||
Komponense a \ref{fig:birdmap-logs}-es ábrán látható.
|
||||
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=75mm, keepaspectratio]{figures/birdmap-logs.png}
|
||||
\caption{A Birdmap naplófájlok letöltésének felülete}
|
||||
\label{fig:birdmap-logs}
|
||||
\end{figure}
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Eszközállapot- és hangüzenet-kezelő szolgáltatás}
|
||||
%----------------------------------------------------------------------------
|
||||
A szakasz további komponenseinek van egy közös ismertetője. Mégpedig, hogy mindegyiknek szüksége van a kihelyezett eszközök adataira
|
||||
és az azok által publikált hangüzenetekből képzett valószínűségre.
|
||||
A Reactnek van egy beépített komponense \verb+Context+ \cite{react-context} néven, mellyel különböző komponensek között lehet adatokat megosztani.
|
||||
Ezt használva készítettem egy \verb+DevicesContextProvider+ osztályt, melynek feladata a szerver eszköz kontrollerével való kommunikáció a megfelelő szolgáltatáson keresztül,
|
||||
illetve a SignalR csatornákra való feliratkozás. Ezekből az adatokból egy \verb+DevicesContext+ készül, mely a \verb+Provider+ által átadásra kerül annak minden gyerekének.
|
||||
A \ref{lst:react-switch}-es listában látható, hogy a \verb+DevicesContextProvider+ szülője a \verb+Dashboard+, \verb+Devices+ és \verb+Heatmap+ komponenseknek.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Dashboard}
|
||||
%----------------------------------------------------------------------------
|
||||
A Dashboard az alkalmazás kezdő oldala. Itt található meg a külső szolgáltatások állapotát vizsgáló komponens,
|
||||
illetve a kihelyezett eszközök működési folyamatában áttekintést nyújtó diagramok mindegyike.
|
||||
|
||||
Az oldal megjelenítésekor elindul egy másodpercenként ismétlődő folyamat,
|
||||
mely a \verb+DevicesContext+-ből kiolvasott értékekből legenerálja a diagramokon megjelenítendő összes adatot.
|
||||
Ez azonban az adat mennyiségétől függően akár egy-két másodpercig is eltarthat, ami rendkívül lassúvá és használhatatlanná tenné a felületet.
|
||||
Ennek elkerülése érdekében az adatfeldolgozó folyamat egyszerre csak egy pár elemet dolgoz fel, mely alfolyamatok között 20 milliszekundum szüneteket iktattam be.
|
||||
Továbbá hogy a különböző diagramok animációi is zökkenőmentesek legyenek, azok adatai cserélése között is van 300 milliszekundum szünet.
|
||||
Így valamivel lasabb az adatfeldolgozás, de a felület használható marad.
|
||||
%----------------------------------------------------------------------------
|
||||
\subsubsection{Külső szolgáltatások}
|
||||
%----------------------------------------------------------------------------
|
||||
Az alkalmazás használatának szempontjából van néhány olyan külső szolgáltatás, melyek elérhetősége hiányában a rendszer működésképtelen.
|
||||
Ilyen például a Birbnetes klasztere vagy a szerver MQTT szolgáltatása.
|
||||
Ezért készítettem el az \ref{fig:dashboard-services-loaded}-ös ábrán látható információs panelt, ahol a szolgáltatások állapotát lehet látni, hogy a felhasználó tudja miért nem működik esetleg az alkalmazás.
|
||||
A felület megvalósításhoz a Material UI \verb+Accordion+ elemét használtam, ami lényegében egy lenyíló lista.
|
||||
Ennek fejlécében a szolgáltatás neve, elérési útvonala és státusza látható. A lenyíló elemben a szolgáltatástól érkezett válasz van megjelenítve.
|
||||
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=150mm, keepaspectratio]{figures/dashboard-services-loaded.png}
|
||||
\caption{Az alkalmazás által használt külső komponensek állapotának megjelenítéséért felelős komponens}
|
||||
\label{fig:dashboard-services-loaded}
|
||||
\end{figure}
|
||||
|
||||
Az oldal betöltése vagy a frissítés gomb megnyomása esetén az adatok lekérésre kerülnek a szervertől.
|
||||
Ez a folyamat akár öt-hat másodpercig is eltarthat, mely közben a felhasználó egy üres listát látna.
|
||||
Ennek elkerülésére használom a Material UI \verb+Skeleton+ komponensét,
|
||||
mely egy megadható méretű töltő csíkkal helyettesíti az \verb+Accordion+-ban található elemeket a \ref{fig:dashboard-services-loading}-os ábrán látható módon.
|
||||
Azért célszerű ennek a használata, mert így a felhasználónak több információja van arról, hogy a felületen milyen adatok és hol fognak megjelenni.
|
||||
A felhasználói élmény maximalizálása érdekében a frissítés előtt lekérdezem a szervertől, hogy hány darab szolgáltatás található az adatbázisban
|
||||
és annyi darab töltőcsíkos \verb+Accordion+-t jelenítek meg.
|
||||
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=150mm, keepaspectratio]{figures/dashboard-services-loading.png}
|
||||
\caption{A Skeletonok alkalmazása a külső szolgáltatások állapotának betöltése közben.}
|
||||
\label{fig:dashboard-services-loading}
|
||||
\end{figure}
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsubsection{Eszközök és szenzorok állapota}
|
||||
%----------------------------------------------------------------------------
|
||||
Ennek a komponensnek a szerepe, hogy áttekintést nyújtson az eszközök és szenzorok állapotáról.
|
||||
Úgy gondoltam, hogy erre a legcélravezetőbb eszköz a \ref{fig:dashboard-donut}-es ábrán is látható Apexcharts fánk diagramja.
|
||||
Látható, hogy hány darab eszköz és szenzor van bekapcsolt, kikapcsolt, illetve hibás állapotban.
|
||||
Az állapotok változása esetén a \verb+DevicesContextProvider+-nek köszönhetően az adatok automatikusan frissülnek.
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=150mm, keepaspectratio]{figures/dashboard-donut-devices.png}
|
||||
\caption{A Dashboard eszköz- és szenzor állapotok diagramja}
|
||||
\label{fig:dashboard-donut}
|
||||
\end{figure}
|
||||
%----------------------------------------------------------------------------
|
||||
\subsubsection{Hőtérkép diagramok}
|
||||
%----------------------------------------------------------------------------
|
||||
Ezekkel a diagramokkal az a célom, hogy az eszközök által küldött észleléseket időrendben vizualizáljam.
|
||||
Megvalósításukhoz az Apexcharts Heatmap típusú diagramját használtam.
|
||||
A \ref{fig:dashboard-heatmap-second}-as ábrán látható diagram az elmúlt egy percben küldött, másodpercenként a legnagyobb, hangüzenetekből képzett valószínűségeket ábrázolja.
|
||||
A \ref{fig:dashboard-heatmap-minute}-es ábrán látható diagram pedig az elmúlt egy órában percenként a legnagyobbakat.
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=150mm, keepaspectratio]{figures/second-heatmap.png}
|
||||
\caption{Másodperc alapú hőtérképes diagram}
|
||||
\label{fig:dashboard-heatmap-second}
|
||||
\end{figure}
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=150mm, keepaspectratio]{figures/minute-heatmap.png}
|
||||
\caption{Perc alapú hőtérképes diagram}
|
||||
\label{fig:dashboard-heatmap-minute}
|
||||
\end{figure}
|
||||
|
||||
A függőleges tengelyen a rendszer eszközei vannak dinamikusan megjelenítve.
|
||||
A vízszintes tengelyen pedig az említett időtartományok.
|
||||
A diagramokon látható négyzetek a valószínűség nagyságától függően sötétebbek vagy világosabbak.
|
||||
\newpage
|
||||
%----------------------------------------------------------------------------
|
||||
\subsubsection{Riasztás számláló}
|
||||
%----------------------------------------------------------------------------
|
||||
Ez egy egyszerű oszlopdiagram, mely aggregálja az egyes eszközök által küldött hangüzeneteket 0.5 valószínűség felett a \ref{fig:dashboard-devices-column}-es ábrán látható módon.
|
||||
Segítségével megvizsgálható, hogy mely eszközök riasztanak a legtöbbet a legnagyobb valószínűséggel.
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=150mm, keepaspectratio]{figures/dashboard-column-devices.png}
|
||||
\caption{Eszközönkénti riasztásokat számláló diagram}
|
||||
\label{fig:dashboard-devices-column}
|
||||
\end{figure}
|
||||
|
||||
Az egyes oszlopok három részre vannak bontva az üzenetek öt tized, hét tized és kilenc tized fölötti valószínűsége szerint.
|
||||
\newpage
|
||||
%----------------------------------------------------------------------------
|
||||
\subsubsection{Üzenetek gyakorisága}
|
||||
%----------------------------------------------------------------------------
|
||||
Az oldalon található utolsó diagram egy vonal diagram, melynek célja, hogy ábrázolja a rendszer által küldött üzenetek számát másodpercenként.
|
||||
A \ref{fig:dashboard-messages-line}-es ábrán látható a komponens.
|
||||
A vízszintes tengelyen a legelső érték az alkalmazás által először észlelt üzenet időpontja.
|
||||
Az utolsó érték a legutoljára észlelt időpontja.
|
||||
A függőleges tengelyen az adott másodpercben érkező üzenetek száma van ábrázolva.
|
||||
Az előzőkkel ellentétben itt az adatok nincsennek szűrve a hangüzenet valószínűsége alapján,
|
||||
tehát a rendszer által küldött összes üzenet látható.
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=150mm, keepaspectratio]{figures/dashboard-line-messages.png}
|
||||
\caption{A másodpercenként érkező üzenetek számát ábrázoló diagram.}
|
||||
\label{fig:dashboard-messages-line}
|
||||
\end{figure}
|
||||
\newpage
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Devices}
|
||||
%----------------------------------------------------------------------------
|
||||
Ez az oldal lehetővé teszi a felhasználók számára az eszközök állapotának áttekintését, \verb+Admin+ felhasználók számára azok menedzselését is.
|
||||
Az eszközök dinamikusan jelennek meg a \verb+DevicesContextProvider+ adatai alapján, melyek megjelenítésére a Material UI \verb+Accrordion+ komponensét használom.
|
||||
Ennek fejlécében az eszköz neve, egyedi azonosítója és státusza található. A lenyíló részben pedig az eszköz által használt szenzorok neve, azonosítója és státusza.
|
||||
\verb+Admin+ felhasználók számára a felület két fajta gombbal bővül, melyekkel be és ki lehet kapcsolni az egyes eszközöket, szenzorokat.
|
||||
Az \verb+Accordion+-ok felett található egy külön panel, mellyel egyszerre lehet kezelni az összes eszközt és azok szenzorjait.
|
||||
A Devices oldal felülete a \ref{fig:frontend-devices}-es ábrán,
|
||||
az \verb+Admin+ felhasználók számára nyújtott plusz funkciók a \ref{fig:frontend-devices-admin}-as ábrán láthatók.
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=150mm, keepaspectratio]{figures/devices.png}
|
||||
\caption{A Devices oldal felülete.}
|
||||
\label{fig:frontend-devices}
|
||||
\end{figure}
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=150mm, keepaspectratio]{figures/devices-admin.png}
|
||||
\caption{Az Admin felhasználók számára elérhető plusz funkciók.}
|
||||
\label{fig:frontend-devices-admin}
|
||||
\end{figure}
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Heatmap}
|
||||
%----------------------------------------------------------------------------
|
||||
Az alkalmazással szemben az egyik legfontosabb követelmény a hőtérképes vizualizáció volt,
|
||||
mely ezen az oldalon található. A Google Maps API segítségével megjelenítek egy térképet a felületen,
|
||||
majd erre kerül a hőtérképes réteg. A térképre szélességi és hosszúsági körök alapján lehet rajzolni.
|
||||
Ezt használva megjelenítem a rendszer összes eszközét azok koordinátái szerint.
|
||||
A kék színű ikonok jelölik a bekapcsolt állapotban lévő, a sárga a kikapcsolt állapotban lévő,
|
||||
a piros pedig a hibás állapotban lévő eszközöket.
|
||||
Ha a felhasználó az egerét az ikonok fölé helyezi, megjelenik egy szövegdoboz, melyben az eszköz azonosítója és státusza látható.
|
||||
Az ikonra kattintva a felhasználó a Devices oldalra kerül, ahol megnyílik a kattintott eszköz \verb+Accordion+-ja.
|
||||
|
||||
A \verb+DevicesContext+ tartalmazza az eszközök által küldött üzenetek adatait,
|
||||
melyeknek a 0.5 valószínűségtől nagyobb részhalmazát a hőtérkép által kezelhető adatokká konvertálok.
|
||||
Egyrészt szükség van az előbb is említett földrajzi koordinátákra, melyeket az üzenetek eszköz azonosítója alapján határozok meg.
|
||||
Másrészt szükség van egy súly értékre, mely a pont színezésének pirosságát határozza meg.
|
||||
Ezt az értéket az üzenetek valószínűség értékével tettem egyenlővé.
|
||||
Minél több magasabb valószínűségű riasztás érkezik egy adott eszköztől, a körülötte lévő terület annál pirosabb lesz.
|
||||
|
||||
A \ref{fig:frontend-heatmap}-ös ábra mutatja a térkép működését miközben 4 eszköz is seregélyeket észelt.
|
||||
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=150mm, keepaspectratio]{figures/heatmap.png}
|
||||
\caption{A Heatmap oldal felülete.}
|
||||
\label{fig:frontend-heatmap}
|
||||
\end{figure}
|
67
docs/thesis/content/birdmap-introduction.tex
Normal file
@ -0,0 +1,67 @@
|
||||
%----------------------------------------------------------------------------
|
||||
\chapter{Tervek és alternatívák}
|
||||
\label{chapt:birdmap-introduction}
|
||||
%----------------------------------------------------------------------------
|
||||
Ebben a fejezetben bemutatom a fejlesztés előtti állapotot, amikor a munkám elején a fontosabb vizualizációs alternatívákat értékeltem.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\section{Tervezés}
|
||||
%----------------------------------------------------------------------------
|
||||
A munkám elején egyeztettem a seregély riasztó keretrendszert fejlesztő kollégákkal,
|
||||
hogy ki tudjam választani a vizualizáció szempontjából legfontosabb komponenseket.
|
||||
A jellemző adatvizualizációs megoldások közül az alábbi hármat találtam kulcsfontosságúnak a következő célokra:
|
||||
\begin{itemize}
|
||||
\item \textbf{Hőtérkép}. Hasznos lenne egy olyan felület, ahol az eszközök GPS koordinátái és a seregély detektálást jelző üzenetek alapján, meg lehetne jeleníteni a seregélyek hozzávetőleges előfordulásának helyeit és gyakoriságát egy térképen, hőtérképes formában.
|
||||
\item \textbf{Eszközállapotok}. Jelenleg a Command and Control mikroszolgáltatás felé indított kéréseken kívül, nincs lehetőség a kihelyezett eszközök állapotának vizsgálatára. Szükség lenne egy olyan felületre, ahol ezek állapotai láthatóak, esetleg dinamikusan is frissülnek.
|
||||
\item \textbf{Diagramok}. A hőtérképen kívül egyéb olyan diagramok is hasznosak lehetnek, ahol látható például, hogy melyik eszköz melyik percben észlelt madárhangot vagy, hogy egy eszköz összesen hány madárhangot észlelt. Minél több információ, annál jobb.
|
||||
\end{itemize}
|
||||
Ezeken kívül fontos követelmény volt még, hogy az alkalmazásom futtatható legyen Linux környezetben is, hogy az telepíthető legyen a Birbnetes Kubernetes \cite{kubernetes} klaszterébe.
|
||||
|
||||
Az alkalmazásom kapott egy nevet is, mely a Birbnetes-t és az említett hőtérképes ötletet ötvözve Birdmap lett.
|
||||
\footnotetext{Microsoft Teams: Csevegő és gyülekezés tartó alkalmazás.}
|
||||
%----------------------------------------------------------------------------
|
||||
\section{Alternatívák}
|
||||
%----------------------------------------------------------------------------
|
||||
Az imént vázolt igények kielégítésére sok, széles körben alkalmazott megoldás létezik már, melyek jó példát mutattak a saját alkalmazásom fejlesztése során.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Grafana}
|
||||
%----------------------------------------------------------------------------
|
||||
A Grafana \cite{grafana} az egy nyílt forráskódú platformfüggetlen vizualizációs web alkalmazás.
|
||||
Egy támogatott adatbázishoz csatlakoztatva különféle interaktív gráfokat és diagramokat generál.
|
||||
A testreszabhatóság maximalizásának érdekében különböző, akár harmadik fél által készített, bővítmények használatát is támogatja,
|
||||
melyekkel új adatforrások és panel típusok integrálhatók.
|
||||
A \ref{fig:grafana}-es ábra egy jó példa arra, hogy hogyan néz ki egy általános Grafana felület.
|
||||
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=150mm, keepaspectratio]{figures/grafana.png}
|
||||
\caption{A Grafana demo oldalának, a \url{https://play.grafana.org}-nak a felülete}
|
||||
\label{fig:grafana}
|
||||
\end{figure}
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Kibana}
|
||||
%----------------------------------------------------------------------------
|
||||
A Kibana \cite{kibana} jelentősen hasonlít a Grafanához, azonban amíg a utóbbit inkább az időben változó metrikák vizualizálására használják például processzor leterheltség vagy memória használat,
|
||||
addig az előbbit elsődlegesen az Elasticsearch\footnote{Ingyenes és nyílt forráskódú index alapú keresőmotor} adatok, főként napló bejegyzések, analizálására használják.
|
||||
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=150mm, keepaspectratio]{figures/kibana-dashboard.png}
|
||||
\caption{Egy példa a Kibana kezelőfelületére}
|
||||
\label{fig:kibana}
|
||||
\end{figure}
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Kubernetes Dashboard (Web UI)}
|
||||
%----------------------------------------------------------------------------
|
||||
A Kubernetes Dashboard \cite{kubernetes-dashboard} elsősorban nem a különböző adatok vizualizálását szolgálja, inkább a klaszter menedzselését próbálja egyszerűbbé és jobban áttekinthetővé tenni.
|
||||
Azonban egy jó példa arra, hogy egy rendszer webes kezelőfelületének, milyennek is kell lennie.
|
||||
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=150mm, keepaspectratio]{figures/kubernetes-dashboard.png}
|
||||
\caption{A Kubernetes Dashboard felülete}
|
||||
\label{fig:kibana}
|
||||
\end{figure}
|
23
docs/thesis/content/birdmap-kubernetes.tex
Normal file
@ -0,0 +1,23 @@
|
||||
%----------------------------------------------------------------------------
|
||||
\chapter{Docker image készítés}
|
||||
\label{chapt:birdmap-kubernetes}
|
||||
%----------------------------------------------------------------------------
|
||||
Az éles rendszerrel való kommunikáció megvalósításához készítenem kell egy Docker image-et, melyet telepíteni lehet a Birbnetes Kubernetes klaszterébe.
|
||||
Ehhez először készítettem egy Dockerfile-t \cite{dockerfile}, mely az image-ek automatikus elkészítését teszi lehetővé.
|
||||
Utasításokat lehet benne felsorolni, melyekkel a konténer környezetét kell felépíteni.
|
||||
Meg lehet adni kiindulópontokat, mely az image alapjául szolgál.
|
||||
Erre a célra én az ASP.NET futtatókörnyeztét használtam, mely tartalmazza az alkalmazás futtatásához szükséges parancsokat.
|
||||
Ezek után a Dockerfile utasításait használva bemásolom a \verb+Release+ konfigurációval fordított alkalmazásomat a konténer egy mappájába,
|
||||
majd a belépési pont utasítással megadom az alkalmazás indításához szükséges parancsot.
|
||||
Ezt futtatva sikeresen elkészül a Docker image.
|
||||
|
||||
Azonban az alkalmazás teljes értékű működéséhez annak szüksége van egy adatbázis konténerre is.
|
||||
Az ilyen jellegű többkonténeres rendszer problémákra nyújt megoldást a Docker Compose \cite{docker-compose}.
|
||||
Egy YAML fájlban meg lehet adni az alkalmazás futtatásához szükséges szolgáltatásokat, illetve hogy ezek között milyen függőségi viszony van.
|
||||
Ennek használatával először készítek egy adatbázis konténert, mely inicializálása után indul csak el az alkalmazásom docker image-ének készítése.
|
||||
A két konténer közötti kommunikációhoz az alkalmazásomnak szüksége van még a kapcsolati karakterláncra, mely meghatározza az adatbázis elérésének paramétereit.
|
||||
A lokális futtatásnál ez az alkalmazás konfigurációs fájljában található, azonban ez a fájl már a konténer fájlrendszerében van, nehézkes hozzáférni.
|
||||
Szerencsére az ASP.NET támogatja a konfigurációk felülírását környezeti változókkal.
|
||||
Ehhez fel kell sorolnom a YAML fájl környezeti változói részében a felülírni kívánt konfigurációkat és értékeiket.
|
||||
Szintén ebben a fájlban megadtam az alkalmazás eléréséhez használni kívánt portokat.
|
||||
Ezek után az alkalmazásom készen áll a klaszterbe való telepítésre.
|
136
docs/thesis/content/birdmap-technologies.tex
Normal file
@ -0,0 +1,136 @@
|
||||
%----------------------------------------------------------------------------
|
||||
\chapter{Használt technológiák}
|
||||
\label{chapt:birdmap-technologies}
|
||||
%----------------------------------------------------------------------------
|
||||
Ezzel a fejezettel az a célom, hogy ismertessem a fejlesztés során, illetve az alkalmazásom által használt technológiákat,
|
||||
hogy a következő fejezetekben alapozni tudjak ezeknek az ismeretére.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\section{A fejlesztési folyamat technológiái}
|
||||
%----------------------------------------------------------------------------
|
||||
Ebben a szakaszban azokat az eszközöket, alkalmazásokat és fejlesztőkörnyezeteket mutatom be, melyeket a fejlesztés során, a fejlesztéshez használtam.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Git}
|
||||
%----------------------------------------------------------------------------
|
||||
A Git \cite{git} egy verziókezelő rendszer. Használatával a felhasználó le tudja menteni egy adott fájlrendszerben található fájlok állapotát.
|
||||
Megkönnyíti az egy projekten dolgozó programozók közötti kooperációt. Manapság lassan elképzelhetetlen a fejlesztés valamilyen verziókezelő használata nélkül.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Trello}
|
||||
%----------------------------------------------------------------------------
|
||||
A Trello \cite{trello} egy webes projektmenedszment alkalmazás.
|
||||
Azért használtam a fejlesztés során, mert szerettem volna egy helyet, ami tükrözi a fejlesztés állapotát, ahova le tudom írni az alkalmazással kapcsolatos ötleteimet.
|
||||
Különböző listákban tároltam a fejlesztésre váró és a kész feladatokat szerver, kliens és egyéb szerint.
|
||||
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=150mm, keepaspectratio]{figures/trello-3.png}
|
||||
\caption{Egy példa állapot a Trello felületére a fejlesztés során}
|
||||
\label{fig:trello}
|
||||
\end{figure}
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Visual Studio}
|
||||
%----------------------------------------------------------------------------
|
||||
A Visual Studio \cite{vs} a Microsoft fejlesztőkörnyezete. Jól alkalmazható a .NET keretrendszer technológiáival, ezért ezt használtam a szerveroldal fejlesztése során.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Visual Studio Code}
|
||||
%----------------------------------------------------------------------------
|
||||
Egy másik Microsoft termék, viszont a fentivel ellentétben a Visual Studio Code \cite{vs-code} inkább szövegszerkesztő, mint fejlesztőkörnyezet.
|
||||
Ennek köszönhetően jelentősen gyorsabb és egyszerűbb a használata. Különféle bővítmények használatával nagyon jó program nyelv támogatottságot lehet elérni.
|
||||
Többek között ezen okok miatt preferáltam a kliensoldal fejlesztésére.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\section{Backend technológiák}
|
||||
%----------------------------------------------------------------------------
|
||||
Ebben a szakaszban a szerveroldal megvalósítására használt .NET technológiákat mutatom be. A választásom több ok miatt esett a .NET keretrendszer használatára.
|
||||
|
||||
Egyrészt úgy gondoltam, hogy az alkalmazásom fajsúlyosabb részét inkább a kliensoldal fogja képezni ezért, hogy arra több energiát tudjak fordítani, valami olyat választottam,
|
||||
amivel már foglalkoztam korábban, amivel gyorsabban és rutinosabban megy a fejlesztés.
|
||||
|
||||
Másrészt nemrég jelent meg a .NET új 5-ös verziója, melynek használatával jelentős teljesítmény javulást ígértek több területen is, és úgy gondoltam, hogy ez a projekt tökéletes lenne
|
||||
ennek próbatételére.
|
||||
|
||||
Mindemellett a .NET teljesen platformfüggetlen, mely az egyik legfontosabb követelmény volt az alkalmazással szemben.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{ASP.NET Core}
|
||||
%----------------------------------------------------------------------------
|
||||
Az ASP.NET Core a .NET család ingyenes, nyílt forráskódú webes keretrendszere. Gyors és moduláris fejlesztést tesz lehetővé,
|
||||
mely főként a csomagkezelő rendszerének, a NuGet-nek \cite{nuget} köszönhető.
|
||||
Használatának egyik előnye, hogy ugyan az a C\# kód tud futni a szerver és a kliens oldalon, de támogat más kliens oldali keretrendszereket is, mint például az Angular-t, a Vue.js-t
|
||||
vagy a React.js-t.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Entity Framework Core}
|
||||
%----------------------------------------------------------------------------
|
||||
Az Entity Framework Core (röviden EF Core) egy objektum-relációs leképző keretrendszer a .NET-hez. Az adatbázissal való kommunikációt könnyítését szolgálja.
|
||||
Használatával C\#-ban lehet adatbázis lekérdezéseket írni a LINQ (Language-Integrated Query) szoftvercsomag segítségével.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{JSON Web Token}
|
||||
%----------------------------------------------------------------------------
|
||||
Az autorizációt többféleképpen meg lehet oldani egy alkalmazás szempontjából. Az egyik ilyen megoldás a JSON Web Token-ek (röviden JWT) \cite{jwt} használata,
|
||||
ami nem más mint egy szabvány, mely módszert ad a felek közötti információ biztonságos továbbítására JSON objektumokkal.
|
||||
Ezen objektumok adatokat tárolhatnak a felhasználóról például a neve vagy a szerepe, melyek segítségével a szerver eldöntheti,
|
||||
hogy van-e jogosultsága hívni az adott végpontot.
|
||||
|
||||
A Microsoft-nak van egy beépített szoftvercsomagja, mellyel ilyen tokeneket lehet készíteni és validálni.
|
||||
A szerveroldal jogosultság kezelését ezzel a csomaggal oldottam meg.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{SignalR}
|
||||
%----------------------------------------------------------------------------
|
||||
A SignalR \cite{signalr} egy .NET szoftvercsomag, mely lehetővé teszi a szerveroldal számára a kliensekkel való aszinkron kommunikációt.
|
||||
A szerver valós időben tud értesítéseket küldeni a kliensek számára, amelyek feliratkoztak az ilyen eseményekre.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{MQTT.NET}
|
||||
%----------------------------------------------------------------------------
|
||||
Az MQTT.NET \cite{mqttnet-github} is egy .NET szoftvercsomag, mely a Birbnetes által is használt, a \ref{subsect:mqtt}-es alfejezetben bemutatott MQTT kommunikáció C\# nyelvű megvalósítását szolgálja.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{NLog}
|
||||
%----------------------------------------------------------------------------
|
||||
A szerveroldali naplózás megvalósítására több szoftvercsomag is létezik. Az NLog \cite{nlog}-ot választottam, egyrészt mert egyszerű a használata,
|
||||
másrészt mert már korábban használtam. Konfigurációs fájljában meg lehet adni a naplózott események célját, mely lehet akár fájl vagy konzol is.
|
||||
Meg lehet még adni az események elrendezését, hogy azok milyen formában kerüljenek a célokba, milyen plusz információt tartalmazzanak.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\section{Frontend technológiák}
|
||||
%----------------------------------------------------------------------------
|
||||
Ebben a szakaszban a kliensoldalon használt technológiákat mutatom be.
|
||||
Választásomnál fő motiváció az volt, hogy szerettem volna valami újat kipróbálni, aminek nincs köze a .NET keretrendszerhez.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{React.js}
|
||||
%----------------------------------------------------------------------------
|
||||
A React.js \cite{react} egy JavaScript szoftvercsomag, melyet webes felületek fejlesztésére használnak.
|
||||
Fő építő elemei a komponensek, melyek elszeparált újrafelhasználható felület egységek.
|
||||
Használatának egyik előnye, hogy automatizált az állapot kezelés, tehát ha változik egy komponens állapota, akkor a React újra-rendereli azt.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Material UI}
|
||||
%----------------------------------------------------------------------------
|
||||
A Material \cite{material} elsősorban egy kezelőfelület tervezési útmutató a Google által, melyet követve szép és minőségi felületeket lehet készíteni.
|
||||
|
||||
A Material UI \cite{material-ui} egy szoftvercsomag, mely ezeket az útmutatásokat követő egyszerű React komponenseket tartalmaz.
|
||||
Alkalmazásával könnyű esztétikus felhasználói felületeket készíteni, minimalizált a CSS használattal.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Apexcharts}
|
||||
%----------------------------------------------------------------------------
|
||||
Az Apexcharts \cite{apexcharts} egy nyílt forráskódú JavaScript szoftvercsomag, amellyel könnyen konfigurálható, modern kinézetű diagramokat lehet készíteni.
|
||||
Sokféle kliensoldali (és szerveroldali) technológiát támogat, köztük a React-et is. A kezelőfelületen található vizualizációk szinte összes elemét ennek használatával csináltam.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Google Maps Api}
|
||||
%----------------------------------------------------------------------------
|
||||
A Google szinte összes termékének van API-ja, ami lehetővé teszi a programozók számára, hogy integrálják ezeket saját alkalmazásaikban.
|
||||
A Google Maps sincs másképp és mivel ennek interfésze külön támogatja a hőtérképes réteg használatát is, nem gondoltam, hogy ettől jobb eszközt tudnék találni a feladat megvalósítására.
|
||||
|
||||
A Google Maps API-t, ami alapvetően csak egy JavaScript csomag, rengetegen újracsomagolják, hogy különböző részét, különböző keretrendszerekben is lehessen használni.
|
||||
Ezek közül én a Google Map React-et \cite{google-map-react} választottam, egyrészt mert támogatja a hőtérképes réteg használatát,
|
||||
másrészt mert lehetővé teszi a térképen való React komponensek renderelését az alapértelmezett markerek helyett.
|
61
docs/thesis/content/birdmap-test.tex
Normal file
@ -0,0 +1,61 @@
|
||||
%----------------------------------------------------------------------------
|
||||
\chapter{Tesztkörnyezet}
|
||||
\label{chapt:birdmap-test}
|
||||
%----------------------------------------------------------------------------
|
||||
Az alkalmazásom fejlesztésének megkönnyítése érdekében nagy hangsúlyt fektettem a tesztelhetőségre.
|
||||
Helyettesíteni akartam az éles rendszer komponenseivel való kommunikációt,
|
||||
hogy abban az esetben is folyni tudjon a fejlesztés, ha a rendszer épp nem elérhető.
|
||||
Ezen kívül hasznos, ha az alkalmazás által feldolgozott adatok személyre szabhatóak,
|
||||
hiszen sokszor olyan problémákra lehet így fényt deríteni, amelyek nem vagy csak jóval később jönnének elő az éles rendszer használata során.
|
||||
|
||||
A tesztelhetőség megvalósításához három szoftver komponenst kell helyettesítenem,
|
||||
melyeket az alábbi szekciókban ismertetek.
|
||||
%----------------------------------------------------------------------------
|
||||
\section{Helyettesítő szolgáltatások}
|
||||
%----------------------------------------------------------------------------
|
||||
Az alkalmazásom szerver oldali szolgáltatásai a Birbnetes Command and Control (a kódban Device) és Input Service-ekkel azok OpenAPI leíróiból generált interfészein keresztül kommunikál.
|
||||
Ezen interfészek mögé bármilyen implementáció regisztrálható, mely helyettesíti az éles rendszer működését.
|
||||
|
||||
Készítettem egy osztályt \verb+DummyDeviceAndInputService+ néven, mely a szerver indulásakor mű eszköz adatokat generál egy lokális változóval állítható darabszámban,
|
||||
majd ezeket egy belső listában tárolja. Az eszközök státuszát és koordinátáit egy véletlenszám generátor segítségével határozom meg.
|
||||
Az osztály implementálja a Device Service interfészét, melynek metódusai az imént említett mű eszközlista elemeivel dolgoznak,
|
||||
azok státuszát olvassák és módosítják.
|
||||
Illetve implementálja az Input Service interfészét,
|
||||
melynek metódusa bármilyen paraméterből kapott egyedi azonosító esetén visszaad egy véletlenszerűen kiválasztott bekapcsolt státuszú eszközt a listából.
|
||||
|
||||
Az alkalmazás által regisztrált és ezáltal használt interfész implementációi a konfigurációs fájl egy logikai értéke alapján cserélhető az éles és a helyettesítő között,
|
||||
a \ref{lst:dummy-service-registration}-es listában látható módon.
|
||||
\newpage
|
||||
\begin{lstlisting}[style=csharp, caption=A helyettesítő és az éles szolgáltatások regisztrálásának logikája, label=lst:dummy-service-registration]
|
||||
if (configuration.GetValue<bool>("UseDummyServices"))
|
||||
{
|
||||
services.AddTransient<IInputService, DummyDeviceAndInputService>();
|
||||
services.AddTransient<IDeviceService, DummyDeviceAndInputService>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddTransient<IInputService, LiveInputService>();
|
||||
services.AddTransient<IDeviceService, LiveDeviceService>();
|
||||
}
|
||||
\end{lstlisting}
|
||||
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\section{MQTT tesztalkalmazás}
|
||||
%----------------------------------------------------------------------------
|
||||
Az MQTT.NET szoftvercsomag github oldalán található néhány példa a csomag használatára \cite{mqttnet-examples}.
|
||||
Ezek között találtam Sepp Penner MQTTnet.TestApp.WinForm \cite{mqttnet-winforms} projektjét,
|
||||
mely egy Windows Forms applikáció az említett szoftvercsomag által nyújtott funkcionalitások tesztelésére.
|
||||
Indítható vele MQTT szerver, feliratkozó kliens és publikáló kliens is.
|
||||
Ezek meglétével az alkalmazás képes az üzenetek manuális publikálására egy a felületen beállítható témában.
|
||||
Én azonban szerettem volna az üzeneteket automatikusan bizonyos időközönként küldeni,
|
||||
ezért átalakítottam az alkalmazást az igényeimnek megfelelően a \ref{fig:mqtt-tester}-es ábrán látható módon.
|
||||
Elhelyeztem a felületen egy csúszkát, mellyel az üzenet küldés intervalluma állítható, illetve két új gombot,
|
||||
melyekkel az üzenet küldő időzítő indítható és megállítható.
|
||||
Az alkalmazás képes üzenetek adatainak generálására, mellyel az AI Service által publikált üzenetek modelljeivel azonos adatokat generálok.
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=150mm, keepaspectratio]{figures/MQTT-Tester.png}
|
||||
\caption{Az MQTT kommunikációt tesztelő alkalmazás felületének egy része}
|
||||
\label{fig:mqtt-tester}
|
||||
\end{figure}
|
135
docs/thesis/content/birdnetes-introduction.tex
Normal file
@ -0,0 +1,135 @@
|
||||
%----------------------------------------------------------------------------
|
||||
\chapter{A vizualizálni kívánt rendszer bemutatása}
|
||||
\label{chapt:birdnetes-introduction}
|
||||
%----------------------------------------------------------------------------
|
||||
Az alkalmazásom célja egy létező rendszer, a Birbnetes folyamatainak vizualizálása.
|
||||
Ebben a fejezetben ismertetem a Birbnetes mikroszolgáltatás rendszerének architektúráját és az általa használt technológiákat.
|
||||
Részletesen kifejtem az alkalmazásom szempontjából fontos komponensek feladatát és működését.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\section{Elméleti összefoglaló}
|
||||
%----------------------------------------------------------------------------
|
||||
A bemutatásra kerülő rendszert a tanszéken egy projekt keretén belül készítették kollégáim,
|
||||
melyet részletesen dokumentálták korábbi nyilvános publikációkban \cite{birdnetes-tdk} \cite{birdnetes-thesis}.
|
||||
A következőkben a rendszer által használt technológiákat és elveket csak olyan szinten részletezem,
|
||||
hogy annak működése érhető legyen.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Cloud, felhőalapú rendszerek}
|
||||
%----------------------------------------------------------------------------
|
||||
A cloud lényegében annyit jelent, hogy a szervert, amin az alkalmazás fut, nem a fejlesztőnek kell üzemeltetnie,
|
||||
hanem valamilyen másik szervezet\footnotemark által vannak karban tartva.
|
||||
Ez több okból is hasznos:
|
||||
\begin{itemize}
|
||||
\item \textbf{Költséghatékonyabb}. Nem szükséges berendezéseket vásárolni, azok üzemeltetési díja nem közvetlen a fejlesztőt éri. Az egyetlen költség a bérlés, ami általában töredéke annak, amit akkor fizetnénk ha magunk csinálnánk az egészet.
|
||||
\item \textbf{Gyorsabb fejlesztés}. Az alkalmazás futtatására használt szervereket általában a fejlesztő nem látja, ezekkel nem kell foglalkoznia. Ha az alkalmazásnak hirtelen nagyobb erőforrás igénye lesz, a rendszer automatikusan skálázódik.
|
||||
\item \textbf{Nagyobb megbízhatóság}. Az ilyen szolgáltatást nyújtó szervezeteknek ez az egyik legnagyobb feladata. Az alkalmazás bárhol és bármikor elérhető.
|
||||
\end{itemize}
|
||||
|
||||
\footnotetext{Ilyenek például a Microsoft Azure, az Amazon Web Services vagy a Google Cloud.}
|
||||
%----------------------------------------------------------------------------
|
||||
\subsubsection{Mikroszolgáltatások}
|
||||
%----------------------------------------------------------------------------
|
||||
A mikroszolgáltatások (microservices) nem sok mindenben különböznek egy általános szolgáltatástól.
|
||||
Ugyan úgy valamilyen kéréseket kiszolgáló egységek, legyen az web kérések kiszolgálása HTTP-n keresztül
|
||||
vagy akár parancssori utasítások feldolgozása. Az egyetlen fő különbség az a szolgáltatások felelősségköre.
|
||||
A mikroszolgáltatások fejlesztésénél a fejlesztők elsősorban arra törekednek, hogy egy komponensnek minél kevesebb feladata és függősége legyen,
|
||||
ezzel megnő a tesztelhetőség és könnyebb a skálázhatóság.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsubsection{Konténerek}
|
||||
%----------------------------------------------------------------------------
|
||||
A konténerek az operációs rendszer virtualizációt megvalósító egyik alkalmazása.
|
||||
Ezekre különböző korlátozások rakhatók például, hogy a konténer nem látja a teljes fájlrendszert, annak csak egy kijelölt részét,
|
||||
megadható a konténer által használható processzor és memória igény vagy akár korlátozható az is, hogy a konténer hogyan használhatja a hálózatot.
|
||||
Léteznek eszközök, például a Docker \cite{docker}, mely lehetővé teszi a fejlesztők számára az ilyen konténerek könnyed létrehozását és futtatását.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsubsection{Kubernetes}
|
||||
%----------------------------------------------------------------------------
|
||||
A Kubernetes \cite{kubernetes} a komplex konténerizált mikroszolgáltatás rendszerek menedzselésének könnyítését szolgálja.
|
||||
Kihasználja és ötvözi az imént említett technológiák előnyeit, hogy egy robosztus rendszert alkosson.
|
||||
Használatával felgyorsulhat és automatizált lehet az egyes konténerek telepítése, futtatása, de talán a legfőbb előnye,
|
||||
hogy segítségével könnyedén megoldható a rendszert ért terhelési igények szerinti dinamikus skálázódás.
|
||||
Azok a mikroszolgáltatások, amikre a rendszernek épp nincs szüksége, minimális erőforrást igényelnek a szerveren,
|
||||
így nem kell utánuk annyit fizetni sem. Ezzel ellentétben, ha valamely szolgáltatás után hirtelen megnő az igény,
|
||||
akkor az könnyedén duplikálható.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{MQTT}
|
||||
\label{subsect:mqtt}
|
||||
%----------------------------------------------------------------------------
|
||||
Az MQTT (Message Queue Telemetry Transport) az egy kliens-szerver publish/subscribe üzenetküldő protokoll. Könnyű implementálni és alacsony a sávszélesség igénye,
|
||||
mellyel tökéletes jelöltje a Machine to Machine (M2M), illetve az Internet of Things (IoT) kommunikáció megvalósítására.
|
||||
Működéséhez szükség van egy szerverre, amelynek feladata a beérkező üzenetek tovább küldése témák alapján. Egyes kliensek fel tudnak iratkozni bizonyos témákra, míg más kliensek publikálnak
|
||||
és a szerver levezényli a két fél között a kommunikációt.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{OpenAPI}
|
||||
%----------------------------------------------------------------------------
|
||||
Az OpenAPI egy nyilvános alkalmazás-programozási leíró, amely a fejlesztők számára hozzáférést biztosít egy másik alkalmazáshoz.
|
||||
Az API-k leírják és meghatározzák, hogy egy alkalmazás hogyan kommunikálhat egy másikkal,
|
||||
melyet használva a fejlesztők könnyedén képesek a kommunikációra képes kódot írni vagy generálni.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\section{Rendszerszintű architektúra}
|
||||
%----------------------------------------------------------------------------
|
||||
A Birbnetes fejlesztése során kifejezetten fontos szerepe volt a mikroszolgáltatás alapú rendszerek elvei követésének.
|
||||
A rendszer egy Kubernetes klaszterben van telepítve és több kisebb komponensből áll, melyek egymás között a HTTP és az MQTT protokollok segítségével kommunikálnak.
|
||||
A rendszer összes szolgáltatásának van egy OpenAPI leírója, melyet használva hamar volt egy olyan kódbázisom, amely képes volt a rendszerrel való kommunikációra.
|
||||
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsection{Főbb komponensek}
|
||||
%----------------------------------------------------------------------------
|
||||
A \ref{fig:birdnetes-components}-es ábrán láthatóak a rendszer komponensei, melyek mindegyike egy-egy mikroszolgáltatás.
|
||||
Az egymás mellett lévő kék levélborítékok az MQTT kommunikációt jelölik,
|
||||
amellyel például a természetben elhelyezett eszközök felé irányuló kommunikáció is történik.
|
||||
A következő alszakaszokban bemutatom az alkalmazásom szempontjából fontosabb komponenseket.
|
||||
|
||||
\begin{figure}[!ht]
|
||||
\centering
|
||||
\includegraphics[width=150mm, keepaspectratio]{figures/architecture-redesigned.png}
|
||||
\caption{A Birbnetes rendszer architektúrája. Forrás: Madárhang azonosító és riasztó felhő-natív rendszer TDK dolgozat \cite{birdnetes-tdk}}
|
||||
\label{fig:birdnetes-components}
|
||||
\end{figure}
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsubsection{IoT eszközök}
|
||||
%----------------------------------------------------------------------------
|
||||
Szőlőültetvényekben telepített eszközök, melyek adott időközönként publikálják állapotaikat egyéb metaadatokkal egy üzenetsoron.
|
||||
Emellett folyamatosan hangfelvételt készítenek a beépített mikrofonjaikkal, mely hangfelvételekről egy másik belső szenzor eldönti,
|
||||
hogy érdemes-e felküldeni a rendszerbe, ha igen, akkor egy másik üzenetsoron publikálják ezeket a hangfelvételeket.
|
||||
Tartalmaznak még egy hangszórót is, mely a madarak elijesztését szolgálja.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsubsection{Input Service}
|
||||
\label{subsect:birdnetes-input-service}
|
||||
%----------------------------------------------------------------------------
|
||||
A kihelyezett IoT eszközök által felvett hangfájlok ezen a komponensen keresztül érkeznek be a rendszerbe.
|
||||
Itt történik a hanganyaghoz tartozó metaadatok lementése az Input Service saját relációs adatbázisába.
|
||||
Ilyenek például a beküldő eszköz azonosítója, a beérkezés dátuma vagy a hangüzenet rendszerszintű egyedi azonosítója.
|
||||
Amint a szolgáltatás a beérkezett üzenettel kapcsolatban elvégezte az összes feladatát,
|
||||
publikál egy üzenetet egy másik üzenetsorra a többi kliensnek feldolgozásra.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsubsection{AI Service}
|
||||
\label{subsect:birdnetes-ai-service}
|
||||
%----------------------------------------------------------------------------
|
||||
Az AI Service példányai fogadják az Input Service-től érkező üzeneteket és elkezdik klasszifikálni az abban található hanganyagot.
|
||||
Meghatározzák, hogy a hanganyag mekkora valószínűséggel volt seregély hang vagy sem.
|
||||
Ennek eredményét a hangminta egyedi azonosítójával együtt publikálják egy másik üzenetsoron.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsubsection{Guard Service}
|
||||
%----------------------------------------------------------------------------
|
||||
A Guard Service feliratkozik az AI Service által publikált üzenetek témájára
|
||||
és valamilyen valószínűségi kritérium alapján eldönti, hogy a hangminta tartalmaz-e seregély hangot.
|
||||
Ha igen, akkor az üzenetsoron küld egy riasztás parancsot a hanganyagot küldő eszköznek.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\subsubsection{Command and Control Service}
|
||||
%----------------------------------------------------------------------------
|
||||
A Command and Control Service az előzőkkel ellentétben egyáltalán nem vesz részt a minták fogadásában, feldolgozásában vagy kezelésében.
|
||||
Felelősége az eszközök és azok szenzorjai állapotának menedzselése és követése.
|
||||
Ezen keresztül lehet az egyes eszközöket ki- és bekapcsolni.
|
40
docs/thesis/content/introduction.tex
Normal file
@ -0,0 +1,40 @@
|
||||
%----------------------------------------------------------------------------
|
||||
\chapter{\bevezetes}
|
||||
%----------------------------------------------------------------------------
|
||||
Szőlőtulajdonosoknak éves szinten jelentős kárt okoznak a seregélyek, akik előszeretettel választják táplálékul a megtermelt szőlőt.
|
||||
Erre a problémára dolgoztak ki a tanszéken diáktársaim egy felhőalapú konténerizált rendszert, a Birbnetes-t
|
||||
mely a természetben elhelyezett eszközökkel kommunikál, azokat vezérli.
|
||||
Az eszközök bizonyos időközönként hangfelvételt készítenek a környezetükről,
|
||||
majd valamilyen formában elküldik ezeket a felvételeket a központi rendszernek,
|
||||
amely egy erre a célra kifejlesztett mesterséges intelligenciát használva eldönti
|
||||
a felvételről, hogy azon található-e seregély hang vagy sem.
|
||||
Ha igen akkor jelez a felvételt küldő eszköznek, hogy szólaltassa meg a riasztó
|
||||
berendezését, hogy elijessze a madarakat.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\section{Probléma}
|
||||
%----------------------------------------------------------------------------
|
||||
A jelen rendszer használata során nincs vizuális visszacsatolás az esetleges riasztásokról azok gyakoriságáról
|
||||
és a rendszer állapotáról sem. Különböző diagnosztikai eszközök ugyan implementálva lettek, mint például
|
||||
a naplózás vagy a hiba bejelentés, de ezek használata nehézkes, nem kézenfekvő.
|
||||
Szükség van egy olyan megoldásra, amivel egy helyen és egyszerűen lehet kezelni és felügyelni a rendszer egyes elemeit.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\section{Megoldás}
|
||||
%----------------------------------------------------------------------------
|
||||
A jelen szakdolgozat egy olyan webes alkalmazás elkészítését dokumentálja, mellyel a felhasználók képesek
|
||||
a természetben elhelyezett eszközök állapotát vizsgálni, azokat akár ki és bekapcsolni igény szerint.
|
||||
Az egyes rendszer eseményeket vizsgálva a szoftver statisztikákat készít, melyeket különböző diagramokon ábrázolok.
|
||||
Ilyen statisztikák például, hogy időben melyik eszköz mikor észlelt madárhangot, vagy hogy hány hang üzenet érkezik
|
||||
az eszközöktől másodpercenként.
|
||||
|
||||
%----------------------------------------------------------------------------
|
||||
\section{A szakdolgozat felépítése}
|
||||
%----------------------------------------------------------------------------
|
||||
A szakdolgozatom első részében, a \ref{chapt:birdnetes-introduction}. fejezetben, bemutatom a vizualizálni kívánt rendszer felépítését, az egyes komponensek közötti kapcsolatokat,
|
||||
valamint a vizualizációs szempontból releváns technológiákat, amire a rendszer épült.
|
||||
A \ref{chapt:birdmap-introduction}. fejezetben ismertetem a jelenleg az iparban is használt mikroszolgáltatás működését vizualizáló alternatívákat, majd a saját megoldásom tervezetét, az arra vonatkozó elvárásokat.
|
||||
A \ref{chapt:birdmap-technologies}. fejezetben az alkalmazásom által használt technológiákat mutatom be,
|
||||
ezzel előkészítve az \ref{chapt:birdmap-backend}. és \ref{chapt:birdmap-frontend}. fejezetet, ahol ismertetem a szerver- és kliensalkalmazások felépítését.
|
||||
A \ref{chapt:birdmap-test}. és \ref{chapt:birdmap-kubernetes}. fejezet az alkalmazás teszteléséről és telepítéséről szól.
|
||||
Az utolsó fejezetben értékelem a munkám eredményét, levonom a tapasztalatokat és bemutatok néhány továbbfejlesztési lehetőséget.
|
20
docs/thesis/content/summary.tex
Normal file
@ -0,0 +1,20 @@
|
||||
%----------------------------------------------------------------------------
|
||||
\chapter{Értékelés}
|
||||
\label{chapt:summary}
|
||||
%----------------------------------------------------------------------------
|
||||
Úgy gondolom, hogy az alkalmazásom elérte a célját.
|
||||
Egy használható felületet nyújt a Birbnetes mikroszolgáltatás rendszere működésének vizualizálására.
|
||||
A fejlesztés közben jelentős figyelmet fordítottam arra, hogy az alkalmazás felületi és kód komponensei között is
|
||||
minimalizáltak legyenek a függőségek, így a rendszerben történő változások esetén azok könnyen cserélhetőek, bővíthetőek.
|
||||
%----------------------------------------------------------------------------
|
||||
\section{Továbbfejlesztési lehetőségek}
|
||||
%----------------------------------------------------------------------------
|
||||
Az kliens oldalon történő diagramok adatainak generálása hamar túl nagy falatnak bizonyult.
|
||||
A bevetett optimalizációk ellenére sem lett hatványozottan gyorsabb a felület.
|
||||
Így az első és legfontosabb továbbfejlesztési teendő az adatok szerveroldalon történő generálása lenne.
|
||||
|
||||
A Logs oldal jelenleg csak a szerveroldalon készült napló fájlokat tartalmazza.
|
||||
Hasznos lenne, ha az egyes mikroszolgáltatások naplófájljai is letölthetőek lennének.
|
||||
|
||||
Ezen kívül előnyös lenne a rendszer belső működését vizualizáló komponensek alkalmazása is,
|
||||
ahol lehetne látni az egyes mikroszoltáltatásokra vonatkozó különböző metrikákat például az adatfeldolgozási időt vagy a beérkezett kérések számát.
|
BIN
docs/thesis/figures/MQTT-Tester.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
docs/thesis/figures/TeXstudio.png
Normal file
After Width: | Height: | Size: 168 KiB |
BIN
docs/thesis/figures/appbar-admin.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
docs/thesis/figures/appbar-user-admin.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
docs/thesis/figures/appbar-user.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
docs/thesis/figures/architecture-redesigned.png
Normal file
After Width: | Height: | Size: 43 KiB |
3
docs/thesis/figures/architecture-redesigned.svg
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
docs/thesis/figures/birdmap-frontend-routes.png
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
docs/thesis/figures/birdmap-login.png
Normal file
After Width: | Height: | Size: 6.4 KiB |