From b94098fea6ecd201d4c4b9457d308584df25395d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=2E=20Mert=20Y=C4=B1ld=C4=B1ran?= Date: Sat, 15 Jan 2022 20:43:22 +0300 Subject: [PATCH 01/15] Sort key-value pairs on client-side in `EntryTableSection` (#645) --- .../components/EntryDetailed/EntrySections.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/ui/src/components/EntryDetailed/EntrySections.tsx b/ui/src/components/EntryDetailed/EntrySections.tsx index 9a777705f..ec06de46b 100644 --- a/ui/src/components/EntryDetailed/EntrySections.tsx +++ b/ui/src/components/EntryDetailed/EntrySections.tsx @@ -205,13 +205,27 @@ interface EntrySectionProps { } export const EntryTableSection: React.FC = ({title, color, arrayToIterate, updateQuery}) => { + 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 { arrayToIterate && arrayToIterate.length > 0 ? - {arrayToIterate.map(({name, value, selector}, index) => Date: Sat, 15 Jan 2022 20:46:10 +0300 Subject: [PATCH 02/15] Display Redis value as `EntryBodySection` (#644) --- tap/extensions/redis/helpers.go | 12 +++++++----- ui/src/components/EntryDetailed/EntrySections.tsx | 4 +++- ui/src/components/EntryDetailed/EntryViewer.tsx | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/tap/extensions/redis/helpers.go b/tap/extensions/redis/helpers.go index 3f8b61791..1661d1e0b 100644 --- a/tap/extensions/redis/helpers.go +++ b/tap/extensions/redis/helpers.go @@ -42,11 +42,6 @@ func representGeneric(generic map[string]interface{}, selectorPrefix string) (re Value: generic["key"].(string), Selector: fmt.Sprintf("%skey", selectorPrefix), }, - { - Name: "Value", - Value: generic["value"].(string), - Selector: fmt.Sprintf("%svalue", selectorPrefix), - }, { Name: "Keyword", Value: generic["keyword"].(string), @@ -59,5 +54,12 @@ func representGeneric(generic map[string]interface{}, selectorPrefix string) (re Data: string(details), }) + representation = append(representation, api.SectionData{ + Type: api.BODY, + Title: "Value", + Data: generic["value"].(string), + Selector: fmt.Sprintf("%svalue", selectorPrefix), + }) + return } diff --git a/ui/src/components/EntryDetailed/EntrySections.tsx b/ui/src/components/EntryDetailed/EntrySections.tsx index ec06de46b..6d871bb75 100644 --- a/ui/src/components/EntryDetailed/EntrySections.tsx +++ b/ui/src/components/EntryDetailed/EntrySections.tsx @@ -107,6 +107,7 @@ export const EntrySectionContainer: React.FC = ({tit } interface EntryBodySectionProps { + title: string, content: any, color: string, updateQuery: any, @@ -116,6 +117,7 @@ interface EntryBodySectionProps { } export const EntryBodySection: React.FC = ({ + title, color, updateQuery, content, @@ -167,7 +169,7 @@ export const EntryBodySection: React.FC = ({ return {content && content?.length > 0 && = ({data, color, updateQuery}) => { break; case SectionTypes.SectionBody: sections.push( - + ) break; default: From 4db8e8902be80c6cb4a4bb04c2c39ed4a2b8c3b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=2E=20Mert=20Y=C4=B1ld=C4=B1ran?= Date: Sun, 16 Jan 2022 09:36:29 +0300 Subject: [PATCH 03/15] Add support of displaying nested data structures of Kafka in the right-pane (#643) * Handle nested `topicData` in `representProduceRequest` * Handle nested `topics` in `representCreateTopicsRequest` and `representCreateTopicsResponse` * Handle nested `responses` in `representProduceResponse` * Handle nested `topics` in `representFetchRequest` and nested `responses` in `representFetchResponse` * Introduce `ignoreKeys` argument to `representMapAsTable` and ignore the keys based on that argument * Bring back the `nil` checks --- tap/extensions/kafka/go.mod | 2 + tap/extensions/kafka/go.sum | 4 + tap/extensions/kafka/helpers.go | 277 +++++++++++++++++++++++++------- 3 files changed, 222 insertions(+), 61 deletions(-) diff --git a/tap/extensions/kafka/go.mod b/tap/extensions/kafka/go.mod index 113627f94..cdb75609a 100644 --- a/tap/extensions/kafka/go.mod +++ b/tap/extensions/kafka/go.mod @@ -3,6 +3,8 @@ module github.com/up9inc/mizu/tap/extensions/kafka go 1.16 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/up9inc/mizu/tap/api v0.0.0 ) diff --git a/tap/extensions/kafka/go.sum b/tap/extensions/kafka/go.sum index 70ec03308..e3d62474b 100644 --- a/tap/extensions/kafka/go.sum +++ b/tap/extensions/kafka/go.sum @@ -1,6 +1,8 @@ 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/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/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= 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/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 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/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/tap/extensions/kafka/helpers.go b/tap/extensions/kafka/helpers.go index dffe30bd8..68264c077 100644 --- a/tap/extensions/kafka/helpers.go +++ b/tap/extensions/kafka/helpers.go @@ -3,8 +3,13 @@ package main import ( "encoding/json" "fmt" + "reflect" "strconv" + "strings" + "github.com/fatih/camelcase" + "github.com/ohler55/ojg/jp" + "github.com/ohler55/ojg/oj" "github.com/up9inc/mizu/tap/api" ) @@ -289,17 +294,12 @@ func representProduceRequest(data map[string]interface{}) []interface{} { rep = representRequestHeader(data, rep) payload := data["payload"].(map[string]interface{}) - topicData := "" - _topicData := payload["topicData"] - if _topicData != nil { - x, _ := json.Marshal(_topicData.([]interface{})) - topicData = string(x) - } + topicData := payload["topicData"] transactionalID := "" if payload["transactionalID"] != nil { transactionalID = payload["transactionalID"].(string) } - repPayload, _ := json.Marshal([]api.TableData{ + repTransactionDetails, _ := json.Marshal([]api.TableData{ { Name: "Transactional ID", Value: transactionalID, @@ -315,18 +315,73 @@ func representProduceRequest(data map[string]interface{}) []interface{} { Value: fmt.Sprintf("%d", int(payload["timeout"].(float64))), Selector: `request.payload.timeout`, }, - { - Name: "Topic Data", - Value: topicData, - Selector: `request.payload.topicData`, - }, }) rep = append(rep, api.SectionData{ Type: api.TABLE, - Title: "Payload", - Data: string(repPayload), + Title: "Transaction Details", + 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 } @@ -336,21 +391,12 @@ func representProduceResponse(data map[string]interface{}) []interface{} { rep = representResponseHeader(data, rep) payload := data["payload"].(map[string]interface{}) - responses := "" - if payload["responses"] != nil { - _responses, _ := json.Marshal(payload["responses"].([]interface{})) - responses = string(_responses) - } + responses := payload["responses"] throttleTimeMs := "" if payload["throttleTimeMs"] != nil { throttleTimeMs = fmt.Sprintf("%d", int(payload["throttleTimeMs"].(float64))) } repPayload, _ := json.Marshal([]api.TableData{ - { - Name: "Responses", - Value: string(responses), - Selector: `response.payload.responses`, - }, { Name: "Throttle Time (ms)", Value: throttleTimeMs, @@ -359,10 +405,31 @@ func representProduceResponse(data map[string]interface{}) []interface{} { }) rep = append(rep, api.SectionData{ Type: api.TABLE, - Title: "Payload", + Title: "Transaction Details", 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 } @@ -372,11 +439,7 @@ func representFetchRequest(data map[string]interface{}) []interface{} { rep = representRequestHeader(data, rep) payload := data["payload"].(map[string]interface{}) - topics := "" - if payload["topics"] != nil { - _topics, _ := json.Marshal(payload["topics"].([]interface{})) - topics = string(_topics) - } + topics := payload["topics"] replicaId := "" if payload["replicaId"] != nil { replicaId = fmt.Sprintf("%d", int(payload["replicaId"].(float64))) @@ -442,11 +505,6 @@ func representFetchRequest(data map[string]interface{}) []interface{} { Value: sessionEpoch, Selector: `request.payload.sessionEpoch`, }, - { - Name: "Topics", - Value: topics, - Selector: `request.payload.topics`, - }, { Name: "Forgotten Topics Data", Value: forgottenTopicsData, @@ -460,10 +518,26 @@ func representFetchRequest(data map[string]interface{}) []interface{} { }) rep = append(rep, api.SectionData{ Type: api.TABLE, - Title: "Payload", + Title: "Transaction Details", 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 } @@ -473,11 +547,7 @@ func representFetchResponse(data map[string]interface{}) []interface{} { rep = representResponseHeader(data, rep) payload := data["payload"].(map[string]interface{}) - responses := "" - if payload["responses"] != nil { - _responses, _ := json.Marshal(payload["responses"].([]interface{})) - responses = string(_responses) - } + responses := payload["responses"] throttleTimeMs := "" if payload["throttleTimeMs"] != nil { throttleTimeMs = fmt.Sprintf("%d", int(payload["throttleTimeMs"].(float64))) @@ -506,18 +576,56 @@ func representFetchResponse(data map[string]interface{}) []interface{} { Value: sessionId, Selector: `response.payload.sessionId`, }, - { - Name: "Responses", - Value: responses, - Selector: `response.payload.responses`, - }, }) rep = append(rep, api.SectionData{ Type: api.TABLE, - Title: "Payload", + Title: "Transaction Details", 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 } @@ -591,17 +699,11 @@ func representCreateTopicsRequest(data map[string]interface{}) []interface{} { rep = representRequestHeader(data, rep) payload := data["payload"].(map[string]interface{}) - topics, _ := json.Marshal(payload["topics"].([]interface{})) validateOnly := "" if payload["validateOnly"] != nil { validateOnly = strconv.FormatBool(payload["validateOnly"].(bool)) } repPayload, _ := json.Marshal([]api.TableData{ - { - Name: "Topics", - Value: string(topics), - Selector: `request.payload.topics`, - }, { Name: "Timeout (ms)", Value: fmt.Sprintf("%d", int(payload["timeoutMs"].(float64))), @@ -615,10 +717,20 @@ func representCreateTopicsRequest(data map[string]interface{}) []interface{} { }) rep = append(rep, api.SectionData{ Type: api.TABLE, - Title: "Payload", + Title: "Transaction Details", 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 } @@ -628,7 +740,6 @@ func representCreateTopicsResponse(data map[string]interface{}) []interface{} { rep = representResponseHeader(data, rep) payload := data["payload"].(map[string]interface{}) - topics, _ := json.Marshal(payload["topics"].([]interface{})) throttleTimeMs := "" if payload["throttleTimeMs"] != nil { throttleTimeMs = fmt.Sprintf("%d", int(payload["throttleTimeMs"].(float64))) @@ -639,18 +750,23 @@ func representCreateTopicsResponse(data map[string]interface{}) []interface{} { Value: throttleTimeMs, Selector: `response.payload.throttleTimeMs`, }, - { - Name: "Topics", - Value: string(topics), - Selector: `response.payload.topics`, - }, }) rep = append(rep, api.SectionData{ Type: api.TABLE, - Title: "Payload", + Title: "Transaction Details", 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 } @@ -727,3 +843,42 @@ func representDeleteTopicsResponse(data map[string]interface{}) []interface{} { 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 +} From 59fbe4c479cba2bd5f9bf49b6d5e3669b3978016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=2E=20Mert=20Y=C4=B1ld=C4=B1ran?= Date: Sun, 16 Jan 2022 09:43:52 +0300 Subject: [PATCH 04/15] Show HTTP path segments as a list of strings in the right pane (#641) * Show HTTP path segments as a list of strings in the right pane * Use better variable names --- tap/extensions/http/helpers.go | 25 +++++++++++++++++++++---- tap/extensions/http/main.go | 11 +++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/tap/extensions/http/helpers.go b/tap/extensions/http/helpers.go index 9348ea3cd..9bf15fca1 100644 --- a/tap/extensions/http/helpers.go +++ b/tap/extensions/http/helpers.go @@ -3,14 +3,15 @@ package main import ( "encoding/json" "fmt" + "strconv" "github.com/up9inc/mizu/tap/api" ) func mapSliceRebuildAsMap(mapSlice []interface{}) (newMap map[string]interface{}) { newMap = make(map[string]interface{}) - for _, header := range mapSlice { - h := header.(map[string]interface{}) + for _, item := range mapSlice { + h := item.(map[string]interface{}) 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) { var table []api.TableData - for _, header := range mapSlice { - h := header.(map[string]interface{}) + for _, item := range mapSlice { + h := item.(map[string]interface{}) selector := fmt.Sprintf("%s[\"%s\"]", selectorPrefix, h["name"].(string)) table = append(table, api.TableData{ Name: h["name"].(string), @@ -33,3 +34,19 @@ func representMapSliceAsTable(mapSlice []interface{}, selectorPrefix string) (re representation = string(obj) 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 +} diff --git a/tap/extensions/http/main.go b/tap/extensions/http/main.go index b443867de..cf6a905f5 100644 --- a/tap/extensions/http/main.go +++ b/tap/extensions/http/main.go @@ -9,6 +9,7 @@ import ( "log" "net/http" "net/url" + "strings" "time" "github.com/up9inc/mizu/tap/api" @@ -209,6 +210,7 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string, request["url"] = reqDetails["url"].(string) reqDetails["targetUri"] = reqDetails["url"] reqDetails["path"] = path + reqDetails["pathSegments"] = strings.Split(path, "/")[1:] reqDetails["summary"] = path // Rearrange the maps for the querying @@ -296,6 +298,15 @@ func representRequest(request map[string]interface{}) (repRequest []interface{}) 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{ Type: api.TABLE, Title: "Headers", From 20d69228d3af9cd9ca7044dcecb0b5a935f7732d Mon Sep 17 00:00:00 2001 From: Adam Kol <93466081+AdamKol-up9@users.noreply.github.com> Date: Sun, 16 Jan 2022 09:57:14 +0200 Subject: [PATCH 05/15] Cypress: new Redact and NoRedact tests (#618) --- acceptanceTests/cypress.json | 3 + .../cypress/integration/tests/RedactTests.js | 23 ++++ acceptanceTests/tap_test.go | 109 +----------------- 3 files changed, 28 insertions(+), 107 deletions(-) create mode 100644 acceptanceTests/cypress/integration/tests/RedactTests.js diff --git a/acceptanceTests/cypress.json b/acceptanceTests/cypress.json index 48150bbb3..44ae98c89 100644 --- a/acceptanceTests/cypress.json +++ b/acceptanceTests/cypress.json @@ -4,10 +4,13 @@ "viewportHeight": 1080, "video": false, "screenshotOnRunFailure": false, + "testFiles": ["tests/GuiPort.js", "tests/MultipleNamespaces.js", + "tests/RedactTests.js", "tests/Regex.js"], + "env": { "testUrl": "http://localhost:8899/" } diff --git a/acceptanceTests/cypress/integration/tests/RedactTests.js b/acceptanceTests/cypress/integration/tests/RedactTests.js new file mode 100644 index 000000000..7005eeebf --- /dev/null +++ b/acceptanceTests/cypress/integration/tests/RedactTests.js @@ -0,0 +1,23 @@ +const inHeader = 'User-Header[REDACTED]'; +const inBody = '{ "User": "[REDACTED]" }'; +const shouldExist = Cypress.env('shouldExist'); + +it('Loading Mizu', function () { + cy.visit(Cypress.env('testUrl')); +}) + +it(`should ${shouldExist ? '' : 'not'} include ${inHeader}`, function () { + cy.get('.CollapsibleContainer', { timeout : 15 * 1000}).first().next().then(headerElements => { //TODO change the path and refactor the body and head functions + const allText = headerElements.text(); + if (allText.includes(inHeader) !== shouldExist) + throw new Error(`The headers panel doesnt include ${inHeader}`); + }); +}); + +it(`should ${shouldExist ? '' : 'not'} include ${inBody}`, function () { + cy.get('.hljs').then(bodyElement => { + const line = bodyElement.text(); + if (line.includes(inBody) !== shouldExist) + throw new Error(`The body panel doesnt include ${inBody}`); + }); +}); diff --git a/acceptanceTests/tap_test.go b/acceptanceTests/tap_test.go index d8f309d11..381c5ed0f 100644 --- a/acceptanceTests/tap_test.go +++ b/acceptanceTests/tap_test.go @@ -3,7 +3,6 @@ package acceptanceTests import ( "archive/zip" "bytes" - "encoding/json" "fmt" "io/ioutil" "net/http" @@ -378,59 +377,7 @@ func TestTapRedact(t *testing.T) { } } - redactCheckFunc := func() error { - 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 - } + runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/RedactTests.js\" --env shouldExist=true")) } func TestTapNoRedact(t *testing.T) { @@ -482,59 +429,7 @@ func TestTapNoRedact(t *testing.T) { } } - redactCheckFunc := func() error { - 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 - } + runCypressTests(t, "npx cypress run --spec \"cypress/integration/tests/RedactTests.js\" --env shouldExist=false") } func TestTapRegexMasking(t *testing.T) { From ae1bcf4c0c6bdbf0113026a0025c367d65e5b6d0 Mon Sep 17 00:00:00 2001 From: RoyUP9 <87927115+RoyUP9@users.noreply.github.com> Date: Sun, 16 Jan 2022 11:48:22 +0200 Subject: [PATCH 06/15] Added api server timeout env for install and tap (#647) --- cli/cmd/installRunner.go | 3 ++- cli/cmd/tapRunner.go | 4 +++- cli/config/envConfig.go | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cli/cmd/installRunner.go b/cli/cmd/installRunner.go index b4912e7ce..2802bc011 100644 --- a/cli/cmd/installRunner.go +++ b/cli/cmd/installRunner.go @@ -101,7 +101,8 @@ func watchApiServerPodReady(ctx context.Context, kubernetesProvider *kubernetes. podWatchHelper := kubernetes.NewPodWatchHelper(kubernetesProvider, podExactRegex) 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 { select { case wEvent, ok := <-eventChan: diff --git a/cli/cmd/tapRunner.go b/cli/cmd/tapRunner.go index 3210acc9a..fec222338 100644 --- a/cli/cmd/tapRunner.go +++ b/cli/cmd/tapRunner.go @@ -306,7 +306,9 @@ func watchApiServerPod(ctx context.Context, kubernetesProvider *kubernetes.Provi podWatchHelper := kubernetes.NewPodWatchHelper(kubernetesProvider, podExactRegex) eventChan, errorChan := kubernetes.FilteredWatch(ctx, podWatchHelper, []string{config.Config.MizuResourcesNamespace}, podWatchHelper) isPodReady := false - timeAfter := time.After(25 * time.Second) + + apiServerTimeoutSec := config.GetIntEnvConfig(config.ApiServerTimeoutSec, 120) + timeAfter := time.After(time.Duration(apiServerTimeoutSec) * time.Second) for { select { case wEvent, ok := <-eventChan: diff --git a/cli/config/envConfig.go b/cli/config/envConfig.go index 78f91e00b..acbe5e1ab 100644 --- a/cli/config/envConfig.go +++ b/cli/config/envConfig.go @@ -7,6 +7,7 @@ import ( const ( ApiServerRetries = "API_SERVER_RETRIES" + ApiServerTimeoutSec = "API_SERVER_TIMEOUT_SEC" ) func GetIntEnvConfig(key string, defaultValue int) int { From e15eb71b77d6614d48ee55d33144a91e6c31584c Mon Sep 17 00:00:00 2001 From: Igor Gov Date: Sun, 16 Jan 2022 11:50:40 +0200 Subject: [PATCH 07/15] Fix: no panic on failure to sync entries to up9.app (#648) --- agent/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/main.go b/agent/main.go index 359caa272..33e7eaa27 100644 --- a/agent/main.go +++ b/agent/main.go @@ -128,7 +128,7 @@ func main() { syncEntriesConfig := getSyncEntriesConfig() if syncEntriesConfig != 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) } } From dacdb69164272d2120a555fe99ac326368a648fd Mon Sep 17 00:00:00 2001 From: Alex Haiut Date: Sun, 16 Jan 2022 13:15:19 +0200 Subject: [PATCH 08/15] added CHANGELOG and updated release README template (#650) --- cli/README.md.TEMPLATE | 3 ++- docs/CHANGELOG.md | 43 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 docs/CHANGELOG.md diff --git a/cli/README.md.TEMPLATE b/cli/README.md.TEMPLATE index ed922a8c8..cc6960e16 100644 --- a/cli/README.md.TEMPLATE +++ b/cli/README.md.TEMPLATE @@ -1,6 +1,7 @@ # 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) ``` diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 100644 index 000000000..f56ad4038 --- /dev/null +++ b/docs/CHANGELOG.md @@ -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) From aae03c52e993fd9353f07eb7896a858f279b5fa4 Mon Sep 17 00:00:00 2001 From: Adam Kol <93466081+AdamKol-up9@users.noreply.github.com> Date: Sun, 16 Jan 2022 14:17:18 +0200 Subject: [PATCH 09/15] UI important identifier (#652) --- ui/src/components/EntryDetailed/EntrySections.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/components/EntryDetailed/EntrySections.tsx b/ui/src/components/EntryDetailed/EntrySections.tsx index 6d871bb75..8122aaaf1 100644 --- a/ui/src/components/EntryDetailed/EntrySections.tsx +++ b/ui/src/components/EntryDetailed/EntrySections.tsx @@ -226,7 +226,7 @@ export const EntryTableSection: React.FC = ({title, color, ar arrayToIterate && arrayToIterate.length > 0 ?
- + {arrayToIterateSorted.map(({name, value, selector}, index) => Date: Sun, 16 Jan 2022 14:58:18 +0200 Subject: [PATCH 10/15] Adding badges: latest release, license, slack (#653) --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index aa50a5bbb..b071f6390 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,17 @@ ![Mizu: The API Traffic Viewer for Kubernetes](assets/mizu-logo.svg) +

+ + GitHub Latest Release + + + GitHub License + + + Slack + +

+ # 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. From 5fed5808d228553c07e053df77b96f8da7349fc8 Mon Sep 17 00:00:00 2001 From: lirazyehezkel <61656597+lirazyehezkel@users.noreply.github.com> Date: Sun, 16 Jan 2022 15:27:09 +0200 Subject: [PATCH 11/15] TRA-4159 Mizu state management (#631) * initiate recoil state management with entPage and tappingStatus * first recoil selector * insert entries and focusedEntryId into recoil * ws connection, entry data * manage query by recoil * identifier for cypress * conflicts fix * css fix * cr fixes Co-authored-by: gadotroee <55343099+gadotroee@users.noreply.github.com> --- ui/package-lock.json | 13 +++ ui/package.json | 1 + ui/src/EntApp.tsx | 52 ++++------ ui/src/components/EntriesList.tsx | 30 +++--- ui/src/components/EntryDetailed.tsx | 66 +++++++++---- .../EntryDetailed/EntrySections.tsx | 24 ++--- .../components/EntryDetailed/EntryViewer.tsx | 22 ++--- .../EntryListItem/EntryListItem.module.sass | 1 - .../EntryListItem/EntryListItem.tsx | 34 +++---- ui/src/components/Filters.tsx | 13 +-- ui/src/components/Header/EntHeader.tsx | 9 +- ui/src/components/InstallPage.tsx | 9 +- ui/src/components/LoginPage.tsx | 9 +- ui/src/components/TrafficPage.tsx | 95 +++++-------------- ui/src/components/UI/Protocol.tsx | 5 +- ui/src/components/UI/Queryable.tsx | 14 ++- ui/src/components/UI/StatusBar.tsx | 25 +---- ui/src/components/UI/StatusCode.tsx | 4 +- ui/src/components/UI/Summary.tsx | 5 +- ui/src/index.tsx | 31 +++--- ui/src/recoil/entPage/atom.ts | 8 ++ ui/src/recoil/entPage/index.ts | 11 +++ ui/src/recoil/entries/atom.ts | 8 ++ ui/src/recoil/entries/index.ts | 3 + ui/src/recoil/focusedEntryId/atom.ts | 8 ++ ui/src/recoil/focusedEntryId/index.ts | 3 + ui/src/recoil/query/atom.ts | 8 ++ ui/src/recoil/query/index.ts | 3 + ui/src/recoil/tappingStatus/atom.ts | 9 ++ ui/src/recoil/tappingStatus/details.ts | 22 +++++ ui/src/recoil/tappingStatus/index.ts | 16 ++++ ui/src/recoil/wsConnection/atom.ts | 8 ++ ui/src/recoil/wsConnection/index.ts | 10 ++ 33 files changed, 317 insertions(+), 262 deletions(-) create mode 100644 ui/src/recoil/entPage/atom.ts create mode 100644 ui/src/recoil/entPage/index.ts create mode 100644 ui/src/recoil/entries/atom.ts create mode 100644 ui/src/recoil/entries/index.ts create mode 100644 ui/src/recoil/focusedEntryId/atom.ts create mode 100644 ui/src/recoil/focusedEntryId/index.ts create mode 100644 ui/src/recoil/query/atom.ts create mode 100644 ui/src/recoil/query/index.ts create mode 100644 ui/src/recoil/tappingStatus/atom.ts create mode 100644 ui/src/recoil/tappingStatus/details.ts create mode 100644 ui/src/recoil/tappingStatus/index.ts create mode 100644 ui/src/recoil/wsConnection/atom.ts create mode 100644 ui/src/recoil/wsConnection/index.ts diff --git a/ui/package-lock.json b/ui/package-lock.json index 98bfc9969..7dbac7c07 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -7856,6 +7856,11 @@ "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -14229,6 +14234,14 @@ "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": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", diff --git a/ui/package.json b/ui/package.json index 64763c557..a3de9574b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -31,6 +31,7 @@ "react-scrollable-feed-virtualized": "^1.4.9", "react-syntax-highlighter": "^15.4.3", "react-toastify": "^8.0.3", + "recoil": "^0.5.2", "typescript": "^4.2.4", "web-vitals": "^1.1.1", "xml-formatter": "^2.6.0" diff --git a/ui/src/EntApp.tsx b/ui/src/EntApp.tsx index 452b8013d..34bebdf32 100644 --- a/ui/src/EntApp.tsx +++ b/ui/src/EntApp.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; import './App.sass'; import {TrafficPage} from "./components/TrafficPage"; import {TLSWarning} from "./components/TLSWarning/TLSWarning"; @@ -9,43 +9,29 @@ import InstallPage from "./components/InstallPage"; import LoginPage from "./components/LoginPage"; import LoadingOverlay from "./components/LoadingOverlay"; import AuthPageBase from './components/AuthPageBase'; +import entPageAtom, {Page} from "./recoil/entPage"; +import {useRecoilState} from "recoil"; 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(null); - const EntApp = () => { const [isLoading, setIsLoading] = useState(true); const [showTLSWarning, setShowTLSWarning] = useState(false); const [userDismissedTLSWarning, setUserDismissedTLSWarning] = useState(false); const [addressesWithTLS, setAddressesWithTLS] = useState(new Set()); - const [page, setPage] = useState(Page.Traffic); // TODO: move to state management + const [entPage, setEntPage] = useRecoilState(entPageAtom); const [isFirstLogin, setIsFirstLogin] = useState(false); - const determinePage = async () => { // TODO: move to state management + const determinePage = useCallback(async () => { // TODO: move to state management try { const isInstallNeeded = await api.isInstallNeeded(); if (isInstallNeeded) { - setPage(Page.Setup); + setEntPage(Page.Setup); } else { const isAuthNeeded = await api.isAuthenticationNeeded(); if(isAuthNeeded) { - setPage(Page.Login); + setEntPage(Page.Login); } } } catch (e) { @@ -54,11 +40,11 @@ const EntApp = () => { } finally { setIsLoading(false); } - } + },[setEntPage]); useEffect(() => { determinePage(); - }, []); + }, [determinePage]); const onTLSDetected = (destAddress: string) => { addressesWithTLS.add(destAddress); @@ -71,7 +57,7 @@ const EntApp = () => { let pageComponent: any; - switch (page) { // TODO: move to state management / proper routing + switch (entPage) { // TODO: move to state management / proper routing case Page.Traffic: pageComponent = ; break; @@ -91,16 +77,14 @@ const EntApp = () => { return (
- - {page === Page.Traffic && } - {pageComponent} - {page === Page.Traffic && } - + {entPage === Page.Traffic && } + {pageComponent} + {entPage === Page.Traffic && }
); } diff --git a/ui/src/components/EntriesList.tsx b/ui/src/components/EntriesList.tsx index 4fb88cbf2..532af5ee0 100644 --- a/ui/src/components/EntriesList.tsx +++ b/ui/src/components/EntriesList.tsx @@ -6,11 +6,12 @@ import {EntryItem} from "./EntryListItem/EntryListItem"; import down from "./assets/downImg.svg"; import spinner from './assets/spinner.svg'; 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 { - entries: any[]; - setEntries: any; - query: string; listEntryREF: any; onSnapBrokenEvent: () => void; isSnappedToBottom: boolean; @@ -22,12 +23,8 @@ interface EntriesListProps { startTime: number; noMoreDataTop: boolean; setNoMoreDataTop: (flag: boolean) => void; - focusedEntryId: string; - setFocusedEntryId: (id: string) => void; - updateQuery: any; leftOffTop: number; setLeftOffTop: (leftOffTop: number) => void; - isWebSocketConnectionClosed: boolean; ws: any; openWebSocket: (query: string, resetEntries: boolean) => void; leftOffBottom: number; @@ -38,7 +35,13 @@ interface EntriesListProps { const api = Api.getInstance(); -export const EntriesList: React.FC = ({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 = ({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 [isLoadingTop, setIsLoadingTop] = useState(false); @@ -95,9 +98,9 @@ export const EntriesList: React.FC = ({entries, setEntries, qu },[setLoadMoreTop, setIsLoadingTop, entries, setEntries, query, setNoMoreDataTop, leftOffTop, setLeftOffTop, queriedCurrent, setQueriedCurrent, setQueriedTotal, setTruncatedTimestamp, scrollableRef]); useEffect(() => { - if(!isWebSocketConnectionClosed || !loadMoreTop || noMoreDataTop) return; + if(!isWsConnectionClosed || !loadMoreTop || noMoreDataTop) return; getOldEntries(); - }, [loadMoreTop, noMoreDataTop, getOldEntries, isWebSocketConnectionClosed]); + }, [loadMoreTop, noMoreDataTop, getOldEntries, isWsConnectionClosed]); const scrollbarVisible = scrollableRef.current?.childWrapperRef.current.clientHeight > scrollableRef.current?.wrapperRef.current.clientHeight; @@ -113,10 +116,7 @@ export const EntriesList: React.FC = ({entries, setEntries, qu {memoizedEntries.map(entry => )} @@ -131,9 +131,9 @@ export const EntriesList: React.FC = ({entries, setEntries, qu
diff --git a/ui/src/components/EntryDetailed/EntryViewer.tsx b/ui/src/components/EntryDetailed/EntryViewer.tsx index e7cf3a40e..71cd99cb5 100644 --- a/ui/src/components/EntryDetailed/EntryViewer.tsx +++ b/ui/src/components/EntryDetailed/EntryViewer.tsx @@ -8,7 +8,7 @@ enum SectionTypes { SectionBody = "body", } -const SectionsRepresentation: React.FC = ({data, color, updateQuery}) => { +const SectionsRepresentation: React.FC = ({data, color}) => { const sections = [] if (data) { @@ -16,12 +16,12 @@ const SectionsRepresentation: React.FC = ({data, color, updateQuery}) => { switch (row.type) { case SectionTypes.SectionTable: sections.push( - + ) break; case SectionTypes.SectionBody: sections.push( - + ) break; default: @@ -33,7 +33,7 @@ const SectionsRepresentation: React.FC = ({data, color, updateQuery}) => { return <>{sections}; } -const AutoRepresentation: React.FC = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color, updateQuery}) => { +const AutoRepresentation: React.FC = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color}) => { var TABS = [ { tab: 'Request' @@ -48,9 +48,9 @@ const AutoRepresentation: React.FC = ({representation, isRulesEnabled, rule const {request, response} = JSON.parse(representation); - var responseTabIndex = 0; - var rulesTabIndex = 0; - var contractTabIndex = 0; + let responseTabIndex = 0; + let rulesTabIndex = 0; + let contractTabIndex = 0; if (response) { TABS.push( @@ -85,10 +85,10 @@ const AutoRepresentation: React.FC = ({representation, isRulesEnabled, rule {currentTab === TABS[0].tab && - + } {response && currentTab === TABS[responseTabIndex].tab && - + } {isRulesEnabled && currentTab === TABS[rulesTabIndex].tab && @@ -110,10 +110,9 @@ interface Props { contractContent: string; color: string; elapsedTime: number; - updateQuery: any; } -const EntryViewer: React.FC = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color, updateQuery}) => { +const EntryViewer: React.FC = ({representation, isRulesEnabled, rulesMatched, contractStatus, requestReason, responseReason, contractContent, elapsedTime, color}) => { return = ({representation, isRulesEnabled, rulesMatc contractContent={contractContent} elapsedTime={elapsedTime} color={color} - updateQuery={updateQuery} /> }; diff --git a/ui/src/components/EntryListItem/EntryListItem.module.sass b/ui/src/components/EntryListItem/EntryListItem.module.sass index bb0fc9424..461066cd1 100644 --- a/ui/src/components/EntryListItem/EntryListItem.module.sass +++ b/ui/src/components/EntryListItem/EntryListItem.module.sass @@ -74,7 +74,6 @@ .separatorRight display: flex border-right: 1px solid $data-background-color - padding: 4px padding-right: 12px .separatorLeft diff --git a/ui/src/components/EntryListItem/EntryListItem.tsx b/ui/src/components/EntryListItem/EntryListItem.tsx index 8cdaeb22a..fb7a6dc8a 100644 --- a/ui/src/components/EntryListItem/EntryListItem.tsx +++ b/ui/src/components/EntryListItem/EntryListItem.tsx @@ -12,6 +12,9 @@ import ingoingIconNeutral from "../assets/ingoing-traffic-neutral.svg" import outgoingIconSuccess from "../assets/outgoing-traffic-success.svg" import outgoingIconFailure from "../assets/outgoing-traffic-failure.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 { ip: string @@ -42,15 +45,14 @@ interface Rules { interface EntryProps { entry: Entry; - focusedEntryId: string; - setFocusedEntryId: (id: string) => void; style: object; - updateQuery: any; headingMode: boolean; } -export const EntryItem: React.FC = ({entry, focusedEntryId, setFocusedEntryId, style, updateQuery, headingMode}) => { +export const EntryItem: React.FC = ({entry, style, headingMode}) => { + const [focusedEntryId, setFocusedEntryId] = useRecoilState(focusedEntryIdAtom); + const [queryState, setQuery] = useRecoilState(queryAtom); const isSelected = focusedEntryId === entry.id.toString(); const classification = getClassification(entry.status) @@ -103,8 +105,8 @@ export const EntryItem: React.FC = ({entry, focusedEntryId, setFocus } } - var contractEnabled = true; - var contractText = ""; + let contractEnabled = true; + let contractText = ""; switch (entry.contractStatus) { case 0: contractEnabled = false; @@ -123,8 +125,9 @@ export const EntryItem: React.FC = ({entry, focusedEntryId, setFocus break; } + const isStatusCodeEnabled = ((entry.proto.name === "http" && "status" in entry) || entry.status !== 0); - var endpointServiceContainer = "10px"; + let endpointServiceContainer = "10px"; if (!isStatusCodeEnabled) endpointServiceContainer = "20px"; return <> @@ -147,17 +150,15 @@ export const EntryItem: React.FC = ({entry, focusedEntryId, setFocus {!headingMode ? : null} {isStatusCodeEnabled &&
- +
}
- +
= ({entry, focusedEntryId, setFocus = ({entry, focusedEntryId, setFocus
= ({entry, focusedEntryId, setFocus {entry.src.port ? ":" : ""} = ({entry, focusedEntryId, setFocus {entry.isOutgoing ? = ({entry, focusedEntryId, setFocus : = ({entry, focusedEntryId, setFocus alt="Outgoing traffic" title="Outgoing" onClick={() => { - updateQuery(`outgoing == false`) + const query = `outgoing == false`; + setQuery(queryState ? `${queryState} and ${query}` : query); }} /> } = ({entry, focusedEntryId, setFocus : @@ -295,7 +290,6 @@ export const EntryItem: React.FC = ({entry, focusedEntryId, setFocus
= datetime("${Moment(+entry.timestamp)?.utc().format('MM/DD/YYYY, h:mm:ss.SSS A')}")`} - updateQuery={updateQuery} displayIconOnMouseOver={true} flipped={false} > diff --git a/ui/src/components/Filters.tsx b/ui/src/components/Filters.tsx index 0e30a21f7..9124c9512 100644 --- a/ui/src/components/Filters.tsx +++ b/ui/src/components/Filters.tsx @@ -7,20 +7,18 @@ import {SyntaxHighlighter} from "./UI/SyntaxHighlighter/index"; import filterUIExample1 from "./assets/filter-ui-example-1.png" import filterUIExample2 from "./assets/filter-ui-example-2.png" import variables from '../variables.module.scss'; +import {useRecoilState} from "recoil"; +import queryAtom from "../recoil/query"; interface FiltersProps { - query: string - setQuery: any backgroundColor: string ws: any openWebSocket: (query: string, resetEntries: boolean) => void; } -export const Filters: React.FC = ({query, setQuery, backgroundColor, ws, openWebSocket}) => { +export const Filters: React.FC = ({backgroundColor, ws, openWebSocket}) => { return
= ({query, setQuery, backgroundColo }; interface QueryFormProps { - query: string - setQuery: any backgroundColor: string ws: any openWebSocket: (query: string, resetEntries: boolean) => void; @@ -50,9 +46,10 @@ export const modalStyle = { color: '#000', }; -export const QueryForm: React.FC = ({query, setQuery, backgroundColor, ws, openWebSocket}) => { +export const QueryForm: React.FC = ({backgroundColor, ws, openWebSocket}) => { const formRef = useRef(null); + const [query, setQuery] = useRecoilState(queryAtom); const [openModal, setOpenModal] = useState(false); diff --git a/ui/src/components/Header/EntHeader.tsx b/ui/src/components/Header/EntHeader.tsx index d331ef144..b67e76d01 100644 --- a/ui/src/components/Header/EntHeader.tsx +++ b/ui/src/components/Header/EntHeader.tsx @@ -1,4 +1,4 @@ -import React, {useContext, useEffect, useState} from "react"; +import React, {useEffect, useState} from "react"; import logo from '../assets/MizuEntLogo.svg'; import './Header.sass'; import userImg from '../assets/user-circle.svg'; @@ -9,7 +9,8 @@ import logoutIcon from '../assets/logout.png'; import {SettingsModal} from "../SettingsModal/SettingModal"; import Api from "../../helpers/api"; import {toast} from "react-toastify"; -import {MizuContext, Page} from "../../EntApp"; +import {useSetRecoilState} from "recoil"; +import entPageAtom, {Page} from "../../recoil/entPage"; const api = Api.getInstance(); @@ -49,12 +50,12 @@ export const EntHeader: React.FC = ({isFirstLogin, setIsFirstLog const ProfileButton = () => { - const {setPage} = useContext(MizuContext); + const setEntPage = useSetRecoilState(entPageAtom); const logout = async (popupState) => { try { await api.logout(); - setPage(Page.Login); + setEntPage(Page.Login); } catch (e) { toast.error("Something went wrong, please check the console"); console.error(e); diff --git a/ui/src/components/InstallPage.tsx b/ui/src/components/InstallPage.tsx index fcc88c559..632c7367b 100644 --- a/ui/src/components/InstallPage.tsx +++ b/ui/src/components/InstallPage.tsx @@ -1,11 +1,12 @@ import { Button } from "@material-ui/core"; -import React, { useContext, useState } from "react"; -import { MizuContext, Page } from "../EntApp"; +import React, { useState } from "react"; import { adminUsername } from "../consts"; import Api, { FormValidationErrorType } from "../helpers/api"; import { toast } from 'react-toastify'; import LoadingOverlay from "./LoadingOverlay"; import { useCommonStyles } from "../helpers/commonStyle"; +import {useSetRecoilState} from "recoil"; +import entPageAtom, {Page} from "../recoil/entPage"; const api = Api.getInstance(); @@ -20,7 +21,7 @@ export const InstallPage: React.FC = ({onFirstLogin}) => { const [password, setPassword] = useState(""); const [passwordConfirm, setPasswordConfirm] = useState(""); - const {setPage} = useContext(MizuContext); + const setEntPage = useSetRecoilState(entPageAtom); const onFormSubmit = async () => { if (password.length < 4) { @@ -35,7 +36,7 @@ export const InstallPage: React.FC = ({onFirstLogin}) => { setIsLoading(true); await api.register(adminUsername, password); if (!await api.isAuthenticationNeeded()) { - setPage(Page.Traffic); + setEntPage(Page.Traffic); onFirstLogin(); } } catch (e) { diff --git a/ui/src/components/LoginPage.tsx b/ui/src/components/LoginPage.tsx index 6f3a71d37..5b7dfebda 100644 --- a/ui/src/components/LoginPage.tsx +++ b/ui/src/components/LoginPage.tsx @@ -1,10 +1,11 @@ import { Button } from "@material-ui/core"; -import React, { useContext, useState } from "react"; +import React, { useState } from "react"; import { toast } from "react-toastify"; -import { MizuContext, Page } from "../EntApp"; import Api from "../helpers/api"; import { useCommonStyles } from "../helpers/commonStyle"; import LoadingOverlay from "./LoadingOverlay"; +import entPageAtom, {Page} from "../recoil/entPage"; +import {useSetRecoilState} from "recoil"; const api = Api.getInstance(); @@ -15,7 +16,7 @@ const LoginPage: React.FC = () => { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); - const {setPage} = useContext(MizuContext); + const setEntPage = useSetRecoilState(entPageAtom); const onFormSubmit = async () => { setIsLoading(true); @@ -23,7 +24,7 @@ const LoginPage: React.FC = () => { try { await api.login(username, password); if (!await api.isAuthenticationNeeded()) { - setPage(Page.Traffic); + setEntPage(Page.Traffic); } else { toast.error("Invalid credentials"); } diff --git a/ui/src/components/TrafficPage.tsx b/ui/src/components/TrafficPage.tsx index 57fc6a603..2b973b5cd 100644 --- a/ui/src/components/TrafficPage.tsx +++ b/ui/src/components/TrafficPage.tsx @@ -12,6 +12,12 @@ import {StatusBar} from "./UI/StatusBar"; import Api, {MizuWebsocketURL} from "../helpers/api"; import { toast } from 'react-toastify'; 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(() => ({ details: { @@ -33,11 +39,6 @@ const useLayoutStyles = makeStyles(() => ({ } })); -enum ConnectionStatus { - Closed, - Connected, -} - interface TrafficPageProps { onTLSDetected: (destAddress: string) => void; setAnalyzeStatus?: (status: any) => void; @@ -48,21 +49,16 @@ const api = Api.getInstance(); export const TrafficPage: React.FC = ({onTLSDetected, setAnalyzeStatus}) => { const classes = useLayoutStyles(); - - const [entries, setEntries] = useState([] as any); - const [focusedEntryId, setFocusedEntryId] = useState(null); - const [selectedEntryData, setSelectedEntryData] = useState(null); - const [connection, setConnection] = useState(ConnectionStatus.Closed); + const [tappingStatus, setTappingStatus] = useRecoilState(tappingStatusAtom); + const [entries, setEntries] = useRecoilState(entriesAtom); + const [focusedEntryId, setFocusedEntryId] = useRecoilState(focusedEntryIdAtom); + const [wsConnection, setWsConnection] = useRecoilState(websocketConnectionAtom); + const query = useRecoilValue(queryAtom); const [noMoreDataTop, setNoMoreDataTop] = useState(false); - - const [tappingStatus, setTappingStatus] = useState(null); - const [isSnappedToBottom, setIsSnappedToBottom] = useState(true); - const [query, setQuery] = useState(""); const [queryBackgroundColor, setQueryBackgroundColor] = useState("#f5f5f5"); - const [addition, updateQuery] = useState(""); const [queriedCurrent, setQueriedCurrent] = useState(0); const [queriedTotal, setQueriedTotal] = useState(0); @@ -94,15 +90,6 @@ export const TrafficPage: React.FC = ({onTLSDetected, setAnaly handleQueryChange(query); }, [query, handleQueryChange]); - useEffect(() => { - if (query) { - setQuery(`${query} and ${addition}`); - } else { - setQuery(addition); - } - // eslint-disable-next-line - }, [addition]); - const ws = useRef(null); const listEntry = useRef(null); @@ -117,11 +104,11 @@ export const TrafficPage: React.FC = ({onTLSDetected, setAnaly } ws.current = new WebSocket(MizuWebsocketURL); ws.current.onopen = () => { - setConnection(ConnectionStatus.Connected); + setWsConnection(WsConnectionStatus.Connected); ws.current.send(query); } ws.current.onclose = () => { - setConnection(ConnectionStatus.Closed); + setWsConnection(WsConnectionStatus.Closed); } ws.current.onerror = (event) => { console.error("WebSocket error:", event); @@ -206,36 +193,9 @@ export const TrafficPage: React.FC = ({onTLSDetected, setAnaly // 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 = () => { ws.current.close(); - if (connection !== ConnectionStatus.Connected) { + if (wsConnection !== WsConnectionStatus.Connected) { if (query) { openWebSocket(`(${query}) and leftOff(-1)`, true); } else { @@ -248,8 +208,8 @@ export const TrafficPage: React.FC = ({onTLSDetected, setAnaly const getConnectionStatusClass = (isContainer) => { const container = isContainer ? "Container" : ""; - switch (connection) { - case ConnectionStatus.Connected: + switch (wsConnection) { + case WsConnectionStatus.Connected: return "greenIndicator" + container; default: return "redIndicator" + container; @@ -257,8 +217,8 @@ export const TrafficPage: React.FC = ({onTLSDetected, setAnaly } const getConnectionTitle = () => { - switch (connection) { - case ConnectionStatus.Connected: + switch (wsConnection) { + case WsConnectionStatus.Connected: return "streaming live traffic" default: return "streaming paused"; @@ -267,7 +227,7 @@ export const TrafficPage: React.FC = ({onTLSDetected, setAnaly const onSnapBrokenEvent = () => { setIsSnappedToBottom(false); - if (connection === ConnectionStatus.Connected) { + if (wsConnection === WsConnectionStatus.Connected) { ws.current.close(); } } @@ -275,9 +235,9 @@ export const TrafficPage: React.FC = ({onTLSDetected, setAnaly return (
- pause - play
{getConnectionTitle()} @@ -289,17 +249,12 @@ export const TrafficPage: React.FC = ({onTLSDetected, setAnaly {
= ({onTLSDetected, setAnaly startTime={startTime} noMoreDataTop={noMoreDataTop} setNoMoreDataTop={setNoMoreDataTop} - focusedEntryId={focusedEntryId} - setFocusedEntryId={setFocusedEntryId} - updateQuery={updateQuery} leftOffTop={leftOffTop} setLeftOffTop={setLeftOffTop} - isWebSocketConnectionClosed={connection === ConnectionStatus.Closed} ws={ws.current} openWebSocket={openWebSocket} leftOffBottom={leftOffBottom} @@ -327,10 +278,10 @@ export const TrafficPage: React.FC = ({onTLSDetected, setAnaly
- {selectedEntryData && } + {focusedEntryId && }
} - {tappingStatus && } + {tappingStatus && }
) }; diff --git a/ui/src/components/UI/Protocol.tsx b/ui/src/components/UI/Protocol.tsx index 3db0c2eb1..c04f7f81c 100644 --- a/ui/src/components/UI/Protocol.tsx +++ b/ui/src/components/UI/Protocol.tsx @@ -18,14 +18,12 @@ export interface ProtocolInterface { interface ProtocolProps { protocol: ProtocolInterface horizontal: boolean - updateQuery: any } -const Protocol: React.FC = ({protocol, horizontal, updateQuery}) => { +const Protocol: React.FC = ({protocol, horizontal}) => { if (horizontal) { return @@ -45,7 +43,6 @@ const Protocol: React.FC = ({protocol, horizontal, updateQuery}) } else { return = ({query, updateQuery, style, iconStyle, className, useTooltip= true, displayIconOnMouseOver = false, flipped = false, children}) => { +const Queryable: React.FC = ({query, style, iconStyle, className, useTooltip= true, displayIconOnMouseOver = false, flipped = false, children}) => { const [showAddedNotification, setAdded] = useState(false); const [showTooltip, setShowTooltip] = useState(false); + const [queryState, setQuery] = useRecoilState(queryAtom); const onCopy = () => { setAdded(true) @@ -25,13 +27,15 @@ const Queryable: React.FC = ({query, updateQuery, style, iconStyle, class useEffect(() => { let timer; if (showAddedNotification) { - updateQuery(query); + setQuery(queryState ? `${queryState} and ${query}` : query); timer = setTimeout(() => { setAdded(false); }, 1000); } return () => clearTimeout(timer); - }, [showAddedNotification, query, updateQuery]); + + // eslint-disable-next-line + }, [showAddedNotification, query, setQuery]); const addButton = query ? = ({query, updateQuery, style, iconStyle, class title={`Add "${query}" to the filter`} style={iconStyle} > - + {showAddedNotification && Added} : null; diff --git a/ui/src/components/UI/StatusBar.tsx b/ui/src/components/UI/StatusBar.tsx index f34ad0d12..4c1fa5fd2 100644 --- a/ui/src/components/UI/StatusBar.tsx +++ b/ui/src/components/UI/StatusBar.tsx @@ -3,33 +3,18 @@ import React, {useState} from "react"; import warningIcon from '../assets/warning_icon.svg'; import failIcon from '../assets/failed.svg'; import successIcon from '../assets/success.svg'; - -export interface TappingStatusPod { - name: string; - namespace: string; - isTapped: boolean; -} - -export interface TappingStatus { - pods: TappingStatusPod[]; -} - -export interface Props { - tappingStatus: TappingStatusPod[] -} +import {useRecoilValue} from "recoil"; +import tappingStatusAtom, {tappingStatusDetails} from "../../recoil/tappingStatus"; const pluralize = (noun: string, amount: number) => { return `${noun}${amount !== 1 ? 's' : ''}` } -export const StatusBar: React.FC = ({tappingStatus}) => { +export const StatusBar = () => { + const tappingStatus = useRecoilValue(tappingStatusAtom); const [expandedBar, setExpandedBar] = useState(false); - - 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; + const {uniqueNamespaces, amountOfPods, amountOfTappedPods, amountOfUntappedPods} = useRecoilValue(tappingStatusDetails); return
setExpandedBar(true)} onMouseLeave={() => setExpandedBar(false)}>
diff --git a/ui/src/components/UI/StatusCode.tsx b/ui/src/components/UI/StatusCode.tsx index 054be378a..88fd04325 100644 --- a/ui/src/components/UI/StatusCode.tsx +++ b/ui/src/components/UI/StatusCode.tsx @@ -10,16 +10,14 @@ export enum StatusCodeClassification { interface EntryProps { statusCode: number - updateQuery: any } -const StatusCode: React.FC = ({statusCode, updateQuery}) => { +const StatusCode: React.FC = ({statusCode}) => { const classification = getClassification(statusCode) return = ({method, summary, updateQuery}) => { +export const Summary: React.FC = ({method, summary}) => { return
{method && @@ -25,7 +23,6 @@ export const Summary: React.FC = ({method, summary, updateQuery}) } {summary &&
- <> - {window["isEnt"] ? : } - - + + <> + {window["isEnt"] ? : } + + + , document.getElementById('root') ); diff --git a/ui/src/recoil/entPage/atom.ts b/ui/src/recoil/entPage/atom.ts new file mode 100644 index 000000000..03650db83 --- /dev/null +++ b/ui/src/recoil/entPage/atom.ts @@ -0,0 +1,8 @@ +import { atom } from "recoil" + +const entPageAtom = atom({ + key: "entPageAtom", + default: 0 +}) + +export default entPageAtom diff --git a/ui/src/recoil/entPage/index.ts b/ui/src/recoil/entPage/index.ts new file mode 100644 index 000000000..900da75d1 --- /dev/null +++ b/ui/src/recoil/entPage/index.ts @@ -0,0 +1,11 @@ +import atom from "./atom"; + +enum Page { + Traffic, + Setup, + Login +} + +export { Page }; + +export default atom; diff --git a/ui/src/recoil/entries/atom.ts b/ui/src/recoil/entries/atom.ts new file mode 100644 index 000000000..3a12120c3 --- /dev/null +++ b/ui/src/recoil/entries/atom.ts @@ -0,0 +1,8 @@ +import { atom } from "recoil"; + +const entriesAtom = atom({ + key: "entriesAtom", + default: [] +}); + +export default entriesAtom; diff --git a/ui/src/recoil/entries/index.ts b/ui/src/recoil/entries/index.ts new file mode 100644 index 000000000..b97835b9f --- /dev/null +++ b/ui/src/recoil/entries/index.ts @@ -0,0 +1,3 @@ +import atom from "./atom"; + +export default atom diff --git a/ui/src/recoil/focusedEntryId/atom.ts b/ui/src/recoil/focusedEntryId/atom.ts new file mode 100644 index 000000000..fa28d0690 --- /dev/null +++ b/ui/src/recoil/focusedEntryId/atom.ts @@ -0,0 +1,8 @@ +import { atom } from "recoil"; + +const focusedEntryIdAtom = atom({ + key: "focusedEntryIdAtom", + default: null +}); + +export default focusedEntryIdAtom; diff --git a/ui/src/recoil/focusedEntryId/index.ts b/ui/src/recoil/focusedEntryId/index.ts new file mode 100644 index 000000000..b97835b9f --- /dev/null +++ b/ui/src/recoil/focusedEntryId/index.ts @@ -0,0 +1,3 @@ +import atom from "./atom"; + +export default atom diff --git a/ui/src/recoil/query/atom.ts b/ui/src/recoil/query/atom.ts new file mode 100644 index 000000000..ae13d8a35 --- /dev/null +++ b/ui/src/recoil/query/atom.ts @@ -0,0 +1,8 @@ +import { atom } from "recoil"; + +const queryAtom = atom({ + key: "queryAtom", + default: "" +}); + +export default queryAtom; diff --git a/ui/src/recoil/query/index.ts b/ui/src/recoil/query/index.ts new file mode 100644 index 000000000..ee28255fc --- /dev/null +++ b/ui/src/recoil/query/index.ts @@ -0,0 +1,3 @@ +import atom from "./atom"; + +export default atom; diff --git a/ui/src/recoil/tappingStatus/atom.ts b/ui/src/recoil/tappingStatus/atom.ts new file mode 100644 index 000000000..e16c72747 --- /dev/null +++ b/ui/src/recoil/tappingStatus/atom.ts @@ -0,0 +1,9 @@ +import { atom } from "recoil"; +import {TappingStatusPod} from "./index"; + +const tappingStatusAtom = atom({ + key: "tappingStatusAtom", + default: null as TappingStatusPod[] +}); + +export default tappingStatusAtom; diff --git a/ui/src/recoil/tappingStatus/details.ts b/ui/src/recoil/tappingStatus/details.ts new file mode 100644 index 000000000..b387686f6 --- /dev/null +++ b/ui/src/recoil/tappingStatus/details.ts @@ -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; diff --git a/ui/src/recoil/tappingStatus/index.ts b/ui/src/recoil/tappingStatus/index.ts new file mode 100644 index 000000000..40f7267eb --- /dev/null +++ b/ui/src/recoil/tappingStatus/index.ts @@ -0,0 +1,16 @@ +import atom from "./atom"; +import tappingStatusDetails from './details'; + +interface TappingStatusPod { + name: string; + namespace: string; + isTapped: boolean; +} + +interface TappingStatus { + pods: TappingStatusPod[]; +} + +export type {TappingStatus, TappingStatusPod, tappingStatusDetails}; + +export default atom; diff --git a/ui/src/recoil/wsConnection/atom.ts b/ui/src/recoil/wsConnection/atom.ts new file mode 100644 index 000000000..e3de12558 --- /dev/null +++ b/ui/src/recoil/wsConnection/atom.ts @@ -0,0 +1,8 @@ +import { atom } from "recoil"; + +const wsConnectionAtom = atom({ + key: "wsConnectionAtom", + default: 0 +}); + +export default wsConnectionAtom; diff --git a/ui/src/recoil/wsConnection/index.ts b/ui/src/recoil/wsConnection/index.ts new file mode 100644 index 000000000..d74f11fb9 --- /dev/null +++ b/ui/src/recoil/wsConnection/index.ts @@ -0,0 +1,10 @@ +import atom from "./atom"; + +enum WsConnectionStatus { + Closed, + Connected, +} + +export {WsConnectionStatus}; + +export default atom From ce477095fdccc7227b29087dc8076fada1c26e4e Mon Sep 17 00:00:00 2001 From: lirazyehezkel <61656597+lirazyehezkel@users.noreply.github.com> Date: Sun, 16 Jan 2022 15:44:08 +0200 Subject: [PATCH 12/15] Mizu recoil fix (#654) --- ui/src/recoil/tappingStatus/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/recoil/tappingStatus/index.ts b/ui/src/recoil/tappingStatus/index.ts index 40f7267eb..672aab9e1 100644 --- a/ui/src/recoil/tappingStatus/index.ts +++ b/ui/src/recoil/tappingStatus/index.ts @@ -11,6 +11,7 @@ interface TappingStatus { pods: TappingStatusPod[]; } -export type {TappingStatus, TappingStatusPod, tappingStatusDetails}; +export type {TappingStatus, TappingStatusPod}; +export {tappingStatusDetails}; export default atom; From 5ca31074224b3aaff6adde0a80217c591b8e56b2 Mon Sep 17 00:00:00 2001 From: RoyUP9 <87927115+RoyUP9@users.noreply.github.com> Date: Sun, 16 Jan 2022 17:43:18 +0200 Subject: [PATCH 13/15] Added build ui to pr validation flow (#655) --- .github/workflows/pr_validation.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/pr_validation.yml b/.github/workflows/pr_validation.yml index 4c1bf53fd..dd196ec18 100644 --- a/.github/workflows/pr_validation.yml +++ b/.github/workflows/pr_validation.yml @@ -44,3 +44,18 @@ jobs: - name: Build 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 From 6b4bcc8abd16fbfd72de98f9c0344041e88fabb7 Mon Sep 17 00:00:00 2001 From: Adam Kol <93466081+AdamKol-up9@users.noreply.github.com> Date: Mon, 17 Jan 2022 10:43:39 +0200 Subject: [PATCH 14/15] Cypress: refactor for Redact and NoRedact tests (#656) --- acceptanceTests/cypress.json | 7 ++++-- .../StatusBarHelper.js} | 0 .../integration/testHelpers/TrafficHelper.js | 9 ++++++++ .../integration/tests/MultipleNamespaces.js | 2 +- .../cypress/integration/tests/NoRedact.js | 8 +++++++ .../cypress/integration/tests/Redact.js | 8 +++++++ .../cypress/integration/tests/RedactTests.js | 23 ------------------- .../cypress/integration/tests/Regex.js | 2 +- acceptanceTests/tap_test.go | 4 ++-- ui/src/components/EntriesList.tsx | 2 +- 10 files changed, 35 insertions(+), 30 deletions(-) rename acceptanceTests/cypress/integration/{page_objects/StatusBar.js => testHelpers/StatusBarHelper.js} (100%) create mode 100644 acceptanceTests/cypress/integration/testHelpers/TrafficHelper.js create mode 100644 acceptanceTests/cypress/integration/tests/NoRedact.js create mode 100644 acceptanceTests/cypress/integration/tests/Redact.js delete mode 100644 acceptanceTests/cypress/integration/tests/RedactTests.js diff --git a/acceptanceTests/cypress.json b/acceptanceTests/cypress.json index 44ae98c89..a0fe7f827 100644 --- a/acceptanceTests/cypress.json +++ b/acceptanceTests/cypress.json @@ -8,10 +8,13 @@ "testFiles": ["tests/GuiPort.js", "tests/MultipleNamespaces.js", - "tests/RedactTests.js", + "tests/Redact.js", + "tests/NoRedact.js", "tests/Regex.js"], "env": { - "testUrl": "http://localhost:8899/" + "testUrl": "http://localhost:8899/", + "redactHeaderContent": "User-Header[REDACTED]", + "redactBodyContent": "{ \"User\": \"[REDACTED]\" }" } } diff --git a/acceptanceTests/cypress/integration/page_objects/StatusBar.js b/acceptanceTests/cypress/integration/testHelpers/StatusBarHelper.js similarity index 100% rename from acceptanceTests/cypress/integration/page_objects/StatusBar.js rename to acceptanceTests/cypress/integration/testHelpers/StatusBarHelper.js diff --git a/acceptanceTests/cypress/integration/testHelpers/TrafficHelper.js b/acceptanceTests/cypress/integration/testHelpers/TrafficHelper.js new file mode 100644 index 000000000..e90ec7e11 --- /dev/null +++ b/acceptanceTests/cypress/integration/testHelpers/TrafficHelper.js @@ -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}`) + }); + }); +} diff --git a/acceptanceTests/cypress/integration/tests/MultipleNamespaces.js b/acceptanceTests/cypress/integration/tests/MultipleNamespaces.js index 110f9d13f..96b032e10 100644 --- a/acceptanceTests/cypress/integration/tests/MultipleNamespaces.js +++ b/acceptanceTests/cypress/integration/tests/MultipleNamespaces.js @@ -1,4 +1,4 @@ -import {findLineAndCheck, getExpectedDetailsDict} from '../page_objects/StatusBar'; +import {findLineAndCheck, getExpectedDetailsDict} from '../testHelpers/StatusBarHelper'; it('opening', function () { cy.visit(Cypress.env('testUrl')); diff --git a/acceptanceTests/cypress/integration/tests/NoRedact.js b/acceptanceTests/cypress/integration/tests/NoRedact.js new file mode 100644 index 000000000..63bef0335 --- /dev/null +++ b/acceptanceTests/cypress/integration/tests/NoRedact.js @@ -0,0 +1,8 @@ +import {isValueExistsInElement, 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'); diff --git a/acceptanceTests/cypress/integration/tests/Redact.js b/acceptanceTests/cypress/integration/tests/Redact.js new file mode 100644 index 000000000..47f0c18a6 --- /dev/null +++ b/acceptanceTests/cypress/integration/tests/Redact.js @@ -0,0 +1,8 @@ +import {isValueExistsInElement, 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'); diff --git a/acceptanceTests/cypress/integration/tests/RedactTests.js b/acceptanceTests/cypress/integration/tests/RedactTests.js deleted file mode 100644 index 7005eeebf..000000000 --- a/acceptanceTests/cypress/integration/tests/RedactTests.js +++ /dev/null @@ -1,23 +0,0 @@ -const inHeader = 'User-Header[REDACTED]'; -const inBody = '{ "User": "[REDACTED]" }'; -const shouldExist = Cypress.env('shouldExist'); - -it('Loading Mizu', function () { - cy.visit(Cypress.env('testUrl')); -}) - -it(`should ${shouldExist ? '' : 'not'} include ${inHeader}`, function () { - cy.get('.CollapsibleContainer', { timeout : 15 * 1000}).first().next().then(headerElements => { //TODO change the path and refactor the body and head functions - const allText = headerElements.text(); - if (allText.includes(inHeader) !== shouldExist) - throw new Error(`The headers panel doesnt include ${inHeader}`); - }); -}); - -it(`should ${shouldExist ? '' : 'not'} include ${inBody}`, function () { - cy.get('.hljs').then(bodyElement => { - const line = bodyElement.text(); - if (line.includes(inBody) !== shouldExist) - throw new Error(`The body panel doesnt include ${inBody}`); - }); -}); diff --git a/acceptanceTests/cypress/integration/tests/Regex.js b/acceptanceTests/cypress/integration/tests/Regex.js index b11ef2e58..de449a7a4 100644 --- a/acceptanceTests/cypress/integration/tests/Regex.js +++ b/acceptanceTests/cypress/integration/tests/Regex.js @@ -1,4 +1,4 @@ -import {getExpectedDetailsDict, checkLine} from '../page_objects/StatusBar'; +import {getExpectedDetailsDict, checkLine} from '../testHelpers/StatusBarHelper'; it('opening', function () { diff --git a/acceptanceTests/tap_test.go b/acceptanceTests/tap_test.go index 381c5ed0f..4a61db1ee 100644 --- a/acceptanceTests/tap_test.go +++ b/acceptanceTests/tap_test.go @@ -377,7 +377,7 @@ func TestTapRedact(t *testing.T) { } } - runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/RedactTests.js\" --env shouldExist=true")) + runCypressTests(t, fmt.Sprintf("npx cypress run --spec \"cypress/integration/tests/Redact.js\"")) } func TestTapNoRedact(t *testing.T) { @@ -429,7 +429,7 @@ func TestTapNoRedact(t *testing.T) { } } - runCypressTests(t, "npx cypress run --spec \"cypress/integration/tests/RedactTests.js\" --env shouldExist=false") + runCypressTests(t, "npx cypress run --spec \"cypress/integration/tests/NoRedact.js\"") } func TestTapRegexMasking(t *testing.T) { diff --git a/ui/src/components/EntriesList.tsx b/ui/src/components/EntriesList.tsx index 532af5ee0..5aa7aac6e 100644 --- a/ui/src/components/EntriesList.tsx +++ b/ui/src/components/EntriesList.tsx @@ -148,7 +148,7 @@ export const EntriesList: React.FC = ({listEntryREF, onSnapBro
-
Displaying {entries?.length} results out of {queriedTotal} total
+
Displaying {entries?.length} results out of {queriedTotal} total
{startTime !== 0 &&
Started listening at {Moment(truncatedTimestamp ? truncatedTimestamp : startTime).utc().format('MM/DD/YYYY, h:mm:ss.SSS A')}
}
From c8a3033f8775b073802ee4ceab5f508e1bd76934 Mon Sep 17 00:00:00 2001 From: Adam Kol <93466081+AdamKol-up9@users.noreply.github.com> Date: Mon, 17 Jan 2022 12:57:12 +0200 Subject: [PATCH 15/15] Cypress: fix redact tests (#658) --- acceptanceTests/cypress/integration/tests/NoRedact.js | 2 +- acceptanceTests/cypress/integration/tests/Redact.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/acceptanceTests/cypress/integration/tests/NoRedact.js b/acceptanceTests/cypress/integration/tests/NoRedact.js index 63bef0335..c76c5c65d 100644 --- a/acceptanceTests/cypress/integration/tests/NoRedact.js +++ b/acceptanceTests/cypress/integration/tests/NoRedact.js @@ -1,4 +1,4 @@ -import {isValueExistsInElement, isValueExistsInElement} from '../testHelpers/TrafficHelper'; +import {isValueExistsInElement} from '../testHelpers/TrafficHelper'; it('Loading Mizu', function () { cy.visit(Cypress.env('testUrl')); diff --git a/acceptanceTests/cypress/integration/tests/Redact.js b/acceptanceTests/cypress/integration/tests/Redact.js index 47f0c18a6..293ebfd14 100644 --- a/acceptanceTests/cypress/integration/tests/Redact.js +++ b/acceptanceTests/cypress/integration/tests/Redact.js @@ -1,4 +1,4 @@ -import {isValueExistsInElement, isValueExistsInElement} from '../testHelpers/TrafficHelper'; +import {isValueExistsInElement} from '../testHelpers/TrafficHelper'; it('Loading Mizu', function () { cy.visit(Cypress.env('testUrl'));
= ({title, color, expanded, setExpanded, query = "", updateQuery = null}) => { +const EntrySectionCollapsibleTitle: React.FC = ({title, color, expanded, setExpanded, query = ""}) => { return
{title} @@ -92,15 +88,14 @@ interface EntrySectionContainerProps { title: string, color: string, query?: string, - updateQuery?: any, } -export const EntrySectionContainer: React.FC = ({title, color, children, query = "", updateQuery = null}) => { +export const EntrySectionContainer: React.FC = ({title, color, children, query = ""}) => { const [expanded, setExpanded] = useState(true); return } + title={} > {children} @@ -110,7 +105,6 @@ interface EntryBodySectionProps { title: string, content: any, color: string, - updateQuery: any, encoding?: string, contentType?: string, selector?: string, @@ -119,7 +113,6 @@ interface EntryBodySectionProps { export const EntryBodySection: React.FC = ({ title, color, - updateQuery, content, encoding, contentType, @@ -172,7 +165,6 @@ export const EntryBodySection: React.FC = ({ title={title} color={color} query={`${selector} == r".*"`} - updateQuery={updateQuery} >
{supportsPrettying &&
@@ -203,10 +195,9 @@ interface EntrySectionProps { title: string, color: string, arrayToIterate: any[], - updateQuery: any, } -export const EntryTableSection: React.FC = ({title, color, arrayToIterate, updateQuery}) => { +export const EntryTableSection: React.FC = ({title, color, arrayToIterate}) => { let arrayToIterateSorted: any[]; if (arrayToIterate) { arrayToIterateSorted = arrayToIterate.sort((a, b) => { @@ -231,7 +222,6 @@ export const EntryTableSection: React.FC = ({title, color, ar key={index} label={name} value={value} - updateQuery={updateQuery} selector={selector} />)}