Compare commits

..

10 Commits

Author SHA1 Message Date
Douglas Barone
7a83bf1216 Patch 2023-02-13 09:50:43 -04:00
Douglas Barone
58077874c6 Bugfix 2023-02-13 09:50:12 -04:00
Douglas Barone
870c6b079e Added Dataloader 2023-02-13 09:32:11 -04:00
Douglas Barone
cbd28d7841 Version patch 2023-02-08 10:46:04 -04:00
Douglas Barone
7a4cf37c2a Lint 2023-02-08 10:45:06 -04:00
Douglas Barone
dd1c2b299a Added links 2023-02-08 10:44:27 -04:00
Douglas Barone
f19d5d72d5 Plot OK 2023-02-08 09:08:08 -04:00
Douglas Barone
a5ba94ad25 Data fetching OK 2023-02-08 08:12:22 -04:00
Douglas Barone
cd8a18090f Use filtering on index 2023-02-07 15:18:53 -04:00
Douglas Barone
547aefe2e1 Added accessPoints filtering 2023-02-07 15:07:52 -04:00
15 changed files with 375 additions and 27 deletions

View File

@ -1,12 +1,12 @@
{ {
"name": "ifms-pti-svr", "name": "ifms-pti-svr",
"version": "3.4.9", "version": "3.6.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ifms-pti-svr", "name": "ifms-pti-svr",
"version": "3.4.9", "version": "3.6.1",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@prisma/client": "^4.7.1", "@prisma/client": "^4.7.1",
@ -17,6 +17,7 @@
"bcrypt": "^5.0.0", "bcrypt": "^5.0.0",
"core-js": "^3.19.1", "core-js": "^3.19.1",
"crypto-js": "^4.0.0", "crypto-js": "^4.0.0",
"dataloader": "^2.2.1",
"date-fns": "^2.16.1", "date-fns": "^2.16.1",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"graphql": "^14.6.0", "graphql": "^14.6.0",
@ -3226,6 +3227,11 @@
"node": ">=0.10" "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": { "node_modules/date-fns": {
"version": "2.29.3", "version": "2.29.3",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
@ -8785,6 +8791,11 @@
"assert-plus": "^1.0.0" "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": { "date-fns": {
"version": "2.29.3", "version": "2.29.3",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "ifms-pti-svr", "name": "ifms-pti-svr",
"version": "3.4.9", "version": "3.6.1",
"description": "Servidor do Portal de TI do IFMS", "description": "Servidor do Portal de TI do IFMS",
"main": "src/index.js", "main": "src/index.js",
"prisma": { "prisma": {
@ -48,6 +48,7 @@
"bcrypt": "^5.0.0", "bcrypt": "^5.0.0",
"core-js": "^3.19.1", "core-js": "^3.19.1",
"crypto-js": "^4.0.0", "crypto-js": "^4.0.0",
"dataloader": "^2.2.1",
"date-fns": "^2.16.1", "date-fns": "^2.16.1",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"graphql": "^14.6.0", "graphql": "^14.6.0",

View File

@ -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;

View File

@ -153,6 +153,9 @@ model AccessPoint {
uplinkSpeed Int? uplinkSpeed Int?
stats AccessPointStats[] @relation("accesspointstats_to_ap") stats AccessPointStats[] @relation("accesspointstats_to_ap")
wifiDevices WifiDevice[] @relation("wifidevice_to_ap") wifiDevices WifiDevice[] @relation("wifidevice_to_ap")
network Network? @relation(fields: [networkId], references: [id])
networkId Int?
} }
model AccessPointStats { model AccessPointStats {
@ -196,6 +199,7 @@ model Network {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
NetworkStats NetworkStats[] NetworkStats NetworkStats[]
WifiDevice WifiDevice[] WifiDevice WifiDevice[]
AccessPoint AccessPoint[]
@@index([id]) @@index([id])
} }

View File

@ -2,6 +2,7 @@ import prisma from '../prisma'
import { getAccessPoints as getCiscoAccessPoints } from './ciscoController' import { getAccessPoints as getCiscoAccessPoints } from './ciscoController'
import { logInfo, logSuccess } from './logger' import { logInfo, logSuccess } from './logger'
import { getSubnetInfo } from './subnetInfo'
import { getAccessPoints as getUnifiAccessPoints } from './unifiController' import { getAccessPoints as getUnifiAccessPoints } from './unifiController'
async function getAccessPoints() { async function getAccessPoints() {
@ -20,11 +21,19 @@ async function updateDB(accessPoints) {
const dbAccessPoints = [] const dbAccessPoints = []
for (const accessPoint of accessPoints) { for (const accessPoint of accessPoints) {
const network = getSubnetInfo(accessPoint.ip)
dbAccessPoints.push( dbAccessPoints.push(
await prisma.accessPoint.upsert({ await prisma.accessPoint.upsert({
where: { mac: accessPoint.mac }, where: { mac: accessPoint.mac },
create: accessPoint, create: {
update: accessPoint ...accessPoint,
networkId: network.id || undefined
},
update: {
...accessPoint,
networkId: network.id || undefined
}
}) })
) )
} }

View File

@ -205,8 +205,6 @@ export async function getOnlineWifiDevices() {
} }
} }
const fs = require('fs')
export async function getAccessPoints() { export async function getAccessPoints() {
try { try {
const [accessPoints] = await unifiController.getAccessDevices('default') const [accessPoints] = await unifiController.getAccessDevices('default')

View File

@ -2,6 +2,9 @@ import prisma from '../prisma'
import { getSubnetInfo } from '../lib/subnetInfo' import { getSubnetInfo } from '../lib/subnetInfo'
import { subMinutes } from 'date-fns' import { subMinutes } from 'date-fns'
import { distributedCopy } from '../utils/distributedCopy' import { distributedCopy } from '../utils/distributedCopy'
import Dataloader from 'dataloader'
const statsLoader = new Dataloader(getStatsUsingAccessPointId)
export const AccessPoint = { export const AccessPoint = {
updatedAt: (parent, data, context, info) => parent.updatedAt?.toISOString(), updatedAt: (parent, data, context, info) => parent.updatedAt?.toISOString(),
@ -26,19 +29,14 @@ export const AccessPoint = {
stats: async (parent, { take, minutesIn = 60, dateOut }, context, info) => { stats: async (parent, { take, minutesIn = 60, dateOut }, context, info) => {
const dateIn = subMinutes(dateOut || Date.now(), minutesIn) const dateIn = subMinutes(dateOut || Date.now(), minutesIn)
const stats = await prisma.accessPointStats.findMany({ const accessPoint = await statsLoader.load({
where: { id: parent.id,
accessPoint: { minutesIn,
id: parent.id dateIn,
}, dateOut
timestamp: { gte: dateIn, lte: dateOut }
},
orderBy: { timestamp: 'desc' }
}) })
if (take) return distributedCopy(stats, take) return distributedCopy(accessPoint.stats, take)
return stats
}, },
latestStats: async (parent, data, context, info) => latestStats: async (parent, data, context, info) =>
@ -47,3 +45,33 @@ export const AccessPoint = {
orderBy: { timestamp: 'desc' } 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
}

View File

@ -1,14 +1,20 @@
import { updateAccessPoints } from '../../lib/accessPoints' import { updateAccessPoints } from '../../lib/accessPoints'
import prisma from '../../prisma' import prisma from '../../prisma'
export async function accessPoints() { export async function accessPoints(_, { networkShortName }) {
updateAccessPoints() updateAccessPoints()
return prisma.accessPoint.findMany({ return prisma.accessPoint.findMany({
where: {
network: {
shortName: networkShortName
}
},
orderBy: { orderBy: {
hostname: 'asc' hostname: 'asc'
}, },
include: { include: {
network: true,
wifiDevices: { wifiDevices: {
where: { where: {
status: 'ONLINE' status: 'ONLINE'

View File

@ -59,7 +59,8 @@ const typeDefs = gql`
pAHosts: [PAHost!]! @auth(roles: ["superAdmin"]) pAHosts: [PAHost!]! @auth(roles: ["superAdmin"])
"All Access Points" "All Access Points"
accessPoints: [AccessPoint!]! @auth(roles: ["superAdmin"]) accessPoints(networkShortName: String): [AccessPoint!]!
@auth(roles: ["superAdmin"])
"One Access Point" "One Access Point"
accessPoint(id: ID!): AccessPoint! @auth(roles: ["superAdmin"]) accessPoint(id: ID!): AccessPoint! @auth(roles: ["superAdmin"])

4
web/package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "ifms-pti", "name": "ifms-pti",
"version": "3.4.9", "version": "3.6.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ifms-pti", "name": "ifms-pti",
"version": "3.4.9", "version": "3.6.1",
"dependencies": { "dependencies": {
"@mdi/font": "^6.9.96", "@mdi/font": "^6.9.96",
"apollo-link-ws": "^1.0.20", "apollo-link-ws": "^1.0.20",

View File

@ -1,6 +1,6 @@
{ {
"name": "ifms-pti", "name": "ifms-pti",
"version": "3.4.9", "version": "3.6.1",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",

View File

@ -217,6 +217,18 @@ const routes = [
component: () => component: () =>
import(/* webpackChunkName: "wifi-stats" */ '../views/WifiStats.vue') 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', path: '/access-points/:id/clients',

View File

@ -62,6 +62,15 @@
hide-details hide-details
:label="`Somente ${me.campusFull}`" :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-toolbar>
<v-data-table <v-data-table
@ -394,8 +403,8 @@ export default {
fetchPolicy: 'cache-and-network', fetchPolicy: 'cache-and-network',
debounce: 200, debounce: 200,
query: gql` query: gql`
query accessPoints { query accessPoints($networkShortName: String) {
accessPoints { accessPoints(networkShortName: $networkShortName) {
id id
mac mac
hostname hostname
@ -420,6 +429,11 @@ export default {
} }
} }
`, `,
variables() {
return {
networkShortName: (this.sameCampus && this.me?.campus) || undefined
}
},
subscribeToMore: { subscribeToMore: {
document: gql` document: gql`
subscription { subscription {

View 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>

View File

@ -3,8 +3,8 @@
<v-toolbar dense flat> <v-toolbar dense flat>
<v-spacer /> <v-spacer />
<v-select <v-select
class="mr-2"
v-model="orderBy" v-model="orderBy"
class="mr-2"
style="max-width: 250px" style="max-width: 250px"
:items="orderByOptions" :items="orderByOptions"
hide-details hide-details
@ -26,7 +26,7 @@
<v-progress-linear <v-progress-linear
v-if="$apollo.queries.subnets.loading" v-if="$apollo.queries.subnets.loading"
class="pa-0 ma-0" class="pa-0 ma-0"
:indeterminate="$apollo.queries.subnets.loading" indeterminate
/> />
</v-expand-transition> </v-expand-transition>
<v-container fluid> <v-container fluid>