[release] v0.12.0-unstable2
This commit is contained in:
parent
af30f256f1
commit
6ba22dc4cb
9 changed files with 326 additions and 58 deletions
|
@ -4,6 +4,7 @@ import * as _config from './config';
|
|||
import * as _docker from './docker';
|
||||
import * as _market from './market';
|
||||
import * as _constellation from './constellation';
|
||||
import * as _metrics from './metrics';
|
||||
|
||||
import * as authDemo from './authentication.demo';
|
||||
import * as usersDemo from './users.demo';
|
||||
|
@ -214,6 +215,7 @@ let config = _config;
|
|||
let docker = _docker;
|
||||
let market = _market;
|
||||
let constellation = _constellation;
|
||||
let metrics = _metrics;
|
||||
|
||||
if(isDemo) {
|
||||
auth = authDemo;
|
||||
|
@ -242,5 +244,6 @@ export {
|
|||
isOnline,
|
||||
checkHost,
|
||||
getDNS,
|
||||
metrics,
|
||||
uploadBackground
|
||||
};
|
14
client/src/api/metrics.jsx
Normal file
14
client/src/api/metrics.jsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import wrap from './wrap';
|
||||
|
||||
function get() {
|
||||
return wrap(fetch('/cosmos/api/metrics', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
export {
|
||||
get,
|
||||
};
|
|
@ -144,4 +144,8 @@
|
|||
|
||||
.pulsing {
|
||||
animation: pulsing 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.force-light > * {
|
||||
color: black !important;
|
||||
}
|
221
client/src/pages/dashboard/components/plot.jsx
Normal file
221
client/src/pages/dashboard/components/plot.jsx
Normal file
|
@ -0,0 +1,221 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
// material-ui
|
||||
import {
|
||||
Avatar,
|
||||
AvatarGroup,
|
||||
Box,
|
||||
Button,
|
||||
Grid,
|
||||
List,
|
||||
ListItemAvatar,
|
||||
ListItemButton,
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
MenuItem,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import MainCard from '../../../components/MainCard';
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
// third-party
|
||||
import ReactApexChart from 'react-apexcharts';
|
||||
|
||||
function formatDate(now, time) {
|
||||
// use as UTC
|
||||
// now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
|
||||
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0'); // Months are 0-based
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
|
||||
return time ? `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` : `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function toUTC(date, time) {
|
||||
let now = new Date(date);
|
||||
now.setMinutes(now.getMinutes() + now.getTimezoneOffset());
|
||||
return formatDate(now, time);
|
||||
}
|
||||
|
||||
const PlotComponent = ({ data, defaultSlot = 'day' }) => {
|
||||
const [slot, setSlot] = useState(defaultSlot);
|
||||
const theme = useTheme();
|
||||
|
||||
// chart options
|
||||
const areaChartOptions = {
|
||||
chart: {
|
||||
height: 450,
|
||||
type: 'area',
|
||||
toolbar: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth',
|
||||
width: 2
|
||||
},
|
||||
grid: {
|
||||
strokeDashArray: 0
|
||||
}
|
||||
};
|
||||
|
||||
let hourlyDates = [];
|
||||
for(let i = 0; i < 48; i++) {
|
||||
let now = new Date();
|
||||
now.setHours(now.getHours() - i);
|
||||
now.setMinutes(0);
|
||||
now.setSeconds(0);
|
||||
// get as YYYY-MM-DD HH:MM:SS
|
||||
hourlyDates.unshift(formatDate(now, true));
|
||||
}
|
||||
|
||||
let dailyDates = [];
|
||||
for(let i = 0; i < 30; i++) {
|
||||
let now = new Date();
|
||||
now.setDate(now.getDate() - i);
|
||||
dailyDates.unshift(formatDate(now));
|
||||
}
|
||||
|
||||
const { primary, secondary } = theme.palette.text;
|
||||
const line = theme.palette.divider;
|
||||
|
||||
const [options, setOptions] = useState(areaChartOptions);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions((prevState) => ({
|
||||
...prevState,
|
||||
colors: [theme.palette.primary.main, theme.palette.secondary.main],
|
||||
xaxis: {
|
||||
categories:
|
||||
slot === 'hourly'
|
||||
? hourlyDates.map((date) => date.split(' ')[1])
|
||||
: dailyDates,
|
||||
labels: {
|
||||
style: {
|
||||
fontSize: '11px',
|
||||
}
|
||||
},
|
||||
axisBorder: {
|
||||
show: true,
|
||||
color: line
|
||||
},
|
||||
tickAmount: slot === 'hourly' ? hourlyDates.length : dailyDates.length,
|
||||
},
|
||||
yaxis: [{
|
||||
labels: {
|
||||
style: {
|
||||
colors: [secondary]
|
||||
}
|
||||
},
|
||||
title: {
|
||||
text: data[0].Label,
|
||||
}
|
||||
},
|
||||
{
|
||||
opposite: true,
|
||||
labels: {
|
||||
style: {
|
||||
colors: [secondary]
|
||||
}
|
||||
},
|
||||
title: {
|
||||
text: data[1].Label,
|
||||
}
|
||||
}
|
||||
],
|
||||
grid: {
|
||||
borderColor: line
|
||||
},
|
||||
tooltip: {
|
||||
theme: 'light'
|
||||
}
|
||||
}));
|
||||
}, [primary, secondary, line, theme, slot]);
|
||||
|
||||
let dataSeries = [];
|
||||
data.forEach((serie) => {
|
||||
dataSeries.push({
|
||||
name: serie.Label,
|
||||
dataDaily: dailyDates.map((date) => {
|
||||
let k = "day_" + toUTC(date);
|
||||
if (k in serie.ValuesAggl) {
|
||||
return serie.ValuesAggl[k].Value;
|
||||
} else {
|
||||
console.log(k)
|
||||
return 0;
|
||||
}
|
||||
}),
|
||||
dataHourly: hourlyDates.map((date) => {
|
||||
let k = "hour_" + toUTC(date, true);
|
||||
if (k in serie.ValuesAggl) {
|
||||
return serie.ValuesAggl[k].Value;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
const [series, setSeries] = useState(dataSeries.map((serie) => {
|
||||
return {
|
||||
name: serie.name,
|
||||
data: slot === 'hourly' ? serie.dataHourly : serie.dataDaily
|
||||
}
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
setSeries(dataSeries.map((serie) => {
|
||||
return {
|
||||
name: serie.name,
|
||||
data: slot === 'hourly' ? serie.dataHourly : serie.dataDaily
|
||||
}
|
||||
}));
|
||||
}, [slot, data]);
|
||||
|
||||
|
||||
return <Grid item xs={12} md={7} lg={8}>
|
||||
<Grid container alignItems="center" justifyContent="space-between">
|
||||
<Grid item>
|
||||
<Typography variant="h5">Server Resources</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Stack direction="row" alignItems="center" spacing={0}>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => setSlot('hourly')}
|
||||
color={slot === 'hourly' ? 'primary' : 'secondary'}
|
||||
variant={slot === 'hourly' ? 'outlined' : 'text'}
|
||||
>
|
||||
Hourly
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => setSlot('daily')}
|
||||
color={slot === 'daily' ? 'primary' : 'secondary'}
|
||||
variant={slot === 'daily' ? 'outlined' : 'text'}
|
||||
>
|
||||
Daily
|
||||
</Button>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<MainCard content={false} sx={{ mt: 1.5 }}>
|
||||
<Box sx={{ pt: 1, pr: 2 }} className="force-light">
|
||||
<ReactApexChart options={options} series={series} type="area" height={450} />;
|
||||
</Box>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
}
|
||||
|
||||
export default PlotComponent;
|
|
@ -38,6 +38,7 @@ import IsLoggedIn from '../../isLoggedIn';
|
|||
|
||||
import * as API from '../../api';
|
||||
import AnimateButton from '../../components/@extended/AnimateButton';
|
||||
import PlotComponent from './components/plot';
|
||||
|
||||
// avatar style
|
||||
const avatarSX = {
|
||||
|
@ -79,8 +80,20 @@ const DashboardDefault = () => {
|
|||
const [slot, setSlot] = useState('week');
|
||||
|
||||
const [coStatus, setCoStatus] = useState(null);
|
||||
const [metrics, setMetrics] = useState(null);
|
||||
const [isCreatingDB, setIsCreatingDB] = useState(false);
|
||||
|
||||
const refreshMetrics = () => {
|
||||
API.metrics.get().then((res) => {
|
||||
let finalMetrics = {};
|
||||
res.data.forEach((metric) => {
|
||||
finalMetrics[metric.Key] = metric;
|
||||
});
|
||||
setMetrics(finalMetrics);
|
||||
setTimeout(refreshMetrics, 10000);
|
||||
});
|
||||
};
|
||||
|
||||
const refreshStatus = () => {
|
||||
API.getStatus().then((res) => {
|
||||
setCoStatus(res.data);
|
||||
|
@ -89,15 +102,9 @@ const DashboardDefault = () => {
|
|||
|
||||
useEffect(() => {
|
||||
refreshStatus();
|
||||
refreshMetrics();
|
||||
}, []);
|
||||
|
||||
const setupDB = () => {
|
||||
setIsCreatingDB(true);
|
||||
API.docker.newDB().then((res) => {
|
||||
refreshStatus();
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<IsLoggedIn />
|
||||
|
@ -145,7 +152,7 @@ const DashboardDefault = () => {
|
|||
<Alert severity="info">Dashboard implementation currently in progress! If you want to voice your opinion on where Cosmos is going, please join us on Discord!</Alert>
|
||||
</Stack>
|
||||
</div>
|
||||
<div style={{filter: 'blur(10px)', marginTop: '30px', pointerEvents: 'none'}}>
|
||||
{metrics && <div style={{marginTop: '30px'}}>
|
||||
<Grid container rowSpacing={4.5} columnSpacing={2.75}>
|
||||
{/* row 1 */}
|
||||
<Grid item xs={12} sx={{ mb: -2.25 }}>
|
||||
|
@ -167,38 +174,9 @@ const DashboardDefault = () => {
|
|||
<Grid item md={8} sx={{ display: { sm: 'none', md: 'block', lg: 'none' } }} />
|
||||
|
||||
{/* row 2 */}
|
||||
<Grid item xs={12} md={7} lg={8}>
|
||||
<Grid container alignItems="center" justifyContent="space-between">
|
||||
<Grid item>
|
||||
<Typography variant="h5">Unique Visitor</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Stack direction="row" alignItems="center" spacing={0}>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => setSlot('month')}
|
||||
color={slot === 'month' ? 'primary' : 'secondary'}
|
||||
variant={slot === 'month' ? 'outlined' : 'text'}
|
||||
>
|
||||
Month
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => setSlot('week')}
|
||||
color={slot === 'week' ? 'primary' : 'secondary'}
|
||||
variant={slot === 'week' ? 'outlined' : 'text'}
|
||||
>
|
||||
Week
|
||||
</Button>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<MainCard content={false} sx={{ mt: 1.5 }}>
|
||||
<Box sx={{ pt: 1, pr: 2 }}>
|
||||
<IncomeAreaChart slot={slot} />
|
||||
</Box>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
|
||||
<PlotComponent data={[metrics["cosmos.system.cpu.0"], metrics["cosmos.system.ram"]]}/>
|
||||
|
||||
<Grid item xs={12} md={5} lg={4}>
|
||||
<Grid container alignItems="center" justifyContent="space-between">
|
||||
<Grid item>
|
||||
|
@ -413,7 +391,7 @@ const DashboardDefault = () => {
|
|||
</MainCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
</div>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.12.0-unstable",
|
||||
"version": "0.12.0-unstable2",
|
||||
"description": "",
|
||||
"main": "test-server.js",
|
||||
"bugs": {
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/azukaar/cosmos-server/src/authorizationserver"
|
||||
"github.com/azukaar/cosmos-server/src/market"
|
||||
"github.com/azukaar/cosmos-server/src/constellation"
|
||||
"github.com/azukaar/cosmos-server/src/metrics"
|
||||
"github.com/gorilla/mux"
|
||||
"strconv"
|
||||
"time"
|
||||
|
@ -339,6 +340,8 @@ func InitServer() *mux.Router {
|
|||
srapi.HandleFunc("/api/constellation/logs", constellation.API_GetLogs)
|
||||
srapi.HandleFunc("/api/constellation/block", constellation.DeviceBlock)
|
||||
|
||||
srapi.HandleFunc("/api/metrics", metrics.API_GetMetrics)
|
||||
|
||||
if(!config.HTTPConfig.AcceptAllInsecureHostname) {
|
||||
srapi.Use(utils.EnsureHostname)
|
||||
}
|
||||
|
|
|
@ -74,8 +74,8 @@ func AggloMetrics() {
|
|||
}
|
||||
|
||||
// if hourly pool does not exist, create it
|
||||
if _, ok := metric.ValuesAggl["hour_" + hourlyPool.String()]; !ok {
|
||||
metric.ValuesAggl["hour_" + hourlyPool.String()] = DataDefDBEntry{
|
||||
if _, ok := metric.ValuesAggl["hour_" + hourlyPool.UTC().Format("2006-01-02 15:04:05")]; !ok {
|
||||
metric.ValuesAggl["hour_" + hourlyPool.UTC().Format("2006-01-02 15:04:05")] = DataDefDBEntry{
|
||||
Date: hourlyPool,
|
||||
Value: 0,
|
||||
Processed: false,
|
||||
|
@ -86,8 +86,8 @@ func AggloMetrics() {
|
|||
}
|
||||
|
||||
// if daily pool does not exist, create it
|
||||
if _, ok := metric.ValuesAggl["day_" + dailyPool.String()]; !ok {
|
||||
metric.ValuesAggl["day_" + dailyPool.String()] = DataDefDBEntry{
|
||||
if _, ok := metric.ValuesAggl["day_" + dailyPool.UTC().Format("2006-01-02")]; !ok {
|
||||
metric.ValuesAggl["day_" + dailyPool.UTC().Format("2006-01-02")] = DataDefDBEntry{
|
||||
Date: dailyPool,
|
||||
Value: 0,
|
||||
Processed: false,
|
||||
|
@ -100,33 +100,33 @@ func AggloMetrics() {
|
|||
for valInd, value := range values {
|
||||
// if not processed
|
||||
if !value.Processed {
|
||||
valueHourlyPool := ModuloTime(value.Date, time.Hour)
|
||||
valueDailyPool := ModuloTime(value.Date, 24 * time.Hour)
|
||||
valueHourlyPool := ModuloTime(value.Date, time.Hour).UTC().Format("2006-01-02 15:04:05")
|
||||
valueDailyPool := ModuloTime(value.Date, 24 * time.Hour).UTC().Format("2006-01-02")
|
||||
|
||||
if _, ok := metric.ValuesAggl["hour_" + valueHourlyPool.String()]; ok {
|
||||
currentPool := metric.ValuesAggl["hour_" + valueHourlyPool.String()]
|
||||
if _, ok := metric.ValuesAggl["hour_" + valueHourlyPool]; ok {
|
||||
currentPool := metric.ValuesAggl["hour_" + valueHourlyPool]
|
||||
|
||||
currentPool.Value = MergeMetric(metric.AggloType, currentPool.Value, value.Value, currentPool.AvgIndex)
|
||||
if metric.AggloType == "avg" {
|
||||
currentPool.AvgIndex++
|
||||
}
|
||||
|
||||
metric.ValuesAggl["hour_" + valueHourlyPool.String()] = currentPool
|
||||
metric.ValuesAggl["hour_" + valueHourlyPool] = currentPool
|
||||
} else {
|
||||
utils.Warn("Metrics: Agglomeration - Pool not found : " + "hour_" + valueHourlyPool.String())
|
||||
utils.Warn("Metrics: Agglomeration - Pool not found : " + "hour_" + valueHourlyPool)
|
||||
}
|
||||
|
||||
if _, ok := metric.ValuesAggl["day_" + valueDailyPool.String()]; ok {
|
||||
currentPool := metric.ValuesAggl["day_" + valueDailyPool.String()]
|
||||
|
||||
if _, ok := metric.ValuesAggl["day_" + valueDailyPool]; ok {
|
||||
currentPool := metric.ValuesAggl["day_" + valueDailyPool]
|
||||
|
||||
currentPool.Value = MergeMetric(metric.AggloType, currentPool.Value, value.Value, currentPool.AvgIndex)
|
||||
if metric.AggloType == "avg" {
|
||||
currentPool.AvgIndex++
|
||||
}
|
||||
|
||||
metric.ValuesAggl["day_" + valueDailyPool.String()] = currentPool
|
||||
metric.ValuesAggl["day_" + valueDailyPool] = currentPool
|
||||
} else {
|
||||
utils.Warn("Metrics: Agglomeration - Pool not found: " + "day_" + valueDailyPool.String())
|
||||
utils.Warn("Metrics: Agglomeration - Pool not found: " + "day_" + valueDailyPool)
|
||||
}
|
||||
|
||||
values[valInd].Processed = true
|
||||
|
@ -171,7 +171,7 @@ func AggloMetrics() {
|
|||
func InitAggl() {
|
||||
go func() {
|
||||
s := gocron.NewScheduler()
|
||||
s.Every(1).Hour().At("00.00").From(gocron.NextTick()).Do(AggloMetrics)
|
||||
s.Every(1).Hour().From(gocron.NextTick()).Do(AggloMetrics)
|
||||
// s.Every(3).Minute().From(gocron.NextTick()).Do(AggloMetrics)
|
||||
|
||||
s.Start()
|
||||
|
|
45
src/metrics/api.go
Normal file
45
src/metrics/api.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
||||
func API_GetMetrics(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.AdminOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if(req.Method == "GET") {
|
||||
c, errCo := utils.GetCollection(utils.GetRootAppId(), "metrics")
|
||||
if errCo != nil {
|
||||
utils.Error("Metrics - Database Connect", errCo)
|
||||
return
|
||||
}
|
||||
|
||||
// get all metrics from database
|
||||
var metrics []DataDefDB
|
||||
cursor, err := c.Find(nil, map[string]interface{}{})
|
||||
if err != nil {
|
||||
utils.Error("Metrics: Error fetching metrics", err)
|
||||
return
|
||||
}
|
||||
defer cursor.Close(nil)
|
||||
|
||||
if err = cursor.All(nil, &metrics); err != nil {
|
||||
utils.Error("Metrics: Error decoding metrics", err)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": metrics,
|
||||
})
|
||||
} else {
|
||||
utils.Error("SettingGet: Method not allowed" + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue