feat(TrafficDashboard): add traffic statistics route and integrate TrafficDashboard component into the application
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m40s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m40s
This commit is contained in:
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* Роуты для сбора статистики трафика с MikroTik RouterOS (интерфейсы по jumphost).
|
||||||
|
* GET /api/traffic/interface-stats — статистика rx/tx по каждому интерфейсу для всех jumphost.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { sendError } = require('../middleware/errorHandler');
|
||||||
|
const { decrypt } = require('../utils/encryption');
|
||||||
|
const { readServersFromS3 } = require('./serversRoutes');
|
||||||
|
const { createRosClient } = require('../services/mikrotikApplyService');
|
||||||
|
|
||||||
|
/** Получить MikroTik credentials из сервера (jumphost или home) */
|
||||||
|
function getMikrotikCredentials(server) {
|
||||||
|
if (!server || (server.type !== 'jumphost' && server.type !== 'home')) return null;
|
||||||
|
const host = server.mikrotikHost || server.ip || server.dns;
|
||||||
|
if (!host) return null;
|
||||||
|
const port = server.mikrotikPort;
|
||||||
|
const restPort = (port === 80 || port === 443 || port === 8443) ? port : 80;
|
||||||
|
const user = server.mikrotikUser || 'admin';
|
||||||
|
let password = '';
|
||||||
|
if (server.encryptedMikrotikPassword) {
|
||||||
|
try {
|
||||||
|
password = decrypt(server.encryptedMikrotikPassword);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { host, port: restPort, user, password, secure: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/traffic/interface-stats
|
||||||
|
* Возвращает по каждому jumphost список интерфейсов с rx-byte, tx-byte (из RouterOS REST /interface).
|
||||||
|
*/
|
||||||
|
async function getInterfaceStats(req, res) {
|
||||||
|
try {
|
||||||
|
const servers = await readServersFromS3();
|
||||||
|
const jumphosts = (Array.isArray(servers) ? servers : []).filter(
|
||||||
|
(s) => s && (String(s.type || '').toLowerCase() === 'jumphost' || String(s.type || '').toLowerCase() === 'home')
|
||||||
|
);
|
||||||
|
|
||||||
|
const results = await Promise.all(
|
||||||
|
jumphosts.map(async (server) => {
|
||||||
|
const creds = getMikrotikCredentials(server);
|
||||||
|
if (!creds) {
|
||||||
|
return {
|
||||||
|
serverId: server.id,
|
||||||
|
name: server.name || server.dns || server.ip || server.id,
|
||||||
|
host: server.mikrotikHost || server.ip || server.dns,
|
||||||
|
error: 'MikroTik API не настроен или нет пароля',
|
||||||
|
interfaces: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const client = createRosClient(creds);
|
||||||
|
const apiRes = await client.print('interface');
|
||||||
|
const raw = apiRes?.data;
|
||||||
|
const list = Array.isArray(raw) ? raw : raw ? [raw] : [];
|
||||||
|
const interfaces = list.map((iface) => {
|
||||||
|
const rx = iface['rx-byte'] != null ? parseInt(String(iface['rx-byte']), 10) : 0;
|
||||||
|
const tx = iface['tx-byte'] != null ? parseInt(String(iface['tx-byte']), 10) : 0;
|
||||||
|
const total = rx + tx;
|
||||||
|
return {
|
||||||
|
name: iface.name != null ? String(iface.name) : '',
|
||||||
|
type: iface.type != null ? String(iface.type) : '',
|
||||||
|
rxBytes: Number.isNaN(rx) ? 0 : rx,
|
||||||
|
txBytes: Number.isNaN(tx) ? 0 : tx,
|
||||||
|
totalBytes: Number.isNaN(total) ? 0 : total,
|
||||||
|
};
|
||||||
|
}).filter((i) => i.name && i.totalBytes >= 0);
|
||||||
|
return {
|
||||||
|
serverId: server.id,
|
||||||
|
name: server.name || server.dns || server.ip || server.id,
|
||||||
|
host: server.mikrotikHost || server.ip || server.dns,
|
||||||
|
interfaces,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err.response?.data?.message || err.message || 'Ошибка запроса к RouterOS';
|
||||||
|
return {
|
||||||
|
serverId: server.id,
|
||||||
|
name: server.name || server.dns || server.ip || server.id,
|
||||||
|
host: server.mikrotikHost || server.ip || server.dns,
|
||||||
|
error: msg,
|
||||||
|
interfaces: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.json({ jumphosts: results });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('getInterfaceStats:', error);
|
||||||
|
return sendError(res, 500, error.message || 'Ошибка сбора статистики трафика', 'E_TRAFFIC');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getInterfaceStats,
|
||||||
|
};
|
||||||
@@ -29,6 +29,7 @@ const miscRoutes = require('./routes/miscRoutes');
|
|||||||
const ipsecPasswordsRoutes = require('./routes/ipsecPasswordsRoutes');
|
const ipsecPasswordsRoutes = require('./routes/ipsecPasswordsRoutes');
|
||||||
const mikrotikConfigRoutes = require('./routes/mikrotikConfigRoutes');
|
const mikrotikConfigRoutes = require('./routes/mikrotikConfigRoutes');
|
||||||
const mikrotikBackupRoutes = require('./routes/mikrotikBackupRoutes');
|
const mikrotikBackupRoutes = require('./routes/mikrotikBackupRoutes');
|
||||||
|
const trafficRoutes = require('./routes/trafficRoutes');
|
||||||
const { initMikrotikBackupScheduler } = require('./services/mikrotikBackupScheduler');
|
const { initMikrotikBackupScheduler } = require('./services/mikrotikBackupScheduler');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -463,6 +464,9 @@ app.post('/api/mikrotik/traceroute', writeLimiter, mikrotikConfigRoutes.tracerou
|
|||||||
app.post('/api/mikrotik/apply', mikrotikConfigRoutes.applyMikrotikConfig);
|
app.post('/api/mikrotik/apply', mikrotikConfigRoutes.applyMikrotikConfig);
|
||||||
app.post('/api/mikrotik/run-script', mikrotikConfigRoutes.runScript);
|
app.post('/api/mikrotik/run-script', mikrotikConfigRoutes.runScript);
|
||||||
|
|
||||||
|
// === TRAFFIC STATS (MikroTik interfaces by jumphost) ===
|
||||||
|
app.get('/api/traffic/interface-stats', trafficRoutes.getInterfaceStats);
|
||||||
|
|
||||||
// === MIKROTIK BACKUPS (S3) ===
|
// === MIKROTIK BACKUPS (S3) ===
|
||||||
app.post('/api/mikrotik/backups', writeLimiter, mikrotikBackupRoutes.createBackup);
|
app.post('/api/mikrotik/backups', writeLimiter, mikrotikBackupRoutes.createBackup);
|
||||||
app.get('/api/mikrotik/backups', mikrotikBackupRoutes.listBackups);
|
app.get('/api/mikrotik/backups', mikrotikBackupRoutes.listBackups);
|
||||||
|
|||||||
Generated
+345
-1
@@ -20,7 +20,8 @@
|
|||||||
"diff": "^8.0.3",
|
"diff": "^8.0.3",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router-dom": "^6.30.1"
|
"react-router-dom": "^6.30.1",
|
||||||
|
"recharts": "^3.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.29.0",
|
"@eslint/js": "^9.29.0",
|
||||||
@@ -1010,6 +1011,42 @@
|
|||||||
"url": "https://opencollective.com/popperjs"
|
"url": "https://opencollective.com/popperjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit": {
|
||||||
|
"version": "2.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||||
|
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"@standard-schema/utils": "^0.3.0",
|
||||||
|
"immer": "^11.0.0",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"redux-thunk": "^3.1.0",
|
||||||
|
"reselect": "^5.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||||
|
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit/node_modules/immer": {
|
||||||
|
"version": "11.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz",
|
||||||
|
"integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@remix-run/router": {
|
"node_modules/@remix-run/router": {
|
||||||
"version": "1.23.0",
|
"version": "1.23.0",
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
|
||||||
@@ -1037,6 +1074,18 @@
|
|||||||
"linux"
|
"linux"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@standard-schema/utils": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tabler/core": {
|
"node_modules/@tabler/core": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tabler/core/-/core-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tabler/core/-/core-1.3.2.tgz",
|
||||||
@@ -1189,6 +1238,12 @@
|
|||||||
"@babel/types": "^7.20.7"
|
"@babel/types": "^7.20.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-array": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/d3-color": {
|
"node_modules/@types/d3-color": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
@@ -1204,6 +1259,12 @@
|
|||||||
"@types/d3-selection": "*"
|
"@types/d3-selection": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-ease": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/d3-interpolate": {
|
"node_modules/@types/d3-interpolate": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
@@ -1213,12 +1274,48 @@
|
|||||||
"@types/d3-color": "*"
|
"@types/d3-color": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-path": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-scale": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-time": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/d3-selection": {
|
"node_modules/@types/d3-selection": {
|
||||||
"version": "3.0.11",
|
"version": "3.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
||||||
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-shape": {
|
||||||
|
"version": "3.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
|
||||||
|
"integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-path": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-time": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-timer": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/d3-transition": {
|
"node_modules/@types/d3-transition": {
|
||||||
"version": "3.0.9",
|
"version": "3.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
||||||
@@ -1264,6 +1361,12 @@
|
|||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -1485,6 +1588,15 @@
|
|||||||
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
|
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/clsx": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -1539,6 +1651,18 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-array": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"internmap": "1 - 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/d3-color": {
|
"node_modules/d3-color": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
@@ -1579,6 +1703,15 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-format": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/d3-interpolate": {
|
"node_modules/d3-interpolate": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||||
@@ -1591,6 +1724,31 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-path": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-scale": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2.10.0 - 3",
|
||||||
|
"d3-format": "1 - 3",
|
||||||
|
"d3-interpolate": "1.2.0 - 3",
|
||||||
|
"d3-time": "2.1.1 - 3",
|
||||||
|
"d3-time-format": "2 - 4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/d3-selection": {
|
"node_modules/d3-selection": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
@@ -1600,6 +1758,42 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-shape": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-path": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time-format": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-time": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/d3-timer": {
|
"node_modules/d3-timer": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||||
@@ -1660,6 +1854,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decimal.js-light": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/deep-is": {
|
"node_modules/deep-is": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -1735,6 +1935,16 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-toolkit": {
|
||||||
|
"version": "1.44.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz",
|
||||||
|
"integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"docs",
|
||||||
|
"benchmarks"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.5",
|
"version": "0.25.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -1951,6 +2161,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -2198,6 +2414,16 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -2221,6 +2447,15 @@
|
|||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/internmap": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -2563,6 +2798,36 @@
|
|||||||
"react": "^19.1.0"
|
"react": "^19.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-is": {
|
||||||
|
"version": "19.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz",
|
||||||
|
"integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/react-redux": {
|
||||||
|
"version": "9.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.2.25 || ^19",
|
||||||
|
"react": "^18.0 || ^19",
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -2603,6 +2868,57 @@
|
|||||||
"react-dom": ">=16.8"
|
"react-dom": ">=16.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/recharts": {
|
||||||
|
"version": "3.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz",
|
||||||
|
"integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"www"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "1.x.x || 2.x.x",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"decimal.js-light": "^2.5.1",
|
||||||
|
"es-toolkit": "^1.39.3",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"immer": "^10.1.1",
|
||||||
|
"react-redux": "8.x.x || 9.x.x",
|
||||||
|
"reselect": "5.1.1",
|
||||||
|
"tiny-invariant": "^1.3.3",
|
||||||
|
"use-sync-external-store": "^1.2.2",
|
||||||
|
"victory-vendor": "^37.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/redux": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/redux-thunk": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resolve-from": {
|
"node_modules/resolve-from": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -2672,6 +2988,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-invariant": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.14",
|
"version": "0.2.14",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -2764,6 +3086,28 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/victory-vendor": {
|
||||||
|
"version": "37.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
||||||
|
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
|
||||||
|
"license": "MIT AND ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-array": "^3.0.3",
|
||||||
|
"@types/d3-ease": "^3.0.0",
|
||||||
|
"@types/d3-interpolate": "^3.0.1",
|
||||||
|
"@types/d3-scale": "^4.0.2",
|
||||||
|
"@types/d3-shape": "^3.1.0",
|
||||||
|
"@types/d3-time": "^3.0.0",
|
||||||
|
"@types/d3-timer": "^3.0.0",
|
||||||
|
"d3-array": "^3.1.6",
|
||||||
|
"d3-ease": "^3.0.1",
|
||||||
|
"d3-interpolate": "^3.0.1",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-shape": "^3.1.0",
|
||||||
|
"d3-time": "^3.0.0",
|
||||||
|
"d3-timer": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.0.2",
|
"version": "7.0.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
"diff": "^8.0.3",
|
"diff": "^8.0.3",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router-dom": "^6.30.1"
|
"react-router-dom": "^6.30.1",
|
||||||
|
"recharts": "^3.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.29.0",
|
"@eslint/js": "^9.29.0",
|
||||||
|
|||||||
+11
-5
@@ -22,7 +22,8 @@ import {
|
|||||||
IconMenu2,
|
IconMenu2,
|
||||||
IconArrowsExchange,
|
IconArrowsExchange,
|
||||||
IconLayoutSidebarLeftExpand,
|
IconLayoutSidebarLeftExpand,
|
||||||
IconLayoutNavbar
|
IconLayoutNavbar,
|
||||||
|
IconChartPie
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import ServerManager from './ServerManager';
|
import ServerManager from './ServerManager';
|
||||||
import FilterManager from './FilterManager';
|
import FilterManager from './FilterManager';
|
||||||
@@ -36,6 +37,7 @@ import CommunitiesManager from './CommunitiesManager';
|
|||||||
import NetworkConfigManager from './NetworkConfigManager';
|
import NetworkConfigManager from './NetworkConfigManager';
|
||||||
import MikrotikTools from './MikrotikTools.jsx';
|
import MikrotikTools from './MikrotikTools.jsx';
|
||||||
import Dashboard from './Dashboard';
|
import Dashboard from './Dashboard';
|
||||||
|
import TrafficDashboard from './TrafficDashboard.jsx';
|
||||||
import MikrotikBackupsManager from './MikrotikBackupsManager.jsx';
|
import MikrotikBackupsManager from './MikrotikBackupsManager.jsx';
|
||||||
import PingServicesManager from './PingServicesManager.jsx';
|
import PingServicesManager from './PingServicesManager.jsx';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
@@ -56,7 +58,7 @@ function LanguageProvider({ children }) {
|
|||||||
const dict = {
|
const dict = {
|
||||||
ru: {
|
ru: {
|
||||||
home: 'Главная', data: 'Данные', management: 'Управление', tools: 'Инструменты',
|
home: 'Главная', data: 'Данные', management: 'Управление', tools: 'Инструменты',
|
||||||
dashboard: 'Панель', domains: 'Домены', ipRanges: 'IP-диапазоны', asns: 'AS',
|
dashboard: 'Панель', trafficTraffic: 'Расход трафика', domains: 'Домены', ipRanges: 'IP-диапазоны', asns: 'AS',
|
||||||
communities: 'Community', servers: 'Серверы', filters: 'Фильтры', billing: 'Биллинг', autoUrls: 'Авто URL',
|
communities: 'Community', servers: 'Серверы', filters: 'Фильтры', billing: 'Биллинг', autoUrls: 'Авто URL',
|
||||||
easySwitch: 'Easy Switch', networkConfig: 'Сетевые настройки', mikrotikBackups: 'MikroTik Бэкапы', pingServices: 'Пинг сервисов',
|
easySwitch: 'Easy Switch', networkConfig: 'Сетевые настройки', mikrotikBackups: 'MikroTik Бэкапы', pingServices: 'Пинг сервисов',
|
||||||
light: 'Светлая', dark: 'Тёмная',
|
light: 'Светлая', dark: 'Тёмная',
|
||||||
@@ -64,7 +66,7 @@ function LanguageProvider({ children }) {
|
|||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
home: 'Home', data: 'Data', management: 'Management', tools: 'Tools',
|
home: 'Home', data: 'Data', management: 'Management', tools: 'Tools',
|
||||||
dashboard: 'Dashboard', domains: 'Domains', ipRanges: 'IP Ranges', asns: 'ASNs',
|
dashboard: 'Dashboard', trafficTraffic: 'Traffic Usage', domains: 'Domains', ipRanges: 'IP Ranges', asns: 'ASNs',
|
||||||
communities: 'Communities', servers: 'Servers', filters: 'Filters', billing: 'Billing', autoUrls: 'Auto URLs',
|
communities: 'Communities', servers: 'Servers', filters: 'Filters', billing: 'Billing', autoUrls: 'Auto URLs',
|
||||||
easySwitch: 'Easy Switch', networkConfig: 'Network Config', mikrotikBackups: 'MikroTik Backups', pingServices: 'Ping Services',
|
easySwitch: 'Easy Switch', networkConfig: 'Network Config', mikrotikBackups: 'MikroTik Backups', pingServices: 'Ping Services',
|
||||||
light: 'Light', dark: 'Dark',
|
light: 'Light', dark: 'Dark',
|
||||||
@@ -189,8 +191,10 @@ function MainLayout() {
|
|||||||
id: 'home',
|
id: 'home',
|
||||||
title: t('home'),
|
title: t('home'),
|
||||||
icon: IconHome,
|
icon: IconHome,
|
||||||
path: '/dashboard',
|
items: [
|
||||||
single: true
|
{ id: 'dashboard', title: t('dashboard'), path: '/dashboard', icon: IconHome },
|
||||||
|
{ id: 'traffic', title: t('trafficTraffic'), path: '/traffic', icon: IconChartPie }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'data',
|
id: 'data',
|
||||||
@@ -370,6 +374,7 @@ function MainLayout() {
|
|||||||
<main id="main-content" className="container-xl mt-4 flex-grow-1" tabIndex="-1">
|
<main id="main-content" className="container-xl mt-4 flex-grow-1" tabIndex="-1">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
|
<Route path="/traffic" element={<TrafficDashboard />} />
|
||||||
<Route path="/domains" element={<DomainsNewManager />} />
|
<Route path="/domains" element={<DomainsNewManager />} />
|
||||||
<Route path="/ip-ranges" element={<IPRangesManager />} />
|
<Route path="/ip-ranges" element={<IPRangesManager />} />
|
||||||
<Route path="/asns" element={<ASNsNewManager />} />
|
<Route path="/asns" element={<ASNsNewManager />} />
|
||||||
@@ -526,6 +531,7 @@ function MainLayout() {
|
|||||||
<main id="main-content" tabIndex="-1">
|
<main id="main-content" tabIndex="-1">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
|
<Route path="/traffic" element={<TrafficDashboard />} />
|
||||||
<Route path="/domains" element={<DomainsNewManager />} />
|
<Route path="/domains" element={<DomainsNewManager />} />
|
||||||
<Route path="/ip-ranges" element={<IPRangesManager />} />
|
<Route path="/ip-ranges" element={<IPRangesManager />} />
|
||||||
<Route path="/asns" element={<ASNsNewManager />} />
|
<Route path="/asns" element={<ASNsNewManager />} />
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import api from './lib/api.js';
|
||||||
|
import {
|
||||||
|
IconChartPie,
|
||||||
|
IconRefresh,
|
||||||
|
IconServer,
|
||||||
|
IconAlertCircle,
|
||||||
|
} from '@tabler/icons-react';
|
||||||
|
import {
|
||||||
|
PieChart,
|
||||||
|
Pie,
|
||||||
|
Cell,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Legend,
|
||||||
|
Tooltip,
|
||||||
|
} from 'recharts';
|
||||||
|
import PageHeader from './components/PageHeader.jsx';
|
||||||
|
|
||||||
|
const CHART_COLORS = [
|
||||||
|
'#206bc4', '#4299e1', '#2fb344', '#5eba00', '#e64980', '#f76707',
|
||||||
|
'#fd7e14', '#fab005', '#e67700', '#20c997', '#0dcaf0', '#6f42c1',
|
||||||
|
'#6366f1', '#8b5cf6', '#a855f7', '#d63384', '#0d6efd', '#198754',
|
||||||
|
];
|
||||||
|
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function JumphostCard({ jumphost }) {
|
||||||
|
const { name, host, error, interfaces } = jumphost;
|
||||||
|
const hasData = Array.isArray(interfaces) && interfaces.length > 0;
|
||||||
|
const totalBytes = hasData ? interfaces.reduce((acc, i) => acc + (i.totalBytes || 0), 0) : 0;
|
||||||
|
const chartData = hasData
|
||||||
|
? interfaces
|
||||||
|
.filter((i) => (i.totalBytes || 0) > 0)
|
||||||
|
.map((i, idx) => ({
|
||||||
|
name: i.name,
|
||||||
|
value: i.totalBytes,
|
||||||
|
rxBytes: i.rxBytes,
|
||||||
|
txBytes: i.txBytes,
|
||||||
|
fill: CHART_COLORS[idx % CHART_COLORS.length],
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card h-100">
|
||||||
|
<div className="card-header d-flex align-items-center">
|
||||||
|
<span className="avatar avatar-sm me-2 bg-blue-lt text-blue">
|
||||||
|
<IconServer size={18} />
|
||||||
|
</span>
|
||||||
|
<div className="flex-grow-1 min-w-0">
|
||||||
|
<h3 className="card-title mb-0 text-truncate" title={name}>
|
||||||
|
{name || host || 'Jumphost'}
|
||||||
|
</h3>
|
||||||
|
{host && (
|
||||||
|
<div className="text-muted small text-truncate" title={host}>
|
||||||
|
{host}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
{error && (
|
||||||
|
<div className="alert alert-warning py-2 mb-0 d-flex align-items-center">
|
||||||
|
<IconAlertCircle className="me-2 flex-shrink-0" size={18} />
|
||||||
|
<span className="small">{error}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!error && !hasData && (
|
||||||
|
<div className="text-muted text-center py-4 small">
|
||||||
|
Нет данных по интерфейсам
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!error && hasData && chartData.length === 0 && (
|
||||||
|
<div className="text-muted text-center py-4 small">
|
||||||
|
Все счётчики нулевые
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!error && chartData.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="mb-2 small text-muted">
|
||||||
|
Всего: <strong>{formatBytes(totalBytes)}</strong> (RX + TX)
|
||||||
|
</div>
|
||||||
|
<ResponsiveContainer width="100%" height={260}>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={chartData}
|
||||||
|
dataKey="value"
|
||||||
|
nameKey="name"
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
outerRadius={90}
|
||||||
|
label={({ name, percent }) =>
|
||||||
|
percent > 0.05 ? `${name} ${(percent * 100).toFixed(0)}%` : ''
|
||||||
|
}
|
||||||
|
labelLine={false}
|
||||||
|
>
|
||||||
|
{chartData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={entry.fill} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip
|
||||||
|
formatter={(value) => formatBytes(value)}
|
||||||
|
contentStyle={{ fontSize: '12px' }}
|
||||||
|
labelFormatter={(name, payload) => {
|
||||||
|
const p = payload?.[0];
|
||||||
|
if (!p) return name;
|
||||||
|
return `${name} — RX: ${formatBytes(p.rxBytes || 0)}, TX: ${formatBytes(p.txBytes || 0)}`;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Legend
|
||||||
|
formatter={(value, entry) => {
|
||||||
|
const item = chartData.find((d) => d.name === value);
|
||||||
|
return item ? `${value} (${formatBytes(item.value)})` : value;
|
||||||
|
}}
|
||||||
|
wrapperStyle={{ fontSize: '11px' }}
|
||||||
|
/>
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TrafficDashboard() {
|
||||||
|
const [jumphosts, setJumphosts] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const fetchStats = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const { data } = await api.get('/traffic/interface-stats');
|
||||||
|
setJumphosts(Array.isArray(data?.jumphosts) ? data.jumphosts : []);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Traffic stats:', e);
|
||||||
|
setError(e?.response?.data?.message || e?.message || 'Не удалось загрузить статистику');
|
||||||
|
setJumphosts([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchStats();
|
||||||
|
}, [fetchStats]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
title="Расход трафика"
|
||||||
|
icon={<IconChartPie size={24} />}
|
||||||
|
meta="Статистика по интерфейсам MikroTik RouterOS (jumphost)"
|
||||||
|
actions={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
onClick={fetchStats}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<IconRefresh className={loading ? 'spin me-2' : 'me-2'} size={18} />
|
||||||
|
{loading ? 'Загрузка…' : 'Обновить'}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<div className="alert alert-danger d-flex align-items-center">
|
||||||
|
<IconAlertCircle className="me-2" size={20} />
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{loading && jumphosts.length === 0 && !error && (
|
||||||
|
<div className="text-center py-5 text-muted">
|
||||||
|
Загрузка статистики с роутеров…
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!loading && !error && jumphosts.length === 0 && (
|
||||||
|
<div className="alert alert-info">
|
||||||
|
Нет jumphost-серверов с настроенным MikroTik API. Добавьте серверы типа «Jumphost» и укажите учётные данные RouterOS.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!loading && jumphosts.length > 0 && (
|
||||||
|
<div className="row row-cards g-3">
|
||||||
|
{jumphosts.map((jh) => (
|
||||||
|
<div key={jh.serverId || jh.host || jh.name} className="col-12 col-md-6 col-xl-4">
|
||||||
|
<JumphostCard jumphost={jh} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
IconDatabase,
|
IconDatabase,
|
||||||
IconCreditCard,
|
IconCreditCard,
|
||||||
IconDownload,
|
IconDownload,
|
||||||
IconKeyboard
|
IconKeyboard,
|
||||||
|
IconChartPie
|
||||||
} from '@tabler/icons-react'
|
} from '@tabler/icons-react'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,6 +39,7 @@ function CommandPalette() {
|
|||||||
// Список команд
|
// Список команд
|
||||||
const commands = [
|
const commands = [
|
||||||
{ icon: IconHome, label: 'Главная', description: 'Панель управления', action: () => navigate('/dashboard'), keywords: ['главная', 'панель', 'dashboard'] },
|
{ icon: IconHome, label: 'Главная', description: 'Панель управления', action: () => navigate('/dashboard'), keywords: ['главная', 'панель', 'dashboard'] },
|
||||||
|
{ icon: IconChartPie, label: 'Расход трафика', description: 'Статистика по интерфейсам MikroTik', action: () => navigate('/traffic'), keywords: ['трафик', 'traffic', 'mikrotik', 'интерфейсы'] },
|
||||||
{ icon: IconWorld, label: 'Домены', description: 'Управление доменами', action: () => navigate('/domains'), keywords: ['домены', 'domains'] },
|
{ icon: IconWorld, label: 'Домены', description: 'Управление доменами', action: () => navigate('/domains'), keywords: ['домены', 'domains'] },
|
||||||
{ icon: IconNetwork, label: 'IP-диапазоны', description: 'Управление IP диапазонами', action: () => navigate('/ip-ranges'), keywords: ['ip', 'диапазоны', 'ranges'] },
|
{ icon: IconNetwork, label: 'IP-диапазоны', description: 'Управление IP диапазонами', action: () => navigate('/ip-ranges'), keywords: ['ip', 'диапазоны', 'ranges'] },
|
||||||
{ icon: IconNetwork, label: 'ASN', description: 'Управление Autonomous Systems', action: () => navigate('/asns'), keywords: ['asn', 'as', 'autonomous'] },
|
{ icon: IconNetwork, label: 'ASN', description: 'Управление Autonomous Systems', action: () => navigate('/asns'), keywords: ['asn', 'as', 'autonomous'] },
|
||||||
|
|||||||
Reference in New Issue
Block a user