mirror of
https://github.com/kubeshark/kubeshark.git
synced 2025-06-25 07:45:01 +00:00
Add time range to stats (#1199)
This commit is contained in:
parent
15f7b889e2
commit
e9719cba3a
@ -1,7 +1,10 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
core "k8s.io/api/core/v1"
|
||||
|
||||
@ -80,7 +83,24 @@ func GetGeneralStats(c *gin.Context) {
|
||||
}
|
||||
|
||||
func GetTrafficStats(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, providers.GetTrafficStats())
|
||||
startTime, endTime, err := getStartEndTime(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, providers.GetTrafficStats(startTime, endTime))
|
||||
}
|
||||
|
||||
func getStartEndTime(c *gin.Context) (time.Time, time.Time, error) {
|
||||
startTimeValue, err := strconv.Atoi(c.Query("startTimeMs"))
|
||||
if err != nil {
|
||||
return time.UnixMilli(0), time.UnixMilli(0), fmt.Errorf("invalid start time: %v", err)
|
||||
}
|
||||
endTimeValue, err := strconv.Atoi(c.Query("endTimeMs"))
|
||||
if err != nil {
|
||||
return time.UnixMilli(0), time.UnixMilli(0), fmt.Errorf("invalid end time: %v", err)
|
||||
}
|
||||
return time.UnixMilli(int64(startTimeValue)), time.UnixMilli(int64(endTimeValue)), nil
|
||||
}
|
||||
|
||||
func GetCurrentResolvingInformation(c *gin.Context) {
|
||||
|
@ -85,8 +85,8 @@ func InitProtocolToColor(protocolMap map[string]*api.Protocol) {
|
||||
}
|
||||
}
|
||||
|
||||
func GetTrafficStats() *TrafficStatsResponse {
|
||||
bucketsStatsCopy := getBucketStatsCopy()
|
||||
func GetTrafficStats(startTime time.Time, endTime time.Time) *TrafficStatsResponse {
|
||||
bucketsStatsCopy := getFilteredBucketStatsCopy(startTime, endTime)
|
||||
|
||||
return &TrafficStatsResponse{
|
||||
Protocols: getAvailableProtocols(bucketsStatsCopy),
|
||||
@ -262,7 +262,7 @@ func convertAccumulativeStatsDictToArray(methodsPerProtocolAggregated map[string
|
||||
return protocolsData
|
||||
}
|
||||
|
||||
func getBucketStatsCopy() BucketStats {
|
||||
func getFilteredBucketStatsCopy(startTime time.Time, endTime time.Time) BucketStats {
|
||||
bucketStatsCopy := BucketStats{}
|
||||
bucketStatsLocker.Lock()
|
||||
if err := copier.Copy(&bucketStatsCopy, bucketsStats); err != nil {
|
||||
@ -270,7 +270,18 @@ func getBucketStatsCopy() BucketStats {
|
||||
return nil
|
||||
}
|
||||
bucketStatsLocker.Unlock()
|
||||
return bucketStatsCopy
|
||||
|
||||
filteredBucketStatsCopy := BucketStats{}
|
||||
interval := InternalBucketThreshold
|
||||
|
||||
for _, bucket := range bucketStatsCopy {
|
||||
if (bucket.BucketTime.After(startTime.Add(-1*interval/2).Round(interval)) && bucket.BucketTime.Before(endTime.Add(-1*interval/2).Round(interval))) ||
|
||||
bucket.BucketTime.Equal(startTime.Add(-1*interval/2).Round(interval)) ||
|
||||
bucket.BucketTime.Equal(endTime.Add(-1*interval/2).Round(interval)) {
|
||||
filteredBucketStatsCopy = append(filteredBucketStatsCopy, bucket)
|
||||
}
|
||||
}
|
||||
return filteredBucketStatsCopy
|
||||
}
|
||||
|
||||
func getAggregatedResultTiming(stats BucketStats, interval time.Duration) map[time.Time]map[string]map[string]*AccumulativeStatsCounter {
|
||||
|
2426
ui-common/package-lock.json
generated
2426
ui-common/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -34,6 +34,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@craco/craco": "^6.4.3",
|
||||
"@elastic/eui": "^60.2.0",
|
||||
"@emotion/react": "^11.9.0",
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@mui/icons-material": "^5.8.2",
|
||||
@ -85,7 +86,7 @@
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-sass": "^1.2.12",
|
||||
"rollup-plugin-scss": "^3.0.0",
|
||||
"typescript": "^4.7.2"
|
||||
"typescript": "^4.5.3"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
@ -0,0 +1,92 @@
|
||||
import React, { useState, Fragment } from 'react';
|
||||
|
||||
import {
|
||||
EuiSuperDatePicker,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import dateMath from '@elastic/datemath';
|
||||
|
||||
interface TimeRangePickerProps {
|
||||
refreshStats: (startTime, endTime) => void;
|
||||
}
|
||||
|
||||
export const TimeRangePicker: React.FC<TimeRangePickerProps> = ({ refreshStats }) => {
|
||||
const [recentlyUsedRanges, setRecentlyUsedRanges] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [start, setStart] = useState('now-30m');
|
||||
const [end, setEnd] = useState('now');
|
||||
const [isPaused, setIsPaused] = useState(true);
|
||||
const [refreshInterval, setRefreshInterval] = useState();
|
||||
|
||||
const dateConvertor = (inputStart, inputEnd) => {
|
||||
const startMoment = dateMath.parse(inputStart);
|
||||
if (!startMoment || !startMoment.isValid()) {
|
||||
console.error("Unable to parse start string");
|
||||
}
|
||||
const endMoment = dateMath.parse(inputEnd, { roundUp: true });
|
||||
if (!endMoment || !endMoment.isValid()) {
|
||||
console.error("Unable to parse end string");
|
||||
}
|
||||
return { startMoment: startMoment.format("x"), endMoment: endMoment.format("x") }
|
||||
}
|
||||
|
||||
const onTimeChange = ({ start, end }) => {
|
||||
const recentlyUsedRange = recentlyUsedRanges.filter(recentlyUsedRange => {
|
||||
const isDuplicate =
|
||||
recentlyUsedRange.start === start && recentlyUsedRange.end === end;
|
||||
return !isDuplicate;
|
||||
});
|
||||
recentlyUsedRange.unshift({ start, end });
|
||||
setStart(start);
|
||||
setEnd(end);
|
||||
setRecentlyUsedRanges(
|
||||
recentlyUsedRange.length > 10
|
||||
? recentlyUsedRange.slice(0, 9)
|
||||
: recentlyUsedRange
|
||||
);
|
||||
const { startMoment, endMoment } = dateConvertor(start, end)
|
||||
refreshStats(startMoment, endMoment)
|
||||
setIsLoading(true);
|
||||
startLoading();
|
||||
};
|
||||
|
||||
const onRefresh = ({ start, end, refreshInterval }) => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, 100);
|
||||
}).then(() => {
|
||||
const { startMoment, endMoment } = dateConvertor(start, end)
|
||||
refreshStats(startMoment, endMoment)
|
||||
});
|
||||
};
|
||||
|
||||
const startLoading = () => {
|
||||
setTimeout(stopLoading, 1000);
|
||||
};
|
||||
const stopLoading = () => {
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const onRefreshChange = ({ isPaused, refreshInterval }) => {
|
||||
setIsPaused(isPaused);
|
||||
setRefreshInterval(refreshInterval);
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiSpacer />
|
||||
<EuiSuperDatePicker
|
||||
width='auto'
|
||||
isLoading={isLoading}
|
||||
start={start}
|
||||
end={end}
|
||||
onTimeChange={onTimeChange}
|
||||
onRefresh={onRefresh}
|
||||
isPaused={isPaused}
|
||||
refreshInterval={refreshInterval}
|
||||
onRefreshChange={onRefreshChange}
|
||||
recentlyUsedRanges={recentlyUsedRanges}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
@ -13,12 +13,12 @@
|
||||
top: 20px
|
||||
|
||||
.mainContainer
|
||||
padding: 30px
|
||||
text-align: center
|
||||
|
||||
.selectContainer
|
||||
display: flex
|
||||
justify-content: space-evenly
|
||||
align-items: center
|
||||
margin-bottom: 4%
|
||||
|
||||
.select
|
||||
|
@ -1,13 +1,14 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Backdrop, Box, Button, debounce, Fade, Modal } from "@mui/material";
|
||||
import { Backdrop, Box, debounce, Fade, Modal } from "@mui/material";
|
||||
import styles from "./TrafficStatsModal.module.sass";
|
||||
import closeIcon from "assets/close.svg";
|
||||
import { TrafficPieChart } from "./TrafficPieChart/TrafficPieChart";
|
||||
import { TimelineBarChart } from "./TimelineBarChart/TimelineBarChart";
|
||||
import refreshIcon from "assets/refresh.svg";
|
||||
import { useCommonStyles } from "../../../helpers/commonStyle";
|
||||
import { LoadingWrapper } from "../../UI/withLoading/withLoading";
|
||||
import { ALL_PROTOCOLS, StatsMode } from "./consts";
|
||||
import { TimeRangePicker } from "./TimelineBarChart/TimeRangePicker/TimeTangePicker";
|
||||
import { EuiProvider } from '@elastic/eui';
|
||||
import '@elastic/eui/dist/eui_theme_light.css';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute',
|
||||
@ -15,7 +16,7 @@ const modalStyle = {
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, 0%)',
|
||||
width: '60vw',
|
||||
height: '82vh',
|
||||
height: '90vh',
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: '5px',
|
||||
boxShadow: 24,
|
||||
@ -26,11 +27,10 @@ const modalStyle = {
|
||||
interface TrafficStatsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
getTrafficStatsDataApi: () => Promise<any>
|
||||
getTrafficStatsDataApi: (start?, end?) => Promise<any>
|
||||
}
|
||||
|
||||
export const TrafficStatsModal: React.FC<TrafficStatsModalProps> = ({ isOpen, onClose, getTrafficStatsDataApi }) => {
|
||||
|
||||
const modes = Object.keys(StatsMode).filter(x => !(parseInt(x) >= 0));
|
||||
const [statsMode, setStatsMode] = useState(modes[0]);
|
||||
const [selectedProtocol, setSelectedProtocol] = useState(ALL_PROTOCOLS);
|
||||
@ -38,14 +38,13 @@ export const TrafficStatsModal: React.FC<TrafficStatsModalProps> = ({ isOpen, on
|
||||
const [timelineStatsData, setTimelineStatsData] = useState(null);
|
||||
const [protocols, setProtocols] = useState([])
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const commonClasses = useCommonStyles();
|
||||
|
||||
const getTrafficStats = useCallback(async () => {
|
||||
const getTrafficStats = useCallback(async (startTime, endTime) => {
|
||||
if (isOpen && getTrafficStatsDataApi) {
|
||||
(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const statsData = await getTrafficStatsDataApi();
|
||||
const statsData = await getTrafficStatsDataApi(startTime, endTime);
|
||||
setPieStatsData(statsData.pie);
|
||||
setTimelineStatsData(statsData.timeline);
|
||||
setProtocols(statsData.protocols)
|
||||
@ -59,14 +58,17 @@ export const TrafficStatsModal: React.FC<TrafficStatsModalProps> = ({ isOpen, on
|
||||
}, [isOpen, getTrafficStatsDataApi, setPieStatsData, setTimelineStatsData])
|
||||
|
||||
useEffect(() => {
|
||||
getTrafficStats();
|
||||
const now = new Date().getTime();
|
||||
const halfAnHourAgo = now - (30 * 60 * 1000);
|
||||
getTrafficStats(halfAnHourAgo, now);
|
||||
}, [getTrafficStats])
|
||||
|
||||
const refreshStats = debounce(() => {
|
||||
getTrafficStats();
|
||||
const refreshStats = debounce((newStartTime, newEndTime) => {
|
||||
getTrafficStats(newStartTime, newEndTime);
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<EuiProvider>
|
||||
<Modal
|
||||
aria-labelledby="transition-modal-title"
|
||||
aria-describedby="transition-modal-description"
|
||||
@ -82,18 +84,12 @@ export const TrafficStatsModal: React.FC<TrafficStatsModalProps> = ({ isOpen, on
|
||||
</div>
|
||||
<div className={styles.headlineContainer}>
|
||||
<div className={styles.title}>Traffic Statistics</div>
|
||||
<Button style={{ marginLeft: "2%", textTransform: 'unset' }}
|
||||
startIcon={<img src={refreshIcon} className="custom" alt="refresh"></img>}
|
||||
size="medium"
|
||||
variant="contained"
|
||||
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
|
||||
onClick={refreshStats}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.mainContainer}>
|
||||
<div className={styles.selectContainer}>
|
||||
<div>
|
||||
<TimeRangePicker refreshStats={refreshStats} />
|
||||
</div>
|
||||
<div>
|
||||
<span style={{ marginRight: 15 }}>Breakdown By</span>
|
||||
<select className={styles.select} value={statsMode} onChange={(e) => setStatsMode(e.target.value)}>
|
||||
@ -119,5 +115,6 @@ export const TrafficStatsModal: React.FC<TrafficStatsModalProps> = ({ isOpen, on
|
||||
</Box>
|
||||
</Fade>
|
||||
</Modal>
|
||||
</EuiProvider>
|
||||
);
|
||||
}
|
||||
|
69916
ui/package-lock.json
generated
69916
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,8 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@craco/craco": "^6.4.3",
|
||||
"@elastic/datemath": "^5.0.3",
|
||||
"@elastic/eui": "^60.2.0",
|
||||
"@emotion/react": "^11.9.0",
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@mui/material": "^5.8.2",
|
||||
@ -23,7 +25,6 @@
|
||||
"mobx": "^6.6.0",
|
||||
"moment": "^2.29.3",
|
||||
"node-fetch": "^3.2.4",
|
||||
"sass": "^1.52.3",
|
||||
"numeral": "^2.0.6",
|
||||
"react": "^17.0.2",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
@ -35,8 +36,9 @@
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-toastify": "^8.2.0",
|
||||
"redoc": "^2.0.0-rc.71",
|
||||
"sass": "^1.52.3",
|
||||
"styled-components": "^5.3.5",
|
||||
"typescript": "^4.7.2",
|
||||
"typescript": "^4.5.3",
|
||||
"web-vitals": "^2.1.4",
|
||||
"xml-formatter": "^2.6.1"
|
||||
},
|
||||
|
@ -9,6 +9,7 @@
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<meta name="eui-style-insert">
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
|
@ -8,7 +8,7 @@ import oasModalOpenAtom from './recoil/oasModalOpen/atom';
|
||||
import trafficStatsModalOpenAtom from "./recoil/trafficStatsModalOpen";
|
||||
import { OasModal } from '@up9/mizu-common';
|
||||
import Api from './helpers/api';
|
||||
import {ThemeProvider, StyledEngineProvider, createTheme} from '@mui/material';
|
||||
import { ThemeProvider, StyledEngineProvider, createTheme } from '@mui/material';
|
||||
import { TrafficStatsModal } from '@up9/mizu-common';
|
||||
|
||||
const api = Api.getInstance()
|
||||
@ -36,7 +36,7 @@ const App = () => {
|
||||
openModal={oasModalOpen}
|
||||
handleCloseModal={() => setOasModalOpen(false)}
|
||||
/>}
|
||||
<TrafficStatsModal isOpen={trafficStatsModalOpen} onClose={() => setTrafficStatsModalOpen(false)} getTrafficStatsDataApi={api.getTrafficStats}/>
|
||||
<TrafficStatsModal isOpen={trafficStatsModalOpen} onClose={() => setTrafficStatsModalOpen(false)} getTrafficStatsDataApi={api.getTrafficStats} />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
|
@ -116,8 +116,8 @@ export default class Api {
|
||||
});
|
||||
}
|
||||
|
||||
getTrafficStats = async () => {
|
||||
const response = await client.get("/status/trafficStats");
|
||||
getTrafficStats = async (startTimeMs, endTimeMs) => {
|
||||
const response = await client.get("/status/trafficStats", {params: {startTimeMs, endTimeMs}});
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user