Compare commits
10 Commits
5ed3d074b0
...
7a83bf1216
Author | SHA1 | Date | |
---|---|---|---|
|
7a83bf1216 | ||
|
58077874c6 | ||
|
870c6b079e | ||
|
cbd28d7841 | ||
|
7a4cf37c2a | ||
|
dd1c2b299a | ||
|
f19d5d72d5 | ||
|
a5ba94ad25 | ||
|
cd8a18090f | ||
|
547aefe2e1 |
15
server/package-lock.json
generated
15
server/package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "ifms-pti-svr",
|
||||
"version": "3.4.9",
|
||||
"version": "3.6.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ifms-pti-svr",
|
||||
"version": "3.4.9",
|
||||
"version": "3.6.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^4.7.1",
|
||||
|
@ -17,6 +17,7 @@
|
|||
"bcrypt": "^5.0.0",
|
||||
"core-js": "^3.19.1",
|
||||
"crypto-js": "^4.0.0",
|
||||
"dataloader": "^2.2.1",
|
||||
"date-fns": "^2.16.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"graphql": "^14.6.0",
|
||||
|
@ -3226,6 +3227,11 @@
|
|||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/dataloader": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.1.tgz",
|
||||
"integrity": "sha512-Zn+tVZo1RKu120rgoe0JsRk56UiKdefPSH47QROJsMHrX8/S9UJvi5A/A6+Sbuk6rE88z5JoM/wIJ09Z7BTfYA=="
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "2.29.3",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
||||
|
@ -8785,6 +8791,11 @@
|
|||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"dataloader": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.1.tgz",
|
||||
"integrity": "sha512-Zn+tVZo1RKu120rgoe0JsRk56UiKdefPSH47QROJsMHrX8/S9UJvi5A/A6+Sbuk6rE88z5JoM/wIJ09Z7BTfYA=="
|
||||
},
|
||||
"date-fns": {
|
||||
"version": "2.29.3",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ifms-pti-svr",
|
||||
"version": "3.4.9",
|
||||
"version": "3.6.1",
|
||||
"description": "Servidor do Portal de TI do IFMS",
|
||||
"main": "src/index.js",
|
||||
"prisma": {
|
||||
|
@ -48,6 +48,7 @@
|
|||
"bcrypt": "^5.0.0",
|
||||
"core-js": "^3.19.1",
|
||||
"crypto-js": "^4.0.0",
|
||||
"dataloader": "^2.2.1",
|
||||
"date-fns": "^2.16.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"graphql": "^14.6.0",
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "AccessPoint" ADD COLUMN "networkId" INTEGER;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "AccessPoint" ADD CONSTRAINT "AccessPoint_networkId_fkey" FOREIGN KEY ("networkId") REFERENCES "Network"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
@ -153,6 +153,9 @@ model AccessPoint {
|
|||
uplinkSpeed Int?
|
||||
stats AccessPointStats[] @relation("accesspointstats_to_ap")
|
||||
wifiDevices WifiDevice[] @relation("wifidevice_to_ap")
|
||||
|
||||
network Network? @relation(fields: [networkId], references: [id])
|
||||
networkId Int?
|
||||
}
|
||||
|
||||
model AccessPointStats {
|
||||
|
@ -196,6 +199,7 @@ model Network {
|
|||
updatedAt DateTime @updatedAt
|
||||
NetworkStats NetworkStats[]
|
||||
WifiDevice WifiDevice[]
|
||||
AccessPoint AccessPoint[]
|
||||
|
||||
@@index([id])
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import prisma from '../prisma'
|
|||
|
||||
import { getAccessPoints as getCiscoAccessPoints } from './ciscoController'
|
||||
import { logInfo, logSuccess } from './logger'
|
||||
import { getSubnetInfo } from './subnetInfo'
|
||||
import { getAccessPoints as getUnifiAccessPoints } from './unifiController'
|
||||
|
||||
async function getAccessPoints() {
|
||||
|
@ -20,11 +21,19 @@ async function updateDB(accessPoints) {
|
|||
const dbAccessPoints = []
|
||||
|
||||
for (const accessPoint of accessPoints) {
|
||||
const network = getSubnetInfo(accessPoint.ip)
|
||||
|
||||
dbAccessPoints.push(
|
||||
await prisma.accessPoint.upsert({
|
||||
where: { mac: accessPoint.mac },
|
||||
create: accessPoint,
|
||||
update: accessPoint
|
||||
create: {
|
||||
...accessPoint,
|
||||
networkId: network.id || undefined
|
||||
},
|
||||
update: {
|
||||
...accessPoint,
|
||||
networkId: network.id || undefined
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
|
@ -205,8 +205,6 @@ export async function getOnlineWifiDevices() {
|
|||
}
|
||||
}
|
||||
|
||||
const fs = require('fs')
|
||||
|
||||
export async function getAccessPoints() {
|
||||
try {
|
||||
const [accessPoints] = await unifiController.getAccessDevices('default')
|
||||
|
|
|
@ -2,6 +2,9 @@ import prisma from '../prisma'
|
|||
import { getSubnetInfo } from '../lib/subnetInfo'
|
||||
import { subMinutes } from 'date-fns'
|
||||
import { distributedCopy } from '../utils/distributedCopy'
|
||||
import Dataloader from 'dataloader'
|
||||
|
||||
const statsLoader = new Dataloader(getStatsUsingAccessPointId)
|
||||
|
||||
export const AccessPoint = {
|
||||
updatedAt: (parent, data, context, info) => parent.updatedAt?.toISOString(),
|
||||
|
@ -26,19 +29,14 @@ export const AccessPoint = {
|
|||
stats: async (parent, { take, minutesIn = 60, dateOut }, context, info) => {
|
||||
const dateIn = subMinutes(dateOut || Date.now(), minutesIn)
|
||||
|
||||
const stats = await prisma.accessPointStats.findMany({
|
||||
where: {
|
||||
accessPoint: {
|
||||
id: parent.id
|
||||
},
|
||||
timestamp: { gte: dateIn, lte: dateOut }
|
||||
},
|
||||
orderBy: { timestamp: 'desc' }
|
||||
const accessPoint = await statsLoader.load({
|
||||
id: parent.id,
|
||||
minutesIn,
|
||||
dateIn,
|
||||
dateOut
|
||||
})
|
||||
|
||||
if (take) return distributedCopy(stats, take)
|
||||
|
||||
return stats
|
||||
return distributedCopy(accessPoint.stats, take)
|
||||
},
|
||||
|
||||
latestStats: async (parent, data, context, info) =>
|
||||
|
@ -47,3 +45,33 @@ export const AccessPoint = {
|
|||
orderBy: { timestamp: 'desc' }
|
||||
})
|
||||
}
|
||||
|
||||
async function getStatsUsingAccessPointId(keys) {
|
||||
const ids = keys.map(key => key.id)
|
||||
|
||||
const dateIn = keys[0].dateIn
|
||||
const dateOut = keys[0].dateOut
|
||||
|
||||
const accessPointsWithStats = await prisma.accessPoint.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: ids
|
||||
}
|
||||
},
|
||||
include: {
|
||||
stats: {
|
||||
where: {
|
||||
timestamp: { gte: dateIn, lte: dateOut }
|
||||
},
|
||||
orderBy: { timestamp: 'desc' }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// order accessPointsWithStats in the same order as keys
|
||||
const accessPointsWithStatsOrdered = ids.map(id =>
|
||||
accessPointsWithStats.find(accessPoint => accessPoint.id === id)
|
||||
)
|
||||
|
||||
return accessPointsWithStatsOrdered
|
||||
}
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
import { updateAccessPoints } from '../../lib/accessPoints'
|
||||
import prisma from '../../prisma'
|
||||
|
||||
export async function accessPoints() {
|
||||
export async function accessPoints(_, { networkShortName }) {
|
||||
updateAccessPoints()
|
||||
|
||||
return prisma.accessPoint.findMany({
|
||||
where: {
|
||||
network: {
|
||||
shortName: networkShortName
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
hostname: 'asc'
|
||||
},
|
||||
include: {
|
||||
network: true,
|
||||
wifiDevices: {
|
||||
where: {
|
||||
status: 'ONLINE'
|
||||
|
|
|
@ -59,7 +59,8 @@ const typeDefs = gql`
|
|||
pAHosts: [PAHost!]! @auth(roles: ["superAdmin"])
|
||||
|
||||
"All Access Points"
|
||||
accessPoints: [AccessPoint!]! @auth(roles: ["superAdmin"])
|
||||
accessPoints(networkShortName: String): [AccessPoint!]!
|
||||
@auth(roles: ["superAdmin"])
|
||||
|
||||
"One Access Point"
|
||||
accessPoint(id: ID!): AccessPoint! @auth(roles: ["superAdmin"])
|
||||
|
|
4
web/package-lock.json
generated
4
web/package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "ifms-pti",
|
||||
"version": "3.4.9",
|
||||
"version": "3.6.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ifms-pti",
|
||||
"version": "3.4.9",
|
||||
"version": "3.6.1",
|
||||
"dependencies": {
|
||||
"@mdi/font": "^6.9.96",
|
||||
"apollo-link-ws": "^1.0.20",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ifms-pti",
|
||||
"version": "3.4.9",
|
||||
"version": "3.6.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
|
|
|
@ -217,6 +217,18 @@ const routes = [
|
|||
component: () =>
|
||||
import(/* webpackChunkName: "wifi-stats" */ '../views/WifiStats.vue')
|
||||
},
|
||||
{
|
||||
path: '/access-points/stats',
|
||||
name: 'access-points-stats',
|
||||
meta: {
|
||||
title: 'Status dos Access Points',
|
||||
roles: ['superAdmin']
|
||||
},
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "access-points-stats" */ '../views/AccessPoints/stats.vue'
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
path: '/access-points/:id/clients',
|
||||
|
|
|
@ -62,6 +62,15 @@
|
|||
hide-details
|
||||
:label="`Somente ${me.campusFull}`"
|
||||
/>
|
||||
<v-btn
|
||||
class="ml-2"
|
||||
text
|
||||
color="secondary"
|
||||
:to="{ name: 'access-points-stats' }"
|
||||
>
|
||||
<v-icon left>mdi-chart-line</v-icon>
|
||||
Status
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-data-table
|
||||
|
@ -394,8 +403,8 @@ export default {
|
|||
fetchPolicy: 'cache-and-network',
|
||||
debounce: 200,
|
||||
query: gql`
|
||||
query accessPoints {
|
||||
accessPoints {
|
||||
query accessPoints($networkShortName: String) {
|
||||
accessPoints(networkShortName: $networkShortName) {
|
||||
id
|
||||
mac
|
||||
hostname
|
||||
|
@ -420,6 +429,11 @@ export default {
|
|||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
networkShortName: (this.sameCampus && this.me?.campus) || undefined
|
||||
}
|
||||
},
|
||||
subscribeToMore: {
|
||||
document: gql`
|
||||
subscription {
|
||||
|
|
259
web/src/views/AccessPoints/stats.vue
Normal file
259
web/src/views/AccessPoints/stats.vue
Normal file
|
@ -0,0 +1,259 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-toolbar dense flat>
|
||||
<v-select
|
||||
v-model="subnet"
|
||||
class="mr-2"
|
||||
:items="subnetSelectItems"
|
||||
style="max-width: 250px"
|
||||
hide-details
|
||||
outlined
|
||||
dense
|
||||
prepend-inner-icon="mdi-home-group"
|
||||
/>
|
||||
|
||||
<v-spacer />
|
||||
<v-select
|
||||
v-model="orderBy"
|
||||
class="mr-2"
|
||||
style="max-width: 250px"
|
||||
:items="orderByOptions"
|
||||
hide-details
|
||||
outlined
|
||||
dense
|
||||
prepend-inner-icon="mdi-sort"
|
||||
/>
|
||||
<v-select
|
||||
v-model="minutesIn"
|
||||
style="max-width: 250px"
|
||||
:items="minutesInItems"
|
||||
hide-details
|
||||
outlined
|
||||
dense
|
||||
prepend-inner-icon="mdi-timer-outline"
|
||||
/>
|
||||
</v-toolbar>
|
||||
<v-expand-transition>
|
||||
<v-progress-linear
|
||||
v-if="$apollo.queries.accessPoints.loading"
|
||||
class="pa-0 ma-0"
|
||||
indeterminate
|
||||
/>
|
||||
</v-expand-transition>
|
||||
<v-container fluid>
|
||||
<v-row dense>
|
||||
<v-col
|
||||
v-for="ap in orderedAccessPoints"
|
||||
:key="ap.id"
|
||||
sm="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
>
|
||||
<v-card class="mb-1" flat outlined>
|
||||
<v-btn
|
||||
block
|
||||
text
|
||||
class="font-weight-light"
|
||||
:to="{
|
||||
name: 'access-point-clients',
|
||||
params: { id: ap.id }
|
||||
}"
|
||||
>
|
||||
{{ ap.name || ap.hostname }}
|
||||
<span v-if="ap.local"> ({{ ap.local }}) </span>
|
||||
</v-btn>
|
||||
<v-card-text class="pa-0">
|
||||
<ClientsChart
|
||||
:stats="ap.stats"
|
||||
:subnet="ap.subnet"
|
||||
:height="90"
|
||||
hide-labels
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import ClientsChart from '../../components/charts/ClientsChart.vue'
|
||||
|
||||
export default {
|
||||
name: 'AccessPointsStats',
|
||||
components: {
|
||||
ClientsChart
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
subnet: null,
|
||||
minutesIn: 720,
|
||||
minutesInItems: [
|
||||
{
|
||||
text: 'Última hora',
|
||||
value: 60
|
||||
},
|
||||
{
|
||||
text: 'Últimas 3 horas',
|
||||
value: 180
|
||||
},
|
||||
{
|
||||
text: 'Últimas 6 horas',
|
||||
value: 360
|
||||
},
|
||||
{
|
||||
text: 'Últimas 12 horas',
|
||||
value: 720
|
||||
},
|
||||
{
|
||||
text: 'Últimas 24 horas',
|
||||
value: 1440
|
||||
},
|
||||
{
|
||||
text: 'Últimos 7 dias',
|
||||
value: 10080
|
||||
},
|
||||
{
|
||||
text: 'Últimos 30 dias',
|
||||
value: 43200
|
||||
}
|
||||
],
|
||||
orderByOptions: [
|
||||
'Alfabética',
|
||||
'Clientes atualmente',
|
||||
'Pico de clientes',
|
||||
'Média de clientes'
|
||||
],
|
||||
orderBy: 'Pico de clientes'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
subnetSelectItems() {
|
||||
return this.subnets?.map(network => {
|
||||
return {
|
||||
text: network.name,
|
||||
value: network.shortName
|
||||
}
|
||||
})
|
||||
},
|
||||
orderedAccessPoints() {
|
||||
const accessPoints = this.accessPoints
|
||||
if (!accessPoints) return []
|
||||
|
||||
const orderBy = this.orderBy
|
||||
|
||||
if (orderBy === 'Alfabética') {
|
||||
return accessPoints.sort((a, b) => {
|
||||
const aName = a.name || a.hostname
|
||||
const bName = b.name || b.hostname
|
||||
return aName.localeCompare(bName)
|
||||
})
|
||||
}
|
||||
|
||||
if (orderBy === 'Clientes atualmente') {
|
||||
return accessPoints.sort((a, b) => {
|
||||
const aClients = a.clients || 0
|
||||
const bClients = b.clients || 0
|
||||
return bClients - aClients
|
||||
})
|
||||
}
|
||||
|
||||
if (orderBy === 'Pico de clientes') {
|
||||
return accessPoints.sort((a, b) => {
|
||||
const aStats = a.stats || []
|
||||
const bStats = b.stats || []
|
||||
const aMaxClients = aStats.reduce((max, stat) => {
|
||||
return Math.max(max, stat.clients)
|
||||
}, 0)
|
||||
const bMaxClients = bStats.reduce((max, stat) => {
|
||||
return Math.max(max, stat.clients)
|
||||
}, 0)
|
||||
return bMaxClients - aMaxClients
|
||||
})
|
||||
}
|
||||
|
||||
if (orderBy === 'Média de clientes') {
|
||||
return accessPoints.sort((a, b) => {
|
||||
const aStats = a.stats || []
|
||||
const bStats = b.stats || []
|
||||
const aAvgClients =
|
||||
aStats.reduce((sum, stat) => {
|
||||
return sum + stat.clients
|
||||
}, 0) / aStats.length
|
||||
const bAvgClients =
|
||||
bStats.reduce((sum, stat) => {
|
||||
return sum + stat.clients
|
||||
}, 0) / bStats.length
|
||||
return bAvgClients - aAvgClients
|
||||
})
|
||||
}
|
||||
|
||||
return accessPoints
|
||||
}
|
||||
},
|
||||
|
||||
apollo: {
|
||||
me: {
|
||||
query: gql`
|
||||
{
|
||||
me {
|
||||
campus
|
||||
}
|
||||
}
|
||||
`,
|
||||
result({ data }) {
|
||||
this.subnet = data?.me.campus
|
||||
}
|
||||
},
|
||||
subnets: {
|
||||
query: gql`
|
||||
{
|
||||
subnets {
|
||||
shortName
|
||||
name
|
||||
cidr
|
||||
}
|
||||
}
|
||||
`
|
||||
},
|
||||
accessPoints: {
|
||||
pollInterval: 10000,
|
||||
debounce: 500,
|
||||
cachePolicy: 'cache-and-network',
|
||||
skip() {
|
||||
return !this.subnet
|
||||
},
|
||||
query: gql`
|
||||
query accessPoints($networkShortName: String!, $minutesIn: Int!) {
|
||||
accessPoints(networkShortName: $networkShortName) {
|
||||
id
|
||||
mac
|
||||
name
|
||||
hostname
|
||||
clients
|
||||
local
|
||||
subnetInfo {
|
||||
shortName
|
||||
}
|
||||
stats(minutesIn: $minutesIn, take: 64) {
|
||||
timestamp
|
||||
id
|
||||
clients
|
||||
avgUsage
|
||||
sumUsage
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables() {
|
||||
return {
|
||||
networkShortName: this.subnet,
|
||||
minutesIn: this.minutesIn
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -3,8 +3,8 @@
|
|||
<v-toolbar dense flat>
|
||||
<v-spacer />
|
||||
<v-select
|
||||
class="mr-2"
|
||||
v-model="orderBy"
|
||||
class="mr-2"
|
||||
style="max-width: 250px"
|
||||
:items="orderByOptions"
|
||||
hide-details
|
||||
|
@ -26,7 +26,7 @@
|
|||
<v-progress-linear
|
||||
v-if="$apollo.queries.subnets.loading"
|
||||
class="pa-0 ma-0"
|
||||
:indeterminate="$apollo.queries.subnets.loading"
|
||||
indeterminate
|
||||
/>
|
||||
</v-expand-transition>
|
||||
<v-container fluid>
|
||||
|
|
Loading…
Reference in New Issue
Block a user