Compare commits

..

8 Commits

Author SHA1 Message Date
AmitUp9
d4b9fea5a7 fix elastic time picker ui css (#1204) 2022-07-14 11:11:05 +03:00
leon-up9
d11770681b full height (#1202)
Co-authored-by: Leon <>
2022-07-13 18:37:22 +03:00
gadotroee
e9719cba3a Add time range to stats (#1199) 2022-07-13 17:21:18 +03:00
leon-up9
15f7b889e2 height change (#1201)
Co-authored-by: Leon <>
2022-07-13 13:37:08 +03:00
RoyUP9
d98ac0e8f7 Removed redundant IgnoredUserAgents field (#1198) 2022-07-12 20:41:42 +03:00
gadotroee
a3c236ff0a Fix colors map initialization (#1200) 2022-07-12 20:05:21 +03:00
gadotroee
4b280ecd6d Hide Response tab if there is no response (#1197) 2022-07-12 18:38:39 +03:00
leon-up9
de554f5fb6 ui/ include scss files in common (#1195)
* include scss files

* exported color

Co-authored-by: Leon <>
2022-07-12 11:50:24 +03:00
19 changed files with 59997 additions and 10418 deletions

View File

@@ -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) {

View File

@@ -5,7 +5,6 @@ import (
"encoding/hex"
"fmt"
"reflect"
"strings"
"sync"
"time"
@@ -82,13 +81,12 @@ func GetGeneralStats() *GeneralStats {
func InitProtocolToColor(protocolMap map[string]*api.Protocol) {
for item, value := range protocolMap {
splitted := strings.SplitN(item, "/", 3)
protocolToColor[splitted[len(splitted)-1]] = value.BackgroundColor
protocolToColor[api.GetProtocolSummary(item).Abbreviation] = value.BackgroundColor
}
}
func GetTrafficStats() *TrafficStatsResponse {
bucketsStatsCopy := getBucketStatsCopy()
func GetTrafficStats(startTime time.Time, endTime time.Time) *TrafficStatsResponse {
bucketsStatsCopy := getFilteredBucketStatsCopy(startTime, endTime)
return &TrafficStatsResponse{
Protocols: getAvailableProtocols(bucketsStatsCopy),
@@ -264,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 {
@@ -272,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 {

View File

@@ -151,17 +151,18 @@ func printTappedPodsPreview(ctx context.Context, kubernetesProvider *kubernetes.
}
}
func startTapperSyncer(ctx context.Context, cancel context.CancelFunc, provider *kubernetes.Provider, targetNamespaces []string, mizuApiFilteringOptions api.TrafficFilteringOptions, startTime time.Time) error {
func startTapperSyncer(ctx context.Context, cancel context.CancelFunc, provider *kubernetes.Provider, targetNamespaces []string, startTime time.Time) error {
tapperSyncer, err := kubernetes.CreateAndStartMizuTapperSyncer(ctx, provider, kubernetes.TapperSyncerConfig{
TargetNamespaces: targetNamespaces,
PodFilterRegex: *config.Config.Tap.PodRegex(),
MizuResourcesNamespace: config.Config.MizuResourcesNamespace,
AgentImage: config.Config.AgentImage,
TapperResources: config.Config.Tap.TapperResources,
ImagePullPolicy: config.Config.ImagePullPolicy(),
LogLevel: config.Config.LogLevel(),
IgnoredUserAgents: config.Config.Tap.IgnoredUserAgents,
MizuApiFilteringOptions: mizuApiFilteringOptions,
TargetNamespaces: targetNamespaces,
PodFilterRegex: *config.Config.Tap.PodRegex(),
MizuResourcesNamespace: config.Config.MizuResourcesNamespace,
AgentImage: config.Config.AgentImage,
TapperResources: config.Config.Tap.TapperResources,
ImagePullPolicy: config.Config.ImagePullPolicy(),
LogLevel: config.Config.LogLevel(),
MizuApiFilteringOptions: api.TrafficFilteringOptions{
IgnoredUserAgents: config.Config.Tap.IgnoredUserAgents,
},
MizuServiceAccountExists: state.mizuServiceAccountExists,
ServiceMesh: config.Config.Tap.ServiceMesh,
Tls: config.Config.Tap.Tls,
@@ -229,12 +230,6 @@ func getErrorDisplayTextForK8sTapManagerError(err kubernetes.K8sTapManagerError)
}
}
func getMizuApiFilteringOptions() (*api.TrafficFilteringOptions, error) {
return &api.TrafficFilteringOptions{
IgnoredUserAgents: config.Config.Tap.IgnoredUserAgents,
}, nil
}
func watchApiServerPod(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
podExactRegex := regexp.MustCompile(fmt.Sprintf("^%s$", kubernetes.ApiServerPodName))
podWatchHelper := kubernetes.NewPodWatchHelper(kubernetesProvider, podExactRegex)
@@ -352,8 +347,7 @@ func watchApiServerEvents(ctx context.Context, kubernetesProvider *kubernetes.Pr
func postApiServerStarted(ctx context.Context, kubernetesProvider *kubernetes.Provider, cancel context.CancelFunc) {
startProxyReportErrorIfAny(kubernetesProvider, ctx, cancel, config.Config.Tap.GuiPort)
options, _ := getMizuApiFilteringOptions()
if err := startTapperSyncer(ctx, cancel, kubernetesProvider, state.targetNamespaces, *options, state.startTime); err != nil {
if err := startTapperSyncer(ctx, cancel, kubernetesProvider, state.targetNamespaces, state.startTime); err != nil {
logger.Log.Errorf(uiUtils.Error, fmt.Sprintf("Error starting mizu tapper syncer: %v", errormessage.FormatError(err)))
cancel()
}

View File

@@ -43,7 +43,6 @@ type TapperSyncerConfig struct {
TapperResources shared.Resources
ImagePullPolicy core.PullPolicy
LogLevel logging.Level
IgnoredUserAgents []string
MizuApiFilteringOptions api.TrafficFilteringOptions
MizuServiceAccountExists bool
ServiceMesh bool

View File

@@ -4,6 +4,7 @@ import (
"bufio"
"fmt"
"net"
"strings"
"sync"
"time"
@@ -25,6 +26,15 @@ func (protocol *ProtocolSummary) ToString() string {
return fmt.Sprintf("%s?%s?%s", protocol.Name, protocol.Version, protocol.Abbreviation)
}
func GetProtocolSummary(inputString string) *ProtocolSummary {
splitted := strings.SplitN(inputString, "?", 3)
return &ProtocolSummary{
Name: splitted[0],
Version: splitted[1],
Abbreviation: splitted[2],
}
}
type Protocol struct {
ProtocolSummary
LongName string `json:"longName"`

File diff suppressed because it is too large Load Diff

View File

@@ -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": [
@@ -94,6 +95,7 @@
]
},
"files": [
"src/*.scss",
"dist"
]
}

View File

@@ -32,7 +32,7 @@ export const AutoRepresentation: React.FC<any> = ({ representation, color, opene
badge: null
}]
if (response) {
if (response && response.length > 0) {
arr.push({
tab: 'Response',
badge: null
@@ -71,7 +71,7 @@ export const AutoRepresentation: React.FC<any> = ({ representation, color, opene
{getOpenedTabIndex() === TabsEnum.Request && <React.Fragment>
<SectionsRepresentation data={request} color={color} requestRepresentation={request} />
</React.Fragment>}
{response && getOpenedTabIndex() === TabsEnum.Response && <React.Fragment>
{response && response.length > 0 && getOpenedTabIndex() === TabsEnum.Response && <React.Fragment>
<SectionsRepresentation data={response} color={color} />
</React.Fragment>}
</div>}

View File

@@ -28,10 +28,6 @@ const SectionsRepresentation: React.FC<any> = ({ data, color }) => {
}
}
if (sections.length === 0) {
sections.push(<div>This request or response has no data.</div>);
}
return <React.Fragment>{sections}</React.Fragment>;
}

View File

@@ -97,4 +97,7 @@ $modalMargin-from-edge : 35px
overflow: hidden
.servicesFilterList
height: calc(100% - 30px - 52px)
height: calc(100% - 30px)
.protocolsFilterList
height: 100%

View File

@@ -228,7 +228,7 @@ export const ServiceMapModal: React.FC<ServiceMapModalProps> = ({ isOpen, onClos
<div className={styles.filterWrapper}>
<div className={styles.card}>
<SelectList items={getProtocolsForFilter} checkBoxWidth="5%" tableName={"PROTOCOLS"} multiSelect={true}
checkedValues={checkedProtocols} setCheckedValues={onProtocolsChange} tableClassName={styles.filters}
checkedValues={checkedProtocols} setCheckedValues={onProtocolsChange} tableClassName={styles.filters + ` ${styles.protocolsFilterList}`}
inputSearchClass={styles.servicesFilterSearch} isFilterable={false} />
</div>
<div className={styles.servicesFilterWrapper + ` ${styles.card}`}>

View File

@@ -0,0 +1,96 @@
import React, { useState, Fragment } from 'react';
import { EuiProvider } from '@elastic/eui';
import {
EuiSuperDatePicker,
EuiSpacer,
} from '@elastic/eui';
import dateMath from '@elastic/datemath';
import '@elastic/eui/dist/eui_theme_light.css';
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 (
<EuiProvider>
<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>
</EuiProvider>
);
};

View File

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

View File

@@ -1,13 +1,12 @@
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";
const modalStyle = {
position: 'absolute',
@@ -15,7 +14,7 @@ const modalStyle = {
left: '50%',
transform: 'translate(-50%, 0%)',
width: '60vw',
height: '82vh',
height: '90vh',
bgcolor: 'background.paper',
borderRadius: '5px',
boxShadow: 24,
@@ -26,11 +25,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 +36,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,11 +56,13 @@ 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 (
@@ -82,18 +81,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)}>

View File

@@ -25,4 +25,5 @@ $light-gray: #8F9BB2;
failureColor: $failure-color;
blueGray: $blue-gray;
lightGray: $light-gray;
contentSectionColor: $content-section-color;
}

67736
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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"
},

View File

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

View File

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