mirror of
https://github.com/kubeshark/kubeshark.git
synced 2025-08-23 02:48:56 +00:00
Merge branch 'develop' into feature/multiarch_build
This commit is contained in:
commit
796457d876
15
.github/workflows/pr_validation.yml
vendored
15
.github/workflows/pr_validation.yml
vendored
@ -44,3 +44,18 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Agent
|
- name: Build Agent
|
||||||
run: make agent
|
run: make agent
|
||||||
|
|
||||||
|
build-ui:
|
||||||
|
name: Build UI
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Set up Node 14
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '14'
|
||||||
|
|
||||||
|
- name: Check out code into the Go module directory
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Build UI
|
||||||
|
run: make ui
|
||||||
|
12
README.md
12
README.md
@ -1,5 +1,17 @@
|
|||||||

|

|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/up9inc/mizu/releases/latest">
|
||||||
|
<img alt="GitHub Latest Release" src="https://img.shields.io/github/v/release/up9inc/mizu?logo=GitHub&style=flat-square">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/up9inc/mizu/blob/main/LICENSE">
|
||||||
|
<img alt="GitHub License" src="https://img.shields.io/github/license/up9inc/mizu?logo=GitHub&style=flat-square">
|
||||||
|
</a>
|
||||||
|
<a href="https://join.slack.com/t/up9/shared_invite/zt-tfjnduli-QzlR8VV4Z1w3YnPIAJfhlQ">
|
||||||
|
<img alt="Slack" src="https://img.shields.io/badge/slack-join_chat-white.svg?logo=slack&style=social">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
# The API Traffic Viewer for Kubernetes
|
# The API Traffic Viewer for Kubernetes
|
||||||
|
|
||||||
A simple-yet-powerful API traffic viewer for Kubernetes enabling you to view all API communication between microservices to help your debug and troubleshoot regressions.
|
A simple-yet-powerful API traffic viewer for Kubernetes enabling you to view all API communication between microservices to help your debug and troubleshoot regressions.
|
||||||
|
@ -4,11 +4,17 @@
|
|||||||
"viewportHeight": 1080,
|
"viewportHeight": 1080,
|
||||||
"video": false,
|
"video": false,
|
||||||
"screenshotOnRunFailure": false,
|
"screenshotOnRunFailure": false,
|
||||||
|
|
||||||
"testFiles":
|
"testFiles":
|
||||||
["tests/GuiPort.js",
|
["tests/GuiPort.js",
|
||||||
"tests/MultipleNamespaces.js",
|
"tests/MultipleNamespaces.js",
|
||||||
|
"tests/Redact.js",
|
||||||
|
"tests/NoRedact.js",
|
||||||
"tests/Regex.js"],
|
"tests/Regex.js"],
|
||||||
|
|
||||||
"env": {
|
"env": {
|
||||||
"testUrl": "http://localhost:8899/"
|
"testUrl": "http://localhost:8899/",
|
||||||
|
"redactHeaderContent": "User-Header[REDACTED]",
|
||||||
|
"redactBodyContent": "{ \"User\": \"[REDACTED]\" }"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
export function isValueExistsInElement(shouldInclude, content, domPathToContainer){
|
||||||
|
it(`should ${shouldInclude ? '' : 'not'} include '${content}'`, function () {
|
||||||
|
cy.get(domPathToContainer).then(htmlText => {
|
||||||
|
const allTextString = htmlText.text();
|
||||||
|
if (allTextString.includes(content) !== shouldInclude)
|
||||||
|
throw new Error(`One of the containers part contains ${content}`)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import {findLineAndCheck, getExpectedDetailsDict} from '../page_objects/StatusBar';
|
import {findLineAndCheck, getExpectedDetailsDict} from '../testHelpers/StatusBarHelper';
|
||||||
|
|
||||||
it('opening', function () {
|
it('opening', function () {
|
||||||
cy.visit(Cypress.env('testUrl'));
|
cy.visit(Cypress.env('testUrl'));
|
||||||
|
8
acceptanceTests/cypress/integration/tests/NoRedact.js
Normal file
8
acceptanceTests/cypress/integration/tests/NoRedact.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import {isValueExistsInElement} from '../testHelpers/TrafficHelper';
|
||||||
|
|
||||||
|
it('Loading Mizu', function () {
|
||||||
|
cy.visit(Cypress.env('testUrl'));
|
||||||
|
})
|
||||||
|
|
||||||
|
isValueExistsInElement(false, Cypress.env('redactHeaderContent'), '#tbody-Headers');
|
||||||
|
isValueExistsInElement(false, Cypress.env('redactBodyContent'), '.hljs');
|
8
acceptanceTests/cypress/integration/tests/Redact.js
Normal file
8
acceptanceTests/cypress/integration/tests/Redact.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import {isValueExistsInElement} from '../testHelpers/TrafficHelper';
|
||||||
|
|
||||||
|
it('Loading Mizu', function () {
|
||||||
|
cy.visit(Cypress.env('testUrl'));
|
||||||
|
})
|
||||||
|
|
||||||
|
isValueExistsInElement(true, Cypress.env('redactHeaderContent'), '#tbody-Headers');
|
||||||
|
isValueExistsInElement(true, Cypress.env('redactBodyContent'), '.hljs');
|
@ -1,4 +1,4 @@
|
|||||||
import {getExpectedDetailsDict, checkLine} from '../page_objects/StatusBar';
|
import {getExpectedDetailsDict, checkLine} from '../testHelpers/StatusBarHelper';
|
||||||
|
|
||||||
|
|
||||||
it('opening', function () {
|
it('opening', function () {
|
||||||
|
@ -3,7 +3,6 @@ package acceptanceTests
|
|||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -378,59 +377,7 @@ func TestTapRedact(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
redactCheckFunc := func() error {
|
runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/Redact.js\""))
|
||||||
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
|
|
||||||
|
|
||||||
entries, err := getDBEntries(timestamp, defaultEntriesCount, 1*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = checkEntriesAtLeast(entries, 1)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
firstEntry := entries[0]
|
|
||||||
|
|
||||||
entryUrl := fmt.Sprintf("%v/entries/%v", apiServerUrl, firstEntry["id"])
|
|
||||||
requestResult, requestErr := executeHttpGetRequest(entryUrl)
|
|
||||||
if requestErr != nil {
|
|
||||||
return fmt.Errorf("failed to get entry, err: %v", requestErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry := requestResult.(map[string]interface{})["data"].(map[string]interface{})
|
|
||||||
request := entry["request"].(map[string]interface{})
|
|
||||||
|
|
||||||
headers := request["_headers"].([]interface{})
|
|
||||||
for _, headerInterface := range headers {
|
|
||||||
header := headerInterface.(map[string]interface{})
|
|
||||||
if header["name"].(string) != "User-Header" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
userHeader := header["value"].(string)
|
|
||||||
if userHeader != "[REDACTED]" {
|
|
||||||
return fmt.Errorf("unexpected result - user agent is not redacted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
postData := request["postData"].(map[string]interface{})
|
|
||||||
textDataStr := postData["text"].(string)
|
|
||||||
|
|
||||||
var textData map[string]string
|
|
||||||
if parseErr := json.Unmarshal([]byte(textDataStr), &textData); parseErr != nil {
|
|
||||||
return fmt.Errorf("failed to parse text data, err: %v", parseErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if textData["User"] != "[REDACTED]" {
|
|
||||||
return fmt.Errorf("unexpected result - user in body is not redacted")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := retriesExecute(shortRetriesCount, redactCheckFunc); err != nil {
|
|
||||||
t.Errorf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTapNoRedact(t *testing.T) {
|
func TestTapNoRedact(t *testing.T) {
|
||||||
@ -482,59 +429,7 @@ func TestTapNoRedact(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
redactCheckFunc := func() error {
|
runCypressTests(t, "npx cypress run --spec \"cypress/integration/tests/NoRedact.js\"")
|
||||||
timestamp := time.Now().UnixNano() / int64(time.Millisecond)
|
|
||||||
|
|
||||||
entries, err := getDBEntries(timestamp, defaultEntriesCount, 1*time.Second)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = checkEntriesAtLeast(entries, 1)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
firstEntry := entries[0]
|
|
||||||
|
|
||||||
entryUrl := fmt.Sprintf("%v/entries/%v", apiServerUrl, firstEntry["id"])
|
|
||||||
requestResult, requestErr := executeHttpGetRequest(entryUrl)
|
|
||||||
if requestErr != nil {
|
|
||||||
return fmt.Errorf("failed to get entry, err: %v", requestErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
entry := requestResult.(map[string]interface{})["data"].(map[string]interface{})
|
|
||||||
request := entry["request"].(map[string]interface{})
|
|
||||||
|
|
||||||
headers := request["_headers"].([]interface{})
|
|
||||||
for _, headerInterface := range headers {
|
|
||||||
header := headerInterface.(map[string]interface{})
|
|
||||||
if header["name"].(string) != "User-Header" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
userHeader := header["value"].(string)
|
|
||||||
if userHeader == "[REDACTED]" {
|
|
||||||
return fmt.Errorf("unexpected result - user agent is redacted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
postData := request["postData"].(map[string]interface{})
|
|
||||||
textDataStr := postData["text"].(string)
|
|
||||||
|
|
||||||
var textData map[string]string
|
|
||||||
if parseErr := json.Unmarshal([]byte(textDataStr), &textData); parseErr != nil {
|
|
||||||
return fmt.Errorf("failed to parse text data, err: %v", parseErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if textData["User"] == "[REDACTED]" {
|
|
||||||
return fmt.Errorf("unexpected result - user in body is redacted")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := retriesExecute(shortRetriesCount, redactCheckFunc); err != nil {
|
|
||||||
t.Errorf("%v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTapRegexMasking(t *testing.T) {
|
func TestTapRegexMasking(t *testing.T) {
|
||||||
|
@ -128,7 +128,7 @@ func main() {
|
|||||||
syncEntriesConfig := getSyncEntriesConfig()
|
syncEntriesConfig := getSyncEntriesConfig()
|
||||||
if syncEntriesConfig != nil {
|
if syncEntriesConfig != nil {
|
||||||
if err := up9.SyncEntries(syncEntriesConfig); err != nil {
|
if err := up9.SyncEntries(syncEntriesConfig); err != nil {
|
||||||
panic(fmt.Sprintf("Error syncing entries, err: %v", err))
|
logger.Log.Error("Error syncing entries, err: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# Mizu release _SEM_VER_
|
# Mizu release _SEM_VER_
|
||||||
|
Full changelog for stable release see in [docs](https://github.com/up9inc/mizu/blob/main/docs/CHANGELOG.md)
|
||||||
|
|
||||||
Download Mizu for your platform
|
## Download Mizu for your platform
|
||||||
|
|
||||||
**Mac** (Intel)
|
**Mac** (Intel)
|
||||||
```
|
```
|
||||||
|
@ -101,7 +101,8 @@ func watchApiServerPodReady(ctx context.Context, kubernetesProvider *kubernetes.
|
|||||||
podWatchHelper := kubernetes.NewPodWatchHelper(kubernetesProvider, podExactRegex)
|
podWatchHelper := kubernetes.NewPodWatchHelper(kubernetesProvider, podExactRegex)
|
||||||
eventChan, errorChan := kubernetes.FilteredWatch(ctx, podWatchHelper, []string{config.Config.MizuResourcesNamespace}, podWatchHelper)
|
eventChan, errorChan := kubernetes.FilteredWatch(ctx, podWatchHelper, []string{config.Config.MizuResourcesNamespace}, podWatchHelper)
|
||||||
|
|
||||||
timeAfter := time.After(1 * time.Minute)
|
apiServerTimeoutSec := config.GetIntEnvConfig(config.ApiServerTimeoutSec, 120)
|
||||||
|
timeAfter := time.After(time.Duration(apiServerTimeoutSec) * time.Second)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case wEvent, ok := <-eventChan:
|
case wEvent, ok := <-eventChan:
|
||||||
|
@ -306,7 +306,9 @@ func watchApiServerPod(ctx context.Context, kubernetesProvider *kubernetes.Provi
|
|||||||
podWatchHelper := kubernetes.NewPodWatchHelper(kubernetesProvider, podExactRegex)
|
podWatchHelper := kubernetes.NewPodWatchHelper(kubernetesProvider, podExactRegex)
|
||||||
eventChan, errorChan := kubernetes.FilteredWatch(ctx, podWatchHelper, []string{config.Config.MizuResourcesNamespace}, podWatchHelper)
|
eventChan, errorChan := kubernetes.FilteredWatch(ctx, podWatchHelper, []string{config.Config.MizuResourcesNamespace}, podWatchHelper)
|
||||||
isPodReady := false
|
isPodReady := false
|
||||||
timeAfter := time.After(25 * time.Second)
|
|
||||||
|
apiServerTimeoutSec := config.GetIntEnvConfig(config.ApiServerTimeoutSec, 120)
|
||||||
|
timeAfter := time.After(time.Duration(apiServerTimeoutSec) * time.Second)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case wEvent, ok := <-eventChan:
|
case wEvent, ok := <-eventChan:
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
ApiServerRetries = "API_SERVER_RETRIES"
|
ApiServerRetries = "API_SERVER_RETRIES"
|
||||||
|
ApiServerTimeoutSec = "API_SERVER_TIMEOUT_SEC"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetIntEnvConfig(key string, defaultValue int) int {
|
func GetIntEnvConfig(key string, defaultValue int) int {
|
||||||
|
43
docs/CHANGELOG.md
Normal file
43
docs/CHANGELOG.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# CHANGELOG
|
||||||
|
This document summarizes main and fixes changes published in stable (aka `main`) branch of this project.
|
||||||
|
Ongoing work and development releases are under `develop` branch.
|
||||||
|
|
||||||
|
## 0.22.0
|
||||||
|
|
||||||
|
### main features
|
||||||
|
* Service Mesh support -- mizu is now capable to tap mTLS traffic between pods connected by Istio service mesh
|
||||||
|
* Use `--service-mesh` option to enable this feature
|
||||||
|
* New installation option - have the same Mizu functionality as long living pods in your cluster, with password protection
|
||||||
|
* To install use `mizu install` command
|
||||||
|
* To access use `mizu view` or `kubectl -n mizu port-forward svc/mizu-api-server`
|
||||||
|
* To uninstall run `mizu clean`
|
||||||
|
* At first login
|
||||||
|
* Set admin password as prompted, use it to login to mizu later on.
|
||||||
|
* After login, user should select cluster namespaces to tap: by default all namespaces in the cluster are selected, user can select/unselect according to their needs. These settings are retained and can be modified at any time via Settings menu (cog icon on the top-right)
|
||||||
|
|
||||||
|
|
||||||
|
### improvements
|
||||||
|
* improved Mizu permissions/roles logic to support clusters with strict PodSecurityPolicy (PSP) -- see [PERMISSIONS](PERMISSIONS.md) doc for more details
|
||||||
|
|
||||||
|
### notable bug fixes
|
||||||
|
* mizu now works properly when API service is exposed via HTTPS url
|
||||||
|
* mizu now properly displays KAFKA message body
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 0.21.0
|
||||||
|
|
||||||
|
### main features
|
||||||
|
* New traffic search & stream exprience
|
||||||
|
* Rich query language with full-text search capabilities on headers & body
|
||||||
|
* Distinct live-streaming vs paging/browsing modes, all with filter applied
|
||||||
|
|
||||||
|
### improvements
|
||||||
|
* GUI - source and destination IP addresses & service names for each traffic item
|
||||||
|
* GUI - Mizu health - display warning sign in top bar when not all requested pods are successfully tapped
|
||||||
|
* GUI - pod tapping status reflected in the list (ok or problem)
|
||||||
|
* Mizu telemetry - report platform type
|
||||||
|
|
||||||
|
### fixes
|
||||||
|
* Request duration and body size properly shown in GUI (instead of -1)
|
@ -3,14 +3,15 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/up9inc/mizu/tap/api"
|
"github.com/up9inc/mizu/tap/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func mapSliceRebuildAsMap(mapSlice []interface{}) (newMap map[string]interface{}) {
|
func mapSliceRebuildAsMap(mapSlice []interface{}) (newMap map[string]interface{}) {
|
||||||
newMap = make(map[string]interface{})
|
newMap = make(map[string]interface{})
|
||||||
for _, header := range mapSlice {
|
for _, item := range mapSlice {
|
||||||
h := header.(map[string]interface{})
|
h := item.(map[string]interface{})
|
||||||
newMap[h["name"].(string)] = h["value"]
|
newMap[h["name"].(string)] = h["value"]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,8 +20,8 @@ func mapSliceRebuildAsMap(mapSlice []interface{}) (newMap map[string]interface{}
|
|||||||
|
|
||||||
func representMapSliceAsTable(mapSlice []interface{}, selectorPrefix string) (representation string) {
|
func representMapSliceAsTable(mapSlice []interface{}, selectorPrefix string) (representation string) {
|
||||||
var table []api.TableData
|
var table []api.TableData
|
||||||
for _, header := range mapSlice {
|
for _, item := range mapSlice {
|
||||||
h := header.(map[string]interface{})
|
h := item.(map[string]interface{})
|
||||||
selector := fmt.Sprintf("%s[\"%s\"]", selectorPrefix, h["name"].(string))
|
selector := fmt.Sprintf("%s[\"%s\"]", selectorPrefix, h["name"].(string))
|
||||||
table = append(table, api.TableData{
|
table = append(table, api.TableData{
|
||||||
Name: h["name"].(string),
|
Name: h["name"].(string),
|
||||||
@ -33,3 +34,19 @@ func representMapSliceAsTable(mapSlice []interface{}, selectorPrefix string) (re
|
|||||||
representation = string(obj)
|
representation = string(obj)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func representSliceAsTable(slice []interface{}, selectorPrefix string) (representation string) {
|
||||||
|
var table []api.TableData
|
||||||
|
for i, item := range slice {
|
||||||
|
selector := fmt.Sprintf("%s[%d]", selectorPrefix, i)
|
||||||
|
table = append(table, api.TableData{
|
||||||
|
Name: strconv.Itoa(i),
|
||||||
|
Value: item.(interface{}),
|
||||||
|
Selector: selector,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
obj, _ := json.Marshal(table)
|
||||||
|
representation = string(obj)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/up9inc/mizu/tap/api"
|
"github.com/up9inc/mizu/tap/api"
|
||||||
@ -209,6 +210,7 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
|
|||||||
request["url"] = reqDetails["url"].(string)
|
request["url"] = reqDetails["url"].(string)
|
||||||
reqDetails["targetUri"] = reqDetails["url"]
|
reqDetails["targetUri"] = reqDetails["url"]
|
||||||
reqDetails["path"] = path
|
reqDetails["path"] = path
|
||||||
|
reqDetails["pathSegments"] = strings.Split(path, "/")[1:]
|
||||||
reqDetails["summary"] = path
|
reqDetails["summary"] = path
|
||||||
|
|
||||||
// Rearrange the maps for the querying
|
// Rearrange the maps for the querying
|
||||||
@ -296,6 +298,15 @@ func representRequest(request map[string]interface{}) (repRequest []interface{})
|
|||||||
Data: string(details),
|
Data: string(details),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
pathSegments := request["pathSegments"].([]interface{})
|
||||||
|
if len(pathSegments) > 1 {
|
||||||
|
repRequest = append(repRequest, api.SectionData{
|
||||||
|
Type: api.TABLE,
|
||||||
|
Title: "Path Segments",
|
||||||
|
Data: representSliceAsTable(pathSegments, `request.pathSegments`),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
repRequest = append(repRequest, api.SectionData{
|
repRequest = append(repRequest, api.SectionData{
|
||||||
Type: api.TABLE,
|
Type: api.TABLE,
|
||||||
Title: "Headers",
|
Title: "Headers",
|
||||||
|
@ -3,6 +3,8 @@ module github.com/up9inc/mizu/tap/extensions/kafka
|
|||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/fatih/camelcase v1.0.0
|
||||||
|
github.com/ohler55/ojg v1.12.12
|
||||||
github.com/segmentio/kafka-go v0.4.17
|
github.com/segmentio/kafka-go v0.4.17
|
||||||
github.com/up9inc/mizu/tap/api v0.0.0
|
github.com/up9inc/mizu/tap/api v0.0.0
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw=
|
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw=
|
||||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||||
|
github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
|
||||||
|
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
|
||||||
github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY=
|
github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY=
|
||||||
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
||||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||||
@ -16,6 +18,8 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
|
|||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/ohler55/ojg v1.12.12 h1:hepbQFn7GHAecTPmwS3j5dCiOLsOpzPLvhiqnlAVAoE=
|
||||||
|
github.com/ohler55/ojg v1.12.12/go.mod h1:LBbIVRAgoFbYBXQhRhuEpaJIqq+goSO63/FQ+nyJU88=
|
||||||
github.com/pierrec/lz4 v2.6.0+incompatible h1:Ix9yFKn1nSPBLFl/yZknTp8TU5G4Ps0JDmguYK6iH1A=
|
github.com/pierrec/lz4 v2.6.0+incompatible h1:Ix9yFKn1nSPBLFl/yZknTp8TU5G4Ps0JDmguYK6iH1A=
|
||||||
github.com/pierrec/lz4 v2.6.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
github.com/pierrec/lz4 v2.6.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
@ -3,8 +3,13 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fatih/camelcase"
|
||||||
|
"github.com/ohler55/ojg/jp"
|
||||||
|
"github.com/ohler55/ojg/oj"
|
||||||
"github.com/up9inc/mizu/tap/api"
|
"github.com/up9inc/mizu/tap/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -289,17 +294,12 @@ func representProduceRequest(data map[string]interface{}) []interface{} {
|
|||||||
rep = representRequestHeader(data, rep)
|
rep = representRequestHeader(data, rep)
|
||||||
|
|
||||||
payload := data["payload"].(map[string]interface{})
|
payload := data["payload"].(map[string]interface{})
|
||||||
topicData := ""
|
topicData := payload["topicData"]
|
||||||
_topicData := payload["topicData"]
|
|
||||||
if _topicData != nil {
|
|
||||||
x, _ := json.Marshal(_topicData.([]interface{}))
|
|
||||||
topicData = string(x)
|
|
||||||
}
|
|
||||||
transactionalID := ""
|
transactionalID := ""
|
||||||
if payload["transactionalID"] != nil {
|
if payload["transactionalID"] != nil {
|
||||||
transactionalID = payload["transactionalID"].(string)
|
transactionalID = payload["transactionalID"].(string)
|
||||||
}
|
}
|
||||||
repPayload, _ := json.Marshal([]api.TableData{
|
repTransactionDetails, _ := json.Marshal([]api.TableData{
|
||||||
{
|
{
|
||||||
Name: "Transactional ID",
|
Name: "Transactional ID",
|
||||||
Value: transactionalID,
|
Value: transactionalID,
|
||||||
@ -315,18 +315,73 @@ func representProduceRequest(data map[string]interface{}) []interface{} {
|
|||||||
Value: fmt.Sprintf("%d", int(payload["timeout"].(float64))),
|
Value: fmt.Sprintf("%d", int(payload["timeout"].(float64))),
|
||||||
Selector: `request.payload.timeout`,
|
Selector: `request.payload.timeout`,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "Topic Data",
|
|
||||||
Value: topicData,
|
|
||||||
Selector: `request.payload.topicData`,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
rep = append(rep, api.SectionData{
|
rep = append(rep, api.SectionData{
|
||||||
Type: api.TABLE,
|
Type: api.TABLE,
|
||||||
Title: "Payload",
|
Title: "Transaction Details",
|
||||||
Data: string(repPayload),
|
Data: string(repTransactionDetails),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if topicData != nil {
|
||||||
|
for _, _topic := range topicData.([]interface{}) {
|
||||||
|
topic := _topic.(map[string]interface{})
|
||||||
|
topicName := topic["topic"].(string)
|
||||||
|
partitions := topic["partitions"].(map[string]interface{})
|
||||||
|
partitionsJson, err := json.Marshal(partitions)
|
||||||
|
if err != nil {
|
||||||
|
return rep
|
||||||
|
}
|
||||||
|
|
||||||
|
repPartitions, _ := json.Marshal([]api.TableData{
|
||||||
|
{
|
||||||
|
Name: "Length",
|
||||||
|
Value: partitions["length"],
|
||||||
|
Selector: `request.payload.transactionalID`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
rep = append(rep, api.SectionData{
|
||||||
|
Type: api.TABLE,
|
||||||
|
Title: fmt.Sprintf("Partitions (topic: %s)", topicName),
|
||||||
|
Data: string(repPartitions),
|
||||||
|
})
|
||||||
|
|
||||||
|
obj, err := oj.ParseString(string(partitionsJson))
|
||||||
|
recordBatchPath, err := jp.ParseString(`partitionData.records.recordBatch`)
|
||||||
|
recordBatchresults := recordBatchPath.Get(obj)
|
||||||
|
if len(recordBatchresults) > 0 {
|
||||||
|
rep = append(rep, api.SectionData{
|
||||||
|
Type: api.TABLE,
|
||||||
|
Title: fmt.Sprintf("Record Batch (topic: %s)", topicName),
|
||||||
|
Data: representMapAsTable(recordBatchresults[0].(map[string]interface{}), `request.payload.topicData.partitions.partitionData.records.recordBatch`, []string{"record"}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
recordsPath, err := jp.ParseString(`partitionData.records.recordBatch.record`)
|
||||||
|
recordsResults := recordsPath.Get(obj)
|
||||||
|
if len(recordsResults) > 0 {
|
||||||
|
records := recordsResults[0].([]interface{})
|
||||||
|
for i, _record := range records {
|
||||||
|
record := _record.(map[string]interface{})
|
||||||
|
value := record["value"]
|
||||||
|
delete(record, "value")
|
||||||
|
|
||||||
|
rep = append(rep, api.SectionData{
|
||||||
|
Type: api.TABLE,
|
||||||
|
Title: fmt.Sprintf("Record [%d] Details (topic: %s)", i, topicName),
|
||||||
|
Data: representMapAsTable(record, fmt.Sprintf(`request.payload.topicData.partitions.partitionData.records.recordBatch.record[%d]`, i), []string{"value"}),
|
||||||
|
})
|
||||||
|
|
||||||
|
rep = append(rep, api.SectionData{
|
||||||
|
Type: api.BODY,
|
||||||
|
Title: fmt.Sprintf("Record [%d] Value", i),
|
||||||
|
Data: value.(string),
|
||||||
|
Selector: fmt.Sprintf(`request.payload.topicData.partitions.partitionData.records.recordBatch.record[%d].value`, i),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return rep
|
return rep
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,21 +391,12 @@ func representProduceResponse(data map[string]interface{}) []interface{} {
|
|||||||
rep = representResponseHeader(data, rep)
|
rep = representResponseHeader(data, rep)
|
||||||
|
|
||||||
payload := data["payload"].(map[string]interface{})
|
payload := data["payload"].(map[string]interface{})
|
||||||
responses := ""
|
responses := payload["responses"]
|
||||||
if payload["responses"] != nil {
|
|
||||||
_responses, _ := json.Marshal(payload["responses"].([]interface{}))
|
|
||||||
responses = string(_responses)
|
|
||||||
}
|
|
||||||
throttleTimeMs := ""
|
throttleTimeMs := ""
|
||||||
if payload["throttleTimeMs"] != nil {
|
if payload["throttleTimeMs"] != nil {
|
||||||
throttleTimeMs = fmt.Sprintf("%d", int(payload["throttleTimeMs"].(float64)))
|
throttleTimeMs = fmt.Sprintf("%d", int(payload["throttleTimeMs"].(float64)))
|
||||||
}
|
}
|
||||||
repPayload, _ := json.Marshal([]api.TableData{
|
repPayload, _ := json.Marshal([]api.TableData{
|
||||||
{
|
|
||||||
Name: "Responses",
|
|
||||||
Value: string(responses),
|
|
||||||
Selector: `response.payload.responses`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Name: "Throttle Time (ms)",
|
Name: "Throttle Time (ms)",
|
||||||
Value: throttleTimeMs,
|
Value: throttleTimeMs,
|
||||||
@ -359,10 +405,31 @@ func representProduceResponse(data map[string]interface{}) []interface{} {
|
|||||||
})
|
})
|
||||||
rep = append(rep, api.SectionData{
|
rep = append(rep, api.SectionData{
|
||||||
Type: api.TABLE,
|
Type: api.TABLE,
|
||||||
Title: "Payload",
|
Title: "Transaction Details",
|
||||||
Data: string(repPayload),
|
Data: string(repPayload),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if responses != nil {
|
||||||
|
for i, _response := range responses.([]interface{}) {
|
||||||
|
response := _response.(map[string]interface{})
|
||||||
|
|
||||||
|
rep = append(rep, api.SectionData{
|
||||||
|
Type: api.TABLE,
|
||||||
|
Title: fmt.Sprintf("Response [%d]", i),
|
||||||
|
Data: representMapAsTable(response, fmt.Sprintf(`response.payload.responses[%d]`, i), []string{"partitionResponses"}),
|
||||||
|
})
|
||||||
|
|
||||||
|
for j, _partitionResponse := range response["partitionResponses"].([]interface{}) {
|
||||||
|
partitionResponse := _partitionResponse.(map[string]interface{})
|
||||||
|
rep = append(rep, api.SectionData{
|
||||||
|
Type: api.TABLE,
|
||||||
|
Title: fmt.Sprintf("Response [%d] Partition Response [%d]", i, j),
|
||||||
|
Data: representMapAsTable(partitionResponse, fmt.Sprintf(`response.payload.responses[%d].partitionResponses[%d]`, i, j), []string{}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return rep
|
return rep
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -372,11 +439,7 @@ func representFetchRequest(data map[string]interface{}) []interface{} {
|
|||||||
rep = representRequestHeader(data, rep)
|
rep = representRequestHeader(data, rep)
|
||||||
|
|
||||||
payload := data["payload"].(map[string]interface{})
|
payload := data["payload"].(map[string]interface{})
|
||||||
topics := ""
|
topics := payload["topics"]
|
||||||
if payload["topics"] != nil {
|
|
||||||
_topics, _ := json.Marshal(payload["topics"].([]interface{}))
|
|
||||||
topics = string(_topics)
|
|
||||||
}
|
|
||||||
replicaId := ""
|
replicaId := ""
|
||||||
if payload["replicaId"] != nil {
|
if payload["replicaId"] != nil {
|
||||||
replicaId = fmt.Sprintf("%d", int(payload["replicaId"].(float64)))
|
replicaId = fmt.Sprintf("%d", int(payload["replicaId"].(float64)))
|
||||||
@ -442,11 +505,6 @@ func representFetchRequest(data map[string]interface{}) []interface{} {
|
|||||||
Value: sessionEpoch,
|
Value: sessionEpoch,
|
||||||
Selector: `request.payload.sessionEpoch`,
|
Selector: `request.payload.sessionEpoch`,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "Topics",
|
|
||||||
Value: topics,
|
|
||||||
Selector: `request.payload.topics`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Name: "Forgotten Topics Data",
|
Name: "Forgotten Topics Data",
|
||||||
Value: forgottenTopicsData,
|
Value: forgottenTopicsData,
|
||||||
@ -460,10 +518,26 @@ func representFetchRequest(data map[string]interface{}) []interface{} {
|
|||||||
})
|
})
|
||||||
rep = append(rep, api.SectionData{
|
rep = append(rep, api.SectionData{
|
||||||
Type: api.TABLE,
|
Type: api.TABLE,
|
||||||
Title: "Payload",
|
Title: "Transaction Details",
|
||||||
Data: string(repPayload),
|
Data: string(repPayload),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if topics != nil {
|
||||||
|
for i, _topic := range topics.([]interface{}) {
|
||||||
|
topic := _topic.(map[string]interface{})
|
||||||
|
topicName := topic["topic"].(string)
|
||||||
|
for j, _partition := range topic["partitions"].([]interface{}) {
|
||||||
|
partition := _partition.(map[string]interface{})
|
||||||
|
|
||||||
|
rep = append(rep, api.SectionData{
|
||||||
|
Type: api.TABLE,
|
||||||
|
Title: fmt.Sprintf("Partition [%d] (topic: %s)", j, topicName),
|
||||||
|
Data: representMapAsTable(partition, fmt.Sprintf(`request.payload.topics[%d].partitions[%d]`, i, j), []string{}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return rep
|
return rep
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -473,11 +547,7 @@ func representFetchResponse(data map[string]interface{}) []interface{} {
|
|||||||
rep = representResponseHeader(data, rep)
|
rep = representResponseHeader(data, rep)
|
||||||
|
|
||||||
payload := data["payload"].(map[string]interface{})
|
payload := data["payload"].(map[string]interface{})
|
||||||
responses := ""
|
responses := payload["responses"]
|
||||||
if payload["responses"] != nil {
|
|
||||||
_responses, _ := json.Marshal(payload["responses"].([]interface{}))
|
|
||||||
responses = string(_responses)
|
|
||||||
}
|
|
||||||
throttleTimeMs := ""
|
throttleTimeMs := ""
|
||||||
if payload["throttleTimeMs"] != nil {
|
if payload["throttleTimeMs"] != nil {
|
||||||
throttleTimeMs = fmt.Sprintf("%d", int(payload["throttleTimeMs"].(float64)))
|
throttleTimeMs = fmt.Sprintf("%d", int(payload["throttleTimeMs"].(float64)))
|
||||||
@ -506,18 +576,56 @@ func representFetchResponse(data map[string]interface{}) []interface{} {
|
|||||||
Value: sessionId,
|
Value: sessionId,
|
||||||
Selector: `response.payload.sessionId`,
|
Selector: `response.payload.sessionId`,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "Responses",
|
|
||||||
Value: responses,
|
|
||||||
Selector: `response.payload.responses`,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
rep = append(rep, api.SectionData{
|
rep = append(rep, api.SectionData{
|
||||||
Type: api.TABLE,
|
Type: api.TABLE,
|
||||||
Title: "Payload",
|
Title: "Transaction Details",
|
||||||
Data: string(repPayload),
|
Data: string(repPayload),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if responses != nil {
|
||||||
|
for i, _response := range responses.([]interface{}) {
|
||||||
|
response := _response.(map[string]interface{})
|
||||||
|
topicName := response["topic"].(string)
|
||||||
|
|
||||||
|
for j, _partitionResponse := range response["partitionResponses"].([]interface{}) {
|
||||||
|
partitionResponse := _partitionResponse.(map[string]interface{})
|
||||||
|
recordSet := partitionResponse["recordSet"].(map[string]interface{})
|
||||||
|
|
||||||
|
rep = append(rep, api.SectionData{
|
||||||
|
Type: api.TABLE,
|
||||||
|
Title: fmt.Sprintf("Response [%d] Partition Response [%d] (topic: %s)", i, j, topicName),
|
||||||
|
Data: representMapAsTable(partitionResponse, fmt.Sprintf(`response.payload.responses[%d].partitionResponses[%d]`, i, j), []string{"recordSet"}),
|
||||||
|
})
|
||||||
|
|
||||||
|
recordBatch := recordSet["recordBatch"].(map[string]interface{})
|
||||||
|
rep = append(rep, api.SectionData{
|
||||||
|
Type: api.TABLE,
|
||||||
|
Title: fmt.Sprintf("Response [%d] Partition Response [%d] Record Batch (topic: %s)", i, j, topicName),
|
||||||
|
Data: representMapAsTable(recordBatch, fmt.Sprintf(`response.payload.responses[%d].partitionResponses[%d].recordSet.recordBatch`, i, j), []string{"record"}),
|
||||||
|
})
|
||||||
|
|
||||||
|
for k, _record := range recordBatch["record"].([]interface{}) {
|
||||||
|
record := _record.(map[string]interface{})
|
||||||
|
value := record["value"]
|
||||||
|
|
||||||
|
rep = append(rep, api.SectionData{
|
||||||
|
Type: api.TABLE,
|
||||||
|
Title: fmt.Sprintf("Response [%d] Partition Response [%d] Record [%d] (topic: %s)", i, j, k, topicName),
|
||||||
|
Data: representMapAsTable(record, fmt.Sprintf(`response.payload.responses[%d].partitionResponses[%d].recordSet.recordBatch.record[%d]`, i, j, k), []string{"value"}),
|
||||||
|
})
|
||||||
|
|
||||||
|
rep = append(rep, api.SectionData{
|
||||||
|
Type: api.BODY,
|
||||||
|
Title: fmt.Sprintf("Response [%d] Partition Response [%d] Record [%d] Value (topic: %s)", i, j, k, topicName),
|
||||||
|
Data: value.(string),
|
||||||
|
Selector: fmt.Sprintf(`response.payload.responses[%d].partitionResponses[%d].recordSet.recordBatch.record[%d].value`, i, j, k),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return rep
|
return rep
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -591,17 +699,11 @@ func representCreateTopicsRequest(data map[string]interface{}) []interface{} {
|
|||||||
rep = representRequestHeader(data, rep)
|
rep = representRequestHeader(data, rep)
|
||||||
|
|
||||||
payload := data["payload"].(map[string]interface{})
|
payload := data["payload"].(map[string]interface{})
|
||||||
topics, _ := json.Marshal(payload["topics"].([]interface{}))
|
|
||||||
validateOnly := ""
|
validateOnly := ""
|
||||||
if payload["validateOnly"] != nil {
|
if payload["validateOnly"] != nil {
|
||||||
validateOnly = strconv.FormatBool(payload["validateOnly"].(bool))
|
validateOnly = strconv.FormatBool(payload["validateOnly"].(bool))
|
||||||
}
|
}
|
||||||
repPayload, _ := json.Marshal([]api.TableData{
|
repPayload, _ := json.Marshal([]api.TableData{
|
||||||
{
|
|
||||||
Name: "Topics",
|
|
||||||
Value: string(topics),
|
|
||||||
Selector: `request.payload.topics`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Name: "Timeout (ms)",
|
Name: "Timeout (ms)",
|
||||||
Value: fmt.Sprintf("%d", int(payload["timeoutMs"].(float64))),
|
Value: fmt.Sprintf("%d", int(payload["timeoutMs"].(float64))),
|
||||||
@ -615,10 +717,20 @@ func representCreateTopicsRequest(data map[string]interface{}) []interface{} {
|
|||||||
})
|
})
|
||||||
rep = append(rep, api.SectionData{
|
rep = append(rep, api.SectionData{
|
||||||
Type: api.TABLE,
|
Type: api.TABLE,
|
||||||
Title: "Payload",
|
Title: "Transaction Details",
|
||||||
Data: string(repPayload),
|
Data: string(repPayload),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
for i, _topic := range payload["topics"].([]interface{}) {
|
||||||
|
topic := _topic.(map[string]interface{})
|
||||||
|
|
||||||
|
rep = append(rep, api.SectionData{
|
||||||
|
Type: api.TABLE,
|
||||||
|
Title: fmt.Sprintf("Topic [%d]", i),
|
||||||
|
Data: representMapAsTable(topic, fmt.Sprintf(`request.payload.topics[%d]`, i), []string{}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return rep
|
return rep
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -628,7 +740,6 @@ func representCreateTopicsResponse(data map[string]interface{}) []interface{} {
|
|||||||
rep = representResponseHeader(data, rep)
|
rep = representResponseHeader(data, rep)
|
||||||
|
|
||||||
payload := data["payload"].(map[string]interface{})
|
payload := data["payload"].(map[string]interface{})
|
||||||
topics, _ := json.Marshal(payload["topics"].([]interface{}))
|
|
||||||
throttleTimeMs := ""
|
throttleTimeMs := ""
|
||||||
if payload["throttleTimeMs"] != nil {
|
if payload["throttleTimeMs"] != nil {
|
||||||
throttleTimeMs = fmt.Sprintf("%d", int(payload["throttleTimeMs"].(float64)))
|
throttleTimeMs = fmt.Sprintf("%d", int(payload["throttleTimeMs"].(float64)))
|
||||||
@ -639,18 +750,23 @@ func representCreateTopicsResponse(data map[string]interface{}) []interface{} {
|
|||||||
Value: throttleTimeMs,
|
Value: throttleTimeMs,
|
||||||
Selector: `response.payload.throttleTimeMs`,
|
Selector: `response.payload.throttleTimeMs`,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "Topics",
|
|
||||||
Value: string(topics),
|
|
||||||
Selector: `response.payload.topics`,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
rep = append(rep, api.SectionData{
|
rep = append(rep, api.SectionData{
|
||||||
Type: api.TABLE,
|
Type: api.TABLE,
|
||||||
Title: "Payload",
|
Title: "Transaction Details",
|
||||||
Data: string(repPayload),
|
Data: string(repPayload),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
for i, _topic := range payload["topics"].([]interface{}) {
|
||||||
|
topic := _topic.(map[string]interface{})
|
||||||
|
|
||||||
|
rep = append(rep, api.SectionData{
|
||||||
|
Type: api.TABLE,
|
||||||
|
Title: fmt.Sprintf("Topic [%d]", i),
|
||||||
|
Data: representMapAsTable(topic, fmt.Sprintf(`response.payload.topics[%d]`, i), []string{}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return rep
|
return rep
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -727,3 +843,42 @@ func representDeleteTopicsResponse(data map[string]interface{}) []interface{} {
|
|||||||
|
|
||||||
return rep
|
return rep
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func contains(s []string, str string) bool {
|
||||||
|
for _, v := range s {
|
||||||
|
if v == str {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func representMapAsTable(mapData map[string]interface{}, selectorPrefix string, ignoreKeys []string) (representation string) {
|
||||||
|
var table []api.TableData
|
||||||
|
for key, value := range mapData {
|
||||||
|
if contains(ignoreKeys, key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch reflect.ValueOf(value).Kind() {
|
||||||
|
case reflect.Map:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Slice:
|
||||||
|
x, err := json.Marshal(value)
|
||||||
|
value = string(x)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selector := fmt.Sprintf("%s[\"%s\"]", selectorPrefix, key)
|
||||||
|
table = append(table, api.TableData{
|
||||||
|
Name: strings.Join(camelcase.Split(strings.Title(key)), " "),
|
||||||
|
Value: value,
|
||||||
|
Selector: selector,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
obj, _ := json.Marshal(table)
|
||||||
|
representation = string(obj)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
@ -42,11 +42,6 @@ func representGeneric(generic map[string]interface{}, selectorPrefix string) (re
|
|||||||
Value: generic["key"].(string),
|
Value: generic["key"].(string),
|
||||||
Selector: fmt.Sprintf("%skey", selectorPrefix),
|
Selector: fmt.Sprintf("%skey", selectorPrefix),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "Value",
|
|
||||||
Value: generic["value"].(string),
|
|
||||||
Selector: fmt.Sprintf("%svalue", selectorPrefix),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Name: "Keyword",
|
Name: "Keyword",
|
||||||
Value: generic["keyword"].(string),
|
Value: generic["keyword"].(string),
|
||||||
@ -59,5 +54,12 @@ func representGeneric(generic map[string]interface{}, selectorPrefix string) (re
|
|||||||
Data: string(details),
|
Data: string(details),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
representation = append(representation, api.SectionData{
|
||||||
|
Type: api.BODY,
|
||||||
|
Title: "Value",
|
||||||
|
Data: generic["value"].(string),
|
||||||
|
Selector: fmt.Sprintf("%svalue", selectorPrefix),
|
||||||
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
13
ui/package-lock.json
generated
13
ui/package-lock.json
generated
@ -7856,6 +7856,11 @@
|
|||||||
"pify": "^4.0.1"
|
"pify": "^4.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"hamt_plus": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-4hwlKWjH4zsg9qGwlM2FeHomVgE="
|
||||||
|
},
|
||||||
"handle-thing": {
|
"handle-thing": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
|
||||||
@ -14229,6 +14234,14 @@
|
|||||||
"picomatch": "^2.2.1"
|
"picomatch": "^2.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"recoil": {
|
||||||
|
"version": "0.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/recoil/-/recoil-0.5.2.tgz",
|
||||||
|
"integrity": "sha512-Edibzpu3dbUMLy6QRg73WL8dvMl9Xqhp+kU+f2sJtXxsaXvAlxU/GcnDE8HXPkprXrhHF2e6SZozptNvjNF5fw==",
|
||||||
|
"requires": {
|
||||||
|
"hamt_plus": "1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"recursive-readdir": {
|
"recursive-readdir": {
|
||||||
"version": "2.2.2",
|
"version": "2.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz",
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
"react-scrollable-feed-virtualized": "^1.4.9",
|
"react-scrollable-feed-virtualized": "^1.4.9",
|
||||||
"react-syntax-highlighter": "^15.4.3",
|
"react-syntax-highlighter": "^15.4.3",
|
||||||
"react-toastify": "^8.0.3",
|
"react-toastify": "^8.0.3",
|
||||||
|
"recoil": "^0.5.2",
|
||||||
"typescript": "^4.2.4",
|
"typescript": "^4.2.4",
|
||||||
"web-vitals": "^1.1.1",
|
"web-vitals": "^1.1.1",
|
||||||
"xml-formatter": "^2.6.0"
|
"xml-formatter": "^2.6.0"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, {useEffect, useState} from 'react';
|
import React, {useCallback, useEffect, useState} from 'react';
|
||||||
import './App.sass';
|
import './App.sass';
|
||||||
import {TrafficPage} from "./components/TrafficPage";
|
import {TrafficPage} from "./components/TrafficPage";
|
||||||
import {TLSWarning} from "./components/TLSWarning/TLSWarning";
|
import {TLSWarning} from "./components/TLSWarning/TLSWarning";
|
||||||
@ -9,43 +9,29 @@ import InstallPage from "./components/InstallPage";
|
|||||||
import LoginPage from "./components/LoginPage";
|
import LoginPage from "./components/LoginPage";
|
||||||
import LoadingOverlay from "./components/LoadingOverlay";
|
import LoadingOverlay from "./components/LoadingOverlay";
|
||||||
import AuthPageBase from './components/AuthPageBase';
|
import AuthPageBase from './components/AuthPageBase';
|
||||||
|
import entPageAtom, {Page} from "./recoil/entPage";
|
||||||
|
import {useRecoilState} from "recoil";
|
||||||
|
|
||||||
const api = Api.getInstance();
|
const api = Api.getInstance();
|
||||||
|
|
||||||
// TODO: move to state management
|
|
||||||
export enum Page {
|
|
||||||
Traffic,
|
|
||||||
Setup,
|
|
||||||
Login
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move to state management
|
|
||||||
export interface MizuContextModel {
|
|
||||||
page: Page;
|
|
||||||
setPage: (page: Page) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move to state management
|
|
||||||
export const MizuContext = React.createContext<MizuContextModel>(null);
|
|
||||||
|
|
||||||
const EntApp = () => {
|
const EntApp = () => {
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [showTLSWarning, setShowTLSWarning] = useState(false);
|
const [showTLSWarning, setShowTLSWarning] = useState(false);
|
||||||
const [userDismissedTLSWarning, setUserDismissedTLSWarning] = useState(false);
|
const [userDismissedTLSWarning, setUserDismissedTLSWarning] = useState(false);
|
||||||
const [addressesWithTLS, setAddressesWithTLS] = useState(new Set<string>());
|
const [addressesWithTLS, setAddressesWithTLS] = useState(new Set<string>());
|
||||||
const [page, setPage] = useState(Page.Traffic); // TODO: move to state management
|
const [entPage, setEntPage] = useRecoilState(entPageAtom);
|
||||||
const [isFirstLogin, setIsFirstLogin] = useState(false);
|
const [isFirstLogin, setIsFirstLogin] = useState(false);
|
||||||
|
|
||||||
const determinePage = async () => { // TODO: move to state management
|
const determinePage = useCallback(async () => { // TODO: move to state management
|
||||||
try {
|
try {
|
||||||
const isInstallNeeded = await api.isInstallNeeded();
|
const isInstallNeeded = await api.isInstallNeeded();
|
||||||
if (isInstallNeeded) {
|
if (isInstallNeeded) {
|
||||||
setPage(Page.Setup);
|
setEntPage(Page.Setup);
|
||||||
} else {
|
} else {
|
||||||
const isAuthNeeded = await api.isAuthenticationNeeded();
|
const isAuthNeeded = await api.isAuthenticationNeeded();
|
||||||
if(isAuthNeeded) {
|
if(isAuthNeeded) {
|
||||||
setPage(Page.Login);
|
setEntPage(Page.Login);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -54,11 +40,11 @@ const EntApp = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
},[setEntPage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
determinePage();
|
determinePage();
|
||||||
}, []);
|
}, [determinePage]);
|
||||||
|
|
||||||
const onTLSDetected = (destAddress: string) => {
|
const onTLSDetected = (destAddress: string) => {
|
||||||
addressesWithTLS.add(destAddress);
|
addressesWithTLS.add(destAddress);
|
||||||
@ -71,7 +57,7 @@ const EntApp = () => {
|
|||||||
|
|
||||||
let pageComponent: any;
|
let pageComponent: any;
|
||||||
|
|
||||||
switch (page) { // TODO: move to state management / proper routing
|
switch (entPage) { // TODO: move to state management / proper routing
|
||||||
case Page.Traffic:
|
case Page.Traffic:
|
||||||
pageComponent = <TrafficPage onTLSDetected={onTLSDetected}/>;
|
pageComponent = <TrafficPage onTLSDetected={onTLSDetected}/>;
|
||||||
break;
|
break;
|
||||||
@ -91,16 +77,14 @@ const EntApp = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mizuApp">
|
<div className="mizuApp">
|
||||||
<MizuContext.Provider value={{page, setPage}}>
|
{entPage === Page.Traffic && <EntHeader isFirstLogin={isFirstLogin} setIsFirstLogin={setIsFirstLogin}/>}
|
||||||
{page === Page.Traffic && <EntHeader isFirstLogin={isFirstLogin} setIsFirstLogin={setIsFirstLogin}/>}
|
{pageComponent}
|
||||||
{pageComponent}
|
{entPage === Page.Traffic && <TLSWarning showTLSWarning={showTLSWarning}
|
||||||
{page === Page.Traffic && <TLSWarning showTLSWarning={showTLSWarning}
|
setShowTLSWarning={setShowTLSWarning}
|
||||||
setShowTLSWarning={setShowTLSWarning}
|
addressesWithTLS={addressesWithTLS}
|
||||||
addressesWithTLS={addressesWithTLS}
|
setAddressesWithTLS={setAddressesWithTLS}
|
||||||
setAddressesWithTLS={setAddressesWithTLS}
|
userDismissedTLSWarning={userDismissedTLSWarning}
|
||||||
userDismissedTLSWarning={userDismissedTLSWarning}
|
setUserDismissedTLSWarning={setUserDismissedTLSWarning}/>}
|
||||||
setUserDismissedTLSWarning={setUserDismissedTLSWarning}/>}
|
|
||||||
</MizuContext.Provider>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,12 @@ import {EntryItem} from "./EntryListItem/EntryListItem";
|
|||||||
import down from "./assets/downImg.svg";
|
import down from "./assets/downImg.svg";
|
||||||
import spinner from './assets/spinner.svg';
|
import spinner from './assets/spinner.svg';
|
||||||
import Api from "../helpers/api";
|
import Api from "../helpers/api";
|
||||||
|
import {useRecoilState, useRecoilValue} from "recoil";
|
||||||
|
import entriesAtom from "../recoil/entries";
|
||||||
|
import wsConnectionAtom, {WsConnectionStatus} from "../recoil/wsConnection";
|
||||||
|
import queryAtom from "../recoil/query";
|
||||||
|
|
||||||
interface EntriesListProps {
|
interface EntriesListProps {
|
||||||
entries: any[];
|
|
||||||
setEntries: any;
|
|
||||||
query: string;
|
|
||||||
listEntryREF: any;
|
listEntryREF: any;
|
||||||
onSnapBrokenEvent: () => void;
|
onSnapBrokenEvent: () => void;
|
||||||
isSnappedToBottom: boolean;
|
isSnappedToBottom: boolean;
|
||||||
@ -22,12 +23,8 @@ interface EntriesListProps {
|
|||||||
startTime: number;
|
startTime: number;
|
||||||
noMoreDataTop: boolean;
|
noMoreDataTop: boolean;
|
||||||
setNoMoreDataTop: (flag: boolean) => void;
|
setNoMoreDataTop: (flag: boolean) => void;
|
||||||
focusedEntryId: string;
|
|
||||||
setFocusedEntryId: (id: string) => void;
|
|
||||||
updateQuery: any;
|
|
||||||
leftOffTop: number;
|
leftOffTop: number;
|
||||||
setLeftOffTop: (leftOffTop: number) => void;
|
setLeftOffTop: (leftOffTop: number) => void;
|
||||||
isWebSocketConnectionClosed: boolean;
|
|
||||||
ws: any;
|
ws: any;
|
||||||
openWebSocket: (query: string, resetEntries: boolean) => void;
|
openWebSocket: (query: string, resetEntries: boolean) => void;
|
||||||
leftOffBottom: number;
|
leftOffBottom: number;
|
||||||
@ -38,7 +35,13 @@ interface EntriesListProps {
|
|||||||
|
|
||||||
const api = Api.getInstance();
|
const api = Api.getInstance();
|
||||||
|
|
||||||
export const EntriesList: React.FC<EntriesListProps> = ({entries, setEntries, query, listEntryREF, onSnapBrokenEvent, isSnappedToBottom, setIsSnappedToBottom, queriedCurrent, setQueriedCurrent, queriedTotal, setQueriedTotal, startTime, noMoreDataTop, setNoMoreDataTop, focusedEntryId, setFocusedEntryId, updateQuery, leftOffTop, setLeftOffTop, isWebSocketConnectionClosed, ws, openWebSocket, leftOffBottom, truncatedTimestamp, setTruncatedTimestamp, scrollableRef}) => {
|
export const EntriesList: React.FC<EntriesListProps> = ({listEntryREF, onSnapBrokenEvent, isSnappedToBottom, setIsSnappedToBottom, queriedCurrent, setQueriedCurrent, queriedTotal, setQueriedTotal, startTime, noMoreDataTop, setNoMoreDataTop, leftOffTop, setLeftOffTop, ws, openWebSocket, leftOffBottom, truncatedTimestamp, setTruncatedTimestamp, scrollableRef}) => {
|
||||||
|
|
||||||
|
const [entries, setEntries] = useRecoilState(entriesAtom);
|
||||||
|
const wsConnection = useRecoilValue(wsConnectionAtom);
|
||||||
|
const query = useRecoilValue(queryAtom);
|
||||||
|
const isWsConnectionClosed = wsConnection === WsConnectionStatus.Closed;
|
||||||
|
|
||||||
const [loadMoreTop, setLoadMoreTop] = useState(false);
|
const [loadMoreTop, setLoadMoreTop] = useState(false);
|
||||||
const [isLoadingTop, setIsLoadingTop] = useState(false);
|
const [isLoadingTop, setIsLoadingTop] = useState(false);
|
||||||
|
|
||||||
@ -95,9 +98,9 @@ export const EntriesList: React.FC<EntriesListProps> = ({entries, setEntries, qu
|
|||||||
},[setLoadMoreTop, setIsLoadingTop, entries, setEntries, query, setNoMoreDataTop, leftOffTop, setLeftOffTop, queriedCurrent, setQueriedCurrent, setQueriedTotal, setTruncatedTimestamp, scrollableRef]);
|
},[setLoadMoreTop, setIsLoadingTop, entries, setEntries, query, setNoMoreDataTop, leftOffTop, setLeftOffTop, queriedCurrent, setQueriedCurrent, setQueriedTotal, setTruncatedTimestamp, scrollableRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(!isWebSocketConnectionClosed || !loadMoreTop || noMoreDataTop) return;
|
if(!isWsConnectionClosed || !loadMoreTop || noMoreDataTop) return;
|
||||||
getOldEntries();
|
getOldEntries();
|
||||||
}, [loadMoreTop, noMoreDataTop, getOldEntries, isWebSocketConnectionClosed]);
|
}, [loadMoreTop, noMoreDataTop, getOldEntries, isWsConnectionClosed]);
|
||||||
|
|
||||||
const scrollbarVisible = scrollableRef.current?.childWrapperRef.current.clientHeight > scrollableRef.current?.wrapperRef.current.clientHeight;
|
const scrollbarVisible = scrollableRef.current?.childWrapperRef.current.clientHeight > scrollableRef.current?.wrapperRef.current.clientHeight;
|
||||||
|
|
||||||
@ -113,10 +116,7 @@ export const EntriesList: React.FC<EntriesListProps> = ({entries, setEntries, qu
|
|||||||
{memoizedEntries.map(entry => <EntryItem
|
{memoizedEntries.map(entry => <EntryItem
|
||||||
key={`entry-${entry.id}`}
|
key={`entry-${entry.id}`}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
focusedEntryId={focusedEntryId}
|
|
||||||
setFocusedEntryId={setFocusedEntryId}
|
|
||||||
style={{}}
|
style={{}}
|
||||||
updateQuery={updateQuery}
|
|
||||||
headingMode={false}
|
headingMode={false}
|
||||||
/>)}
|
/>)}
|
||||||
</ScrollableFeedVirtualized>
|
</ScrollableFeedVirtualized>
|
||||||
@ -131,9 +131,9 @@ export const EntriesList: React.FC<EntriesListProps> = ({entries, setEntries, qu
|
|||||||
</button>
|
</button>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
title="Snap to bottom"
|
title="Snap to bottom"
|
||||||
className={`${styles.btnLive} ${isSnappedToBottom && !isWebSocketConnectionClosed ? styles.hideButton : styles.showButton}`}
|
className={`${styles.btnLive} ${isSnappedToBottom && !isWsConnectionClosed ? styles.hideButton : styles.showButton}`}
|
||||||
onClick={(_) => {
|
onClick={(_) => {
|
||||||
if (isWebSocketConnectionClosed) {
|
if (isWsConnectionClosed) {
|
||||||
if (query) {
|
if (query) {
|
||||||
openWebSocket(`(${query}) and leftOff(${leftOffBottom})`, false);
|
openWebSocket(`(${query}) and leftOff(${leftOffBottom})`, false);
|
||||||
} else {
|
} else {
|
||||||
@ -148,7 +148,7 @@ export const EntriesList: React.FC<EntriesListProps> = ({entries, setEntries, qu
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
<div>Displaying <b>{entries?.length}</b> results out of <b>{queriedTotal}</b> total</div>
|
<div>Displaying <b id="entries-length">{entries?.length}</b> results out of <b id="total-entries">{queriedTotal}</b> total</div>
|
||||||
{startTime !== 0 && <div>Started listening at <span style={{marginRight: 5, fontWeight: 600, fontSize: 13}}>{Moment(truncatedTimestamp ? truncatedTimestamp : startTime).utc().format('MM/DD/YYYY, h:mm:ss.SSS A')}</span></div>}
|
{startTime !== 0 && <div>Started listening at <span style={{marginRight: 5, fontWeight: 600, fontSize: 13}}>{Moment(truncatedTimestamp ? truncatedTimestamp : startTime).utc().format('MM/DD/YYYY, h:mm:ss.SSS A')}</span></div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import React from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import EntryViewer from "./EntryDetailed/EntryViewer";
|
import EntryViewer from "./EntryDetailed/EntryViewer";
|
||||||
import {EntryItem} from "./EntryListItem/EntryListItem";
|
import {EntryItem} from "./EntryListItem/EntryListItem";
|
||||||
import {makeStyles} from "@material-ui/core";
|
import {makeStyles} from "@material-ui/core";
|
||||||
import Protocol from "./UI/Protocol"
|
import Protocol from "./UI/Protocol"
|
||||||
import Queryable from "./UI/Queryable";
|
import Queryable from "./UI/Queryable";
|
||||||
|
import {toast} from "react-toastify";
|
||||||
|
import {useRecoilValue} from "recoil";
|
||||||
|
import focusedEntryIdAtom from "../recoil/focusedEntryId";
|
||||||
|
import Api from "../helpers/api";
|
||||||
|
|
||||||
const useStyles = makeStyles(() => ({
|
const useStyles = makeStyles(() => ({
|
||||||
entryTitle: {
|
entryTitle: {
|
||||||
@ -27,23 +31,17 @@ const useStyles = makeStyles(() => ({
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface EntryDetailedProps {
|
|
||||||
entryData: any
|
|
||||||
updateQuery: any
|
|
||||||
}
|
|
||||||
|
|
||||||
export const formatSize = (n: number) => n > 1000 ? `${Math.round(n / 1000)}KB` : `${n} B`;
|
export const formatSize = (n: number) => n > 1000 ? `${Math.round(n / 1000)}KB` : `${n} B`;
|
||||||
|
|
||||||
const EntryTitle: React.FC<any> = ({protocol, data, bodySize, elapsedTime, updateQuery}) => {
|
const EntryTitle: React.FC<any> = ({protocol, data, bodySize, elapsedTime}) => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const response = data.response;
|
const response = data.response;
|
||||||
|
|
||||||
return <div className={classes.entryTitle}>
|
return <div className={classes.entryTitle}>
|
||||||
<Protocol protocol={protocol} horizontal={true} updateQuery={updateQuery}/>
|
<Protocol protocol={protocol} horizontal={true}/>
|
||||||
<div style={{right: "30px", position: "absolute", display: "flex"}}>
|
<div style={{right: "30px", position: "absolute", display: "flex"}}>
|
||||||
{response && <Queryable
|
{response && <Queryable
|
||||||
query={`response.bodySize == ${bodySize}`}
|
query={`response.bodySize == ${bodySize}`}
|
||||||
updateQuery={updateQuery}
|
|
||||||
style={{margin: "0 18px"}}
|
style={{margin: "0 18px"}}
|
||||||
displayIconOnMouseOver={true}
|
displayIconOnMouseOver={true}
|
||||||
>
|
>
|
||||||
@ -55,7 +53,6 @@ const EntryTitle: React.FC<any> = ({protocol, data, bodySize, elapsedTime, updat
|
|||||||
</Queryable>}
|
</Queryable>}
|
||||||
{response && <Queryable
|
{response && <Queryable
|
||||||
query={`elapsedTime >= ${elapsedTime}`}
|
query={`elapsedTime >= ${elapsedTime}`}
|
||||||
updateQuery={updateQuery}
|
|
||||||
style={{marginRight: 18}}
|
style={{marginRight: 18}}
|
||||||
displayIconOnMouseOver={true}
|
displayIconOnMouseOver={true}
|
||||||
>
|
>
|
||||||
@ -69,30 +66,58 @@ const EntryTitle: React.FC<any> = ({protocol, data, bodySize, elapsedTime, updat
|
|||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EntrySummary: React.FC<any> = ({entry, updateQuery}) => {
|
const EntrySummary: React.FC<any> = ({entry}) => {
|
||||||
return <EntryItem
|
return <EntryItem
|
||||||
key={`entry-${entry.id}`}
|
key={`entry-${entry.id}`}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
focusedEntryId={null}
|
|
||||||
setFocusedEntryId={null}
|
|
||||||
style={{}}
|
style={{}}
|
||||||
updateQuery={updateQuery}
|
|
||||||
headingMode={true}
|
headingMode={true}
|
||||||
/>;
|
/>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EntryDetailed: React.FC<EntryDetailedProps> = ({entryData, updateQuery}) => {
|
const api = Api.getInstance();
|
||||||
|
|
||||||
|
export const EntryDetailed = () => {
|
||||||
|
|
||||||
|
const focusedEntryId = useRecoilValue(focusedEntryIdAtom);
|
||||||
|
const [entryData, setEntryData] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!focusedEntryId) return;
|
||||||
|
setEntryData(null);
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const entryData = await api.getEntry(focusedEntryId);
|
||||||
|
setEntryData(entryData);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.data?.type) {
|
||||||
|
toast[error.response.data.type](`Entry[${focusedEntryId}]: ${error.response.data.msg}`, {
|
||||||
|
position: "bottom-right",
|
||||||
|
theme: "colored",
|
||||||
|
autoClose: error.response.data.autoClose,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [focusedEntryId]);
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<EntryTitle
|
{entryData && <EntryTitle
|
||||||
protocol={entryData.protocol}
|
protocol={entryData.protocol}
|
||||||
data={entryData.data}
|
data={entryData.data}
|
||||||
bodySize={entryData.bodySize}
|
bodySize={entryData.bodySize}
|
||||||
elapsedTime={entryData.data.elapsedTime}
|
elapsedTime={entryData.data.elapsedTime}
|
||||||
updateQuery={updateQuery}
|
/>}
|
||||||
/>
|
{entryData && <EntrySummary entry={entryData.data}/>}
|
||||||
{entryData.data && <EntrySummary entry={entryData.data} updateQuery={updateQuery}/>}
|
|
||||||
<>
|
<>
|
||||||
{entryData.data && <EntryViewer
|
{entryData && <EntryViewer
|
||||||
representation={entryData.representation}
|
representation={entryData.representation}
|
||||||
isRulesEnabled={entryData.isRulesEnabled}
|
isRulesEnabled={entryData.isRulesEnabled}
|
||||||
rulesMatched={entryData.rulesMatched}
|
rulesMatched={entryData.rulesMatched}
|
||||||
@ -102,7 +127,6 @@ export const EntryDetailed: React.FC<EntryDetailedProps> = ({entryData, updateQu
|
|||||||
contractContent={entryData.data.contractContent}
|
contractContent={entryData.data.contractContent}
|
||||||
elapsedTime={entryData.data.elapsedTime}
|
elapsedTime={entryData.data.elapsedTime}
|
||||||
color={entryData.protocol.backgroundColor}
|
color={entryData.protocol.backgroundColor}
|
||||||
updateQuery={updateQuery}
|
|
||||||
/>}
|
/>}
|
||||||
</>
|
</>
|
||||||
</>
|
</>
|
||||||
|
@ -12,14 +12,13 @@ import {default as xmlBeautify} from "xml-formatter";
|
|||||||
interface EntryViewLineProps {
|
interface EntryViewLineProps {
|
||||||
label: string;
|
label: string;
|
||||||
value: number | string;
|
value: number | string;
|
||||||
updateQuery?: any;
|
|
||||||
selector?: string;
|
selector?: string;
|
||||||
overrideQueryValue?: string;
|
overrideQueryValue?: string;
|
||||||
displayIconOnMouseOver?: boolean;
|
displayIconOnMouseOver?: boolean;
|
||||||
useTooltip?: boolean;
|
useTooltip?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EntryViewLine: React.FC<EntryViewLineProps> = ({label, value, updateQuery = null, selector = "", overrideQueryValue = "", displayIconOnMouseOver = true, useTooltip = true}) => {
|
const EntryViewLine: React.FC<EntryViewLineProps> = ({label, value, selector = "", overrideQueryValue = "", displayIconOnMouseOver = true, useTooltip = true}) => {
|
||||||
let query: string;
|
let query: string;
|
||||||
if (!selector) {
|
if (!selector) {
|
||||||
query = "";
|
query = "";
|
||||||
@ -34,7 +33,6 @@ const EntryViewLine: React.FC<EntryViewLineProps> = ({label, value, updateQuery
|
|||||||
<td className={`${styles.dataKey}`}>
|
<td className={`${styles.dataKey}`}>
|
||||||
<Queryable
|
<Queryable
|
||||||
query={query}
|
query={query}
|
||||||
updateQuery={updateQuery}
|
|
||||||
style={{float: "right", height: "18px"}}
|
style={{float: "right", height: "18px"}}
|
||||||
iconStyle={{marginRight: "20px"}}
|
iconStyle={{marginRight: "20px"}}
|
||||||
flipped={true}
|
flipped={true}
|
||||||
@ -63,10 +61,9 @@ interface EntrySectionCollapsibleTitleProps {
|
|||||||
expanded: boolean,
|
expanded: boolean,
|
||||||
setExpanded: any,
|
setExpanded: any,
|
||||||
query?: string,
|
query?: string,
|
||||||
updateQuery?: any,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const EntrySectionCollapsibleTitle: React.FC<EntrySectionCollapsibleTitleProps> = ({title, color, expanded, setExpanded, query = "", updateQuery = null}) => {
|
const EntrySectionCollapsibleTitle: React.FC<EntrySectionCollapsibleTitleProps> = ({title, color, expanded, setExpanded, query = ""}) => {
|
||||||
return <div className={styles.title}>
|
return <div className={styles.title}>
|
||||||
<div
|
<div
|
||||||
className={`${styles.button} ${expanded ? styles.expanded : ''}`}
|
className={`${styles.button} ${expanded ? styles.expanded : ''}`}
|
||||||
@ -79,9 +76,8 @@ const EntrySectionCollapsibleTitle: React.FC<EntrySectionCollapsibleTitleProps>
|
|||||||
</div>
|
</div>
|
||||||
<Queryable
|
<Queryable
|
||||||
query={query}
|
query={query}
|
||||||
updateQuery={updateQuery}
|
useTooltip={!!query}
|
||||||
useTooltip={updateQuery ? true : false}
|
displayIconOnMouseOver={!!query}
|
||||||
displayIconOnMouseOver={updateQuery ? true : false}
|
|
||||||
>
|
>
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
</Queryable>
|
</Queryable>
|
||||||
@ -92,32 +88,31 @@ interface EntrySectionContainerProps {
|
|||||||
title: string,
|
title: string,
|
||||||
color: string,
|
color: string,
|
||||||
query?: string,
|
query?: string,
|
||||||
updateQuery?: any,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EntrySectionContainer: React.FC<EntrySectionContainerProps> = ({title, color, children, query = "", updateQuery = null}) => {
|
export const EntrySectionContainer: React.FC<EntrySectionContainerProps> = ({title, color, children, query = ""}) => {
|
||||||
const [expanded, setExpanded] = useState(true);
|
const [expanded, setExpanded] = useState(true);
|
||||||
return <CollapsibleContainer
|
return <CollapsibleContainer
|
||||||
className={styles.collapsibleContainer}
|
className={styles.collapsibleContainer}
|
||||||
expanded={expanded}
|
expanded={expanded}
|
||||||
title={<EntrySectionCollapsibleTitle title={title} color={color} expanded={expanded} setExpanded={setExpanded} query={query} updateQuery={updateQuery}/>}
|
title={<EntrySectionCollapsibleTitle title={title} color={color} expanded={expanded} setExpanded={setExpanded} query={query}/>}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</CollapsibleContainer>
|
</CollapsibleContainer>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EntryBodySectionProps {
|
interface EntryBodySectionProps {
|
||||||
|
title: string,
|
||||||
content: any,
|
content: any,
|
||||||
color: string,
|
color: string,
|
||||||
updateQuery: any,
|
|
||||||
encoding?: string,
|
encoding?: string,
|
||||||
contentType?: string,
|
contentType?: string,
|
||||||
selector?: string,
|
selector?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EntryBodySection: React.FC<EntryBodySectionProps> = ({
|
export const EntryBodySection: React.FC<EntryBodySectionProps> = ({
|
||||||
|
title,
|
||||||
color,
|
color,
|
||||||
updateQuery,
|
|
||||||
content,
|
content,
|
||||||
encoding,
|
encoding,
|
||||||
contentType,
|
contentType,
|
||||||
@ -167,10 +162,9 @@ export const EntryBodySection: React.FC<EntryBodySectionProps> = ({
|
|||||||
|
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
{content && content?.length > 0 && <EntrySectionContainer
|
{content && content?.length > 0 && <EntrySectionContainer
|
||||||
title='Body'
|
title={title}
|
||||||
color={color}
|
color={color}
|
||||||
query={`${selector} == r".*"`}
|
query={`${selector} == r".*"`}
|
||||||
updateQuery={updateQuery}
|
|
||||||
>
|
>
|
||||||
<div style={{display: 'flex', alignItems: 'center', alignContent: 'center', margin: "5px 0"}}>
|
<div style={{display: 'flex', alignItems: 'center', alignContent: 'center', margin: "5px 0"}}>
|
||||||
{supportsPrettying && <div style={{paddingTop: 3}}>
|
{supportsPrettying && <div style={{paddingTop: 3}}>
|
||||||
@ -201,21 +195,33 @@ interface EntrySectionProps {
|
|||||||
title: string,
|
title: string,
|
||||||
color: string,
|
color: string,
|
||||||
arrayToIterate: any[],
|
arrayToIterate: any[],
|
||||||
updateQuery: any,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EntryTableSection: React.FC<EntrySectionProps> = ({title, color, arrayToIterate, updateQuery}) => {
|
export const EntryTableSection: React.FC<EntrySectionProps> = ({title, color, arrayToIterate}) => {
|
||||||
|
let arrayToIterateSorted: any[];
|
||||||
|
if (arrayToIterate) {
|
||||||
|
arrayToIterateSorted = arrayToIterate.sort((a, b) => {
|
||||||
|
if (a.name > b.name) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.name < b.name) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
{
|
{
|
||||||
arrayToIterate && arrayToIterate.length > 0 ?
|
arrayToIterate && arrayToIterate.length > 0 ?
|
||||||
<EntrySectionContainer title={title} color={color}>
|
<EntrySectionContainer title={title} color={color}>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody id={`tbody-${title}`}>
|
||||||
{arrayToIterate.map(({name, value, selector}, index) => <EntryViewLine
|
{arrayToIterateSorted.map(({name, value, selector}, index) => <EntryViewLine
|
||||||
key={index}
|
key={index}
|
||||||
label={name}
|
label={name}
|
||||||
value={value}
|
value={value}
|
||||||
updateQuery={updateQuery}
|
|
||||||
selector={selector}
|
selector={selector}
|
||||||
/>)}
|
/>)}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -8,7 +8,7 @@ enum SectionTypes {
|
|||||||
SectionBody = "body",
|
SectionBody = "body",
|
||||||
}
|
}
|
||||||
|
|
||||||
const SectionsRepresentation: React.FC<any> = ({data, color, updateQuery}) => {
|
const SectionsRepresentation: React.FC<any> = ({data, color}) => {
|
||||||
const sections = []
|
const sections = []
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
@ -16,12 +16,12 @@ const SectionsRepresentation: React.FC<any> = ({data, color, updateQuery}) => {
|
|||||||
switch (row.type) {
|
switch (row.type) {
|
||||||
case SectionTypes.SectionTable:
|
case SectionTypes.SectionTable:
|
||||||
sections.push(
|
sections.push(
|
||||||
<EntryTableSection key={i} title={row.title} color={color} arrayToIterate={JSON.parse(row.data)} updateQuery={updateQuery}/>
|
<EntryTableSection key={i} title={row.title} color={color} arrayToIterate={JSON.parse(row.data)}/>
|
||||||
)
|
)
|
||||||
break;
|
break;
|
||||||
case SectionTypes.SectionBody:
|
case SectionTypes.SectionBody:
|
||||||
sections.push(
|
sections.push(
|
||||||
<EntryBodySection key={i} color={color} content={row.data} updateQuery={updateQuery} encoding={row.encoding} contentType={row.mimeType} selector={row.selector}/>
|
<EntryBodySection key={i} title={row.title} color={color} content={row.data} encoding={row.encoding} contentType={row.mimeType} selector={row.selector}/>
|
||||||
)
|
)
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -33,7 +33,7 @@ const SectionsRepresentation: React.FC<any> = ({data, color, updateQuery}) => {
|
|||||||
return <>{sections}</>;
|
return <>{sections}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AutoRepresentation: React.FC<any> = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color, updateQuery}) => {
|
const AutoRepresentation: React.FC<any> = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color}) => {
|
||||||
var TABS = [
|
var TABS = [
|
||||||
{
|
{
|
||||||
tab: 'Request'
|
tab: 'Request'
|
||||||
@ -48,9 +48,9 @@ const AutoRepresentation: React.FC<any> = ({representation, isRulesEnabled, rule
|
|||||||
|
|
||||||
const {request, response} = JSON.parse(representation);
|
const {request, response} = JSON.parse(representation);
|
||||||
|
|
||||||
var responseTabIndex = 0;
|
let responseTabIndex = 0;
|
||||||
var rulesTabIndex = 0;
|
let rulesTabIndex = 0;
|
||||||
var contractTabIndex = 0;
|
let contractTabIndex = 0;
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
TABS.push(
|
TABS.push(
|
||||||
@ -85,10 +85,10 @@ const AutoRepresentation: React.FC<any> = ({representation, isRulesEnabled, rule
|
|||||||
<Tabs tabs={TABS} currentTab={currentTab} color={color} onChange={setCurrentTab} leftAligned/>
|
<Tabs tabs={TABS} currentTab={currentTab} color={color} onChange={setCurrentTab} leftAligned/>
|
||||||
</div>
|
</div>
|
||||||
{currentTab === TABS[0].tab && <React.Fragment>
|
{currentTab === TABS[0].tab && <React.Fragment>
|
||||||
<SectionsRepresentation data={request} color={color} updateQuery={updateQuery}/>
|
<SectionsRepresentation data={request} color={color}/>
|
||||||
</React.Fragment>}
|
</React.Fragment>}
|
||||||
{response && currentTab === TABS[responseTabIndex].tab && <React.Fragment>
|
{response && currentTab === TABS[responseTabIndex].tab && <React.Fragment>
|
||||||
<SectionsRepresentation data={response} color={color} updateQuery={updateQuery}/>
|
<SectionsRepresentation data={response} color={color}/>
|
||||||
</React.Fragment>}
|
</React.Fragment>}
|
||||||
{isRulesEnabled && currentTab === TABS[rulesTabIndex].tab && <React.Fragment>
|
{isRulesEnabled && currentTab === TABS[rulesTabIndex].tab && <React.Fragment>
|
||||||
<EntryTablePolicySection title={'Rule'} color={color} latency={elapsedTime} arrayToIterate={rulesMatched ? rulesMatched : []}/>
|
<EntryTablePolicySection title={'Rule'} color={color} latency={elapsedTime} arrayToIterate={rulesMatched ? rulesMatched : []}/>
|
||||||
@ -110,10 +110,9 @@ interface Props {
|
|||||||
contractContent: string;
|
contractContent: string;
|
||||||
color: string;
|
color: string;
|
||||||
elapsedTime: number;
|
elapsedTime: number;
|
||||||
updateQuery: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const EntryViewer: React.FC<Props> = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color, updateQuery}) => {
|
const EntryViewer: React.FC<Props> = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color}) => {
|
||||||
return <AutoRepresentation
|
return <AutoRepresentation
|
||||||
representation={representation}
|
representation={representation}
|
||||||
isRulesEnabled={isRulesEnabled}
|
isRulesEnabled={isRulesEnabled}
|
||||||
@ -124,7 +123,6 @@ const EntryViewer: React.FC<Props> = ({representation, isRulesEnabled, rulesMatc
|
|||||||
contractContent={contractContent}
|
contractContent={contractContent}
|
||||||
elapsedTime={elapsedTime}
|
elapsedTime={elapsedTime}
|
||||||
color={color}
|
color={color}
|
||||||
updateQuery={updateQuery}
|
|
||||||
/>
|
/>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -74,7 +74,6 @@
|
|||||||
.separatorRight
|
.separatorRight
|
||||||
display: flex
|
display: flex
|
||||||
border-right: 1px solid $data-background-color
|
border-right: 1px solid $data-background-color
|
||||||
padding: 4px
|
|
||||||
padding-right: 12px
|
padding-right: 12px
|
||||||
|
|
||||||
.separatorLeft
|
.separatorLeft
|
||||||
|
@ -12,6 +12,9 @@ import ingoingIconNeutral from "../assets/ingoing-traffic-neutral.svg"
|
|||||||
import outgoingIconSuccess from "../assets/outgoing-traffic-success.svg"
|
import outgoingIconSuccess from "../assets/outgoing-traffic-success.svg"
|
||||||
import outgoingIconFailure from "../assets/outgoing-traffic-failure.svg"
|
import outgoingIconFailure from "../assets/outgoing-traffic-failure.svg"
|
||||||
import outgoingIconNeutral from "../assets/outgoing-traffic-neutral.svg"
|
import outgoingIconNeutral from "../assets/outgoing-traffic-neutral.svg"
|
||||||
|
import {useRecoilState} from "recoil";
|
||||||
|
import focusedEntryIdAtom from "../../recoil/focusedEntryId";
|
||||||
|
import queryAtom from "../../recoil/query";
|
||||||
|
|
||||||
interface TCPInterface {
|
interface TCPInterface {
|
||||||
ip: string
|
ip: string
|
||||||
@ -42,15 +45,14 @@ interface Rules {
|
|||||||
|
|
||||||
interface EntryProps {
|
interface EntryProps {
|
||||||
entry: Entry;
|
entry: Entry;
|
||||||
focusedEntryId: string;
|
|
||||||
setFocusedEntryId: (id: string) => void;
|
|
||||||
style: object;
|
style: object;
|
||||||
updateQuery: any;
|
|
||||||
headingMode: boolean;
|
headingMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocusedEntryId, style, updateQuery, headingMode}) => {
|
export const EntryItem: React.FC<EntryProps> = ({entry, style, headingMode}) => {
|
||||||
|
|
||||||
|
const [focusedEntryId, setFocusedEntryId] = useRecoilState(focusedEntryIdAtom);
|
||||||
|
const [queryState, setQuery] = useRecoilState(queryAtom);
|
||||||
const isSelected = focusedEntryId === entry.id.toString();
|
const isSelected = focusedEntryId === entry.id.toString();
|
||||||
|
|
||||||
const classification = getClassification(entry.status)
|
const classification = getClassification(entry.status)
|
||||||
@ -103,8 +105,8 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var contractEnabled = true;
|
let contractEnabled = true;
|
||||||
var contractText = "";
|
let contractText = "";
|
||||||
switch (entry.contractStatus) {
|
switch (entry.contractStatus) {
|
||||||
case 0:
|
case 0:
|
||||||
contractEnabled = false;
|
contractEnabled = false;
|
||||||
@ -123,8 +125,9 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const isStatusCodeEnabled = ((entry.proto.name === "http" && "status" in entry) || entry.status !== 0);
|
const isStatusCodeEnabled = ((entry.proto.name === "http" && "status" in entry) || entry.status !== 0);
|
||||||
var endpointServiceContainer = "10px";
|
let endpointServiceContainer = "10px";
|
||||||
if (!isStatusCodeEnabled) endpointServiceContainer = "20px";
|
if (!isStatusCodeEnabled) endpointServiceContainer = "20px";
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
@ -147,17 +150,15 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
|
|||||||
{!headingMode ? <Protocol
|
{!headingMode ? <Protocol
|
||||||
protocol={entry.proto}
|
protocol={entry.proto}
|
||||||
horizontal={false}
|
horizontal={false}
|
||||||
updateQuery={updateQuery}
|
|
||||||
/> : null}
|
/> : null}
|
||||||
{isStatusCodeEnabled && <div>
|
{isStatusCodeEnabled && <div>
|
||||||
<StatusCode statusCode={entry.status} updateQuery={updateQuery}/>
|
<StatusCode statusCode={entry.status}/>
|
||||||
</div>}
|
</div>}
|
||||||
<div className={styles.endpointServiceContainer} style={{paddingLeft: endpointServiceContainer}}>
|
<div className={styles.endpointServiceContainer} style={{paddingLeft: endpointServiceContainer}}>
|
||||||
<Summary method={entry.method} summary={entry.summary} updateQuery={updateQuery}/>
|
<Summary method={entry.method} summary={entry.summary}/>
|
||||||
<div className={styles.resolvedName}>
|
<div className={styles.resolvedName}>
|
||||||
<Queryable
|
<Queryable
|
||||||
query={`src.name == "${entry.src.name}"`}
|
query={`src.name == "${entry.src.name}"`}
|
||||||
updateQuery={updateQuery}
|
|
||||||
displayIconOnMouseOver={true}
|
displayIconOnMouseOver={true}
|
||||||
flipped={true}
|
flipped={true}
|
||||||
style={{marginTop: "-4px", overflow: "visible"}}
|
style={{marginTop: "-4px", overflow: "visible"}}
|
||||||
@ -174,7 +175,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
|
|||||||
<SwapHorizIcon style={{color: entry.proto.backgroundColor, marginTop: "-2px"}}></SwapHorizIcon>
|
<SwapHorizIcon style={{color: entry.proto.backgroundColor, marginTop: "-2px"}}></SwapHorizIcon>
|
||||||
<Queryable
|
<Queryable
|
||||||
query={`dst.name == "${entry.dst.name}"`}
|
query={`dst.name == "${entry.dst.name}"`}
|
||||||
updateQuery={updateQuery}
|
|
||||||
displayIconOnMouseOver={true}
|
displayIconOnMouseOver={true}
|
||||||
style={{marginTop: "-4px"}}
|
style={{marginTop: "-4px"}}
|
||||||
iconStyle={{marginTop: "4px", marginLeft: "-2px"}}
|
iconStyle={{marginTop: "4px", marginLeft: "-2px"}}
|
||||||
@ -204,7 +204,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
|
|||||||
<div className={styles.separatorRight}>
|
<div className={styles.separatorRight}>
|
||||||
<Queryable
|
<Queryable
|
||||||
query={`src.ip == "${entry.src.ip}"`}
|
query={`src.ip == "${entry.src.ip}"`}
|
||||||
updateQuery={updateQuery}
|
|
||||||
displayIconOnMouseOver={true}
|
displayIconOnMouseOver={true}
|
||||||
flipped={true}
|
flipped={true}
|
||||||
iconStyle={{marginRight: "16px"}}
|
iconStyle={{marginRight: "16px"}}
|
||||||
@ -219,7 +218,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
|
|||||||
<span className={`${styles.tcpInfo}`} style={{marginTop: "18px"}}>{entry.src.port ? ":" : ""}</span>
|
<span className={`${styles.tcpInfo}`} style={{marginTop: "18px"}}>{entry.src.port ? ":" : ""}</span>
|
||||||
<Queryable
|
<Queryable
|
||||||
query={`src.port == "${entry.src.port}"`}
|
query={`src.port == "${entry.src.port}"`}
|
||||||
updateQuery={updateQuery}
|
|
||||||
displayIconOnMouseOver={true}
|
displayIconOnMouseOver={true}
|
||||||
flipped={true}
|
flipped={true}
|
||||||
iconStyle={{marginTop: "28px"}}
|
iconStyle={{marginTop: "28px"}}
|
||||||
@ -234,7 +232,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
|
|||||||
{entry.isOutgoing ?
|
{entry.isOutgoing ?
|
||||||
<Queryable
|
<Queryable
|
||||||
query={`outgoing == true`}
|
query={`outgoing == true`}
|
||||||
updateQuery={updateQuery}
|
|
||||||
displayIconOnMouseOver={true}
|
displayIconOnMouseOver={true}
|
||||||
flipped={true}
|
flipped={true}
|
||||||
iconStyle={{marginTop: "28px"}}
|
iconStyle={{marginTop: "28px"}}
|
||||||
@ -248,7 +245,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
|
|||||||
:
|
:
|
||||||
<Queryable
|
<Queryable
|
||||||
query={`outgoing == true`}
|
query={`outgoing == true`}
|
||||||
updateQuery={updateQuery}
|
|
||||||
displayIconOnMouseOver={true}
|
displayIconOnMouseOver={true}
|
||||||
flipped={true}
|
flipped={true}
|
||||||
iconStyle={{marginTop: "28px"}}
|
iconStyle={{marginTop: "28px"}}
|
||||||
@ -258,14 +254,14 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
|
|||||||
alt="Outgoing traffic"
|
alt="Outgoing traffic"
|
||||||
title="Outgoing"
|
title="Outgoing"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateQuery(`outgoing == false`)
|
const query = `outgoing == false`;
|
||||||
|
setQuery(queryState ? `${queryState} and ${query}` : query);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Queryable>
|
</Queryable>
|
||||||
}
|
}
|
||||||
<Queryable
|
<Queryable
|
||||||
query={`dst.ip == "${entry.dst.ip}"`}
|
query={`dst.ip == "${entry.dst.ip}"`}
|
||||||
updateQuery={updateQuery}
|
|
||||||
displayIconOnMouseOver={true}
|
displayIconOnMouseOver={true}
|
||||||
flipped={false}
|
flipped={false}
|
||||||
iconStyle={{marginTop: "28px"}}
|
iconStyle={{marginTop: "28px"}}
|
||||||
@ -280,7 +276,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
|
|||||||
<span className={`${styles.tcpInfo}`} style={{marginTop: "18px"}}>:</span>
|
<span className={`${styles.tcpInfo}`} style={{marginTop: "18px"}}>:</span>
|
||||||
<Queryable
|
<Queryable
|
||||||
query={`dst.port == "${entry.dst.port}"`}
|
query={`dst.port == "${entry.dst.port}"`}
|
||||||
updateQuery={updateQuery}
|
|
||||||
displayIconOnMouseOver={true}
|
displayIconOnMouseOver={true}
|
||||||
flipped={false}
|
flipped={false}
|
||||||
>
|
>
|
||||||
@ -295,7 +290,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocus
|
|||||||
<div className={styles.timestamp}>
|
<div className={styles.timestamp}>
|
||||||
<Queryable
|
<Queryable
|
||||||
query={`timestamp >= datetime("${Moment(+entry.timestamp)?.utc().format('MM/DD/YYYY, h:mm:ss.SSS A')}")`}
|
query={`timestamp >= datetime("${Moment(+entry.timestamp)?.utc().format('MM/DD/YYYY, h:mm:ss.SSS A')}")`}
|
||||||
updateQuery={updateQuery}
|
|
||||||
displayIconOnMouseOver={true}
|
displayIconOnMouseOver={true}
|
||||||
flipped={false}
|
flipped={false}
|
||||||
>
|
>
|
||||||
|
@ -7,20 +7,18 @@ import {SyntaxHighlighter} from "./UI/SyntaxHighlighter/index";
|
|||||||
import filterUIExample1 from "./assets/filter-ui-example-1.png"
|
import filterUIExample1 from "./assets/filter-ui-example-1.png"
|
||||||
import filterUIExample2 from "./assets/filter-ui-example-2.png"
|
import filterUIExample2 from "./assets/filter-ui-example-2.png"
|
||||||
import variables from '../variables.module.scss';
|
import variables from '../variables.module.scss';
|
||||||
|
import {useRecoilState} from "recoil";
|
||||||
|
import queryAtom from "../recoil/query";
|
||||||
|
|
||||||
interface FiltersProps {
|
interface FiltersProps {
|
||||||
query: string
|
|
||||||
setQuery: any
|
|
||||||
backgroundColor: string
|
backgroundColor: string
|
||||||
ws: any
|
ws: any
|
||||||
openWebSocket: (query: string, resetEntries: boolean) => void;
|
openWebSocket: (query: string, resetEntries: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Filters: React.FC<FiltersProps> = ({query, setQuery, backgroundColor, ws, openWebSocket}) => {
|
export const Filters: React.FC<FiltersProps> = ({backgroundColor, ws, openWebSocket}) => {
|
||||||
return <div className={styles.container}>
|
return <div className={styles.container}>
|
||||||
<QueryForm
|
<QueryForm
|
||||||
query={query}
|
|
||||||
setQuery={setQuery}
|
|
||||||
backgroundColor={backgroundColor}
|
backgroundColor={backgroundColor}
|
||||||
ws={ws}
|
ws={ws}
|
||||||
openWebSocket={openWebSocket}
|
openWebSocket={openWebSocket}
|
||||||
@ -29,8 +27,6 @@ export const Filters: React.FC<FiltersProps> = ({query, setQuery, backgroundColo
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface QueryFormProps {
|
interface QueryFormProps {
|
||||||
query: string
|
|
||||||
setQuery: any
|
|
||||||
backgroundColor: string
|
backgroundColor: string
|
||||||
ws: any
|
ws: any
|
||||||
openWebSocket: (query: string, resetEntries: boolean) => void;
|
openWebSocket: (query: string, resetEntries: boolean) => void;
|
||||||
@ -50,9 +46,10 @@ export const modalStyle = {
|
|||||||
color: '#000',
|
color: '#000',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, backgroundColor, ws, openWebSocket}) => {
|
export const QueryForm: React.FC<QueryFormProps> = ({backgroundColor, ws, openWebSocket}) => {
|
||||||
|
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
|
const [query, setQuery] = useRecoilState(queryAtom);
|
||||||
|
|
||||||
const [openModal, setOpenModal] = useState(false);
|
const [openModal, setOpenModal] = useState(false);
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, {useContext, useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import logo from '../assets/MizuEntLogo.svg';
|
import logo from '../assets/MizuEntLogo.svg';
|
||||||
import './Header.sass';
|
import './Header.sass';
|
||||||
import userImg from '../assets/user-circle.svg';
|
import userImg from '../assets/user-circle.svg';
|
||||||
@ -9,7 +9,8 @@ import logoutIcon from '../assets/logout.png';
|
|||||||
import {SettingsModal} from "../SettingsModal/SettingModal";
|
import {SettingsModal} from "../SettingsModal/SettingModal";
|
||||||
import Api from "../../helpers/api";
|
import Api from "../../helpers/api";
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import {MizuContext, Page} from "../../EntApp";
|
import {useSetRecoilState} from "recoil";
|
||||||
|
import entPageAtom, {Page} from "../../recoil/entPage";
|
||||||
|
|
||||||
const api = Api.getInstance();
|
const api = Api.getInstance();
|
||||||
|
|
||||||
@ -49,12 +50,12 @@ export const EntHeader: React.FC<EntHeaderProps> = ({isFirstLogin, setIsFirstLog
|
|||||||
|
|
||||||
const ProfileButton = () => {
|
const ProfileButton = () => {
|
||||||
|
|
||||||
const {setPage} = useContext(MizuContext);
|
const setEntPage = useSetRecoilState(entPageAtom);
|
||||||
|
|
||||||
const logout = async (popupState) => {
|
const logout = async (popupState) => {
|
||||||
try {
|
try {
|
||||||
await api.logout();
|
await api.logout();
|
||||||
setPage(Page.Login);
|
setEntPage(Page.Login);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error("Something went wrong, please check the console");
|
toast.error("Something went wrong, please check the console");
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { Button } from "@material-ui/core";
|
import { Button } from "@material-ui/core";
|
||||||
import React, { useContext, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { MizuContext, Page } from "../EntApp";
|
|
||||||
import { adminUsername } from "../consts";
|
import { adminUsername } from "../consts";
|
||||||
import Api, { FormValidationErrorType } from "../helpers/api";
|
import Api, { FormValidationErrorType } from "../helpers/api";
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import LoadingOverlay from "./LoadingOverlay";
|
import LoadingOverlay from "./LoadingOverlay";
|
||||||
import { useCommonStyles } from "../helpers/commonStyle";
|
import { useCommonStyles } from "../helpers/commonStyle";
|
||||||
|
import {useSetRecoilState} from "recoil";
|
||||||
|
import entPageAtom, {Page} from "../recoil/entPage";
|
||||||
|
|
||||||
const api = Api.getInstance();
|
const api = Api.getInstance();
|
||||||
|
|
||||||
@ -20,7 +21,7 @@ export const InstallPage: React.FC<InstallPageProps> = ({onFirstLogin}) => {
|
|||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [passwordConfirm, setPasswordConfirm] = useState("");
|
const [passwordConfirm, setPasswordConfirm] = useState("");
|
||||||
|
|
||||||
const {setPage} = useContext(MizuContext);
|
const setEntPage = useSetRecoilState(entPageAtom);
|
||||||
|
|
||||||
const onFormSubmit = async () => {
|
const onFormSubmit = async () => {
|
||||||
if (password.length < 4) {
|
if (password.length < 4) {
|
||||||
@ -35,7 +36,7 @@ export const InstallPage: React.FC<InstallPageProps> = ({onFirstLogin}) => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await api.register(adminUsername, password);
|
await api.register(adminUsername, password);
|
||||||
if (!await api.isAuthenticationNeeded()) {
|
if (!await api.isAuthenticationNeeded()) {
|
||||||
setPage(Page.Traffic);
|
setEntPage(Page.Traffic);
|
||||||
onFirstLogin();
|
onFirstLogin();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { Button } from "@material-ui/core";
|
import { Button } from "@material-ui/core";
|
||||||
import React, { useContext, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { MizuContext, Page } from "../EntApp";
|
|
||||||
import Api from "../helpers/api";
|
import Api from "../helpers/api";
|
||||||
import { useCommonStyles } from "../helpers/commonStyle";
|
import { useCommonStyles } from "../helpers/commonStyle";
|
||||||
import LoadingOverlay from "./LoadingOverlay";
|
import LoadingOverlay from "./LoadingOverlay";
|
||||||
|
import entPageAtom, {Page} from "../recoil/entPage";
|
||||||
|
import {useSetRecoilState} from "recoil";
|
||||||
|
|
||||||
const api = Api.getInstance();
|
const api = Api.getInstance();
|
||||||
|
|
||||||
@ -15,7 +16,7 @@ const LoginPage: React.FC = () => {
|
|||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
const {setPage} = useContext(MizuContext);
|
const setEntPage = useSetRecoilState(entPageAtom);
|
||||||
|
|
||||||
const onFormSubmit = async () => {
|
const onFormSubmit = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@ -23,7 +24,7 @@ const LoginPage: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
await api.login(username, password);
|
await api.login(username, password);
|
||||||
if (!await api.isAuthenticationNeeded()) {
|
if (!await api.isAuthenticationNeeded()) {
|
||||||
setPage(Page.Traffic);
|
setEntPage(Page.Traffic);
|
||||||
} else {
|
} else {
|
||||||
toast.error("Invalid credentials");
|
toast.error("Invalid credentials");
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,12 @@ import {StatusBar} from "./UI/StatusBar";
|
|||||||
import Api, {MizuWebsocketURL} from "../helpers/api";
|
import Api, {MizuWebsocketURL} from "../helpers/api";
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
import {useRecoilState, useRecoilValue} from "recoil";
|
||||||
|
import tappingStatusAtom from "../recoil/tappingStatus";
|
||||||
|
import entriesAtom from "../recoil/entries";
|
||||||
|
import focusedEntryIdAtom from "../recoil/focusedEntryId";
|
||||||
|
import websocketConnectionAtom, {WsConnectionStatus} from "../recoil/wsConnection";
|
||||||
|
import queryAtom from "../recoil/query";
|
||||||
|
|
||||||
const useLayoutStyles = makeStyles(() => ({
|
const useLayoutStyles = makeStyles(() => ({
|
||||||
details: {
|
details: {
|
||||||
@ -33,11 +39,6 @@ const useLayoutStyles = makeStyles(() => ({
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
enum ConnectionStatus {
|
|
||||||
Closed,
|
|
||||||
Connected,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TrafficPageProps {
|
interface TrafficPageProps {
|
||||||
onTLSDetected: (destAddress: string) => void;
|
onTLSDetected: (destAddress: string) => void;
|
||||||
setAnalyzeStatus?: (status: any) => void;
|
setAnalyzeStatus?: (status: any) => void;
|
||||||
@ -48,21 +49,16 @@ const api = Api.getInstance();
|
|||||||
export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnalyzeStatus}) => {
|
export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnalyzeStatus}) => {
|
||||||
|
|
||||||
const classes = useLayoutStyles();
|
const classes = useLayoutStyles();
|
||||||
|
const [tappingStatus, setTappingStatus] = useRecoilState(tappingStatusAtom);
|
||||||
const [entries, setEntries] = useState([] as any);
|
const [entries, setEntries] = useRecoilState(entriesAtom);
|
||||||
const [focusedEntryId, setFocusedEntryId] = useState(null);
|
const [focusedEntryId, setFocusedEntryId] = useRecoilState(focusedEntryIdAtom);
|
||||||
const [selectedEntryData, setSelectedEntryData] = useState(null);
|
const [wsConnection, setWsConnection] = useRecoilState(websocketConnectionAtom);
|
||||||
const [connection, setConnection] = useState(ConnectionStatus.Closed);
|
const query = useRecoilValue(queryAtom);
|
||||||
|
|
||||||
const [noMoreDataTop, setNoMoreDataTop] = useState(false);
|
const [noMoreDataTop, setNoMoreDataTop] = useState(false);
|
||||||
|
|
||||||
const [tappingStatus, setTappingStatus] = useState(null);
|
|
||||||
|
|
||||||
const [isSnappedToBottom, setIsSnappedToBottom] = useState(true);
|
const [isSnappedToBottom, setIsSnappedToBottom] = useState(true);
|
||||||
|
|
||||||
const [query, setQuery] = useState("");
|
|
||||||
const [queryBackgroundColor, setQueryBackgroundColor] = useState("#f5f5f5");
|
const [queryBackgroundColor, setQueryBackgroundColor] = useState("#f5f5f5");
|
||||||
const [addition, updateQuery] = useState("");
|
|
||||||
|
|
||||||
const [queriedCurrent, setQueriedCurrent] = useState(0);
|
const [queriedCurrent, setQueriedCurrent] = useState(0);
|
||||||
const [queriedTotal, setQueriedTotal] = useState(0);
|
const [queriedTotal, setQueriedTotal] = useState(0);
|
||||||
@ -94,15 +90,6 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
|
|||||||
handleQueryChange(query);
|
handleQueryChange(query);
|
||||||
}, [query, handleQueryChange]);
|
}, [query, handleQueryChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (query) {
|
|
||||||
setQuery(`${query} and ${addition}`);
|
|
||||||
} else {
|
|
||||||
setQuery(addition);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line
|
|
||||||
}, [addition]);
|
|
||||||
|
|
||||||
const ws = useRef(null);
|
const ws = useRef(null);
|
||||||
|
|
||||||
const listEntry = useRef(null);
|
const listEntry = useRef(null);
|
||||||
@ -117,11 +104,11 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
|
|||||||
}
|
}
|
||||||
ws.current = new WebSocket(MizuWebsocketURL);
|
ws.current = new WebSocket(MizuWebsocketURL);
|
||||||
ws.current.onopen = () => {
|
ws.current.onopen = () => {
|
||||||
setConnection(ConnectionStatus.Connected);
|
setWsConnection(WsConnectionStatus.Connected);
|
||||||
ws.current.send(query);
|
ws.current.send(query);
|
||||||
}
|
}
|
||||||
ws.current.onclose = () => {
|
ws.current.onclose = () => {
|
||||||
setConnection(ConnectionStatus.Closed);
|
setWsConnection(WsConnectionStatus.Closed);
|
||||||
}
|
}
|
||||||
ws.current.onerror = (event) => {
|
ws.current.onerror = (event) => {
|
||||||
console.error("WebSocket error:", event);
|
console.error("WebSocket error:", event);
|
||||||
@ -206,36 +193,9 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!focusedEntryId) return;
|
|
||||||
setSelectedEntryData(null);
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const entryData = await api.getEntry(focusedEntryId, query);
|
|
||||||
setSelectedEntryData(entryData);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.response?.data?.type) {
|
|
||||||
toast[error.response.data.type](`Entry[${focusedEntryId}]: ${error.response.data.msg}`, {
|
|
||||||
position: "bottom-right",
|
|
||||||
theme: "colored",
|
|
||||||
autoClose: error.response.data.autoClose,
|
|
||||||
hideProgressBar: false,
|
|
||||||
closeOnClick: true,
|
|
||||||
pauseOnHover: true,
|
|
||||||
draggable: true,
|
|
||||||
progress: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
// eslint-disable-next-line
|
|
||||||
}, [focusedEntryId]);
|
|
||||||
|
|
||||||
const toggleConnection = () => {
|
const toggleConnection = () => {
|
||||||
ws.current.close();
|
ws.current.close();
|
||||||
if (connection !== ConnectionStatus.Connected) {
|
if (wsConnection !== WsConnectionStatus.Connected) {
|
||||||
if (query) {
|
if (query) {
|
||||||
openWebSocket(`(${query}) and leftOff(-1)`, true);
|
openWebSocket(`(${query}) and leftOff(-1)`, true);
|
||||||
} else {
|
} else {
|
||||||
@ -248,8 +208,8 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
|
|||||||
|
|
||||||
const getConnectionStatusClass = (isContainer) => {
|
const getConnectionStatusClass = (isContainer) => {
|
||||||
const container = isContainer ? "Container" : "";
|
const container = isContainer ? "Container" : "";
|
||||||
switch (connection) {
|
switch (wsConnection) {
|
||||||
case ConnectionStatus.Connected:
|
case WsConnectionStatus.Connected:
|
||||||
return "greenIndicator" + container;
|
return "greenIndicator" + container;
|
||||||
default:
|
default:
|
||||||
return "redIndicator" + container;
|
return "redIndicator" + container;
|
||||||
@ -257,8 +217,8 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getConnectionTitle = () => {
|
const getConnectionTitle = () => {
|
||||||
switch (connection) {
|
switch (wsConnection) {
|
||||||
case ConnectionStatus.Connected:
|
case WsConnectionStatus.Connected:
|
||||||
return "streaming live traffic"
|
return "streaming live traffic"
|
||||||
default:
|
default:
|
||||||
return "streaming paused";
|
return "streaming paused";
|
||||||
@ -267,7 +227,7 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
|
|||||||
|
|
||||||
const onSnapBrokenEvent = () => {
|
const onSnapBrokenEvent = () => {
|
||||||
setIsSnappedToBottom(false);
|
setIsSnappedToBottom(false);
|
||||||
if (connection === ConnectionStatus.Connected) {
|
if (wsConnection === WsConnectionStatus.Connected) {
|
||||||
ws.current.close();
|
ws.current.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -275,9 +235,9 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
|
|||||||
return (
|
return (
|
||||||
<div className="TrafficPage">
|
<div className="TrafficPage">
|
||||||
<div className="TrafficPageHeader">
|
<div className="TrafficPageHeader">
|
||||||
<img className="playPauseIcon" style={{visibility: connection === ConnectionStatus.Connected ? "visible" : "hidden"}} alt="pause"
|
<img className="playPauseIcon" style={{visibility: wsConnection === WsConnectionStatus.Connected ? "visible" : "hidden"}} alt="pause"
|
||||||
src={pauseIcon} onClick={toggleConnection}/>
|
src={pauseIcon} onClick={toggleConnection}/>
|
||||||
<img className="playPauseIcon" style={{position: "absolute", visibility: connection === ConnectionStatus.Connected ? "hidden" : "visible"}} alt="play"
|
<img className="playPauseIcon" style={{position: "absolute", visibility: wsConnection === WsConnectionStatus.Connected ? "hidden" : "visible"}} alt="play"
|
||||||
src={playIcon} onClick={toggleConnection}/>
|
src={playIcon} onClick={toggleConnection}/>
|
||||||
<div className="connectionText">
|
<div className="connectionText">
|
||||||
{getConnectionTitle()}
|
{getConnectionTitle()}
|
||||||
@ -289,17 +249,12 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
|
|||||||
{<div className="TrafficPage-Container">
|
{<div className="TrafficPage-Container">
|
||||||
<div className="TrafficPage-ListContainer">
|
<div className="TrafficPage-ListContainer">
|
||||||
<Filters
|
<Filters
|
||||||
query={query}
|
|
||||||
setQuery={setQuery}
|
|
||||||
backgroundColor={queryBackgroundColor}
|
backgroundColor={queryBackgroundColor}
|
||||||
ws={ws.current}
|
ws={ws.current}
|
||||||
openWebSocket={openWebSocket}
|
openWebSocket={openWebSocket}
|
||||||
/>
|
/>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<EntriesList
|
<EntriesList
|
||||||
entries={entries}
|
|
||||||
setEntries={setEntries}
|
|
||||||
query={query}
|
|
||||||
listEntryREF={listEntry}
|
listEntryREF={listEntry}
|
||||||
onSnapBrokenEvent={onSnapBrokenEvent}
|
onSnapBrokenEvent={onSnapBrokenEvent}
|
||||||
isSnappedToBottom={isSnappedToBottom}
|
isSnappedToBottom={isSnappedToBottom}
|
||||||
@ -311,12 +266,8 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
|
|||||||
startTime={startTime}
|
startTime={startTime}
|
||||||
noMoreDataTop={noMoreDataTop}
|
noMoreDataTop={noMoreDataTop}
|
||||||
setNoMoreDataTop={setNoMoreDataTop}
|
setNoMoreDataTop={setNoMoreDataTop}
|
||||||
focusedEntryId={focusedEntryId}
|
|
||||||
setFocusedEntryId={setFocusedEntryId}
|
|
||||||
updateQuery={updateQuery}
|
|
||||||
leftOffTop={leftOffTop}
|
leftOffTop={leftOffTop}
|
||||||
setLeftOffTop={setLeftOffTop}
|
setLeftOffTop={setLeftOffTop}
|
||||||
isWebSocketConnectionClosed={connection === ConnectionStatus.Closed}
|
|
||||||
ws={ws.current}
|
ws={ws.current}
|
||||||
openWebSocket={openWebSocket}
|
openWebSocket={openWebSocket}
|
||||||
leftOffBottom={leftOffBottom}
|
leftOffBottom={leftOffBottom}
|
||||||
@ -327,10 +278,10 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({onTLSDetected, setAnaly
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.details}>
|
<div className={classes.details}>
|
||||||
{selectedEntryData && <EntryDetailed entryData={selectedEntryData} updateQuery={updateQuery}/>}
|
{focusedEntryId && <EntryDetailed/>}
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
{tappingStatus && <StatusBar tappingStatus={tappingStatus}/>}
|
{tappingStatus && <StatusBar/>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
@ -18,14 +18,12 @@ export interface ProtocolInterface {
|
|||||||
interface ProtocolProps {
|
interface ProtocolProps {
|
||||||
protocol: ProtocolInterface
|
protocol: ProtocolInterface
|
||||||
horizontal: boolean
|
horizontal: boolean
|
||||||
updateQuery: any
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Protocol: React.FC<ProtocolProps> = ({protocol, horizontal, updateQuery}) => {
|
const Protocol: React.FC<ProtocolProps> = ({protocol, horizontal}) => {
|
||||||
if (horizontal) {
|
if (horizontal) {
|
||||||
return <Queryable
|
return <Queryable
|
||||||
query={protocol.macro}
|
query={protocol.macro}
|
||||||
updateQuery={updateQuery}
|
|
||||||
displayIconOnMouseOver={true}
|
displayIconOnMouseOver={true}
|
||||||
>
|
>
|
||||||
<a target="_blank" rel="noopener noreferrer" href={protocol.referenceLink}>
|
<a target="_blank" rel="noopener noreferrer" href={protocol.referenceLink}>
|
||||||
@ -45,7 +43,6 @@ const Protocol: React.FC<ProtocolProps> = ({protocol, horizontal, updateQuery})
|
|||||||
} else {
|
} else {
|
||||||
return <Queryable
|
return <Queryable
|
||||||
query={protocol.macro}
|
query={protocol.macro}
|
||||||
updateQuery={updateQuery}
|
|
||||||
displayIconOnMouseOver={true}
|
displayIconOnMouseOver={true}
|
||||||
flipped={false}
|
flipped={false}
|
||||||
iconStyle={{marginTop: "52px", marginRight: "10px", zIndex: 1000}}
|
iconStyle={{marginTop: "52px", marginRight: "10px", zIndex: 1000}}
|
||||||
|
@ -2,10 +2,11 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||||
import AddCircleIcon from '@material-ui/icons/AddCircle';
|
import AddCircleIcon from '@material-ui/icons/AddCircle';
|
||||||
import './style/Queryable.sass';
|
import './style/Queryable.sass';
|
||||||
|
import {useRecoilState} from "recoil";
|
||||||
|
import queryAtom from "../../recoil/query";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
query: string,
|
query: string,
|
||||||
updateQuery: any,
|
|
||||||
style?: object,
|
style?: object,
|
||||||
iconStyle?: object,
|
iconStyle?: object,
|
||||||
className?: string,
|
className?: string,
|
||||||
@ -14,9 +15,10 @@ interface Props {
|
|||||||
flipped?: boolean,
|
flipped?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Queryable: React.FC<Props> = ({query, updateQuery, style, iconStyle, className, useTooltip= true, displayIconOnMouseOver = false, flipped = false, children}) => {
|
const Queryable: React.FC<Props> = ({query, style, iconStyle, className, useTooltip= true, displayIconOnMouseOver = false, flipped = false, children}) => {
|
||||||
const [showAddedNotification, setAdded] = useState(false);
|
const [showAddedNotification, setAdded] = useState(false);
|
||||||
const [showTooltip, setShowTooltip] = useState(false);
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
|
const [queryState, setQuery] = useRecoilState(queryAtom);
|
||||||
|
|
||||||
const onCopy = () => {
|
const onCopy = () => {
|
||||||
setAdded(true)
|
setAdded(true)
|
||||||
@ -25,13 +27,15 @@ const Queryable: React.FC<Props> = ({query, updateQuery, style, iconStyle, class
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timer;
|
let timer;
|
||||||
if (showAddedNotification) {
|
if (showAddedNotification) {
|
||||||
updateQuery(query);
|
setQuery(queryState ? `${queryState} and ${query}` : query);
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
setAdded(false);
|
setAdded(false);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [showAddedNotification, query, updateQuery]);
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [showAddedNotification, query, setQuery]);
|
||||||
|
|
||||||
const addButton = query ? <CopyToClipboard text={query} onCopy={onCopy}>
|
const addButton = query ? <CopyToClipboard text={query} onCopy={onCopy}>
|
||||||
<span
|
<span
|
||||||
@ -39,7 +43,7 @@ const Queryable: React.FC<Props> = ({query, updateQuery, style, iconStyle, class
|
|||||||
title={`Add "${query}" to the filter`}
|
title={`Add "${query}" to the filter`}
|
||||||
style={iconStyle}
|
style={iconStyle}
|
||||||
>
|
>
|
||||||
<AddCircleIcon fontSize="small" color="inherit"></AddCircleIcon>
|
<AddCircleIcon fontSize="small" color="inherit"/>
|
||||||
{showAddedNotification && <span className={'Queryable-AddNotifier'}>Added</span>}
|
{showAddedNotification && <span className={'Queryable-AddNotifier'}>Added</span>}
|
||||||
</span>
|
</span>
|
||||||
</CopyToClipboard> : null;
|
</CopyToClipboard> : null;
|
||||||
|
@ -3,33 +3,18 @@ import React, {useState} from "react";
|
|||||||
import warningIcon from '../assets/warning_icon.svg';
|
import warningIcon from '../assets/warning_icon.svg';
|
||||||
import failIcon from '../assets/failed.svg';
|
import failIcon from '../assets/failed.svg';
|
||||||
import successIcon from '../assets/success.svg';
|
import successIcon from '../assets/success.svg';
|
||||||
|
import {useRecoilValue} from "recoil";
|
||||||
export interface TappingStatusPod {
|
import tappingStatusAtom, {tappingStatusDetails} from "../../recoil/tappingStatus";
|
||||||
name: string;
|
|
||||||
namespace: string;
|
|
||||||
isTapped: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TappingStatus {
|
|
||||||
pods: TappingStatusPod[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
tappingStatus: TappingStatusPod[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const pluralize = (noun: string, amount: number) => {
|
const pluralize = (noun: string, amount: number) => {
|
||||||
return `${noun}${amount !== 1 ? 's' : ''}`
|
return `${noun}${amount !== 1 ? 's' : ''}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StatusBar: React.FC<Props> = ({tappingStatus}) => {
|
export const StatusBar = () => {
|
||||||
|
|
||||||
|
const tappingStatus = useRecoilValue(tappingStatusAtom);
|
||||||
const [expandedBar, setExpandedBar] = useState(false);
|
const [expandedBar, setExpandedBar] = useState(false);
|
||||||
|
const {uniqueNamespaces, amountOfPods, amountOfTappedPods, amountOfUntappedPods} = useRecoilValue(tappingStatusDetails);
|
||||||
const uniqueNamespaces = Array.from(new Set(tappingStatus.map(pod => pod.namespace)));
|
|
||||||
const amountOfPods = tappingStatus.length;
|
|
||||||
const amountOfTappedPods = tappingStatus.filter(pod => pod.isTapped).length;
|
|
||||||
const amountOfUntappedPods = amountOfPods - amountOfTappedPods;
|
|
||||||
|
|
||||||
return <div className={'statusBar' + (expandedBar ? ' expandedStatusBar' : "")} onMouseOver={() => setExpandedBar(true)} onMouseLeave={() => setExpandedBar(false)}>
|
return <div className={'statusBar' + (expandedBar ? ' expandedStatusBar' : "")} onMouseOver={() => setExpandedBar(true)} onMouseLeave={() => setExpandedBar(false)}>
|
||||||
<div className="podsCount">
|
<div className="podsCount">
|
||||||
|
@ -10,16 +10,14 @@ export enum StatusCodeClassification {
|
|||||||
|
|
||||||
interface EntryProps {
|
interface EntryProps {
|
||||||
statusCode: number
|
statusCode: number
|
||||||
updateQuery: any
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusCode: React.FC<EntryProps> = ({statusCode, updateQuery}) => {
|
const StatusCode: React.FC<EntryProps> = ({statusCode}) => {
|
||||||
|
|
||||||
const classification = getClassification(statusCode)
|
const classification = getClassification(statusCode)
|
||||||
|
|
||||||
return <Queryable
|
return <Queryable
|
||||||
query={`response.status == ${statusCode}`}
|
query={`response.status == ${statusCode}`}
|
||||||
updateQuery={updateQuery}
|
|
||||||
displayIconOnMouseOver={true}
|
displayIconOnMouseOver={true}
|
||||||
flipped={true}
|
flipped={true}
|
||||||
iconStyle={{marginTop: "40px", paddingLeft: "10px"}}
|
iconStyle={{marginTop: "40px", paddingLeft: "10px"}}
|
||||||
|
@ -6,16 +6,14 @@ import Queryable from "./Queryable";
|
|||||||
interface SummaryProps {
|
interface SummaryProps {
|
||||||
method: string
|
method: string
|
||||||
summary: string
|
summary: string
|
||||||
updateQuery: any
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Summary: React.FC<SummaryProps> = ({method, summary, updateQuery}) => {
|
export const Summary: React.FC<SummaryProps> = ({method, summary}) => {
|
||||||
|
|
||||||
return <div className={styles.container}>
|
return <div className={styles.container}>
|
||||||
{method && <Queryable
|
{method && <Queryable
|
||||||
query={`method == "${method}"`}
|
query={`method == "${method}"`}
|
||||||
className={`${miscStyles.protocol} ${miscStyles.method}`}
|
className={`${miscStyles.protocol} ${miscStyles.method}`}
|
||||||
updateQuery={updateQuery}
|
|
||||||
displayIconOnMouseOver={true}
|
displayIconOnMouseOver={true}
|
||||||
style={{whiteSpace: "nowrap"}}
|
style={{whiteSpace: "nowrap"}}
|
||||||
>
|
>
|
||||||
@ -25,7 +23,6 @@ export const Summary: React.FC<SummaryProps> = ({method, summary, updateQuery})
|
|||||||
</Queryable>}
|
</Queryable>}
|
||||||
{summary && <Queryable
|
{summary && <Queryable
|
||||||
query={`summary == "${summary}"`}
|
query={`summary == "${summary}"`}
|
||||||
updateQuery={updateQuery}
|
|
||||||
displayIconOnMouseOver={true}
|
displayIconOnMouseOver={true}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -5,23 +5,26 @@ import App from './App';
|
|||||||
import EntApp from "./EntApp";
|
import EntApp from "./EntApp";
|
||||||
import {ToastContainer} from "react-toastify";
|
import {ToastContainer} from "react-toastify";
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
import {RecoilRoot} from "recoil";
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<>
|
<RecoilRoot>
|
||||||
{window["isEnt"] ? <EntApp/> : <App/>}
|
<>
|
||||||
<ToastContainer
|
{window["isEnt"] ? <EntApp/> : <App/>}
|
||||||
position="bottom-right"
|
<ToastContainer
|
||||||
autoClose={5000}
|
position="bottom-right"
|
||||||
hideProgressBar={false}
|
autoClose={5000}
|
||||||
newestOnTop={false}
|
hideProgressBar={false}
|
||||||
closeOnClick
|
newestOnTop={false}
|
||||||
rtl={false}
|
closeOnClick
|
||||||
pauseOnFocusLoss
|
rtl={false}
|
||||||
draggable
|
pauseOnFocusLoss
|
||||||
pauseOnHover
|
draggable
|
||||||
/>
|
pauseOnHover
|
||||||
</>
|
/>
|
||||||
|
</>
|
||||||
|
</RecoilRoot>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
|
8
ui/src/recoil/entPage/atom.ts
Normal file
8
ui/src/recoil/entPage/atom.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { atom } from "recoil"
|
||||||
|
|
||||||
|
const entPageAtom = atom({
|
||||||
|
key: "entPageAtom",
|
||||||
|
default: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
export default entPageAtom
|
11
ui/src/recoil/entPage/index.ts
Normal file
11
ui/src/recoil/entPage/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import atom from "./atom";
|
||||||
|
|
||||||
|
enum Page {
|
||||||
|
Traffic,
|
||||||
|
Setup,
|
||||||
|
Login
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Page };
|
||||||
|
|
||||||
|
export default atom;
|
8
ui/src/recoil/entries/atom.ts
Normal file
8
ui/src/recoil/entries/atom.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { atom } from "recoil";
|
||||||
|
|
||||||
|
const entriesAtom = atom({
|
||||||
|
key: "entriesAtom",
|
||||||
|
default: []
|
||||||
|
});
|
||||||
|
|
||||||
|
export default entriesAtom;
|
3
ui/src/recoil/entries/index.ts
Normal file
3
ui/src/recoil/entries/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import atom from "./atom";
|
||||||
|
|
||||||
|
export default atom
|
8
ui/src/recoil/focusedEntryId/atom.ts
Normal file
8
ui/src/recoil/focusedEntryId/atom.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { atom } from "recoil";
|
||||||
|
|
||||||
|
const focusedEntryIdAtom = atom({
|
||||||
|
key: "focusedEntryIdAtom",
|
||||||
|
default: null
|
||||||
|
});
|
||||||
|
|
||||||
|
export default focusedEntryIdAtom;
|
3
ui/src/recoil/focusedEntryId/index.ts
Normal file
3
ui/src/recoil/focusedEntryId/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import atom from "./atom";
|
||||||
|
|
||||||
|
export default atom
|
8
ui/src/recoil/query/atom.ts
Normal file
8
ui/src/recoil/query/atom.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { atom } from "recoil";
|
||||||
|
|
||||||
|
const queryAtom = atom({
|
||||||
|
key: "queryAtom",
|
||||||
|
default: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
export default queryAtom;
|
3
ui/src/recoil/query/index.ts
Normal file
3
ui/src/recoil/query/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import atom from "./atom";
|
||||||
|
|
||||||
|
export default atom;
|
9
ui/src/recoil/tappingStatus/atom.ts
Normal file
9
ui/src/recoil/tappingStatus/atom.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { atom } from "recoil";
|
||||||
|
import {TappingStatusPod} from "./index";
|
||||||
|
|
||||||
|
const tappingStatusAtom = atom({
|
||||||
|
key: "tappingStatusAtom",
|
||||||
|
default: null as TappingStatusPod[]
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tappingStatusAtom;
|
22
ui/src/recoil/tappingStatus/details.ts
Normal file
22
ui/src/recoil/tappingStatus/details.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import {selector} from "recoil";
|
||||||
|
import tappingStatusAtom from "./atom";
|
||||||
|
|
||||||
|
const tappingStatusDetails = selector({
|
||||||
|
key: 'tappingStatusDetails',
|
||||||
|
get: ({get}) => {
|
||||||
|
const tappingStatus = get(tappingStatusAtom);
|
||||||
|
const uniqueNamespaces = Array.from(new Set(tappingStatus.map(pod => pod.namespace)));
|
||||||
|
const amountOfPods = tappingStatus.length;
|
||||||
|
const amountOfTappedPods = tappingStatus.filter(pod => pod.isTapped).length;
|
||||||
|
const amountOfUntappedPods = amountOfPods - amountOfTappedPods;
|
||||||
|
|
||||||
|
return {
|
||||||
|
uniqueNamespaces,
|
||||||
|
amountOfPods,
|
||||||
|
amountOfTappedPods,
|
||||||
|
amountOfUntappedPods,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tappingStatusDetails;
|
17
ui/src/recoil/tappingStatus/index.ts
Normal file
17
ui/src/recoil/tappingStatus/index.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import atom from "./atom";
|
||||||
|
import tappingStatusDetails from './details';
|
||||||
|
|
||||||
|
interface TappingStatusPod {
|
||||||
|
name: string;
|
||||||
|
namespace: string;
|
||||||
|
isTapped: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TappingStatus {
|
||||||
|
pods: TappingStatusPod[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type {TappingStatus, TappingStatusPod};
|
||||||
|
export {tappingStatusDetails};
|
||||||
|
|
||||||
|
export default atom;
|
8
ui/src/recoil/wsConnection/atom.ts
Normal file
8
ui/src/recoil/wsConnection/atom.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { atom } from "recoil";
|
||||||
|
|
||||||
|
const wsConnectionAtom = atom({
|
||||||
|
key: "wsConnectionAtom",
|
||||||
|
default: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
export default wsConnectionAtom;
|
10
ui/src/recoil/wsConnection/index.ts
Normal file
10
ui/src/recoil/wsConnection/index.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import atom from "./atom";
|
||||||
|
|
||||||
|
enum WsConnectionStatus {
|
||||||
|
Closed,
|
||||||
|
Connected,
|
||||||
|
}
|
||||||
|
|
||||||
|
export {WsConnectionStatus};
|
||||||
|
|
||||||
|
export default atom
|
Loading…
Reference in New Issue
Block a user