mirror of
https://github.com/kubeshark/kubeshark.git
synced 2026-02-17 03:19:54 +00:00
Compare commits
5 Commits
36.0-dev8
...
36.0-dev13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2544aea12 | ||
|
|
57e60073f5 | ||
|
|
f220ad2f1a | ||
|
|
5875ba0eb3 | ||
|
|
9aaf3f1423 |
4
.github/workflows/static_code_analysis.yml
vendored
4
.github/workflows/static_code_analysis.yml
vendored
@@ -32,6 +32,10 @@ jobs:
|
||||
id: agent_modified_files
|
||||
run: devops/check_modified_files.sh agent/
|
||||
|
||||
- name: Generate eBPF object files and go bindings
|
||||
id: generate_ebpf
|
||||
run: make bpf
|
||||
|
||||
- name: Go lint - agent
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
if: steps.agent_modified_files.outputs.matched == 'true'
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -40,6 +40,10 @@ jobs:
|
||||
run: |
|
||||
./devops/install-capstone.sh
|
||||
|
||||
- name: Generate eBPF object files and go bindings
|
||||
id: generate_ebpf
|
||||
run: make bpf
|
||||
|
||||
- name: Check CLI modified files
|
||||
id: cli_modified_files
|
||||
run: devops/check_modified_files.sh cli/
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -56,3 +56,6 @@ tap/extensions/*/expect
|
||||
|
||||
# Ignore *.log files
|
||||
*.log
|
||||
|
||||
# Object files
|
||||
*.o
|
||||
|
||||
20
Makefile
20
Makefile
@@ -8,7 +8,7 @@ SHELL=/bin/bash
|
||||
# HELP
|
||||
# This will output the help for each task
|
||||
# thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
||||
.PHONY: help ui agent agent-debug cli tap docker
|
||||
.PHONY: help ui agent agent-debug cli tap docker bpf clean-bpf
|
||||
|
||||
help: ## This help.
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
@@ -20,6 +20,13 @@ TS_SUFFIX="$(shell date '+%s')"
|
||||
GIT_BRANCH="$(shell git branch | grep \* | cut -d ' ' -f2 | tr '[:upper:]' '[:lower:]' | tr '/' '_')"
|
||||
BUCKET_PATH=static.up9.io/mizu/$(GIT_BRANCH)
|
||||
export VER?=0.0
|
||||
ARCH=$(shell uname -m)
|
||||
ifeq ($(ARCH),$(filter $(ARCH),aarch64 arm64))
|
||||
BPF_O_ARCH_LABEL=arm64
|
||||
else
|
||||
BPF_O_ARCH_LABEL=x86
|
||||
endif
|
||||
BPF_O_FILES = tap/tlstapper/tlstapper46_bpfel_$(BPF_O_ARCH_LABEL).o tap/tlstapper/tlstapper_bpfel_$(BPF_O_ARCH_LABEL).o
|
||||
|
||||
ui: ## Build UI.
|
||||
@(cd ui; npm i ; npm run build; )
|
||||
@@ -31,11 +38,17 @@ cli: ## Build CLI.
|
||||
cli-debug: ## Build CLI.
|
||||
@echo "building cli"; cd cli && $(MAKE) build-debug
|
||||
|
||||
agent: ## Build agent.
|
||||
agent: bpf ## Build agent.
|
||||
@(echo "building mizu agent .." )
|
||||
@(cd agent; go build -o build/mizuagent main.go)
|
||||
@ls -l agent/build
|
||||
|
||||
bpf: $(BPF_O_FILES)
|
||||
|
||||
$(BPF_O_FILES): $(wildcard tap/tlstapper/bpf/**/*.[ch])
|
||||
@(echo "building tlstapper bpf")
|
||||
@(./tap/tlstapper/bpf-builder/build.sh)
|
||||
|
||||
agent-debug: ## Build agent for debug.
|
||||
@(echo "building mizu agent for debug.." )
|
||||
@(cd agent; go build -gcflags="all=-N -l" -o build/mizuagent main.go)
|
||||
@@ -76,6 +89,9 @@ clean-cli: ## Clean CLI.
|
||||
clean-docker: ## Run clean docker
|
||||
@(echo "DOCKER cleanup - NOT IMPLEMENTED YET " )
|
||||
|
||||
clean-bpf:
|
||||
@(rm $(BPF_O_FILES) ; echo "bpf cleanup done" )
|
||||
|
||||
test-lint: ## Run lint on all modules
|
||||
cd agent && golangci-lint run
|
||||
cd shared && golangci-lint run
|
||||
|
||||
@@ -97,7 +97,9 @@ func websocketHandler(c *gin.Context, eventHandlers EventHandlers, isTapper bool
|
||||
websocketIdsLock.Unlock()
|
||||
|
||||
defer func() {
|
||||
socketCleanup(socketId, connectedWebsockets[socketId])
|
||||
if socketConnection := connectedWebsockets[socketId]; socketConnection != nil {
|
||||
socketCleanup(socketId, socketConnection)
|
||||
}
|
||||
}()
|
||||
|
||||
eventHandlers.WebSocketConnect(c, socketId, isTapper)
|
||||
|
||||
@@ -11,75 +11,30 @@ import (
|
||||
"github.com/up9inc/mizu/logger"
|
||||
)
|
||||
|
||||
// Keep it because we might want cookies in the future
|
||||
//func BuildCookies(rawCookies []interface{}) []har.Cookie {
|
||||
// cookies := make([]har.Cookie, 0, len(rawCookies))
|
||||
//
|
||||
// for _, cookie := range rawCookies {
|
||||
// c := cookie.(map[string]interface{})
|
||||
// expiresStr := ""
|
||||
// if c["expires"] != nil {
|
||||
// expiresStr = c["expires"].(string)
|
||||
// }
|
||||
// expires, _ := time.Parse(time.RFC3339, expiresStr)
|
||||
// httpOnly := false
|
||||
// if c["httponly"] != nil {
|
||||
// httpOnly, _ = strconv.ParseBool(c["httponly"].(string))
|
||||
// }
|
||||
// secure := false
|
||||
// if c["secure"] != nil {
|
||||
// secure, _ = strconv.ParseBool(c["secure"].(string))
|
||||
// }
|
||||
// path := ""
|
||||
// if c["path"] != nil {
|
||||
// path = c["path"].(string)
|
||||
// }
|
||||
// domain := ""
|
||||
// if c["domain"] != nil {
|
||||
// domain = c["domain"].(string)
|
||||
// }
|
||||
//
|
||||
// cookies = append(cookies, har.Cookie{
|
||||
// Name: c["name"].(string),
|
||||
// Value: c["value"].(string),
|
||||
// Path: path,
|
||||
// Domain: domain,
|
||||
// HTTPOnly: httpOnly,
|
||||
// Secure: secure,
|
||||
// Expires: expires,
|
||||
// Expires8601: expiresStr,
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// return cookies
|
||||
//}
|
||||
|
||||
func BuildHeaders(rawHeaders []interface{}) ([]Header, string, string, string, string, string) {
|
||||
func BuildHeaders(rawHeaders map[string]interface{}) ([]Header, string, string, string, string, string) {
|
||||
var host, scheme, authority, path, status string
|
||||
headers := make([]Header, 0, len(rawHeaders))
|
||||
|
||||
for _, header := range rawHeaders {
|
||||
h := header.(map[string]interface{})
|
||||
|
||||
for key, value := range rawHeaders {
|
||||
headers = append(headers, Header{
|
||||
Name: h["name"].(string),
|
||||
Value: h["value"].(string),
|
||||
Name: key,
|
||||
Value: value.(string),
|
||||
})
|
||||
|
||||
if h["name"] == "Host" {
|
||||
host = h["value"].(string)
|
||||
if key == "Host" {
|
||||
host = value.(string)
|
||||
}
|
||||
if h["name"] == ":authority" {
|
||||
authority = h["value"].(string)
|
||||
if key == ":authority" {
|
||||
authority = value.(string)
|
||||
}
|
||||
if h["name"] == ":scheme" {
|
||||
scheme = h["value"].(string)
|
||||
if key == ":scheme" {
|
||||
scheme = value.(string)
|
||||
}
|
||||
if h["name"] == ":path" {
|
||||
path = h["value"].(string)
|
||||
if key == ":path" {
|
||||
path = value.(string)
|
||||
}
|
||||
if h["name"] == ":status" {
|
||||
status = h["value"].(string)
|
||||
if key == ":status" {
|
||||
status = value.(string)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,8 +74,8 @@ func BuildPostParams(rawParams []interface{}) []Param {
|
||||
}
|
||||
|
||||
func NewRequest(request map[string]interface{}) (harRequest *Request, err error) {
|
||||
headers, host, scheme, authority, path, _ := BuildHeaders(request["_headers"].([]interface{}))
|
||||
cookies := make([]Cookie, 0) // BuildCookies(request["_cookies"].([]interface{}))
|
||||
headers, host, scheme, authority, path, _ := BuildHeaders(request["headers"].(map[string]interface{}))
|
||||
cookies := make([]Cookie, 0)
|
||||
|
||||
postData, _ := request["postData"].(map[string]interface{})
|
||||
mimeType := postData["mimeType"]
|
||||
@@ -134,12 +89,20 @@ func NewRequest(request map[string]interface{}) (harRequest *Request, err error)
|
||||
}
|
||||
|
||||
queryString := make([]QueryString, 0)
|
||||
for _, _qs := range request["_queryString"].([]interface{}) {
|
||||
qs := _qs.(map[string]interface{})
|
||||
queryString = append(queryString, QueryString{
|
||||
Name: qs["name"].(string),
|
||||
Value: qs["value"].(string),
|
||||
})
|
||||
for key, value := range request["queryString"].(map[string]interface{}) {
|
||||
if valuesInterface, ok := value.([]interface{}); ok {
|
||||
for _, valueInterface := range valuesInterface {
|
||||
queryString = append(queryString, QueryString{
|
||||
Name: key,
|
||||
Value: valueInterface.(string),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
queryString = append(queryString, QueryString{
|
||||
Name: key,
|
||||
Value: value.(string),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("http://%s%s", host, request["url"].(string))
|
||||
@@ -172,8 +135,8 @@ func NewRequest(request map[string]interface{}) (harRequest *Request, err error)
|
||||
}
|
||||
|
||||
func NewResponse(response map[string]interface{}) (harResponse *Response, err error) {
|
||||
headers, _, _, _, _, _status := BuildHeaders(response["_headers"].([]interface{}))
|
||||
cookies := make([]Cookie, 0) // BuildCookies(response["_cookies"].([]interface{}))
|
||||
headers, _, _, _, _, _status := BuildHeaders(response["headers"].(map[string]interface{}))
|
||||
cookies := make([]Cookie, 0)
|
||||
|
||||
content, _ := response["content"].(map[string]interface{})
|
||||
mimeType := content["mimeType"]
|
||||
|
||||
@@ -13,4 +13,4 @@ test-pull-bin:
|
||||
|
||||
test-pull-expect:
|
||||
@mkdir -p expect
|
||||
@[ "${skipexpect}" ] && echo "Skipping downloading expected JSONs" || gsutil -o 'GSUtil:parallel_process_count=5' -o 'GSUtil:parallel_thread_count=5' -m cp -r gs://static.up9.io/mizu/test-pcap/expect15/http/\* expect
|
||||
@[ "${skipexpect}" ] && echo "Skipping downloading expected JSONs" || gsutil -o 'GSUtil:parallel_process_count=5' -o 'GSUtil:parallel_thread_count=5' -m cp -r gs://static.up9.io/mizu/test-pcap/expect16/http/\* expect
|
||||
|
||||
@@ -6,13 +6,16 @@ import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/up9inc/mizu/tap/api"
|
||||
)
|
||||
|
||||
func mapSliceRebuildAsMap(mapSlice []interface{}) (newMap map[string]interface{}) {
|
||||
newMap = make(map[string]interface{})
|
||||
for _, item := range mapSlice {
|
||||
|
||||
mergedMapSlice := mapSliceMergeRepeatedKeys(mapSlice)
|
||||
for _, item := range mergedMapSlice {
|
||||
h := item.(map[string]interface{})
|
||||
newMap[h["name"].(string)] = h["value"]
|
||||
}
|
||||
@@ -20,6 +23,28 @@ func mapSliceRebuildAsMap(mapSlice []interface{}) (newMap map[string]interface{}
|
||||
return
|
||||
}
|
||||
|
||||
func mapSliceRebuildAsMergedMap(mapSlice []interface{}) (newMap map[string]interface{}) {
|
||||
newMap = make(map[string]interface{})
|
||||
|
||||
mergedMapSlice := mapSliceMergeRepeatedKeys(mapSlice)
|
||||
for _, item := range mergedMapSlice {
|
||||
h := item.(map[string]interface{})
|
||||
|
||||
if valuesInterface, ok := h["value"].([]interface{}); ok {
|
||||
var values []string
|
||||
for _, valueInterface := range valuesInterface {
|
||||
values = append(values, valueInterface.(string))
|
||||
}
|
||||
|
||||
newMap[h["name"].(string)] = strings.Join(values, ",")
|
||||
} else {
|
||||
newMap[h["name"].(string)] = h["value"]
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func mapSliceMergeRepeatedKeys(mapSlice []interface{}) (newMapSlice []interface{}) {
|
||||
newMapSlice = make([]interface{}, 0)
|
||||
valuesMap := make(map[string][]interface{})
|
||||
@@ -47,6 +72,24 @@ func mapSliceMergeRepeatedKeys(mapSlice []interface{}) (newMapSlice []interface{
|
||||
return
|
||||
}
|
||||
|
||||
func representMapAsTable(mapToTable map[string]interface{}, selectorPrefix string) (representation string) {
|
||||
var table []api.TableData
|
||||
|
||||
keys := make([]string, 0, len(mapToTable))
|
||||
for k := range mapToTable {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, key := range keys {
|
||||
table = append(table, createTableForKey(key, mapToTable[key], selectorPrefix)...)
|
||||
}
|
||||
|
||||
obj, _ := json.Marshal(table)
|
||||
representation = string(obj)
|
||||
return
|
||||
}
|
||||
|
||||
func representMapSliceAsTable(mapSlice []interface{}, selectorPrefix string) (representation string) {
|
||||
var table []api.TableData
|
||||
for _, item := range mapSlice {
|
||||
@@ -54,34 +97,7 @@ func representMapSliceAsTable(mapSlice []interface{}, selectorPrefix string) (re
|
||||
key := h["name"].(string)
|
||||
value := h["value"]
|
||||
|
||||
var reflectKind reflect.Kind
|
||||
reflectType := reflect.TypeOf(value)
|
||||
if reflectType == nil {
|
||||
reflectKind = reflect.Interface
|
||||
} else {
|
||||
reflectKind = reflect.TypeOf(value).Kind()
|
||||
}
|
||||
|
||||
switch reflectKind {
|
||||
case reflect.Slice:
|
||||
fallthrough
|
||||
case reflect.Array:
|
||||
for i, el := range value.([]interface{}) {
|
||||
selector := fmt.Sprintf("%s.%s[%d]", selectorPrefix, key, i)
|
||||
table = append(table, api.TableData{
|
||||
Name: fmt.Sprintf("%s [%d]", key, i),
|
||||
Value: el,
|
||||
Selector: selector,
|
||||
})
|
||||
}
|
||||
default:
|
||||
selector := fmt.Sprintf("%s[\"%s\"]", selectorPrefix, key)
|
||||
table = append(table, api.TableData{
|
||||
Name: key,
|
||||
Value: value,
|
||||
Selector: selector,
|
||||
})
|
||||
}
|
||||
table = append(table, createTableForKey(key, value, selectorPrefix)...)
|
||||
}
|
||||
|
||||
obj, _ := json.Marshal(table)
|
||||
@@ -89,6 +105,41 @@ func representMapSliceAsTable(mapSlice []interface{}, selectorPrefix string) (re
|
||||
return
|
||||
}
|
||||
|
||||
func createTableForKey(key string, value interface{}, selectorPrefix string) []api.TableData {
|
||||
var table []api.TableData
|
||||
|
||||
var reflectKind reflect.Kind
|
||||
reflectType := reflect.TypeOf(value)
|
||||
if reflectType == nil {
|
||||
reflectKind = reflect.Interface
|
||||
} else {
|
||||
reflectKind = reflect.TypeOf(value).Kind()
|
||||
}
|
||||
|
||||
switch reflectKind {
|
||||
case reflect.Slice:
|
||||
fallthrough
|
||||
case reflect.Array:
|
||||
for i, el := range value.([]interface{}) {
|
||||
selector := fmt.Sprintf("%s.%s[%d]", selectorPrefix, key, i)
|
||||
table = append(table, api.TableData{
|
||||
Name: fmt.Sprintf("%s [%d]", key, i),
|
||||
Value: el,
|
||||
Selector: selector,
|
||||
})
|
||||
}
|
||||
default:
|
||||
selector := fmt.Sprintf("%s[\"%s\"]", selectorPrefix, key)
|
||||
table = append(table, api.TableData{
|
||||
Name: key,
|
||||
Value: value,
|
||||
Selector: selector,
|
||||
})
|
||||
}
|
||||
|
||||
return table
|
||||
}
|
||||
|
||||
func representSliceAsTable(slice []interface{}, selectorPrefix string) (representation string) {
|
||||
var table []api.TableData
|
||||
for i, item := range slice {
|
||||
|
||||
@@ -286,19 +286,13 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
|
||||
reqDetails["pathSegments"] = strings.Split(path, "/")[1:]
|
||||
|
||||
// Rearrange the maps for the querying
|
||||
reqDetails["_headers"] = reqDetails["headers"]
|
||||
reqDetails["headers"] = mapSliceRebuildAsMap(reqDetails["_headers"].([]interface{}))
|
||||
resDetails["_headers"] = resDetails["headers"]
|
||||
resDetails["headers"] = mapSliceRebuildAsMap(resDetails["_headers"].([]interface{}))
|
||||
reqDetails["headers"] = mapSliceRebuildAsMergedMap(reqDetails["headers"].([]interface{}))
|
||||
resDetails["headers"] = mapSliceRebuildAsMergedMap(resDetails["headers"].([]interface{}))
|
||||
|
||||
reqDetails["_cookies"] = reqDetails["cookies"]
|
||||
reqDetails["cookies"] = mapSliceRebuildAsMap(reqDetails["_cookies"].([]interface{}))
|
||||
resDetails["_cookies"] = resDetails["cookies"]
|
||||
resDetails["cookies"] = mapSliceRebuildAsMap(resDetails["_cookies"].([]interface{}))
|
||||
reqDetails["cookies"] = mapSliceRebuildAsMergedMap(reqDetails["cookies"].([]interface{}))
|
||||
resDetails["cookies"] = mapSliceRebuildAsMergedMap(resDetails["cookies"].([]interface{}))
|
||||
|
||||
reqDetails["_queryString"] = reqDetails["queryString"]
|
||||
reqDetails["_queryStringMerged"] = mapSliceMergeRepeatedKeys(reqDetails["_queryString"].([]interface{}))
|
||||
reqDetails["queryString"] = mapSliceRebuildAsMap(reqDetails["_queryStringMerged"].([]interface{}))
|
||||
reqDetails["queryString"] = mapSliceRebuildAsMap(reqDetails["queryString"].([]interface{}))
|
||||
|
||||
elapsedTime := item.Pair.Response.CaptureTime.Sub(item.Pair.Request.CaptureTime).Round(time.Millisecond).Milliseconds()
|
||||
if elapsedTime < 0 {
|
||||
@@ -397,19 +391,19 @@ func representRequest(request map[string]interface{}) (repRequest []interface{})
|
||||
repRequest = append(repRequest, api.SectionData{
|
||||
Type: api.TABLE,
|
||||
Title: "Headers",
|
||||
Data: representMapSliceAsTable(request["_headers"].([]interface{}), `request.headers`),
|
||||
Data: representMapAsTable(request["headers"].(map[string]interface{}), `request.headers`),
|
||||
})
|
||||
|
||||
repRequest = append(repRequest, api.SectionData{
|
||||
Type: api.TABLE,
|
||||
Title: "Cookies",
|
||||
Data: representMapSliceAsTable(request["_cookies"].([]interface{}), `request.cookies`),
|
||||
Data: representMapAsTable(request["cookies"].(map[string]interface{}), `request.cookies`),
|
||||
})
|
||||
|
||||
repRequest = append(repRequest, api.SectionData{
|
||||
Type: api.TABLE,
|
||||
Title: "Query String",
|
||||
Data: representMapSliceAsTable(request["_queryStringMerged"].([]interface{}), `request.queryString`),
|
||||
Data: representMapAsTable(request["queryString"].(map[string]interface{}), `request.queryString`),
|
||||
})
|
||||
|
||||
postData, _ := request["postData"].(map[string]interface{})
|
||||
@@ -485,13 +479,13 @@ func representResponse(response map[string]interface{}) (repResponse []interface
|
||||
repResponse = append(repResponse, api.SectionData{
|
||||
Type: api.TABLE,
|
||||
Title: "Headers",
|
||||
Data: representMapSliceAsTable(response["_headers"].([]interface{}), `response.headers`),
|
||||
Data: representMapAsTable(response["headers"].(map[string]interface{}), `response.headers`),
|
||||
})
|
||||
|
||||
repResponse = append(repResponse, api.SectionData{
|
||||
Type: api.TABLE,
|
||||
Title: "Cookies",
|
||||
Data: representMapSliceAsTable(response["_cookies"].([]interface{}), `response.cookies`),
|
||||
Data: representMapAsTable(response["cookies"].(map[string]interface{}), `response.cookies`),
|
||||
})
|
||||
|
||||
content, _ := response["content"].(map[string]interface{})
|
||||
|
||||
@@ -9,7 +9,7 @@ docker build -t mizu-ebpf-builder . || exit 1
|
||||
BPF_TARGET=amd64
|
||||
BPF_CFLAGS="-O2 -g -D__TARGET_ARCH_x86"
|
||||
ARCH=$(uname -m)
|
||||
if [[ $ARCH == "aarch64" ]]; then
|
||||
if [[ $ARCH == "aarch64" || $ARCH == "arm64" ]]; then
|
||||
BPF_TARGET=arm64
|
||||
BPF_CFLAGS="-O2 -g -D__TARGET_ARCH_arm64"
|
||||
fi
|
||||
@@ -18,7 +18,7 @@ docker run --rm \
|
||||
--name mizu-ebpf-builder \
|
||||
-v $MIZU_HOME:/mizu \
|
||||
-v $(go env GOPATH):/root/go \
|
||||
-it mizu-ebpf-builder \
|
||||
mizu-ebpf-builder \
|
||||
sh -c "
|
||||
BPF_TARGET=\"$BPF_TARGET\" BPF_CFLAGS=\"$BPF_CFLAGS\" go generate tap/tlstapper/tls_tapper.go
|
||||
chown $(id -u):$(id -g) tap/tlstapper/tlstapper*_bpf*
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -65,6 +65,7 @@
|
||||
"recharts": "^2.1.10",
|
||||
"redoc": "^2.0.0-rc.71",
|
||||
"styled-components": "^5.3.5",
|
||||
"use-file-picker": "^1.4.2",
|
||||
"web-vitals": "^2.1.4",
|
||||
"xml-formatter": "^2.6.1"
|
||||
},
|
||||
|
||||
@@ -17,6 +17,6 @@
|
||||
width: 100%;
|
||||
width: -moz-available;
|
||||
width: -webkit-fill-available;
|
||||
width: strech;
|
||||
width: stretch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import React, {useCallback, useEffect, useMemo, useState} from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import styles from './EntriesList.module.sass';
|
||||
import ScrollableFeedVirtualized from "react-scrollable-feed-virtualized";
|
||||
import Moment from 'moment';
|
||||
import {EntryItem} from "../EntryListItem/EntryListItem";
|
||||
import { EntryItem } from "../EntryListItem/EntryListItem";
|
||||
import down from "assets/downImg.svg";
|
||||
import spinner from 'assets/spinner.svg';
|
||||
import {RecoilState, useRecoilState, useRecoilValue, useSetRecoilState} from "recoil";
|
||||
import { RecoilState, useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
|
||||
import entriesAtom from "../../recoil/entries";
|
||||
import queryAtom from "../../recoil/query";
|
||||
import TrafficViewerApiAtom from "../../recoil/TrafficViewerApi";
|
||||
import TrafficViewerApi from "../TrafficViewer/TrafficViewerApi";
|
||||
import focusedEntryIdAtom from "../../recoil/focusedEntryId";
|
||||
import {toast} from "react-toastify";
|
||||
import {MAX_ENTRIES, TOAST_CONTAINER_ID} from "../../configs/Consts";
|
||||
import { toast } from "react-toastify";
|
||||
import { MAX_ENTRIES, TOAST_CONTAINER_ID } from "../../configs/Consts";
|
||||
import tappingStatusAtom from "../../recoil/tappingStatus";
|
||||
import leftOffTopAtom from "../../recoil/leftOffTop";
|
||||
import Moment from "moment";
|
||||
|
||||
interface EntriesListProps {
|
||||
listEntryREF: any;
|
||||
|
||||
@@ -37,11 +37,6 @@ const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||
theme="github"
|
||||
onChange={onChange}
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
setOptions={{
|
||||
enableBasicAutocompletion: true,
|
||||
enableLiveAutocompletion: true,
|
||||
enableSnippets: true
|
||||
}}
|
||||
showPrintMargin={false}
|
||||
value={code}
|
||||
width="100%"
|
||||
|
||||
33
ui-common/src/components/UI/FilePicker/FilePicker.tsx
Normal file
33
ui-common/src/components/UI/FilePicker/FilePicker.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useFilePicker } from 'use-file-picker';
|
||||
import { FileContent } from 'use-file-picker/dist/interfaces';
|
||||
|
||||
interface IFilePickerProps {
|
||||
onLoadingComplete: (file: FileContent) => void;
|
||||
elem: any
|
||||
}
|
||||
|
||||
const FilePicker = ({ elem, onLoadingComplete }: IFilePickerProps) => {
|
||||
const [openFileSelector, { filesContent }] = useFilePicker({
|
||||
accept: ['.json'],
|
||||
limitFilesConfig: { max: 1 },
|
||||
maxFileSize: 1
|
||||
});
|
||||
|
||||
const onFileSelectorClick = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openFileSelector();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
filesContent.length && onLoadingComplete(filesContent[0])
|
||||
}, [filesContent, onLoadingComplete]);
|
||||
|
||||
return (<React.Fragment>
|
||||
{React.cloneElement(elem, { onClick: onFileSelectorClick })}
|
||||
</React.Fragment>)
|
||||
}
|
||||
|
||||
export default FilePicker;
|
||||
@@ -75,5 +75,6 @@ const KeyValueTable: React.FC<KeyValueTableProps> = ({ data, onDataChange, keyPl
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
|
||||
export const convertParamsToArr = (paramsObj) => Object.entries(paramsObj).map(([key, value]) => { return { key, value } })
|
||||
export const convertArrToKeyValueObject = (arr) => arr.reduce((acc, curr) => { acc[curr.key] = curr.value; return acc }, {})
|
||||
export default KeyValueTable
|
||||
|
||||
@@ -79,5 +79,12 @@
|
||||
overflow: hidden
|
||||
|
||||
b::after
|
||||
content: '\b'
|
||||
content: '\b'
|
||||
display: inline
|
||||
|
||||
.icon
|
||||
width: 24px
|
||||
height: 26px
|
||||
stroke-width: 0px
|
||||
fill: $blue-color
|
||||
stroke: $blue-color
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
import { Accordion, AccordionDetails, AccordionSummary, Backdrop, Box, Button, Fade, Modal } from "@mui/material";
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import DownloadIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||
import UploadIcon from '@mui/icons-material/UploadFile';
|
||||
import closeIcon from "assets/close.svg";
|
||||
import refreshImg from "assets/refresh.svg";
|
||||
import { Accordion, AccordionDetails, AccordionSummary, Backdrop, Box, Button, Fade, Modal } from "@mui/material";
|
||||
import React, { Fragment, useCallback, useEffect, useState } from "react";
|
||||
import { useCommonStyles } from "../../../helpers/commonStyle";
|
||||
import { Tabs } from "../../UI";
|
||||
import KeyValueTable from "../../UI/KeyValueTable/KeyValueTable";
|
||||
import CodeEditor from "../../UI/CodeEditor/CodeEditor";
|
||||
import { useRecoilValue, RecoilState, useRecoilState } from "recoil";
|
||||
import TrafficViewerApiAtom from "../../../recoil/TrafficViewerApi/atom";
|
||||
import TrafficViewerApi from "../../TrafficViewer/TrafficViewerApi";
|
||||
import { toast } from "react-toastify";
|
||||
import { RecoilState, useRecoilState, useRecoilValue } from "recoil";
|
||||
import { FileContent } from "use-file-picker/dist/interfaces";
|
||||
import { TOAST_CONTAINER_ID } from "../../../configs/Consts";
|
||||
import styles from './ReplayRequestModal.module.sass'
|
||||
import closeIcon from "assets/close.svg"
|
||||
import refreshImg from "assets/refresh.svg"
|
||||
import { formatRequestWithOutError } from "../../EntryDetailed/EntrySections/EntrySections";
|
||||
import entryDataAtom from "../../../recoil/entryData";
|
||||
import { AutoRepresentation, TabsEnum } from "../../EntryDetailed/EntryViewer/AutoRepresentation";
|
||||
import useDebounce from "../../../hooks/useDebounce"
|
||||
import replayRequestModalOpenAtom from "../../../recoil/replayRequestModalOpen";
|
||||
import { useCommonStyles } from "../../../helpers/commonStyle";
|
||||
import { Utils } from "../../../helpers/Utils";
|
||||
import useDebounce from "../../../hooks/useDebounce";
|
||||
import entryDataAtom from "../../../recoil/entryData";
|
||||
import replayRequestModalOpenAtom from "../../../recoil/replayRequestModalOpen";
|
||||
import TrafficViewerApiAtom from "../../../recoil/TrafficViewerApi/atom";
|
||||
import { formatRequestWithOutError } from "../../EntryDetailed/EntrySections/EntrySections";
|
||||
import { AutoRepresentation, TabsEnum } from "../../EntryDetailed/EntryViewer/AutoRepresentation";
|
||||
import TrafficViewerApi from "../../TrafficViewer/TrafficViewerApi";
|
||||
import { Tabs } from "../../UI";
|
||||
import CodeEditor from "../../UI/CodeEditor/CodeEditor";
|
||||
import FilePicker from '../../UI/FilePicker/FilePicker';
|
||||
import KeyValueTable, { convertArrToKeyValueObject, convertParamsToArr } from "../../UI/KeyValueTable/KeyValueTable";
|
||||
import { LoadingWrapper } from "../../UI/withLoading/withLoading";
|
||||
import { IReplayRequestData, KeyValuePair } from './interfaces';
|
||||
import styles from './ReplayRequestModal.module.sass';
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute',
|
||||
@@ -37,11 +42,6 @@ const modalStyle = {
|
||||
paddingBottom: "15px"
|
||||
};
|
||||
|
||||
interface ReplayRequestModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
enum RequestTabs {
|
||||
Params = "params",
|
||||
Headers = "headers",
|
||||
@@ -51,8 +51,6 @@ enum RequestTabs {
|
||||
const HTTP_METHODS = ["get", "post", "put", "head", "options", "delete"]
|
||||
const TABS = [{ tab: RequestTabs.Headers }, { tab: RequestTabs.Params }, { tab: RequestTabs.Body }];
|
||||
|
||||
const convertParamsToArr = (paramsObj) => Object.entries(paramsObj).map(([key, value]) => { return { key, value } })
|
||||
|
||||
const getQueryStringParams = (link: String) => {
|
||||
|
||||
if (link) {
|
||||
@@ -69,43 +67,61 @@ const decodeQueryParam = (p) => {
|
||||
return decodeURIComponent(p.replace(/\+/g, ' '));
|
||||
}
|
||||
|
||||
interface ReplayRequestModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ReplayRequestModal: React.FC<ReplayRequestModalProps> = ({ isOpen, onClose }) => {
|
||||
const entryData = useRecoilValue(entryDataAtom)
|
||||
const request = entryData.data.request
|
||||
const [method, setMethod] = useState(request?.method?.toLowerCase() as string)
|
||||
const getHostUrl = useCallback(() => {
|
||||
return entryData.data.dst.name ? entryData.data?.dst?.name : entryData.data.dst.ip
|
||||
}, [entryData.data.dst.ip, entryData.data.dst.name])
|
||||
const [hostPortInput, setHostPortInput] = useState(`${entryData.base.proto.name}://${getHostUrl()}:${entryData.data.dst.port}`)
|
||||
const getHostPortVal = useCallback(() => {
|
||||
return `${entryData.base.proto.name}://${getHostUrl()}:${entryData.data.dst.port}`
|
||||
}, [entryData.base.proto.name, entryData.data.dst.port, getHostUrl])
|
||||
const [hostPortInput, setHostPortInput] = useState(getHostPortVal())
|
||||
const [pathInput, setPathInput] = useState(request.path);
|
||||
const commonClasses = useCommonStyles();
|
||||
const [currentTab, setCurrentTab] = useState(TABS[0].tab);
|
||||
const [response, setResponse] = useState(null);
|
||||
const [postData, setPostData] = useState(request?.postData?.text || JSON.stringify(request?.postData?.params));
|
||||
const [params, setParams] = useState(convertParamsToArr(request?.queryString || {}))
|
||||
const [headers, setHeaders] = useState(convertParamsToArr(request?.headers || {}))
|
||||
const trafficViewerApi = useRecoilValue(TrafficViewerApiAtom as RecoilState<TrafficViewerApi>)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [requestExpanded, setRequestExpanded] = useState(true)
|
||||
const [responseExpanded, setResponseExpanded] = useState(false)
|
||||
|
||||
const getInitialRequestData = useCallback((): IReplayRequestData => {
|
||||
return {
|
||||
method: request?.method?.toLowerCase() as string,
|
||||
hostPort: `${entryData.base.proto.name}://${getHostUrl()}:${entryData.data.dst.port}`,
|
||||
path: request.path,
|
||||
postData: request.postData?.text || JSON.stringify(request.postData?.params),
|
||||
headers: convertParamsToArr(request.headers || {}),
|
||||
params: convertParamsToArr(request.queryString || {})
|
||||
}
|
||||
}, [entryData.base.proto.name, entryData.data.dst.port, getHostUrl, request.headers, request?.method, request.path, request.postData?.params, request.postData?.text, request.queryString])
|
||||
|
||||
const [requestDataModel, setRequestData] = useState<IReplayRequestData>(getInitialRequestData())
|
||||
|
||||
const debouncedPath = useDebounce(pathInput, 500);
|
||||
|
||||
const addParamsToUrl = useCallback((url: string, params: KeyValuePair[]) => {
|
||||
const urlParams = new URLSearchParams("");
|
||||
params.forEach(param => urlParams.append(param.key, param.value as string))
|
||||
return `${url}?${urlParams.toString()}`
|
||||
}, [])
|
||||
|
||||
const onParamsChange = useCallback((newParams) => {
|
||||
setParams(newParams);
|
||||
let newUrl = `${debouncedPath ? debouncedPath.split('?')[0] : ""}`
|
||||
newParams.forEach(({ key, value }, index) => {
|
||||
newUrl += index > 0 ? '&' : '?'
|
||||
newUrl += `${key}` + (value ? `=${value}` : "")
|
||||
})
|
||||
|
||||
newUrl = addParamsToUrl(newUrl, newParams)
|
||||
setPathInput(newUrl)
|
||||
|
||||
}, [debouncedPath])
|
||||
}, [addParamsToUrl, debouncedPath])
|
||||
|
||||
useEffect(() => {
|
||||
const newParams = getQueryStringParams(debouncedPath);
|
||||
setParams(convertParamsToArr(newParams))
|
||||
const params = convertParamsToArr(getQueryStringParams(debouncedPath));
|
||||
setRequestData({ ...requestDataModel, params })
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedPath])
|
||||
|
||||
const onModalClose = () => {
|
||||
@@ -114,33 +130,28 @@ const ReplayRequestModal: React.FC<ReplayRequestModalProps> = ({ isOpen, onClose
|
||||
onClose()
|
||||
}
|
||||
|
||||
const resetModel = useCallback(() => {
|
||||
setMethod(request?.method?.toLowerCase() as string)
|
||||
setHostPortInput(`${entryData.base.proto.name}://${getHostUrl()}:${entryData.data.dst.port}`)
|
||||
setPathInput(request.path);
|
||||
const resetModal = useCallback((requestDataModel: IReplayRequestData, hostPortInputVal, pathVal) => {
|
||||
setRequestData(requestDataModel)
|
||||
setHostPortInput(hostPortInputVal)
|
||||
setPathInput(addParamsToUrl(pathVal, requestDataModel.params));
|
||||
setResponse(null);
|
||||
setPostData(request?.postData?.text || JSON.stringify(request?.postData?.params));
|
||||
setParams(convertParamsToArr(request?.queryString || {}))
|
||||
setHeaders(convertParamsToArr(request?.headers || {}))
|
||||
setRequestExpanded(true)
|
||||
}, [entryData.base.proto.name, entryData.data.dst.port, getHostUrl, request?.headers, request?.method, request.path, request?.postData?.params, request?.postData?.text, request?.queryString])
|
||||
setRequestExpanded(true);
|
||||
}, [addParamsToUrl])
|
||||
|
||||
const onRefreshRequest = useCallback((event) => {
|
||||
event.stopPropagation()
|
||||
resetModel()
|
||||
}, [resetModel])
|
||||
event.stopPropagation();
|
||||
const hostPortInputVal = getHostPortVal();
|
||||
resetModal(getInitialRequestData(), hostPortInputVal, request.path);
|
||||
}, [getHostPortVal, getInitialRequestData, request.path, resetModal])
|
||||
|
||||
|
||||
const sendRequest = useCallback(async () => {
|
||||
setResponse(null)
|
||||
const headersData = headers.reduce((prev, corrent) => {
|
||||
prev[corrent.key] = corrent.value
|
||||
return prev
|
||||
}, {})
|
||||
const buildUrl = `${hostPortInput}${pathInput}`
|
||||
const requestData = { url: buildUrl, headers: headersData, data: postData, method }
|
||||
const headersData = convertArrToKeyValueObject(requestDataModel.headers)
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const requestData = { url: `${hostPortInput}${pathInput}`, headers: headersData, data: requestDataModel.postData, method: requestDataModel.method }
|
||||
const response = await trafficViewerApi.replayRequest(requestData)
|
||||
setResponse(response?.data?.representation)
|
||||
if (response.errorMessage) {
|
||||
@@ -150,7 +161,6 @@ const ReplayRequestModal: React.FC<ReplayRequestModalProps> = ({ isOpen, onClose
|
||||
setRequestExpanded(false)
|
||||
setResponseExpanded(true)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
setRequestExpanded(true)
|
||||
toast.error("Error occurred while fetching response", { containerId: TOAST_CONTAINER_ID });
|
||||
@@ -159,27 +169,37 @@ const ReplayRequestModal: React.FC<ReplayRequestModalProps> = ({ isOpen, onClose
|
||||
finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [hostPortInput, pathInput, requestDataModel.headers, requestDataModel.method, requestDataModel.postData, trafficViewerApi])
|
||||
|
||||
}, [headers, hostPortInput, method, pathInput, postData, trafficViewerApi])
|
||||
const onDownloadRequest = useCallback((e) => {
|
||||
e.stopPropagation()
|
||||
const date = Utils.getNow()
|
||||
Utils.exportToJson(requestDataModel, `${getHostUrl()} - ${date}`)
|
||||
}, [getHostUrl, requestDataModel])
|
||||
|
||||
const onLoadingComplete = useCallback((fileContent: FileContent) => {
|
||||
const requestData = JSON.parse(fileContent.content) as IReplayRequestData
|
||||
resetModal(requestData, requestData.hostPort, requestData.path)
|
||||
}, [resetModal])
|
||||
|
||||
let innerComponent
|
||||
switch (currentTab) {
|
||||
case RequestTabs.Params:
|
||||
innerComponent = <div className={styles.keyValueContainer}><KeyValueTable data={params} onDataChange={onParamsChange} key={"params"} valuePlaceholder="New Param Value" keyPlaceholder="New param Key" /></div>
|
||||
innerComponent = <div className={styles.keyValueContainer}><KeyValueTable data={requestDataModel.params} onDataChange={onParamsChange} key={"params"} valuePlaceholder="New Param Value" keyPlaceholder="New param Key" /></div>
|
||||
break;
|
||||
case RequestTabs.Headers:
|
||||
innerComponent = <Fragment>
|
||||
<div className={styles.keyValueContainer}><KeyValueTable data={headers} onDataChange={(heaedrs) => setHeaders(heaedrs)} key={"Header"} valuePlaceholder="New Headers Value" keyPlaceholder="New Headers Key" />
|
||||
<div className={styles.keyValueContainer}><KeyValueTable data={requestDataModel.headers} onDataChange={(headers) => setRequestData({ ...requestDataModel, headers: headers })} key={"Header"} valuePlaceholder="New Headers Value" keyPlaceholder="New Headers Key" />
|
||||
</div>
|
||||
<span className={styles.note}><b>* </b> X-Mizu Header added to reuqests</span>
|
||||
<span className={styles.note}><b>* </b> X-Mizu Header added to requests</span>
|
||||
</Fragment>
|
||||
break;
|
||||
case RequestTabs.Body:
|
||||
const formatedCode = formatRequestWithOutError(postData || "", request?.postData?.mimeType)
|
||||
const formattedCode = formatRequestWithOutError(requestDataModel.postData || "", request?.postData?.mimeType)
|
||||
innerComponent = <div className={styles.codeEditor}>
|
||||
<CodeEditor language={request?.postData?.mimeType.split("/")[1]}
|
||||
code={Utils.isJson(formatedCode) ? JSON.stringify(JSON.parse(formatedCode || "{}"), null, 2) : formatedCode}
|
||||
onChange={setPostData} />
|
||||
code={Utils.isJson(formattedCode) ? JSON.stringify(JSON.parse(formattedCode || "{}"), null, 2) : formattedCode}
|
||||
onChange={(postData) => setRequestData({ ...requestDataModel, postData })} />
|
||||
</div>
|
||||
break;
|
||||
default:
|
||||
@@ -204,17 +224,43 @@ const ReplayRequestModal: React.FC<ReplayRequestModalProps> = ({ isOpen, onClose
|
||||
<div className={styles.headerContainer}>
|
||||
<div className={styles.headerSection}>
|
||||
<span className={styles.title}>Replay Request</span>
|
||||
<Button style={{ marginLeft: "2%", textTransform: 'unset' }}
|
||||
startIcon={<img src={refreshImg} className="custom" alt="Refresh Request"></img>}
|
||||
size="medium"
|
||||
variant="contained"
|
||||
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
|
||||
onClick={onRefreshRequest}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
<Button style={{ marginLeft: "2%", textTransform: 'unset' }}
|
||||
startIcon={<DownloadIcon className={`custom ${styles.icon}`} />}
|
||||
size="medium"
|
||||
variant="contained"
|
||||
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
|
||||
onClick={onDownloadRequest}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
<FilePicker onLoadingComplete={onLoadingComplete}
|
||||
elem={<Button style={{ marginLeft: "2%", textTransform: 'unset' }}
|
||||
startIcon={<UploadIcon className={`custom ${styles.icon}`} />}
|
||||
size="medium"
|
||||
variant="contained"
|
||||
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}>
|
||||
Upload
|
||||
</Button>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.modalContainer}>
|
||||
<Accordion TransitionProps={{ unmountOnExit: true }} expanded={requestExpanded} onChange={() => setRequestExpanded(!requestExpanded)}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls="response-content">
|
||||
<span className={styles.sectionHeader}>REQUEST</span>
|
||||
<img src={refreshImg} style={{ marginLeft: "10px" }} title="Refresh Reuqest" alt="Refresh Reuqest" onClick={onRefreshRequest} />
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<div className={styles.path}>
|
||||
<select className={styles.select} value={method} onChange={(e) => setMethod(e.target.value)}>
|
||||
<select className={styles.select} value={requestDataModel.method} onChange={(e) => setRequestData({ ...requestDataModel, method: e.target.value })}>
|
||||
{HTTP_METHODS.map(method => <option value={method} key={method}>{method}</option>)}
|
||||
</select>
|
||||
<input placeholder="Host:Port" value={hostPortInput} onChange={(event) => setHostPortInput(event.target.value)} className={`${commonClasses.textField} ${styles.hostPort}`} />
|
||||
@@ -246,7 +292,7 @@ const ReplayRequestModal: React.FC<ReplayRequestModalProps> = ({ isOpen, onClose
|
||||
</div>
|
||||
</Box>
|
||||
</Fade>
|
||||
</Modal>
|
||||
</Modal >
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export interface KeyValuePair {
|
||||
key: string;
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
export interface IReplayRequestData {
|
||||
method: string;
|
||||
hostPort: string;
|
||||
path: string;
|
||||
postData: string;
|
||||
headers: KeyValuePair[]
|
||||
params: KeyValuePair[]
|
||||
}
|
||||
@@ -35,6 +35,7 @@ $modalMargin-from-edge : 35px
|
||||
color: $blue-gray
|
||||
font-weight: 600
|
||||
margin-right: 35px
|
||||
white-space: nowrap
|
||||
|
||||
.graphSection
|
||||
flex: 85%
|
||||
|
||||
@@ -35,7 +35,7 @@ export const TimelineBarChart: React.FC<TimelineBarChartProps> = ({ timeLineBarC
|
||||
})
|
||||
protocolsBarsData.push(newProtocolObj);
|
||||
})
|
||||
const uniqueObjArray = Utils.creatUniqueObjArrayByProp(prtcNames, "name")
|
||||
const uniqueObjArray = Utils.createUniqueObjArrayByProp(prtcNames, "name")
|
||||
setProtocolStats(protocolsBarsData);
|
||||
setProtocolsNamesAndColors(uniqueObjArray);
|
||||
}, [data, timeLineBarChartMode])
|
||||
@@ -57,7 +57,7 @@ export const TimelineBarChart: React.FC<TimelineBarChartProps> = ({ timeLineBarC
|
||||
})
|
||||
protocolsMethods.push(newMethodObj);
|
||||
})
|
||||
const uniqueObjArray = Utils.creatUniqueObjArrayByProp(protocolsMethodsNamesAndColors, "name")
|
||||
const uniqueObjArray = Utils.createUniqueObjArrayByProp(protocolsMethodsNamesAndColors, "name")
|
||||
setMethodsNamesAndColors(uniqueObjArray);
|
||||
setMethodsStats(protocolsMethods);
|
||||
}, [data, timeLineBarChartMode, selectedProtocol])
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import Moment from 'moment';
|
||||
|
||||
const IP_ADDRESS_REGEX = /([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3})(:([0-9]{1,5}))?/
|
||||
|
||||
type JSONValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Object
|
||||
|
||||
|
||||
export class Utils {
|
||||
static isIpAddress = (address: string): boolean => IP_ADDRESS_REGEX.test(address)
|
||||
@@ -51,7 +59,7 @@ export class Utils {
|
||||
return [hoursAndMinutes, newDate].join(' ');
|
||||
}
|
||||
|
||||
static creatUniqueObjArrayByProp = (objArray, prop) => {
|
||||
static createUniqueObjArrayByProp = (objArray, prop) => {
|
||||
const map = new Map(objArray.map((item) => [item[prop], item])).values()
|
||||
return Array.from(map);
|
||||
}
|
||||
@@ -65,4 +73,24 @@ export class Utils {
|
||||
return true;
|
||||
}
|
||||
|
||||
static downloadFile = (data: string, filename: string, fileType: string) => {
|
||||
const blob = new Blob([data], { type: fileType })
|
||||
const a = document.createElement('a');
|
||||
a.href = window.URL.createObjectURL(blob);
|
||||
a.download = filename;
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
static exportToJson = (data: JSONValue, name) => {
|
||||
Utils.downloadFile(JSON.stringify(data), `${name}.json`, 'text/json')
|
||||
}
|
||||
|
||||
static getTimeFormatted = (time: Moment.MomentInput) => {
|
||||
return Moment(time).utc().format('MM/DD/YYYY, h:mm:ss.SSS A')
|
||||
}
|
||||
|
||||
static getNow = (format: string = 'MM/DD/YYYY, HH:mm:ss.SSS') => {
|
||||
return Moment().format(format)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user