Compare commits

...

19 Commits

Author SHA1 Message Date
M. Mert Yıldıran
72a1aba3e5 TRA-4410 Display namespace field in the UI (#974) 2022-04-08 21:16:25 +03:00
RoyUP9
d8fb8ff710 Fix for OAS reset not working (#978) 2022-04-07 18:14:03 +03:00
Nimrod Gilboa Markevich
f344bd2633 Make minor changes to OasGenerator (#977)
* Added log message
* Remove Reset function from OasGenerator interface, use Stop+Start instead
* SetEntriesQuery returns a bool stating whether the query changed
2022-04-07 10:46:30 +03:00
M. Mert Yıldıran
6575495fa5 Remove gRPC related modifications (#958)
* Remove gRPC related modifications

* Remove gRPC status text related modifications as well

* Fixing gRPC vertical image

detect grpc when content type is 'application/grpc' as well  (and not only from the grpc-status)

Co-authored-by: gadotroee <55343099+gadotroee@users.noreply.github.com>
2022-04-06 18:50:36 +03:00
RoyUP9
cf5c03d45c Fixed service map returning nil values (#975) 2022-04-06 13:12:38 +03:00
Nimrod Gilboa Markevich
491da24c63 Add ability to set query in OAS Generator (#964) 2022-04-06 11:54:55 +03:00
leon-up9
832162ae0f Ui/Service-map-split-to-ui-common (#966)
* added serviceModal & selectList & noDataMessage
removed leftovers from split

* scroll fix

* sort by name

* search alightment

* space removed

* margin-bottom

* utils class

Co-authored-by: Leon <>
Co-authored-by: Igor Gov <iggvrv@gmail.com>
2022-04-05 13:23:46 +03:00
lirazyehezkel
866378b451 Mizu cant show more than 10000 entries (#973) 2022-04-05 12:25:01 +03:00
lirazyehezkel
0b0b9ce6d1 Performance fixes (#972) 2022-04-04 20:03:57 +03:00
RoyUP9
d99c632102 Fixed golint strings.Title is deprecated error (#971) 2022-04-04 18:06:22 +03:00
RamiBerm
76a6a77a14 Refactor ws (#961)
* Separate socket and basenine logic

* WIP

* Update socket_server_handlers.go

* Update socket_data_streamer.go and socket_server_handlers.go

* Update socket_server_handlers.go

* Merge branch 'develop' into refactor_ws
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.

* empty commit for actions

* empty commit for actions

* commit for actions

* Revert "commit for actions"

This reverts commit 8ba2ecf7d3.

Co-authored-by: RoyUP9 <87927115+RoyUP9@users.noreply.github.com>
2022-04-04 17:33:53 +03:00
M. Mert Yıldıran
2bfc523bbc Handle reflect.TypeOf returning nil case (#970) 2022-04-04 16:25:18 +03:00
RoyUP9
66ba778384 Fixed golint modified files (#969) 2022-04-04 15:32:22 +03:00
leon-up9
7adbf7bf1b Ui/TRA-4461_service-map-&-OAS---GUI-changes (#962)
* OpenAPI renamed to Service Catalog

* Docs icon change
Hide Navbar on serviceMap modal open

* PR comments

Co-authored-by: Leon <>
Co-authored-by: RoyUP9 <87927115+RoyUP9@users.noreply.github.com>
2022-04-04 14:49:41 +03:00
Igor Gov
a97b5b3b38 Add conditional Go lint validation to CI (#967) 2022-04-04 14:35:47 +03:00
Nimrod Gilboa Markevich
aa8dcc5f5c Format commit message as code to handle multi line messages (#963)
Co-authored-by: gadotroee <55343099+gadotroee@users.noreply.github.com>
2022-04-03 22:10:43 +03:00
lirazyehezkel
9d08dbdd5d UI performance fix 2022-04-03 21:35:09 +03:00
lirazyehezkel
b47718e094 TRA-4442 Improve UI performance (#960)
* Move ws entry listener to entriesList component

* unused code
2022-04-03 15:51:20 +03:00
Igor Gov
6a7fad430c Adding resolved prop to service map node (#959)
* Adding resolved prop to service map node

* fixing tests
2022-04-03 15:32:21 +03:00
73 changed files with 1489 additions and 1339 deletions

View File

@@ -43,7 +43,7 @@ jobs:
with:
status: ${{ job.status }}
notification_title: 'Mizu {workflow} has {status_message}'
message_format: '{emoji} *{workflow}* {status_message} during <{run_url}|run>, after commit <{commit_url}|{commit_sha} ${{ github.event.head_commit.message }}> ${{ github.event.head_commit.committer.name }} <${{ github.event.head_commit.committer.email }}>'
message_format: '{emoji} *{workflow}* {status_message} during <{run_url}|run>, after commit <{commit_url}|{commit_sha}> by ${{ github.event.head_commit.committer.name }} <${{ github.event.head_commit.committer.email }}> ```${{ github.event.head_commit.message }}```'
footer: 'Linked Repo <{repo_url}|{repo}>'
notify_when: 'failure'
env:

View File

@@ -15,6 +15,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 2
- uses: actions/setup-go@v2
with:
go-version: '^1.17'
@@ -24,67 +27,117 @@ jobs:
sudo apt update
sudo apt install -y libpcap-dev
- name: Check Agent modified files
id: agent_modified_files
run: devops/check_modified_files.sh agent/
- name: Go lint - agent
uses: golangci/golangci-lint-action@v2
if: steps.agent_modified_files.outputs.matched == 'true'
with:
version: latest
working-directory: agent
args: --timeout=3m
- name: Check shared modified files
id: shared_modified_files
run: devops/check_modified_files.sh shared/
- name: Go lint - shared
uses: golangci/golangci-lint-action@v2
if: steps.shared_modified_files.outputs.matched == 'true'
with:
version: latest
working-directory: shared
args: --timeout=3m
- name: Check tap modified files
id: tap_modified_files
run: devops/check_modified_files.sh tap/
- name: Go lint - tap
uses: golangci/golangci-lint-action@v2
if: steps.tap_modified_files.outputs.matched == 'true'
with:
version: latest
working-directory: tap
args: --timeout=3m
- name: Check cli modified files
id: cli_modified_files
run: devops/check_modified_files.sh cli/
- name: Go lint - CLI
uses: golangci/golangci-lint-action@v2
if: steps.cli_modified_files.outputs.matched == 'true'
with:
version: latest
working-directory: cli
args: --timeout=3m
- name: Check acceptanceTests modified files
id: acceptanceTests_modified_files
run: devops/check_modified_files.sh acceptanceTests/
- name: Go lint - acceptanceTests
uses: golangci/golangci-lint-action@v2
if: steps.acceptanceTests_modified_files.outputs.matched == 'true'
with:
version: latest
working-directory: acceptanceTests
args: --timeout=3m
- name: Check tap/api modified files
id: tap_api_modified_files
run: devops/check_modified_files.sh tap/api/
- name: Go lint - tap/api
uses: golangci/golangci-lint-action@v2
if: steps.tap_api_modified_files.outputs.matched == 'true'
with:
version: latest
working-directory: tap/api
- name: Check tap/extensions/amqp modified files
id: tap_amqp_modified_files
run: devops/check_modified_files.sh tap/extensions/amqp/
- name: Go lint - tap/extensions/amqp
uses: golangci/golangci-lint-action@v2
if: steps.tap_amqp_modified_files.outputs.matched == 'true'
with:
version: latest
working-directory: tap/extensions/amqp
- name: Check tap/extensions/http modified files
id: tap_http_modified_files
run: devops/check_modified_files.sh tap/extensions/http/
- name: Go lint - tap/extensions/http
uses: golangci/golangci-lint-action@v2
if: steps.tap_http_modified_files.outputs.matched == 'true'
with:
version: latest
working-directory: tap/extensions/http
- name: Check tap/extensions/kafka modified files
id: tap_kafka_modified_files
run: devops/check_modified_files.sh tap/extensions/kafka/
- name: Go lint - tap/extensions/kafka
uses: golangci/golangci-lint-action@v2
if: steps.tap_kafka_modified_files.outputs.matched == 'true'
with:
version: latest
working-directory: tap/extensions/kafka
- name: Check tap/extensions/redis modified files
id: tap_redis_modified_files
run: devops/check_modified_files.sh tap/extensions/redis/
- name: Go lint - tap/extensions/redis
uses: golangci/golangci-lint-action@v2
if: steps.tap_redis_modified_files.outputs.matched == 'true'
with:
version: latest
working-directory: tap/extensions/redis

View File

@@ -199,7 +199,7 @@ func runInHarReaderMode() {
func enableExpFeatureIfNeeded() {
if config.Config.OAS {
oasGenerator := dependency.GetInstance(dependency.OasGeneratorDependency).(oas.OasGenerator)
oasGenerator.Start()
oasGenerator.Start(nil)
}
if config.Config.ServiceMap {
serviceMapGenerator := dependency.GetInstance(dependency.ServiceMapGeneratorDependency).(servicemap.ServiceMap)
@@ -371,6 +371,8 @@ func handleIncomingMessageAsTapper(socketConnection *websocket.Conn) {
func initializeDependencies() {
dependency.RegisterGenerator(dependency.ServiceMapGeneratorDependency, func() interface{} { return servicemap.GetDefaultServiceMapInstance() })
dependency.RegisterGenerator(dependency.OasGeneratorDependency, func() interface{} { return oas.GetDefaultOasGeneratorInstance(nil) })
dependency.RegisterGenerator(dependency.OasGeneratorDependency, func() interface{} { return oas.GetDefaultOasGeneratorInstance() })
dependency.RegisterGenerator(dependency.EntriesProvider, func() interface{} { return &entries.BasenineEntriesProvider{} })
dependency.RegisterGenerator(dependency.EntriesSocketStreamer, func() interface{} { return &api.BasenineEntryStreamer{} })
dependency.RegisterGenerator(dependency.EntryStreamerSocketConnector, func() interface{} { return &api.DefaultEntryStreamerSocketConnector{} })
}

View File

@@ -0,0 +1,57 @@
package api
import (
"fmt"
basenine "github.com/up9inc/basenine/client/go"
"github.com/up9inc/mizu/agent/pkg/models"
"github.com/up9inc/mizu/shared/logger"
tapApi "github.com/up9inc/mizu/tap/api"
)
type EntryStreamerSocketConnector interface {
SendEntry(socketId int, entry *tapApi.Entry, params *WebSocketParams)
SendMetadata(socketId int, metadata *basenine.Metadata)
SendToastError(socketId int, err error)
CleanupSocket(socketId int)
}
type DefaultEntryStreamerSocketConnector struct{}
func (e *DefaultEntryStreamerSocketConnector) SendEntry(socketId int, entry *tapApi.Entry, params *WebSocketParams) {
var message []byte
if params.EnableFullEntries {
message, _ = models.CreateFullEntryWebSocketMessage(entry)
} else {
extension := extensionsMap[entry.Protocol.Name]
base := extension.Dissector.Summarize(entry)
message, _ = models.CreateBaseEntryWebSocketMessage(base)
}
if err := SendToSocket(socketId, message); err != nil {
logger.Log.Error(err)
}
}
func (e *DefaultEntryStreamerSocketConnector) SendMetadata(socketId int, metadata *basenine.Metadata) {
metadataBytes, _ := models.CreateWebsocketQueryMetadataMessage(metadata)
if err := SendToSocket(socketId, metadataBytes); err != nil {
logger.Log.Error(err)
}
}
func (e *DefaultEntryStreamerSocketConnector) SendToastError(socketId int, err error) {
toastBytes, _ := models.CreateWebsocketToastMessage(&models.ToastMessage{
Type: "error",
AutoClose: 5000,
Text: fmt.Sprintf("Syntax error: %s", err.Error()),
})
if err := SendToSocket(socketId, toastBytes); err != nil {
logger.Log.Error(err)
}
}
func (e *DefaultEntryStreamerSocketConnector) CleanupSocket(socketId int) {
socketObj := connectedWebsockets[socketId]
socketCleanup(socketId, socketObj)
}

View File

@@ -0,0 +1,92 @@
package api
import (
"context"
"encoding/json"
basenine "github.com/up9inc/basenine/client/go"
"github.com/up9inc/mizu/agent/pkg/dependency"
"github.com/up9inc/mizu/shared"
"github.com/up9inc/mizu/shared/logger"
tapApi "github.com/up9inc/mizu/tap/api"
)
type EntryStreamer interface {
Get(ctx context.Context, socketId int, params *WebSocketParams) error
}
type BasenineEntryStreamer struct{}
func (e *BasenineEntryStreamer) Get(ctx context.Context, socketId int, params *WebSocketParams) error {
var connection *basenine.Connection
entryStreamerSocketConnector := dependency.GetInstance(dependency.EntryStreamerSocketConnector).(EntryStreamerSocketConnector)
connection, err := basenine.NewConnection(shared.BasenineHost, shared.BaseninePort)
if err != nil {
logger.Log.Errorf("failed to establish a connection to Basenine: %v", err)
entryStreamerSocketConnector.CleanupSocket(socketId)
return err
}
data := make(chan []byte)
meta := make(chan []byte)
query := params.Query
err = basenine.Validate(shared.BasenineHost, shared.BaseninePort, query)
if err != nil {
entryStreamerSocketConnector.SendToastError(socketId, err)
}
handleDataChannel := func(c *basenine.Connection, data chan []byte) {
for {
bytes := <-data
if string(bytes) == basenine.CloseChannel {
return
}
var entry *tapApi.Entry
err = json.Unmarshal(bytes, &entry)
if err != nil {
logger.Log.Debugf("error unmarshalling entry: %v", err.Error())
continue
}
entryStreamerSocketConnector.SendEntry(socketId, entry, params)
}
}
handleMetaChannel := func(c *basenine.Connection, meta chan []byte) {
for {
bytes := <-meta
if string(bytes) == basenine.CloseChannel {
return
}
var metadata *basenine.Metadata
err = json.Unmarshal(bytes, &metadata)
if err != nil {
logger.Log.Debugf("Error unmarshalling metadata: %v", err.Error())
continue
}
entryStreamerSocketConnector.SendMetadata(socketId, metadata)
}
}
go handleDataChannel(connection, data)
go handleMetaChannel(connection, meta)
connection.Query(query, data, meta)
go func() {
<-ctx.Done()
data <- []byte(basenine.CloseChannel)
meta <- []byte(basenine.CloseChannel)
connection.Close()
}()
return nil
}

View File

@@ -1,19 +1,15 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
"github.com/up9inc/mizu/agent/pkg/models"
"github.com/up9inc/mizu/agent/pkg/utils"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
basenine "github.com/up9inc/basenine/client/go"
"github.com/up9inc/mizu/shared"
"github.com/up9inc/mizu/agent/pkg/models"
"github.com/up9inc/mizu/agent/pkg/utils"
"github.com/up9inc/mizu/shared/logger"
tapApi "github.com/up9inc/mizu/tap/api"
)
@@ -25,9 +21,9 @@ func InitExtensionsMap(ref map[string]*tapApi.Extension) {
}
type EventHandlers interface {
WebSocketConnect(socketId int, isTapper bool)
WebSocketConnect(c *gin.Context, socketId int, isTapper bool)
WebSocketDisconnect(socketId int, isTapper bool)
WebSocketMessage(socketId int, message []byte)
WebSocketMessage(socketId int, isTapper bool, message []byte)
}
type SocketConnection struct {
@@ -62,11 +58,11 @@ func init() {
func WebSocketRoutes(app *gin.Engine, eventHandlers EventHandlers) {
SocketGetBrowserHandler = func(c *gin.Context) {
websocketHandler(c.Writer, c.Request, eventHandlers, false)
websocketHandler(c, eventHandlers, false)
}
SocketGetTapperHandler = func(c *gin.Context) {
websocketHandler(c.Writer, c.Request, eventHandlers, true)
websocketHandler(c, eventHandlers, true)
}
app.GET("/ws", func(c *gin.Context) {
@@ -78,10 +74,10 @@ func WebSocketRoutes(app *gin.Engine, eventHandlers EventHandlers) {
})
}
func websocketHandler(w http.ResponseWriter, r *http.Request, eventHandlers EventHandlers, isTapper bool) {
ws, err := websocketUpgrader.Upgrade(w, r, nil)
func websocketHandler(c *gin.Context, eventHandlers EventHandlers, isTapper bool) {
ws, err := websocketUpgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Log.Errorf("Failed to set websocket upgrade: %v", err)
logger.Log.Errorf("failed to set websocket upgrade: %v", err)
return
}
@@ -93,30 +89,11 @@ func websocketHandler(w http.ResponseWriter, r *http.Request, eventHandlers Even
websocketIdsLock.Unlock()
var connection *basenine.Connection
var isQuerySet bool
// `!isTapper` means it's a connection from the web UI
if !isTapper {
connection, err = basenine.NewConnection(shared.BasenineHost, shared.BaseninePort)
if err != nil {
logger.Log.Errorf("Failed to establish a connection to Basenine: %v", err)
socketCleanup(socketId, connectedWebsockets[socketId])
return
}
}
data := make(chan []byte)
meta := make(chan []byte)
defer func() {
socketCleanup(socketId, connectedWebsockets[socketId])
data <- []byte(basenine.CloseChannel)
meta <- []byte(basenine.CloseChannel)
connection.Close()
}()
eventHandlers.WebSocketConnect(socketId, isTapper)
eventHandlers.WebSocketConnect(c, socketId, isTapper)
startTimeBytes, _ := models.CreateWebsocketStartTimeMessage(utils.StartTime)
@@ -124,127 +101,32 @@ func websocketHandler(w http.ResponseWriter, r *http.Request, eventHandlers Even
logger.Log.Error(err)
}
var params WebSocketParams
for {
_, msg, err := ws.ReadMessage()
if err != nil {
if _, ok := err.(*websocket.CloseError); ok {
logger.Log.Debugf("Received websocket close message, socket id: %d", socketId)
logger.Log.Debugf("received websocket close message, socket id: %d", socketId)
} else {
logger.Log.Errorf("Error reading message, socket id: %d, error: %v", socketId, err)
logger.Log.Errorf("error reading message, socket id: %d, error: %v", socketId, err)
}
break
}
if !isTapper && !isQuerySet {
if err := json.Unmarshal(msg, &params); err != nil {
logger.Log.Errorf("Error unmarshalling parameters: %v", socketId, err)
continue
}
query := params.Query
err = basenine.Validate(shared.BasenineHost, shared.BaseninePort, query)
if err != nil {
toastBytes, _ := models.CreateWebsocketToastMessage(&models.ToastMessage{
Type: "error",
AutoClose: 5000,
Text: fmt.Sprintf("Syntax error: %s", err.Error()),
})
if err := SendToSocket(socketId, toastBytes); err != nil {
logger.Log.Error(err)
}
break
}
isQuerySet = true
handleDataChannel := func(c *basenine.Connection, data chan []byte) {
for {
bytes := <-data
if string(bytes) == basenine.CloseChannel {
return
}
var entry *tapApi.Entry
err = json.Unmarshal(bytes, &entry)
if err != nil {
logger.Log.Debugf("Error unmarshalling entry: %v", err.Error())
continue
}
var message []byte
if params.EnableFullEntries {
message, _ = models.CreateFullEntryWebSocketMessage(entry)
} else {
extension := extensionsMap[entry.Protocol.Name]
base := extension.Dissector.Summarize(entry)
message, _ = models.CreateBaseEntryWebSocketMessage(base)
}
if err := SendToSocket(socketId, message); err != nil {
logger.Log.Error(err)
}
}
}
handleMetaChannel := func(c *basenine.Connection, meta chan []byte) {
for {
bytes := <-meta
if string(bytes) == basenine.CloseChannel {
return
}
var metadata *basenine.Metadata
err = json.Unmarshal(bytes, &metadata)
if err != nil {
logger.Log.Debugf("Error unmarshalling metadata: %v", err.Error())
continue
}
metadataBytes, _ := models.CreateWebsocketQueryMetadataMessage(metadata)
if err := SendToSocket(socketId, metadataBytes); err != nil {
logger.Log.Error(err)
}
}
}
go handleDataChannel(connection, data)
go handleMetaChannel(connection, meta)
connection.Query(query, data, meta)
} else {
eventHandlers.WebSocketMessage(socketId, msg)
}
eventHandlers.WebSocketMessage(socketId, isTapper, msg)
}
}
func socketCleanup(socketId int, socketConnection *SocketConnection) {
err := socketConnection.connection.Close()
if err != nil {
logger.Log.Errorf("Error closing socket connection for socket id %d: %v", socketId, err)
}
websocketIdsLock.Lock()
connectedWebsockets[socketId] = nil
websocketIdsLock.Unlock()
socketConnection.eventHandlers.WebSocketDisconnect(socketId, socketConnection.isTapper)
}
func SendToSocket(socketId int, message []byte) error {
socketObj := connectedWebsockets[socketId]
if socketObj == nil {
return fmt.Errorf("Socket %v is disconnected", socketId)
return fmt.Errorf("socket %v is disconnected", socketId)
}
var sent = false
time.AfterFunc(time.Second*5, func() {
if !sent {
logger.Log.Error("Socket timed out")
logger.Log.Error("socket timed out")
socketCleanup(socketId, socketObj)
}
})
@@ -255,7 +137,20 @@ func SendToSocket(socketId int, message []byte) error {
sent = true
if err != nil {
return fmt.Errorf("Failed to write message to socket %v, err: %w", socketId, err)
return fmt.Errorf("failed to write message to socket %v, err: %w", socketId, err)
}
return nil
}
func socketCleanup(socketId int, socketConnection *SocketConnection) {
err := socketConnection.connection.Close()
if err != nil {
logger.Log.Errorf("error closing socket connection for socket id %d: %v", socketId, err)
}
websocketIdsLock.Lock()
connectedWebsockets[socketId] = nil
websocketIdsLock.Unlock()
socketConnection.eventHandlers.WebSocketDisconnect(socketId, socketConnection.isTapper)
}

View File

@@ -1,12 +1,13 @@
package api
import (
"context"
"encoding/json"
"fmt"
"sync"
"github.com/gin-gonic/gin"
"github.com/up9inc/mizu/agent/pkg/dependency"
"github.com/up9inc/mizu/agent/pkg/models"
"github.com/up9inc/mizu/agent/pkg/providers"
"github.com/up9inc/mizu/agent/pkg/providers/tappedPods"
"github.com/up9inc/mizu/agent/pkg/providers/tappers"
"github.com/up9inc/mizu/agent/pkg/up9"
@@ -17,7 +18,11 @@ import (
"github.com/up9inc/mizu/shared/logger"
)
var browserClientSocketUUIDs = make([]int, 0)
type BrowserClient struct {
dataStreamCancelFunc context.CancelFunc
}
var browserClients = make(map[int]*BrowserClient, 0)
var tapperClientSocketUUIDs = make([]int, 0)
var socketListLock = sync.Mutex{}
@@ -30,7 +35,7 @@ func init() {
go up9.UpdateAnalyzeStatus(BroadcastToBrowserClients)
}
func (h *RoutesEventHandlers) WebSocketConnect(socketId int, isTapper bool) {
func (h *RoutesEventHandlers) WebSocketConnect(_ *gin.Context, socketId int, isTapper bool) {
if isTapper {
logger.Log.Infof("Websocket event - Tapper connected, socket ID: %d", socketId)
tappers.Connected()
@@ -45,7 +50,7 @@ func (h *RoutesEventHandlers) WebSocketConnect(socketId int, isTapper bool) {
logger.Log.Infof("Websocket event - Browser socket connected, socket ID: %d", socketId)
socketListLock.Lock()
browserClientSocketUUIDs = append(browserClientSocketUUIDs, socketId)
browserClients[socketId] = &BrowserClient{}
socketListLock.Unlock()
BroadcastTappedPodsStatus()
@@ -63,13 +68,16 @@ func (h *RoutesEventHandlers) WebSocketDisconnect(socketId int, isTapper bool) {
} else {
logger.Log.Infof("Websocket event - Browser socket disconnected, socket ID: %d", socketId)
socketListLock.Lock()
removeSocketUUIDFromBrowserSlice(socketId)
if browserClients[socketId] != nil && browserClients[socketId].dataStreamCancelFunc != nil {
browserClients[socketId].dataStreamCancelFunc()
}
delete(browserClients, socketId)
socketListLock.Unlock()
}
}
func BroadcastToBrowserClients(message []byte) {
for _, socketId := range browserClientSocketUUIDs {
for socketId := range browserClients {
go func(socketId int) {
if err := SendToSocket(socketId, message); err != nil {
logger.Log.Error(err)
@@ -88,7 +96,33 @@ func BroadcastToTapperClients(message []byte) {
}
}
func (h *RoutesEventHandlers) WebSocketMessage(_ int, message []byte) {
func (h *RoutesEventHandlers) WebSocketMessage(socketId int, isTapper bool, message []byte) {
if isTapper {
HandleTapperIncomingMessage(message, h.SocketOutChannel, BroadcastToBrowserClients)
} else {
// we initiate the basenine stream after the first websocket message we receive (it contains the entry query), we then store a cancelfunc to later cancel this stream
if browserClients[socketId] != nil && browserClients[socketId].dataStreamCancelFunc == nil {
var params WebSocketParams
if err := json.Unmarshal(message, &params); err != nil {
logger.Log.Errorf("Error: %v", socketId, err)
return
}
entriesStreamer := dependency.GetInstance(dependency.EntriesSocketStreamer).(EntryStreamer)
ctx, cancelFunc := context.WithCancel(context.Background())
err := entriesStreamer.Get(ctx, socketId, &params)
if err != nil {
logger.Log.Errorf("error initializing basenine stream for browser socket %d %+v", socketId, err)
cancelFunc()
} else {
browserClients[socketId].dataStreamCancelFunc = cancelFunc
}
}
}
}
func HandleTapperIncomingMessage(message []byte, socketOutChannel chan<- *tapApi.OutputChannelItem, broadcastMessageFunc func([]byte)) {
var socketMessageBase shared.WebSocketMessageMetadata
err := json.Unmarshal(message, &socketMessageBase)
if err != nil {
@@ -102,7 +136,7 @@ func (h *RoutesEventHandlers) WebSocketMessage(_ int, message []byte) {
logger.Log.Infof("Could not unmarshal message of message type %s %v", socketMessageBase.MessageType, err)
} else {
// NOTE: This is where the message comes back from the intermediate WebSocket to code.
h.SocketOutChannel <- tappedEntryMessage.Data
socketOutChannel <- tappedEntryMessage.Data
}
case shared.WebSocketMessageTypeUpdateStatus:
var statusMessage shared.WebSocketStatusMessage
@@ -110,15 +144,7 @@ func (h *RoutesEventHandlers) WebSocketMessage(_ int, message []byte) {
if err != nil {
logger.Log.Infof("Could not unmarshal message of message type %s %v", socketMessageBase.MessageType, err)
} else {
BroadcastToBrowserClients(message)
}
case shared.WebsocketMessageTypeOutboundLink:
var outboundLinkMessage models.WebsocketOutboundLinkMessage
err := json.Unmarshal(message, &outboundLinkMessage)
if err != nil {
logger.Log.Infof("Could not unmarshal message of message type %s %v", socketMessageBase.MessageType, err)
} else {
handleTLSLink(outboundLinkMessage)
broadcastMessageFunc(message)
}
default:
logger.Log.Infof("Received socket message of type %s for which no handlers are defined", socketMessageBase.MessageType)
@@ -126,39 +152,6 @@ func (h *RoutesEventHandlers) WebSocketMessage(_ int, message []byte) {
}
}
func handleTLSLink(outboundLinkMessage models.WebsocketOutboundLinkMessage) {
resolvedNameObject := k8sResolver.Resolve(outboundLinkMessage.Data.DstIP)
if resolvedNameObject != nil {
outboundLinkMessage.Data.DstIP = resolvedNameObject.FullAddress
} else if outboundLinkMessage.Data.SuggestedResolvedName != "" {
outboundLinkMessage.Data.DstIP = outboundLinkMessage.Data.SuggestedResolvedName
}
cacheKey := fmt.Sprintf("%s -> %s:%d", outboundLinkMessage.Data.Src, outboundLinkMessage.Data.DstIP, outboundLinkMessage.Data.DstPort)
_, isInCache := providers.RecentTLSLinks.Get(cacheKey)
if isInCache {
return
} else {
providers.RecentTLSLinks.SetDefault(cacheKey, outboundLinkMessage.Data)
}
marshaledMessage, err := json.Marshal(outboundLinkMessage)
if err != nil {
logger.Log.Errorf("Error marshaling outbound link message for broadcasting: %v", err)
} else {
logger.Log.Errorf("Broadcasting outboundlink message %s", string(marshaledMessage))
BroadcastToBrowserClients(marshaledMessage)
}
}
func removeSocketUUIDFromBrowserSlice(uuidToRemove int) {
newUUIDSlice := make([]int, 0, len(browserClientSocketUUIDs))
for _, uuid := range browserClientSocketUUIDs {
if uuid != uuidToRemove {
newUUIDSlice = append(newUUIDSlice, uuid)
}
}
browserClientSocketUUIDs = newUUIDSlice
}
func removeSocketUUIDFromTapperSlice(uuidToRemove int) {
newUUIDSlice := make([]int, 0, len(tapperClientSocketUUIDs))
for _, uuid := range tapperClientSocketUUIDs {

View File

@@ -58,12 +58,12 @@ func getRecorderAndContext() (*httptest.ResponseRecorder, *gin.Context) {
receiveBuffer: bytes.NewBufferString("\n"),
}
dependency.RegisterGenerator(dependency.OasGeneratorDependency, func() interface{} {
return oas.GetDefaultOasGeneratorInstance(dummyConn)
return oas.GetDefaultOasGeneratorInstance()
})
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
oas.GetDefaultOasGeneratorInstance(dummyConn).Start()
oas.GetDefaultOasGeneratorInstance(dummyConn).GetServiceSpecs().Store("some", oas.NewGen("some"))
oas.GetDefaultOasGeneratorInstance().Start(dummyConn)
oas.GetDefaultOasGeneratorInstance().GetServiceSpecs().Store("some", oas.NewGen("some"))
return recorder, c
}

View File

@@ -101,16 +101,18 @@ func (s *ServiceMapControllerSuite) TestGet() {
// response nodes
aNode := servicemap.ServiceMapNode{
Id: 1,
Name: TCPEntryA.Name,
Entry: TCPEntryA,
Count: 1,
Id: 1,
Name: TCPEntryA.Name,
Entry: TCPEntryA,
Resolved: true,
Count: 1,
}
bNode := servicemap.ServiceMapNode{
Id: 2,
Name: TCPEntryB.Name,
Entry: TCPEntryB,
Count: 1,
Id: 2,
Name: TCPEntryB.Name,
Entry: TCPEntryB,
Resolved: true,
Count: 1,
}
assert.Contains(response.Nodes, aNode)
assert.Contains(response.Nodes, bNode)

View File

@@ -6,4 +6,6 @@ const (
ServiceMapGeneratorDependency = "ServiceMapGeneratorDependency"
OasGeneratorDependency = "OasGeneratorDependency"
EntriesProvider = "EntriesProvider"
EntriesSocketStreamer = "EntriesSocketStreamer"
EntryStreamerSocketConnector = "EntryStreamerSocketConnector"
)

View File

@@ -19,46 +19,57 @@ var (
)
type OasGenerator interface {
Start()
Start(conn *basenine.Connection)
Stop()
IsStarted() bool
Reset()
GetServiceSpecs() *sync.Map
SetEntriesQuery(query string) bool
}
type defaultOasGenerator struct {
started bool
ctx context.Context
cancel context.CancelFunc
serviceSpecs *sync.Map
dbConn *basenine.Connection
started bool
ctx context.Context
cancel context.CancelFunc
serviceSpecs *sync.Map
dbConn *basenine.Connection
entriesQuery string
}
func GetDefaultOasGeneratorInstance(conn *basenine.Connection) *defaultOasGenerator {
func GetDefaultOasGeneratorInstance() *defaultOasGenerator {
syncOnce.Do(func() {
if conn == nil {
c, err := basenine.NewConnection(shared.BasenineHost, shared.BaseninePort)
if err != nil {
panic(err)
}
conn = c
}
instance = NewDefaultOasGenerator(conn)
instance = NewDefaultOasGenerator()
logger.Log.Debug("OAS Generator Initialized")
})
return instance
}
func (g *defaultOasGenerator) Start() {
func (g *defaultOasGenerator) Start(conn *basenine.Connection) {
if g.started {
return
}
if g.dbConn == nil {
if conn == nil {
logger.Log.Infof("Creating new DB connection for OAS generator to address %s:%s", shared.BasenineHost, shared.BaseninePort)
newConn, err := basenine.NewConnection(shared.BasenineHost, shared.BaseninePort)
if err != nil {
logger.Log.Error("Error connecting to DB for OAS generator, err: %v", err)
return
}
conn = newConn
}
g.dbConn = conn
}
ctx, cancel := context.WithCancel(context.Background())
g.cancel = cancel
g.ctx = ctx
g.serviceSpecs = &sync.Map{}
g.started = true
go g.runGenerator()
}
@@ -66,8 +77,15 @@ func (g *defaultOasGenerator) Stop() {
if !g.started {
return
}
if g.dbConn != nil {
g.dbConn.Close()
g.dbConn = nil
}
g.cancel()
g.Reset()
g.reset()
g.started = false
}
@@ -76,16 +94,19 @@ func (g *defaultOasGenerator) IsStarted() bool {
}
func (g *defaultOasGenerator) runGenerator() {
// Make []byte channels to recieve the data and the meta
// Make []byte channels to receive the data and the meta
dataChan := make(chan []byte)
metaChan := make(chan []byte)
g.dbConn.Query("", dataChan, metaChan)
logger.Log.Infof("Querying DB for OAS generator with query '%s'", g.entriesQuery)
g.dbConn.Query(g.entriesQuery, dataChan, metaChan)
for {
select {
case <-g.ctx.Done():
logger.Log.Infof("OAS Generator was canceled")
close(dataChan)
close(metaChan)
return
case metaBytes, ok := <-metaChan:
@@ -173,7 +194,7 @@ func (g *defaultOasGenerator) getGen(dest string, urlStr string) *SpecGen {
return gen
}
func (g *defaultOasGenerator) Reset() {
func (g *defaultOasGenerator) reset() {
g.serviceSpecs = &sync.Map{}
}
@@ -181,12 +202,18 @@ func (g *defaultOasGenerator) GetServiceSpecs() *sync.Map {
return g.serviceSpecs
}
func NewDefaultOasGenerator(c *basenine.Connection) *defaultOasGenerator {
func (g *defaultOasGenerator) SetEntriesQuery(query string) bool {
changed := g.entriesQuery != query
g.entriesQuery = query
return changed
}
func NewDefaultOasGenerator() *defaultOasGenerator {
return &defaultOasGenerator{
started: false,
ctx: nil,
cancel: nil,
serviceSpecs: nil,
dbConn: c,
dbConn: nil,
}
}

View File

@@ -3,13 +3,11 @@ package oas
import (
"encoding/json"
"github.com/up9inc/mizu/agent/pkg/har"
"sync"
"testing"
)
func TestOASGen(t *testing.T) {
gen := new(defaultOasGenerator)
gen.serviceSpecs = &sync.Map{}
e := new(har.Entry)
err := json.Unmarshal([]byte(`{"startedDateTime": "20000101","request": {"url": "https://host/path", "method": "GET"}, "response": {"status": 200}}`), e)
@@ -21,6 +19,9 @@ func TestOASGen(t *testing.T) {
Destination: "some",
Entry: *e,
}
dummyConn := GetFakeDBConn(`{"startedDateTime": "20000101","request": {"url": "https://host/path", "method": "GET"}, "response": {"status": 200}}`)
gen.Start(dummyConn)
gen.handleHARWithSource(ews)
g, ok := gen.serviceSpecs.Load("some")
if !ok {
@@ -33,4 +34,9 @@ func TestOASGen(t *testing.T) {
}
specText, _ := json.Marshal(spec)
t.Log(string(specText))
if !gen.IsStarted() {
t.Errorf("Should be started")
}
gen.Stop()
}

View File

@@ -1,8 +1,10 @@
package oas
import (
"bytes"
"encoding/json"
"io/ioutil"
"net"
"os"
"regexp"
"strings"
@@ -11,13 +13,22 @@ import (
"time"
"github.com/chanced/openapi"
"github.com/op/go-logging"
"github.com/up9inc/mizu/shared/logger"
"github.com/wI2L/jsondiff"
basenine "github.com/up9inc/basenine/client/go"
"github.com/up9inc/mizu/agent/pkg/har"
)
func GetFakeDBConn(send string) *basenine.Connection {
dummyConn := new(basenine.Connection)
dummyConn.Conn = FakeConn{
sendBuffer: bytes.NewBufferString(send),
receiveBuffer: bytes.NewBufferString(""),
}
return dummyConn
}
// if started via env, write file into subdir
func outputSpec(label string, spec *openapi.OpenAPI, t *testing.T) string {
content, err := json.MarshalIndent(spec, "", " ")
@@ -43,14 +54,14 @@ func outputSpec(label string, spec *openapi.OpenAPI, t *testing.T) string {
}
func TestEntries(t *testing.T) {
logger.InitLoggerStd(logging.INFO)
//logger.InitLoggerStd(logging.INFO) causes race condition
files, err := getFiles("./test_artifacts/")
if err != nil {
t.Log(err)
t.FailNow()
}
gen := NewDefaultOasGenerator(nil)
gen := NewDefaultOasGenerator()
gen.serviceSpecs = new(sync.Map)
loadStartingOAS("test_artifacts/catalogue.json", "catalogue", gen.serviceSpecs)
loadStartingOAS("test_artifacts/trcc.json", "trcc-api-service", gen.serviceSpecs)
@@ -124,7 +135,7 @@ func TestEntries(t *testing.T) {
}
func TestFileSingle(t *testing.T) {
gen := NewDefaultOasGenerator(nil)
gen := NewDefaultOasGenerator()
gen.serviceSpecs = new(sync.Map)
// loadStartingOAS()
file := "test_artifacts/params.har"
@@ -214,7 +225,7 @@ func loadStartingOAS(file string, label string, specs *sync.Map) {
}
func TestEntriesNegative(t *testing.T) {
gen := NewDefaultOasGenerator(nil)
gen := NewDefaultOasGenerator()
gen.serviceSpecs = new(sync.Map)
files := []string{"invalid"}
_, err := feedEntries(files, false, gen)
@@ -225,7 +236,7 @@ func TestEntriesNegative(t *testing.T) {
}
func TestEntriesPositive(t *testing.T) {
gen := NewDefaultOasGenerator(nil)
gen := NewDefaultOasGenerator()
gen.serviceSpecs = new(sync.Map)
files := []string{"test_artifacts/params.har"}
_, err := feedEntries(files, false, gen)
@@ -267,3 +278,17 @@ func TestLoadValid3_1(t *testing.T) {
t.FailNow()
}
}
type FakeConn struct {
sendBuffer *bytes.Buffer
receiveBuffer *bytes.Buffer
}
func (f FakeConn) Read(p []byte) (int, error) { return f.sendBuffer.Read(p) }
func (f FakeConn) Write(p []byte) (int, error) { return f.receiveBuffer.Write(p) }
func (FakeConn) Close() error { return nil }
func (FakeConn) LocalAddr() net.Addr { return nil }
func (FakeConn) RemoteAddr() net.Addr { return nil }
func (FakeConn) SetDeadline(t time.Time) error { return nil }
func (FakeConn) SetReadDeadline(t time.Time) error { return nil }
func (FakeConn) SetWriteDeadline(t time.Time) error { return nil }

View File

@@ -18,10 +18,11 @@ type ServiceMapResponse struct {
}
type ServiceMapNode struct {
Id int `json:"id"`
Name string `json:"name"`
Entry *tapApi.TCP `json:"entry"`
Count int `json:"count"`
Id int `json:"id"`
Name string `json:"name"`
Entry *tapApi.TCP `json:"entry"`
Count int `json:"count"`
Resolved bool `json:"resolved"`
}
type ServiceMapEdge struct {

View File

@@ -224,35 +224,41 @@ func (s *defaultServiceMap) GetStatus() ServiceMapStatus {
}
func (s *defaultServiceMap) GetNodes() []ServiceMapNode {
var nodes []ServiceMapNode
nodes := []ServiceMapNode{}
for i, n := range s.graph.Nodes {
nodes = append(nodes, ServiceMapNode{
Id: n.id,
Name: string(i),
Entry: n.entry,
Count: n.count,
Id: n.id,
Name: string(i),
Resolved: n.entry.Name != UnresolvedNodeName,
Entry: n.entry,
Count: n.count,
})
}
return nodes
}
func (s *defaultServiceMap) GetEdges() []ServiceMapEdge {
var edges []ServiceMapEdge
edges := []ServiceMapEdge{}
for u, m := range s.graph.Edges {
for v := range m {
for _, p := range s.graph.Edges[u][v].data {
edges = append(edges, ServiceMapEdge{
Source: ServiceMapNode{
Id: s.graph.Nodes[u].id,
Name: string(u),
Entry: s.graph.Nodes[u].entry,
Count: s.graph.Nodes[u].count,
Id: s.graph.Nodes[u].id,
Name: string(u),
Entry: s.graph.Nodes[u].entry,
Resolved: s.graph.Nodes[u].entry.Name != UnresolvedNodeName,
Count: s.graph.Nodes[u].count,
},
Destination: ServiceMapNode{
Id: s.graph.Nodes[v].id,
Name: string(v),
Entry: s.graph.Nodes[v].entry,
Count: s.graph.Nodes[v].count,
Id: s.graph.Nodes[v].id,
Name: string(v),
Entry: s.graph.Nodes[v].entry,
Resolved: s.graph.Nodes[v].entry.Name != UnresolvedNodeName,
Count: s.graph.Nodes[v].count,
},
Count: p.count,
Protocol: p.protocol,
@@ -260,6 +266,7 @@ func (s *defaultServiceMap) GetEdges() []ServiceMapEdge {
}
}
}
return edges
}

View File

@@ -403,10 +403,10 @@ func (s *ServiceMapEnabledSuite) TestServiceMap() {
assert.Equal(0, status.EdgeCount)
// Nodes after reset
assert.Equal([]ServiceMapNode(nil), nodes)
assert.Equal([]ServiceMapNode{}, nodes)
// Edges after reset
assert.Equal([]ServiceMapEdge(nil), edges)
assert.Equal([]ServiceMapEdge{}, edges)
}
func TestServiceMapSuite(t *testing.T) {

View File

@@ -161,7 +161,7 @@ type Entry struct {
Capture Capture `json:"capture"`
Source *TCP `json:"src"`
Destination *TCP `json:"dst"`
Namespace string `json:"namespace,omitempty"`
Namespace string `json:"namespace"`
Outgoing bool `json:"outgoing"`
Timestamp int64 `json:"timestamp"`
StartTime time.Time `json:"startTime"`

View File

@@ -53,7 +53,16 @@ func representMapSliceAsTable(mapSlice []interface{}, selectorPrefix string) (re
h := item.(map[string]interface{})
key := h["name"].(string)
value := h["value"]
switch reflect.TypeOf(value).Kind() {
var reflectKind reflect.Kind
reflectType := reflect.TypeOf(value)
if reflectType == nil {
reflectKind = reflect.Interface
} else {
reflectKind = reflect.TypeOf(value).Kind()
}
switch reflectKind {
case reflect.Slice:
fallthrough
case reflect.Array:

View File

@@ -28,26 +28,6 @@ const protoMinorHTTP2 = 0
var maxHTTP2DataLen = 1 * 1024 * 1024 // 1MB
var grpcStatusCodes = []string{
"OK",
"CANCELLED",
"UNKNOWN",
"INVALID_ARGUMENT",
"DEADLINE_EXCEEDED",
"NOT_FOUND",
"ALREADY_EXISTS",
"PERMISSION_DENIED",
"RESOURCE_EXHAUSTED",
"FAILED_PRECONDITION",
"ABORTED",
"OUT_OF_RANGE",
"UNIMPLEMENTED",
"INTERNAL",
"UNAVAILABLE",
"DATA_LOSS",
"UNAUTHENTICATED",
}
type messageFragment struct {
headers []hpack.HeaderField
data []byte
@@ -142,18 +122,8 @@ func (ga *Http2Assembler) readMessage() (streamID uint32, messageHTTP1 interface
// gRPC detection
grpcStatus := headersHTTP1.Get("Grpc-Status")
if grpcStatus != "" {
if grpcStatus != "" || strings.Contains(headersHTTP1.Get("Content-Type"), "application/grpc") {
isGrpc = true
status = grpcStatus
}
if strings.Contains(headersHTTP1.Get("Content-Type"), "application/grpc") {
isGrpc = true
grpcPath := headersHTTP1.Get(":path")
pathSegments := strings.Split(grpcPath, "/")
if len(pathSegments) > 0 {
method = pathSegments[len(pathSegments)-1]
}
}
if method != "" {

View File

@@ -248,11 +248,6 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
reqDetails["_queryStringMerged"] = mapSliceMergeRepeatedKeys(reqDetails["_queryString"].([]interface{}))
reqDetails["queryString"] = mapSliceRebuildAsMap(reqDetails["_queryStringMerged"].([]interface{}))
statusCode := int(resDetails["status"].(float64))
if item.Protocol.Abbreviation == "gRPC" {
resDetails["statusText"] = grpcStatusCodes[statusCode]
}
elapsedTime := item.Pair.Response.CaptureTime.Sub(item.Pair.Request.CaptureTime).Round(time.Millisecond).Milliseconds()
if elapsedTime < 0 {
elapsedTime = 0

View File

@@ -8,6 +8,7 @@ require (
github.com/segmentio/kafka-go v0.4.27
github.com/stretchr/testify v1.6.1
github.com/up9inc/mizu/tap/api v0.0.0
golang.org/x/text v0.3.0
)
require (

View File

@@ -40,6 +40,7 @@ golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -3,6 +3,8 @@ package kafka
import (
"encoding/json"
"fmt"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"reflect"
"sort"
"strconv"
@@ -891,8 +893,9 @@ func representMapAsTable(mapData map[string]interface{}, selectorPrefix string,
}
}
selector := fmt.Sprintf("%s[\"%s\"]", selectorPrefix, key)
caser := cases.Title(language.Und, cases.NoLower)
table = append(table, api.TableData{
Name: strings.Join(camelcase.Split(strings.Title(key)), " "),
Name: strings.Join(camelcase.Split(caser.String(key)), " "),
Value: value,
Selector: selector,
})

View File

@@ -49,6 +49,7 @@
"node-fetch": "^3.1.1",
"numeral": "^2.0.6",
"protobuf-decoder": "^0.1.0",
"react-resizable": "^3.0.4",
"react-graph-vis": "^1.0.7",
"react-lowlight": "^3.0.0",
"react-router-dom": "^6.2.1",
@@ -90,4 +91,4 @@
"files": [
"dist"
]
}
}

View File

@@ -42,10 +42,10 @@ const OasModal = ({ openModal, handleCloseModal, getOasServices, getOasByService
try {
const data = await getOasByService(selectedService ? selectedService : oasServices[0]);
setSelectedServiceSpec(data);
} catch (e) {
toast.error("Error occurred while fetching service OAS spec");
console.error(e);
}
} catch (e) {
toast.error("Error occurred while fetching service OAS spec");
console.error(e);
}
};
useEffect(() => {
@@ -61,7 +61,7 @@ const OasModal = ({ openModal, handleCloseModal, getOasServices, getOasByService
useEffect(() => {
onSelectedOASService(null);
},[oasServices])
}, [oasServices])
return (
<Modal
@@ -80,28 +80,28 @@ const OasModal = ({ openModal, handleCloseModal, getOasServices, getOasByService
<div className={style.boxContainer}>
<div className={style.selectHeader}>
<div><img src={openApiLogo} alt="openAPI" className={style.openApilogo} /></div>
<div className={style.title}>OpenAPI</div>
<div className={style.title}>Service Catalog</div>
</div>
<div style={{ cursor: "pointer" }}>
<img src={closeIcon} alt="close" onClick={handleCloseModal} />
</div>
</div>
<div className={style.selectContainer} >
<FormControl>
<Select
labelId="service-select-label"
id="service-select"
value={selectedServiceName}
onChangeCb={onSelectedOASService}
>
{oasServices.map((service) => (
<MenuItem key={service} value={service}>
{service}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<FormControl>
<Select
labelId="service-select-label"
id="service-select"
value={selectedServiceName}
onChangeCb={onSelectedOASService}
>
{oasServices.map((service) => (
<MenuItem key={service} value={service}>
{service}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div className={style.borderLine}></div>
<div className={style.redoc}>
{selectedServiceSpec && <RedocStandalone

View File

@@ -0,0 +1,60 @@
@import "../../variables.module"
.modalContainer
display: flex
flex-wrap: nowrap
width: 100%
height: 100%
.graphSection
flex: 85%
.filterSection
flex: 15%
border-right: 1px solid $blue-color
margin-right: 2%
height: 100%
.filters table
margin-top: 0px
tr
border-style: none
td
color: #8f9bb2
font-size: 11px
font-weight: 600
padding-top: 2px
padding-bottom: 2px
.colorBlock
display: inline-block
height: 15px
width: 50px
.filterWrapper
height: 100%
display: flex
flex-direction: column
margin-right: 10px
.servicesFilterSearch
width: calc(100% - 10px)
max-width: 300px
box-shadow: 0px 1px 5px #979797
margin-left: 10px
margin-bottom: 5px
.servicesFilter
margin-top: clamp(25px,15%,35px)
height: 100%
overflow: hidden
& .servicesFilterList
overflow-y: auto
height: 92%
.separtorLine
margin-top: 10px
border: 1px solid #E9EBF8

View File

@@ -0,0 +1,227 @@
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Box, Fade, Modal, Backdrop, Button } from "@material-ui/core";
import { toast } from "react-toastify";
import spinnerStyle from '../UI/style/Spinner.module.sass';
import spinnerImg from 'assets/spinner.svg';
import Graph from "react-graph-vis";
import debounce from 'lodash/debounce';
import ServiceMapOptions from './ServiceMapOptions'
import { useCommonStyles } from "../../helpers/commonStyle";
import refreshIcon from "assets/refresh.svg";
import closeIcon from "assets/close.svg"
import styles from './ServiceMapModal.module.sass'
import SelectList from "../UI/SelectList";
import { GraphData, ServiceMapGraph } from "./ServiceMapModalTypes"
import { ResizableBox } from "react-resizable"
import "react-resizable/css/styles.css"
import { Utils } from "../../helpers/Utils";
const modalStyle = {
position: 'absolute',
top: '6%',
left: '50%',
transform: 'translate(-50%, 0%)',
width: '89vw',
height: '82vh',
bgcolor: 'background.paper',
borderRadius: '5px',
boxShadow: 24,
p: 4,
color: '#000',
padding: "25px 15px"
};
interface LegentLabelProps {
color: string,
name: string
}
const LegentLabel: React.FC<LegentLabelProps> = ({ color, name }) => {
return <React.Fragment>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span title={name}>{name}</span>
<span style={{ background: color }} className={styles.colorBlock}></span>
</div>
</React.Fragment>
}
const protocols = [
{ key: "http", value: "HTTP", component: <LegentLabel color="#205cf5" name="HTTP" /> },
{ key: "http/2", value: "HTTP/2", component: <LegentLabel color='#244c5a' name="HTTP/2" /> },
{ key: "grpc", value: "gRPC", component: <LegentLabel color='#244c5a' name="gRPC" /> },
{ key: "amqp", value: "AMQP", component: <LegentLabel color='#ff6600' name="AMQP" /> },
{ key: "kafka", value: "KAFKA", component: <LegentLabel color='#000000' name="KAFKA" /> },
{ key: "redis", value: "REDIS", component: <LegentLabel color='#a41e11' name="REDIS" /> },]
interface ServiceMapModalProps {
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
getServiceMapDataApi: () => Promise<any>
}
export const ServiceMapModal: React.FC<ServiceMapModalProps> = ({ isOpen, onClose, getServiceMapDataApi }) => {
const commonClasses = useCommonStyles();
const [isLoading, setIsLoading] = useState<boolean>(true);
const [graphData, setGraphData] = useState<GraphData>({ nodes: [], edges: [] });
const [filteredProtocols, setFilteredProtocols] = useState(protocols.map(x => x.key))
const [filteredServices, setFilteredServices] = useState([])
const [serviceMapApiData, setServiceMapApiData] = useState<ServiceMapGraph>({ edges: [], nodes: [] })
const [servicesSearchVal, setServicesSearchVal] = useState("")
const [graphOptions, setGraphOptions] = useState(ServiceMapOptions);
const getServiceMapData = useCallback(async () => {
try {
setIsLoading(true)
const serviceMapData: ServiceMapGraph = await getServiceMapDataApi()
setServiceMapApiData(serviceMapData)
const newGraphData: GraphData = { nodes: [], edges: [] }
if (serviceMapData.nodes) {
newGraphData.nodes = serviceMapData.nodes.map(mapNodesDatatoGraph)
}
if (serviceMapData.edges) {
newGraphData.edges = serviceMapData.edges.map(mapEdgesDatatoGraph)
}
setGraphData(newGraphData)
} catch (ex) {
toast.error("An error occurred while loading Mizu Service Map, see console for mode details");
console.error(ex);
} finally {
setIsLoading(false)
}
// eslint-disable-next-line
}, [isOpen])
const mapNodesDatatoGraph = node => {
return {
id: node.id,
value: node.count,
label: (node.entry.name === "unresolved") ? node.name : `${node.entry.name} (${node.name})`,
title: "Count: " + node.name,
isResolved: node.entry.resolved
}
}
const mapEdgesDatatoGraph = edge => {
return {
from: edge.source.id,
to: edge.destination.id,
value: edge.count,
label: edge.count.toString(),
color: {
color: edge.protocol.backgroundColor,
highlight: edge.protocol.backgroundColor
},
}
}
const mapToKeyValForFilter = (arr) => arr.map(mapNodesDatatoGraph)
.map((edge) => { return { key: edge.label, value: edge.label } })
.sort((a, b) => { return a.key.localeCompare(b.key) });
const getServicesForFilter = useMemo(() => {
const resolved = mapToKeyValForFilter(serviceMapApiData.nodes?.filter(x => x.resolved))
const unResolved = mapToKeyValForFilter(serviceMapApiData.nodes?.filter(x => !x.resolved))
return [...resolved, ...unResolved]
}, [serviceMapApiData])
const filterServiceMap = (newProtocolsFilters?: any[], newServiceFilters?: string[]) => {
const filterProt = newProtocolsFilters || filteredProtocols
const filterService = newServiceFilters || filteredServices || getServicesForFilter.map(x => x.key)
setFilteredProtocols(filterProt)
setFilteredServices(filterService)
const newGraphData: GraphData = {
nodes: serviceMapApiData.nodes?.map(mapNodesDatatoGraph).filter(node => filterService.includes(node.label)),
edges: serviceMapApiData.edges?.filter(edge => filterProt.includes(edge.protocol.name)).map(mapEdgesDatatoGraph)
}
setGraphData(newGraphData);
}
useEffect(() => {
const resolvedServices = getServicesForFilter.map(x => x.key).filter(serviceName => !Utils.isIpAddress(serviceName))
setFilteredServices(resolvedServices)
filterServiceMap(filteredProtocols, resolvedServices)
}, [getServicesForFilter])
useEffect(() => {
getServiceMapData()
}, [getServiceMapData])
useEffect(() => {
if (graphData?.nodes?.length === 0) return;
let options = { ...graphOptions };
options.physics.barnesHut.avoidOverlap = graphData?.nodes?.length > 10 ? 0 : 1;
setGraphOptions(options);
// eslint-disable-next-line
}, [graphData?.nodes?.length])
const refreshServiceMap = debounce(() => {
getServiceMapData();
}, 500);
return (
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={isOpen}
onClose={onClose}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{ timeout: 500 }}>
<Fade in={isOpen}>
<Box sx={modalStyle}>
<div className={styles.modalContainer}>
{/* TODO: remove error missing height */}
<ResizableBox width={200} style={{ height: '100%', minWidth: "200px" }} axis={"x"}>
<div className={styles.filterSection}>
<div className={styles.filterWrapper}>
<div className={styles.protocolsFilterList}>
<SelectList items={protocols} checkBoxWidth="5%" tableName={"Protocols"} multiSelect={true}
checkedValues={filteredProtocols} setCheckedValues={filterServiceMap} tableClassName={styles.filters} />
</div>
<div className={styles.separtorLine}></div>
<div className={styles.servicesFilter}>
<input className={commonClasses.textField + ` ${styles.servicesFilterSearch}`} placeholder="search service" value={servicesSearchVal} onChange={(event) => setServicesSearchVal(event.target.value)} />
<div className={styles.servicesFilterList}>
<SelectList items={getServicesForFilter} tableName={"Services"} tableClassName={styles.filters} multiSelect={true} searchValue={servicesSearchVal}
checkBoxWidth="5%" checkedValues={filteredServices} setCheckedValues={(newServicesForFilter) => filterServiceMap(null, newServicesForFilter)} />
</div>
</div>
</div>
</div>
</ResizableBox>
<div className={styles.graphSection}>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<Button style={{ marginRight: "3%" }}
startIcon={<img src={refreshIcon} className="custom" alt="refresh" style={{ marginRight: "8%" }}></img>}
size="medium"
variant="contained"
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
onClick={refreshServiceMap}
>
Refresh
</Button>
<img src={closeIcon} alt="close" onClick={() => onClose()} style={{ cursor: "pointer" }}></img>
</div>
{isLoading && <div className={spinnerStyle.spinnerContainer}>
<img alt="spinner" src={spinnerImg} style={{ height: 50 }} />
</div>}
{!isLoading && <div style={{ height: "100%", width: "100%" }}>
<Graph
graph={graphData}
options={graphOptions}
/>
</div>
}
</div>
</div>
</Box>
</Fade>
</Modal>
);
}

View File

@@ -0,0 +1,60 @@
export interface GraphData {
nodes: Node[];
edges: Edge[];
}
export interface Node {
id: number;
value: number;
label: string;
title?: string;
color?: object;
}
export interface Edge {
from: number;
to: number;
value: number;
label: string;
title?: string;
color?: object;
}
export interface ServiceMapNode {
id: number;
name: string;
entry: Entry;
count: number;
resolved: boolean;
}
export interface ServiceMapEdge {
source: ServiceMapNode;
destination: ServiceMapNode;
count: number;
protocol: Protocol;
}
export interface ServiceMapGraph {
nodes: ServiceMapNode[];
edges: ServiceMapEdge[];
}
export interface Entry {
ip: string;
port: string;
name: string;
}
export interface Protocol {
name: string;
abbr: string;
macro: string;
version: string;
backgroundColor: string;
foregroundColor: string;
fontSize: number;
referenceLink: string;
ports: string[];
priority: number;
}

View File

@@ -0,0 +1,83 @@
const ServiceMapOptions = {
physics: {
enabled: true,
solver: 'barnesHut',
barnesHut: {
theta: 0.5,
gravitationalConstant: -2000,
centralGravity: 0.3,
springLength: 180,
springConstant: 0.04,
damping: 0.09,
avoidOverlap: 0
},
},
layout: {
hierarchical: false,
randomSeed: 1 // always on node 1
},
nodes: {
shape: 'dot',
chosen: true,
color: {
background: '#27AE60',
border: '#000000',
highlight: {
background: '#27AE60',
border: '#000000',
},
},
font: {
color: '#343434',
size: 14, // px
face: 'arial',
background: 'none',
strokeWidth: 0, // px
strokeColor: '#ffffff',
align: 'center',
multi: false
},
borderWidth: 1.5,
borderWidthSelected: 2.5,
labelHighlightBold: true,
opacity: 1,
shadow: true,
},
edges: {
chosen: true,
dashes: false,
arrowStrikethrough: false,
arrows: {
to: {
enabled: true,
},
middle: {
enabled: false,
},
from: {
enabled: false,
}
},
smooth: {
enabled: true,
type: 'dynamic',
roundness: 1.0
},
font: {
color: '#343434',
size: 12, // px
face: 'arial',
background: 'none',
strokeWidth: 2, // px
strokeColor: '#ffffff',
align: 'horizontal',
multi: false,
},
labelHighlightBold: true,
selectionWidth: 1,
shadow: true,
},
autoResize: true,
};
export default ServiceMapOptions

View File

@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.591 9.99997C18.591 14.7446 14.7447 18.5909 10.0001 18.5909C5.25546 18.5909 1.40918 14.7446 1.40918 9.99997C1.40918 5.25534 5.25546 1.40906 10.0001 1.40906C14.7447 1.40906 18.591 5.25534 18.591 9.99997Z" fill="#E9EBF8" stroke="#BCCEFD"/>
<path d="M13.1604 8.23038L11.95 7.01994L10.1392 8.83078L8.32832 7.01994L7.11789 8.23038L8.92872 10.0412L7.12046 11.8495L8.33089 13.0599L10.1392 11.2517L11.9474 13.0599L13.1579 11.8495L11.3496 10.0412L13.1604 8.23038Z" fill="#205CF5"/>
</svg>

After

Width:  |  Height:  |  Size: 588 B

View File

@@ -0,0 +1,3 @@
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.8337 11.9167H7.69308L7.69416 11.907C7.83561 11.2143 8.11247 10.5564 8.50883 9.97105C9.09865 9.10202 9.92598 8.42097 10.8922 8.00913C11.2193 7.87046 11.5606 7.7643 11.9083 7.69388C12.6297 7.54762 13.3731 7.54762 14.0945 7.69388C15.1312 7.90631 16.0825 8.41908 16.8299 9.1683L18.3639 7.63863C17.6725 6.94707 16.8546 6.39501 15.9546 6.01255C15.4956 5.81823 15.0184 5.67016 14.53 5.57055C13.5223 5.36581 12.4838 5.36581 11.4761 5.57055C10.9873 5.67057 10.5098 5.819 10.0504 6.01363C8.69682 6.58791 7.53808 7.54123 6.71374 8.7588C6.15895 9.5798 5.77099 10.5019 5.57191 11.4725C5.54158 11.6188 5.52533 11.7683 5.50366 11.9167H2.16699L6.50033 16.25L10.8337 11.9167ZM15.167 14.0834H18.3076L18.3065 14.092C18.0234 15.4806 17.205 16.7019 16.0282 17.4915C15.443 17.8882 14.7851 18.1651 14.0923 18.3062C13.3713 18.4525 12.6283 18.4525 11.9072 18.3062C11.2146 18.1648 10.5567 17.8879 9.97133 17.4915C9.68383 17.2971 9.41541 17.0758 9.16966 16.8307L7.63783 18.3625C8.32954 19.0539 9.14791 19.6056 10.0482 19.9875C10.5076 20.1825 10.9875 20.331 11.4728 20.4295C12.4801 20.6344 13.5184 20.6344 14.5257 20.4295C16.4676 20.0265 18.1757 18.8819 19.2869 17.2391C19.8412 16.4187 20.2288 15.4974 20.4277 14.5275C20.4569 14.3813 20.4742 14.2318 20.4959 14.0834H23.8337L19.5003 9.75005L15.167 14.0834Z" fill="#205CF5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<circle cx="50" cy="50" fill="none" stroke="#1d3f72" stroke-width="10" r="35" stroke-dasharray="164.93361431346415 56.97787143782138" transform="rotate(275.903 50 50)">
<animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" values="0 50 50;360 50 50" keyTimes="0;1"></animateTransform>
</circle>
<!-- [ldio] generated by https://loading.io/ --></svg>

After

Width:  |  Height:  |  Size: 673 B

View File

@@ -5,151 +5,214 @@ import Moment from 'moment';
import {EntryItem} from "./EntryListItem/EntryListItem";
import down from "assets/downImg.svg";
import spinner from 'assets/spinner.svg';
import {RecoilState, useRecoilState, useRecoilValue} from "recoil";
import {RecoilState, useRecoilState, useRecoilValue, useSetRecoilState} from "recoil";
import entriesAtom from "../../recoil/entries";
import queryAtom from "../../recoil/query";
import TrafficViewerApiAtom from "../../recoil/TrafficViewerApi";
import TrafficViewerApi from "./TrafficViewerApi";
import focusedEntryIdAtom from "../../recoil/focusedEntryId";
import {toast} from "react-toastify";
import {TOAST_CONTAINER_ID} from "../../configs/Consts";
import tappingStatusAtom from "../../recoil/tappingStatus";
import leftOffTopAtom from "../../recoil/leftOffTop";
interface EntriesListProps {
listEntryREF: any;
onSnapBrokenEvent: () => void;
isSnappedToBottom: boolean;
setIsSnappedToBottom: any;
queriedCurrent: number;
setQueriedCurrent: any;
queriedTotal: number;
setQueriedTotal: any;
startTime: number;
noMoreDataTop: boolean;
setNoMoreDataTop: (flag: boolean) => void;
leftOffTop: number;
setLeftOffTop: (leftOffTop: number) => void;
openWebSocket: (query: string, resetEntries: boolean) => void;
leftOffBottom: number;
truncatedTimestamp: number;
setTruncatedTimestamp: any;
scrollableRef: any;
ws: any;
listEntryREF: any;
onSnapBrokenEvent: () => void;
isSnappedToBottom: boolean;
setIsSnappedToBottom: any;
noMoreDataTop: boolean;
setNoMoreDataTop: (flag: boolean) => void;
openWebSocket: (query: string, resetEntries: boolean) => void;
scrollableRef: any;
ws: any;
}
export const EntriesList: React.FC<EntriesListProps> = ({listEntryREF, onSnapBrokenEvent, isSnappedToBottom, setIsSnappedToBottom, queriedCurrent, setQueriedCurrent, queriedTotal, setQueriedTotal, startTime, noMoreDataTop, setNoMoreDataTop, leftOffTop, setLeftOffTop, openWebSocket, leftOffBottom, truncatedTimestamp, setTruncatedTimestamp, scrollableRef, ws}) => {
export const EntriesList: React.FC<EntriesListProps> = ({
listEntryREF,
onSnapBrokenEvent,
isSnappedToBottom,
setIsSnappedToBottom,
noMoreDataTop,
setNoMoreDataTop,
openWebSocket,
scrollableRef,
ws
}) => {
const [entries, setEntries] = useRecoilState(entriesAtom);
const query = useRecoilValue(queryAtom);
const isWsConnectionClosed = ws?.current?.readyState !== WebSocket.OPEN;
const [entries, setEntries] = useRecoilState(entriesAtom);
const query = useRecoilValue(queryAtom);
const isWsConnectionClosed = ws?.current?.readyState !== WebSocket.OPEN;
const [focusedEntryId, setFocusedEntryId] = useRecoilState(focusedEntryIdAtom);
const [leftOffTop, setLeftOffTop] = useRecoilState(leftOffTopAtom);
const setTappingStatus = useSetRecoilState(tappingStatusAtom);
const trafficViewerApi = useRecoilValue(TrafficViewerApiAtom as RecoilState<TrafficViewerApi>)
const trafficViewerApi = useRecoilValue(TrafficViewerApiAtom as RecoilState<TrafficViewerApi>)
const [loadMoreTop, setLoadMoreTop] = useState(false);
const [isLoadingTop, setIsLoadingTop] = useState(false);
const [loadMoreTop, setLoadMoreTop] = useState(false);
const [isLoadingTop, setIsLoadingTop] = useState(false);
const [queriedTotal, setQueriedTotal] = useState(0);
const [startTime, setStartTime] = useState(0);
const [truncatedTimestamp, setTruncatedTimestamp] = useState(0);
useEffect(() => {
const list = document.getElementById('list').firstElementChild;
list.addEventListener('scroll', (e) => {
const el: any = e.target;
if(el.scrollTop === 0) {
setLoadMoreTop(true);
} else {
setNoMoreDataTop(false);
setLoadMoreTop(false);
}
});
}, [setLoadMoreTop, setNoMoreDataTop]);
const leftOffBottom = entries.length > 0 ? entries[entries.length - 1].id : -1;
const memoizedEntries = useMemo(() => {
return entries;
},[entries]);
const getOldEntries = useCallback(async () => {
useEffect(() => {
const list = document.getElementById('list').firstElementChild;
list.addEventListener('scroll', (e) => {
const el: any = e.target;
if (el.scrollTop === 0) {
setLoadMoreTop(true);
} else {
setNoMoreDataTop(false);
setLoadMoreTop(false);
if (leftOffTop === null || leftOffTop <= 0) {
return;
}
setIsLoadingTop(true);
const data = await trafficViewerApi.fetchEntries(leftOffTop, -1, query, 100, 3000);
if (!data || data.data === null || data.meta === null) {
setNoMoreDataTop(true);
setIsLoadingTop(false);
return;
}
setLeftOffTop(data.meta.leftOff);
}
});
}, [setLoadMoreTop, setNoMoreDataTop]);
let scrollTo: boolean;
if (data.meta.leftOff === 0) {
setNoMoreDataTop(true);
scrollTo = false;
} else {
scrollTo = true;
}
setIsLoadingTop(false);
const memoizedEntries = useMemo(() => {
return entries;
}, [entries]);
const newEntries = [...data.data.reverse(), ...entries];
setEntries(newEntries);
const getOldEntries = useCallback(async () => {
setLoadMoreTop(false);
if (leftOffTop === null || leftOffTop <= 0) {
return;
}
setIsLoadingTop(true);
const data = await trafficViewerApi.fetchEntries(leftOffTop, -1, query, 100, 3000);
if (!data || data.data === null || data.meta === null) {
setNoMoreDataTop(true);
setIsLoadingTop(false);
return;
}
setLeftOffTop(data.meta.leftOff);
setQueriedCurrent(queriedCurrent + data.meta.current);
setQueriedTotal(data.meta.total);
setTruncatedTimestamp(data.meta.truncatedTimestamp);
let scrollTo: boolean;
if (data.meta.leftOff === 0) {
setNoMoreDataTop(true);
scrollTo = false;
} else {
scrollTo = true;
}
setIsLoadingTop(false);
if (scrollTo) {
scrollableRef.current.scrollToIndex(data.data.length - 1);
}
},[setLoadMoreTop, setIsLoadingTop, entries, setEntries, query, setNoMoreDataTop, leftOffTop, setLeftOffTop, queriedCurrent, setQueriedCurrent, setQueriedTotal, setTruncatedTimestamp, scrollableRef]);
const newEntries = [...data.data.reverse(), ...entries];
setEntries(newEntries);
useEffect(() => {
if(!isWsConnectionClosed || !loadMoreTop || noMoreDataTop) return;
getOldEntries();
}, [loadMoreTop, noMoreDataTop, getOldEntries, isWsConnectionClosed]);
setQueriedTotal(data.meta.total);
setTruncatedTimestamp(data.meta.truncatedTimestamp);
const scrollbarVisible = scrollableRef.current?.childWrapperRef.current.clientHeight > scrollableRef.current?.wrapperRef.current.clientHeight;
if (scrollTo) {
scrollableRef.current.scrollToIndex(data.data.length - 1);
}
}, [setLoadMoreTop, setIsLoadingTop, entries, setEntries, query, setNoMoreDataTop, leftOffTop, setLeftOffTop, setQueriedTotal, setTruncatedTimestamp, scrollableRef]);
return <React.Fragment>
<div className={styles.list}>
<div id="list" ref={listEntryREF} className={styles.list}>
{isLoadingTop && <div className={styles.spinnerContainer}>
<img alt="spinner" src={spinner} style={{height: 25}}/>
</div>}
{noMoreDataTop && <div id="noMoreDataTop" className={styles.noMoreDataAvailable}>No more data available</div>}
<ScrollableFeedVirtualized ref={scrollableRef} itemHeight={48} marginTop={10} onSnapBroken={onSnapBrokenEvent}>
{false /* It's because the first child is ignored by ScrollableFeedVirtualized */}
{memoizedEntries.map(entry => <EntryItem
key={`entry-${entry.id}`}
entry={entry}
style={{}}
headingMode={false}
/>)}
</ScrollableFeedVirtualized>
<button type="button"
title="Fetch old records"
className={`${styles.btnOld} ${!scrollbarVisible && leftOffTop > 0 ? styles.showButton : styles.hideButton}`}
onClick={(_) => {
trafficViewerApi.webSocket.close()
getOldEntries();
}}>
<img alt="down" src={down} />
</button>
<button type="button"
title="Snap to bottom"
className={`${styles.btnLive} ${isSnappedToBottom && !isWsConnectionClosed ? styles.hideButton : styles.showButton}`}
onClick={(_) => {
if (isWsConnectionClosed) {
if (query) {
openWebSocket(`(${query}) and leftOff(${leftOffBottom})`, false);
} else {
openWebSocket(`leftOff(${leftOffBottom})`, false);
}
}
scrollableRef.current.jumpToBottom();
setIsSnappedToBottom(true);
}}>
<img alt="down" src={down} />
</button>
</div>
useEffect(() => {
if (!isWsConnectionClosed || !loadMoreTop || noMoreDataTop) return;
getOldEntries();
}, [loadMoreTop, noMoreDataTop, getOldEntries, isWsConnectionClosed]);
<div className={styles.footer}>
<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>}
</div>
</div>
</React.Fragment>;
const scrollbarVisible = scrollableRef.current?.childWrapperRef.current.clientHeight > scrollableRef.current?.wrapperRef.current.clientHeight;
if (ws.current) {
ws.current.onmessage = (e) => {
if (!e?.data) return;
const message = JSON.parse(e.data);
switch (message.messageType) {
case "entry":
const entry = message.data;
if (!focusedEntryId) setFocusedEntryId(entry.id.toString());
const newEntries = [...entries, entry];
if (newEntries.length === 10001) {
setLeftOffTop(newEntries[0].id);
newEntries.shift();
setNoMoreDataTop(false);
}
setEntries(newEntries);
break;
case "status":
setTappingStatus(message.tappingStatus);
break;
case "toast":
toast[message.data.type](message.data.text, {
theme: "colored",
autoClose: message.data.autoClose,
pauseOnHover: true,
progress: undefined,
containerId: TOAST_CONTAINER_ID
});
break;
case "queryMetadata":
setTruncatedTimestamp(message.data.truncatedTimestamp);
setQueriedTotal(message.data.total);
if (leftOffTop === null) {
setLeftOffTop(message.data.leftOff - 1);
}
break;
case "startTime":
setStartTime(message.data);
break;
};
}
}
return <React.Fragment>
<div className={styles.list}>
<div id="list" ref={listEntryREF} className={styles.list}>
{isLoadingTop && <div className={styles.spinnerContainer}>
<img alt="spinner" src={spinner} style={{height: 25}}/>
</div>}
{noMoreDataTop && <div id="noMoreDataTop" className={styles.noMoreDataAvailable}>No more data available</div>}
<ScrollableFeedVirtualized ref={scrollableRef} itemHeight={48} marginTop={10} onSnapBroken={onSnapBrokenEvent}>
{false /* It's because the first child is ignored by ScrollableFeedVirtualized */}
{memoizedEntries.map(entry => <EntryItem
key={`entry-${entry.id}`}
entry={entry}
style={{}}
headingMode={false}
/>)}
</ScrollableFeedVirtualized>
<button type="button"
title="Fetch old records"
className={`${styles.btnOld} ${!scrollbarVisible && leftOffTop > 0 ? styles.showButton : styles.hideButton}`}
onClick={(_) => {
trafficViewerApi.webSocket.close()
getOldEntries();
}}>
<img alt="down" src={down}/>
</button>
<button type="button"
title="Snap to bottom"
className={`${styles.btnLive} ${isSnappedToBottom && !isWsConnectionClosed ? styles.hideButton : styles.showButton}`}
onClick={(_) => {
if (isWsConnectionClosed) {
if (query) {
openWebSocket(`(${query}) and leftOff(${leftOffBottom})`, false);
} else {
openWebSocket(`leftOff(${leftOffBottom})`, false);
}
}
scrollableRef.current.jumpToBottom();
setIsSnappedToBottom(true);
}}>
<img alt="down" src={down}/>
</button>
</div>
<div className={styles.footer}>
<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>}
</div>
</div>
</React.Fragment>;
};

View File

@@ -5,14 +5,14 @@ import { makeStyles } from "@material-ui/core";
import Protocol from "../UI/Protocol"
import Queryable from "../UI/Queryable";
import { toast } from "react-toastify";
import { RecoilState, useRecoilState, useRecoilValue } from "recoil";
import { RecoilState, useRecoilValue } from "recoil";
import focusedEntryIdAtom from "../../recoil/focusedEntryId";
import trafficViewerApi from "../../recoil/TrafficViewerApi";
import TrafficViewerApi from "./TrafficViewerApi";
import TrafficViewerApiAtom from "../../recoil/TrafficViewerApi/atom";
import queryAtom from "../../recoil/query/atom";
import useWindowDimensions, { useRequestTextByWidth } from "../../hooks/WindowDimensionsHook";
import { TOAST_CONTAINER_ID } from "../../configs/Consts";
import spinner from "assets/spinner.svg";
const useStyles = makeStyles(() => ({
entryTitle: {
@@ -89,12 +89,13 @@ const EntryTitle: React.FC<any> = ({ protocol, data, elapsedTime }) => {
</div>;
};
const EntrySummary: React.FC<any> = ({ entry }) => {
const EntrySummary: React.FC<any> = ({ entry, namespace }) => {
return <EntryItem
key={`entry-${entry.id}`}
entry={entry}
style={{}}
headingMode={true}
namespace={namespace}
/>;
};
@@ -105,12 +106,13 @@ export const EntryDetailed = () => {
const focusedEntryId = useRecoilValue(focusedEntryIdAtom);
const trafficViewerApi = useRecoilValue(TrafficViewerApiAtom as RecoilState<TrafficViewerApi>)
const query = useRecoilValue(queryAtom);
const [isLoading, setIsLoading] = useState(false);
const [entryData, setEntryData] = useState(null);
useEffect(() => {
if (!focusedEntryId) return;
setEntryData(null);
setIsLoading(true);
(async () => {
try {
const entryData = await trafficViewerApi.getEntry(focusedEntryId, query);
@@ -125,20 +127,23 @@ export const EntryDetailed = () => {
});
}
console.error(error);
} finally {
setIsLoading(false);
}
})();
// eslint-disable-next-line
}, [focusedEntryId]);
return <React.Fragment>
{entryData && <EntryTitle
{isLoading && <div style={{textAlign: "center", width: "100%", marginTop: 50}}><img alt="spinner" src={spinner} style={{height: 60}}/></div>}
{!isLoading && entryData && <EntryTitle
protocol={entryData.protocol}
data={entryData.data}
elapsedTime={entryData.data.elapsedTime}
/>}
{entryData && <EntrySummary entry={entryData.base} />}
{!isLoading && entryData && <EntrySummary entry={entryData.base} namespace={entryData.data.namespace} />}
<React.Fragment>
{entryData && <EntryViewer
{!isLoading && entryData && <EntryViewer
representation={entryData.representation}
isRulesEnabled={entryData.isRulesEnabled}
rulesMatched={entryData.rulesMatched}

View File

@@ -52,6 +52,7 @@ interface EntryProps {
entry: Entry;
style: object;
headingMode: boolean;
namespace?: string;
}
enum CaptureTypes {
@@ -62,7 +63,7 @@ enum CaptureTypes {
Ebpf = "ebpf",
}
export const EntryItem: React.FC<EntryProps> = ({entry, style, headingMode}) => {
export const EntryItem: React.FC<EntryProps> = ({entry, style, headingMode, namespace}) => {
const [focusedEntryId, setFocusedEntryId] = useRecoilState(focusedEntryIdAtom);
const [queryState, setQuery] = useRecoilState(queryAtom);
@@ -224,6 +225,19 @@ export const EntryItem: React.FC<EntryProps> = ({entry, style, headingMode}) =>
: ""
}
<div className={styles.separatorRight}>
{headingMode ? <Queryable
query={`namespace == "${namespace}"`}
displayIconOnMouseOver={true}
flipped={true}
iconStyle={{marginRight: "16px"}}
>
<span
className={`${styles.tcpInfo} ${styles.ip}`}
title="Namespace"
>
{namespace}
</span>
</Queryable> : null}
<Queryable
query={`src.ip == "${entry.src.ip}"`}
displayIconOnMouseOver={true}

View File

@@ -16,21 +16,21 @@ import trafficViewerApiAtom from "../../recoil/TrafficViewerApi"
interface FiltersProps {
backgroundColor: string
openWebSocket: (query: string, resetEntries: boolean) => void;
reopenConnection: any;
}
export const Filters: React.FC<FiltersProps> = ({backgroundColor, openWebSocket}) => {
export const Filters: React.FC<FiltersProps> = ({backgroundColor, reopenConnection}) => {
return <div className={styles.container}>
<QueryForm
backgroundColor={backgroundColor}
openWebSocket={openWebSocket}
reopenConnection={reopenConnection}
/>
</div>;
};
interface QueryFormProps {
backgroundColor: string
openWebSocket: (query: string, resetEntries: boolean) => void;
reopenConnection: any;
}
export const modalStyle = {
@@ -47,11 +47,10 @@ export const modalStyle = {
color: '#000',
};
export const QueryForm: React.FC<QueryFormProps> = ({backgroundColor, openWebSocket}) => {
export const QueryForm: React.FC<QueryFormProps> = ({backgroundColor, reopenConnection}) => {
const formRef = useRef<HTMLFormElement>(null);
const [query, setQuery] = useRecoilState(queryAtom);
const trafficViewerApi = useRecoilValue(trafficViewerApiAtom)
const [openModal, setOpenModal] = useState(false);
@@ -63,12 +62,7 @@ export const QueryForm: React.FC<QueryFormProps> = ({backgroundColor, openWebSoc
}
const handleSubmit = (e) => {
trafficViewerApi.webSocket.close()
if (query) {
openWebSocket(`(${query}) and leftOff(-1)`, true);
} else {
openWebSocket(`leftOff(-1)`, true);
}
reopenConnection();
e.preventDefault();
}

View File

@@ -1,25 +1,26 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Filters } from "./Filters";
import { EntriesList } from "./EntriesList";
import { makeStyles } from "@material-ui/core";
import React, {useEffect, useMemo, useRef, useState} from "react";
import {Filters} from "./Filters";
import {EntriesList} from "./EntriesList";
import {makeStyles} from "@material-ui/core";
import TrafficViewerStyles from "./TrafficViewer.module.sass";
import styles from '../style/EntriesList.module.sass';
import { EntryDetailed } from "./EntryDetailed";
import {EntryDetailed} from "./EntryDetailed";
import playIcon from 'assets/run.svg';
import pauseIcon from 'assets/pause.svg';
import variables from '../../variables.module.scss';
import { toast, ToastContainer } from 'react-toastify';
import {ToastContainer} from 'react-toastify';
import debounce from 'lodash/debounce';
import { RecoilRoot, RecoilState, useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
import {RecoilRoot, RecoilState, useRecoilState, useRecoilValue, useSetRecoilState} from "recoil";
import entriesAtom from "../../recoil/entries";
import focusedEntryIdAtom from "../../recoil/focusedEntryId";
import queryAtom from "../../recoil/query";
import { TLSWarning } from "../TLSWarning/TLSWarning";
import {TLSWarning} from "../TLSWarning/TLSWarning";
import trafficViewerApiAtom from "../../recoil/TrafficViewerApi"
import TrafficViewerApi from "./TrafficViewerApi";
import { StatusBar } from "../UI/StatusBar";
import {StatusBar} from "../UI/StatusBar";
import tappingStatusAtom from "../../recoil/tappingStatus/atom";
import { TOAST_CONTAINER_ID } from "../../configs/Consts";
import {TOAST_CONTAINER_ID} from "../../configs/Consts";
import leftOffTopAtom from "../../recoil/leftOffTop";
const useLayoutStyles = makeStyles(() => ({
details: {
@@ -52,30 +53,26 @@ interface TrafficViewerProps {
isDemoBannerView: boolean
}
export const TrafficViewer: React.FC<TrafficViewerProps> = ({ setAnalyzeStatus, trafficViewerApiProp,
actionButtons, isShowStatusBar, webSocketUrl,
isCloseWebSocket, isDemoBannerView }) => {
export const TrafficViewer: React.FC<TrafficViewerProps> = ({
setAnalyzeStatus, trafficViewerApiProp,
actionButtons, isShowStatusBar, webSocketUrl,
isCloseWebSocket, isDemoBannerView
}) => {
const classes = useLayoutStyles();
const [entries, setEntries] = useRecoilState(entriesAtom);
const [focusedEntryId, setFocusedEntryId] = useRecoilState(focusedEntryIdAtom);
const setEntries = useSetRecoilState(entriesAtom);
const setFocusedEntryId = useSetRecoilState(focusedEntryIdAtom);
const query = useRecoilValue(queryAtom);
const setTrafficViewerApiState = useSetRecoilState(trafficViewerApiAtom as RecoilState<TrafficViewerApi>)
const [tappingStatus, setTappingStatus] = useRecoilState(tappingStatusAtom);
const [noMoreDataTop, setNoMoreDataTop] = useState(false);
const [isSnappedToBottom, setIsSnappedToBottom] = useState(true);
const [forceRender, setForceRender] = useState(0);
const [wsReadyState, setWsReadyState] = useState(0);
const [queryBackgroundColor, setQueryBackgroundColor] = useState("#f5f5f5");
const [queriedCurrent, setQueriedCurrent] = useState(0);
const [queriedTotal, setQueriedTotal] = useState(0);
const [leftOffBottom, setLeftOffBottom] = useState(0);
const [leftOffTop, setLeftOffTop] = useState(null);
const [truncatedTimestamp, setTruncatedTimestamp] = useState(0);
const [startTime, setStartTime] = useState(0);
const setLeftOffTop = useSetRecoilState(leftOffTopAtom);
const scrollableRef = useRef(null);
const [showTLSWarning, setShowTLSWarning] = useState(false);
@@ -125,7 +122,7 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({ setAnalyzeStatus,
}
const closeWebSocket = () => {
if(ws?.current?.readyState === WebSocket.OPEN) {
if (ws?.current?.readyState === WebSocket.OPEN) {
ws.current.close();
return true;
}
@@ -136,7 +133,6 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({ setAnalyzeStatus,
if (resetEntries) {
setFocusedEntryId(null);
setEntries([]);
setQueriedCurrent(0);
setLeftOffTop(null);
setNoMoreDataTop(false);
}
@@ -144,92 +140,38 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({ setAnalyzeStatus,
ws.current = new WebSocket(webSocketUrl);
sendQueryWhenWsOpen(query);
ws.current.onopen = () => {
setWsReadyState(ws?.current?.readyState);
}
ws.current.onclose = () => {
if (window.location.pathname === "/")
setForceRender(forceRender + 1);
setWsReadyState(ws?.current?.readyState);
}
ws.current.onerror = (event) => {
console.error("WebSocket error:", event);
if (ws?.current?.readyState === WebSocket.OPEN) {
ws.current.close();
}
if (query) {
openWebSocket(`(${query}) and leftOff(${leftOffBottom})`, false);
} else {
openWebSocket(`leftOff(${leftOffBottom})`, false);
}
}
} catch (e) { }
} catch (e) {
}
}
const sendQueryWhenWsOpen = (query) => {
setTimeout(() => {
if (ws?.current?.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify({ "query": query, "enableFullEntries": false }));
ws.current.send(JSON.stringify({"query": query, "enableFullEntries": false}));
} else {
sendQueryWhenWsOpen(query);
}
}, 500)
}
if (ws.current) {
ws.current.onmessage = (e) => {
if (!e?.data) return;
const message = JSON.parse(e.data);
switch (message.messageType) {
case "entry":
const entry = message.data;
if (!focusedEntryId) setFocusedEntryId(entry.id.toString());
const newEntries = [...entries, entry];
if (newEntries.length === 10001) {
setLeftOffTop(newEntries[0].entry.id);
newEntries.shift();
setNoMoreDataTop(false);
}
setEntries(newEntries);
break;
case "status":
setTappingStatus(message.tappingStatus);
break;
case "analyzeStatus":
setAnalyzeStatus(message.analyzeStatus);
break;
case "outboundLink":
onTLSDetected(message.Data.DstIP);
break;
case "toast":
toast[message.data.type](message.data.text, {
theme: "colored",
autoClose: message.data.autoClose,
pauseOnHover: true,
progress: undefined,
containerId: TOAST_CONTAINER_ID
});
break;
case "queryMetadata":
setQueriedCurrent(queriedCurrent + message.data.current);
setQueriedTotal(message.data.total);
setLeftOffBottom(message.data.leftOff);
setTruncatedTimestamp(message.data.truncatedTimestamp);
if (leftOffTop === null) {
setLeftOffTop(message.data.leftOff - 1);
}
break;
case "startTime":
setStartTime(message.data);
break;
default:
console.error(
`unsupported websocket message type, Got: ${message.messageType}`
);
}
};
}
useEffect(() => {
setTrafficViewerApiState({ ...trafficViewerApiProp, webSocket: { close: closeWebSocket } });
setTrafficViewerApiState({...trafficViewerApiProp, webSocket: {close: closeWebSocket}});
(async () => {
try{
try {
const tapStatusResponse = await trafficViewerApiProp.tapStatus();
setTappingStatus(tapStatusResponse);
if (setAnalyzeStatus) {
@@ -240,11 +182,10 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({ setAnalyzeStatus,
console.error(error);
}
})()
// eslint-disable-next-line
}, []);
const toggleConnection = () => {
if(!closeWebSocket()) {
if (!closeWebSocket()) {
openEmptyWebSocket();
scrollableRef.current.jumpToBottom();
setIsSnappedToBottom(true);
@@ -254,6 +195,8 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({ setAnalyzeStatus,
const reopenConnection = async () => {
closeWebSocket()
openEmptyWebSocket();
scrollableRef.current.jumpToBottom();
setIsSnappedToBottom(true);
}
useEffect(() => {
@@ -262,30 +205,23 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({ setAnalyzeStatus,
};
}, []);
const onTLSDetected = (destAddress: string) => {
addressesWithTLS.add(destAddress);
setAddressesWithTLS(new Set(addressesWithTLS));
if (!userDismissedTLSWarning) {
setShowTLSWarning(true);
}
};
const getConnectionIndicator = () => {
switch (ws?.current?.readyState) {
switch (wsReadyState) {
case WebSocket.OPEN:
return <div className={`${TrafficViewerStyles.indicatorContainer} ${TrafficViewerStyles.greenIndicatorContainer}`}>
<div className={`${TrafficViewerStyles.indicator} ${TrafficViewerStyles.greenIndicator}`} />
return <div
className={`${TrafficViewerStyles.indicatorContainer} ${TrafficViewerStyles.greenIndicatorContainer}`}>
<div className={`${TrafficViewerStyles.indicator} ${TrafficViewerStyles.greenIndicator}`}/>
</div>
default:
return <div className={`${TrafficViewerStyles.indicatorContainer} ${TrafficViewerStyles.redIndicatorContainer}`}>
<div className={`${TrafficViewerStyles.indicator} ${TrafficViewerStyles.redIndicator}`} />
return <div
className={`${TrafficViewerStyles.indicatorContainer} ${TrafficViewerStyles.redIndicatorContainer}`}>
<div className={`${TrafficViewerStyles.indicator} ${TrafficViewerStyles.redIndicator}`}/>
</div>
}
}
const getConnectionTitle = () => {
switch (ws?.current?.readyState) {
switch (wsReadyState) {
case WebSocket.OPEN:
return "streaming live traffic"
default:
@@ -302,13 +238,16 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({ setAnalyzeStatus,
return (
<div className={TrafficViewerStyles.TrafficPage}>
{tappingStatus && isShowStatusBar && <StatusBar isDemoBannerView={isDemoBannerView} />}
{tappingStatus && isShowStatusBar && <StatusBar isDemoBannerView={isDemoBannerView}/>}
<div className={TrafficViewerStyles.TrafficPageHeader}>
<div className={TrafficViewerStyles.TrafficPageStreamStatus}>
<img className={TrafficViewerStyles.playPauseIcon} style={{ visibility: ws?.current?.readyState === WebSocket.OPEN ? "visible" : "hidden" }} alt="pause"
src={pauseIcon} onClick={toggleConnection} />
<img className={TrafficViewerStyles.playPauseIcon} style={{ position: "absolute", visibility: ws?.current?.readyState === WebSocket.OPEN ? "hidden" : "visible" }} alt="play"
src={playIcon} onClick={toggleConnection} />
<img className={TrafficViewerStyles.playPauseIcon}
style={{visibility: wsReadyState === WebSocket.OPEN ? "visible" : "hidden"}} alt="pause"
src={pauseIcon} onClick={toggleConnection}/>
<img className={TrafficViewerStyles.playPauseIcon}
style={{position: "absolute", visibility: wsReadyState === WebSocket.OPEN ? "hidden" : "visible"}}
alt="play"
src={playIcon} onClick={toggleConnection}/>
<div className={TrafficViewerStyles.connectionText}>
{getConnectionTitle()}
{getConnectionIndicator()}
@@ -320,8 +259,7 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({ setAnalyzeStatus,
<div className={TrafficViewerStyles.TrafficPageListContainer}>
<Filters
backgroundColor={queryBackgroundColor}
openWebSocket={openWebSocket}
reopenConnection={reopenConnection}
/>
<div className={styles.container}>
<EntriesList
@@ -329,56 +267,48 @@ export const TrafficViewer: React.FC<TrafficViewerProps> = ({ setAnalyzeStatus,
onSnapBrokenEvent={onSnapBrokenEvent}
isSnappedToBottom={isSnappedToBottom}
setIsSnappedToBottom={setIsSnappedToBottom}
queriedCurrent={queriedCurrent}
setQueriedCurrent={setQueriedCurrent}
queriedTotal={queriedTotal}
setQueriedTotal={setQueriedTotal}
startTime={startTime}
noMoreDataTop={noMoreDataTop}
setNoMoreDataTop={setNoMoreDataTop}
leftOffTop={leftOffTop}
setLeftOffTop={setLeftOffTop}
openWebSocket={openWebSocket}
leftOffBottom={leftOffBottom}
truncatedTimestamp={truncatedTimestamp}
setTruncatedTimestamp={setTruncatedTimestamp}
scrollableRef={scrollableRef}
ws={ws}
/>
</div>
</div>
<div className={classes.details} id="rightSideContainer">
{focusedEntryId && <EntryDetailed />}
<EntryDetailed/>
</div>
</div>}
<TLSWarning showTLSWarning={showTLSWarning}
setShowTLSWarning={setShowTLSWarning}
addressesWithTLS={addressesWithTLS}
setAddressesWithTLS={setAddressesWithTLS}
userDismissedTLSWarning={userDismissedTLSWarning}
setUserDismissedTLSWarning={setUserDismissedTLSWarning} />
setShowTLSWarning={setShowTLSWarning}
addressesWithTLS={addressesWithTLS}
setAddressesWithTLS={setAddressesWithTLS}
userDismissedTLSWarning={userDismissedTLSWarning}
setUserDismissedTLSWarning={setUserDismissedTLSWarning}/>
</div>
);
};
const MemoiedTrafficViewer = React.memo(TrafficViewer)
const TrafficViewerContainer: React.FC<TrafficViewerProps> = ({ setAnalyzeStatus, trafficViewerApiProp,
actionButtons, isShowStatusBar = true,
webSocketUrl, isCloseWebSocket, isDemoBannerView }) => {
const TrafficViewerContainer: React.FC<TrafficViewerProps> = ({
setAnalyzeStatus, trafficViewerApiProp,
actionButtons, isShowStatusBar = true,
webSocketUrl, isCloseWebSocket, isDemoBannerView
}) => {
return <RecoilRoot>
<MemoiedTrafficViewer actionButtons={actionButtons} isShowStatusBar={isShowStatusBar} webSocketUrl={webSocketUrl}
isCloseWebSocket={isCloseWebSocket} trafficViewerApiProp={trafficViewerApiProp}
setAnalyzeStatus={setAnalyzeStatus} isDemoBannerView={isDemoBannerView} />
isCloseWebSocket={isCloseWebSocket} trafficViewerApiProp={trafficViewerApiProp}
setAnalyzeStatus={setAnalyzeStatus} isDemoBannerView={isDemoBannerView}/>
<ToastContainer enableMultiContainer containerId={TOAST_CONTAINER_ID}
position="bottom-right"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover />
position="bottom-right"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover/>
</RecoilRoot>
}

View File

@@ -1,18 +1,17 @@
import React, { CSSProperties } from "react";
import infoImg from 'assets/info.svg';
import styles from "./style/InformationIcon.module.sass"
const DEFUALT_LINK = "https://getmizu.io/docs"
export interface InformationIconProps{
export interface InformationIconProps {
link?: string,
style? : CSSProperties
style?: CSSProperties
}
export const InformationIcon: React.FC<InformationIconProps> = ({link,style}) => {
export const InformationIcon: React.FC<InformationIconProps> = ({ link, style }) => {
return <React.Fragment>
<a href={DEFUALT_LINK ? DEFUALT_LINK : link} style={style} className={styles.flex} title="documentation" target="_blank">
<img className="headerIcon" src={infoImg} alt="Info icon"/>
<a href={DEFUALT_LINK ? DEFUALT_LINK : link} style={style} className={styles.linkStyle} title="documentation" target="_blank">
<span>Docs</span>
</a>
</React.Fragment>
}

View File

@@ -0,0 +1,20 @@
import React from "react";
import circleImg from 'assets/dotted-circle.svg';
import styles from './style/NoDataMessage.module.sass'
export interface Props {
messageText: string;
}
const NoDataMessage: React.FC<Props> = ({ messageText = "No data found" }) => {
return (
<div data-cy="noDataMessage" className={styles.messageContainer__noData}>
<div className={styles.container}>
<img src={circleImg} alt="No data Found"></img>
<div className={styles.messageContainer__noDataMessage}>{messageText}</div>
</div>
</div>
);
};
export default NoDataMessage;

View File

@@ -0,0 +1,17 @@
import React from "react";
export interface Props {
checked: boolean;
onToggle: (checked: boolean) => any;
disabled?: boolean;
}
const Radio: React.FC<Props> = ({ checked, onToggle, disabled, ...props }) => {
return (
<div>
<input style={!disabled ? { cursor: "pointer" } : {}} type="radio" checked={checked} disabled={disabled} onChange={(event) => onToggle(event.target.checked)} {...props} />
</div>
);
};
export default Radio;

View File

@@ -0,0 +1,103 @@
import React, { useMemo } from "react";
import Radio from "./Radio";
import styles from './style/SelectList.module.sass'
import NoDataMessage from "./NoDataMessage";
import Checkbox from "./Checkbox";
export interface Props {
items;
tableName: string;
checkedValues?: string[];
multiSelect: boolean;
searchValue?: string;
setCheckedValues: (newValues) => void;
tableClassName?
checkBoxWidth?: string
}
const SelectList: React.FC<Props> = ({ items, tableName, checkedValues = [], multiSelect = true, searchValue = "", setCheckedValues, tableClassName,
checkBoxWidth = 50 }) => {
const noItemsMessage = "No items to show";
const enabledItemsLength = useMemo(() => items.filter(item => !item.disabled).length, [items]);
const filteredValues = useMemo(() => {
return items.filter((listValue) => listValue?.value?.includes(searchValue));
}, [items, searchValue])
const toggleValue = (checkedKey) => {
if (!multiSelect) {
const newCheckedValues = [];
newCheckedValues.push(checkedKey);
setCheckedValues(newCheckedValues);
}
else {
const newCheckedValues = [...checkedValues];
let index = newCheckedValues.indexOf(checkedKey);
if (index > -1)
newCheckedValues.splice(index, 1);
else
newCheckedValues.push(checkedKey);
setCheckedValues(newCheckedValues);
}
}
const toggleAll = () => {
const newCheckedValues = [...checkedValues];
if (newCheckedValues.length === enabledItemsLength) setCheckedValues([]);
else {
items.forEach((obj) => {
if (!obj.disabled && !newCheckedValues.includes(obj.key))
newCheckedValues.push(obj.key);
})
setCheckedValues(newCheckedValues);
}
}
const dataFieldFunc = (listValue) => listValue.component ? listValue.component :
<span className={styles.nowrap} title={listValue.value}>
{listValue.value}
</span>
const tableHead = multiSelect ? <tr style={{ borderBottomWidth: "2px" }}>
<th style={{ width: checkBoxWidth }}><Checkbox data-cy="checkbox-all" checked={enabledItemsLength === checkedValues.length}
onToggle={toggleAll} /></th>
<th>{tableName}</th>
</tr> :
<tr style={{ borderBottomWidth: "2px" }}>
<th>{tableName}</th>
</tr>
const tableBody = filteredValues.length === 0 ?
<tr>
<td>
<NoDataMessage messageText={noItemsMessage} />
</td>
</tr>
:
filteredValues?.map(listValue => {
return <tr key={listValue.key}>
<td style={{ width: checkBoxWidth }}>
{multiSelect && <Checkbox data-cy={"checkbox-" + listValue.value} disabled={listValue.disabled} checked={checkedValues.includes(listValue.key)} onToggle={() => toggleValue(listValue.key)} />}
{!multiSelect && <Radio data-cy={"radio-" + listValue.value} disabled={listValue.disabled} checked={checkedValues.includes(listValue.key)} onToggle={() => toggleValue(listValue.key)} />}
</td>
<td>
{dataFieldFunc(listValue)}
</td>
</tr>
}
)
return <div className={tableClassName ? tableClassName + ` ${styles.selectListTable}` : ` ${styles.selectListTable}`}>
<table cellPadding={5} style={{ borderCollapse: "collapse" }}>
<thead>
{tableHead}
</thead>
<tbody>
{tableBody}
</tbody>
</table>
</div>
}
export default SelectList;

View File

@@ -14,11 +14,10 @@ interface StatusBarProps {
isDemoBannerView: boolean;
}
export const StatusBar = ({isDemoBannerView}) => {
export const StatusBar: React.FC<StatusBarProps> = ({isDemoBannerView}) => {
const tappingStatus = useRecoilValue(tappingStatusAtom);
const [expandedBar, setExpandedBar] = useState(false);
const {uniqueNamespaces, amountOfPods, amountOfTappedPods, amountOfUntappedPods} = useRecoilValue(tappingStatusDetails);
return <div className={`${isDemoBannerView ? `${style.banner}` : ''} ${style.statusBar} ${(expandedBar ? `${style.expandedStatusBar}` : "")}`} onMouseOver={() => setExpandedBar(true)} onMouseLeave={() => setExpandedBar(false)} data-cy="expandedStatusBar">
<div className={style.podsCount}>
{tappingStatus.some(pod => !pod.isTapped) && <img src={warningIcon} alt="warning"/>}
@@ -39,7 +38,7 @@ export const StatusBar = ({isDemoBannerView}) => {
{tappingStatus.map(pod => <tr key={pod.name}>
<td style={{width: "40%"}}>{pod.name}</td>
<td style={{width: "40%"}}>{pod.namespace}</td>
<td style={{width: "20%", textAlign: "center"}}><img style={{height: 20}} alt="status" src={pod.isTapped ? successIcon : failIcon}/></td>
<td style={{width: "20%", textAlign: "center"}}>{pod.isTapped ? <img style={{height: 20}} alt="status" src={successIcon}/> : <img style={{height: 20}} alt="status" src={failIcon}/>}</td>
</tr>)}
</tbody>
</table>

View File

@@ -37,9 +37,9 @@ export function getClassification(statusCode: number): string {
// 1 - 16 HTTP/2 (gRPC) status codes
// 2xx - 5xx HTTP/1.x status codes
if ((statusCode >= 200 && statusCode <= 399) || statusCode === 0) {
if (statusCode >= 200 && statusCode <= 399) {
classification = StatusCodeClassification.SUCCESS;
} else if (statusCode >= 400 || (statusCode >= 1 && statusCode <= 16)) {
} else if (statusCode >= 400) {
classification = StatusCodeClassification.FAILURE;
}

View File

@@ -0,0 +1,3 @@
<svg width="55" height="55" viewBox="0 0 55 55" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="27.5" cy="27.5" r="27" stroke="#BCCEFD" stroke-dasharray="6 6"/>
</svg>

After

Width:  |  Height:  |  Size: 180 B

View File

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 21H6.14286C5.07143 21 4 20.32 4 18.96C4 17.6 5.07143 16.92 6.14286 16.92H19V4H6.14286C5.07143 4 4 5.02 4 6.04V18.96M16.8571 17.6V20.32V17.6Z" stroke="#627EF7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="8" y="7" width="7" height="2" fill="#627EF7"/>
<rect x="8" y="11" width="4" height="2" fill="#627EF7"/>
</svg>

Before

Width:  |  Height:  |  Size: 454 B

View File

@@ -6,7 +6,9 @@ import Checkbox from "./Checkbox"
import { StatusBar } from "./StatusBar";
import CustomModal from "./CustomModal";
import { InformationIcon } from "./InformationIcon";
import SelectList from "./SelectList";
import NoDataMessage from "./NoDataMessage";
export {LoadingOverlay,Select,Tabs,Tooltip,Checkbox,CustomModal,InformationIcon}
export {StatusBar}
export { LoadingOverlay, Select, Tabs, Tooltip, Checkbox, CustomModal, InformationIcon, SelectList, NoDataMessage }
export { StatusBar }

View File

@@ -1,2 +1,8 @@
.flex
display: flex
.linkStyle
display: flex
color: #18253d
text-decoration: none
font-family: "Ubuntu", sans-serif
font-style: normal
font-weight: 600
font-size: 14px

View File

@@ -0,0 +1,32 @@
@import '../../../variables.module'
.messageContainer
width: 100%
margin-top: 20px
&__noData
display: flex
justify-content: space-between
flex-direction: column
height: 95px
margin: 2%
align-items: center
align-content: center
padding-top: 3%
padding-bottom: 3%
& .container
display: flex
justify-content: space-between
flex-direction: column
height: 95px
margin: 1%
align-items: center
align-content: center
&-message
font-style: normal
font-weight: 600
font-size: 12px
line-height: 15px
color: $light-gray

View File

@@ -0,0 +1,31 @@
@import '../../../variables.module'
.selectListTable
table
width: 100%
margin-top: 20px
height: 100%
tbody
display: block
th
color: $blue-gray
text-align: left
padding: 10px
tr
border-bottom-width: 1px
border-bottom-color: $data-background-color
border-bottom-style: solid
display: table
table-layout: fixed
width: 100%
td
color: $light-gray
padding: 10px
font-size: 16px
.nowrap
white-space: nowrap

View File

@@ -1,7 +1,6 @@
@import "../../variables.module"
@import "../../../variables.module"
.spinnerContainer
display: flex
justify-content: center
margin-bottom: 10px

View File

@@ -4,7 +4,7 @@
position: absolute
transform: translate(-50%, -3px)
left: 50%
z-index: 9999
z-index: 100
min-width: 200px
background: $blue-color
color: rgba(255,255,255,0.75)
@@ -19,7 +19,7 @@
overflow: hidden
max-width: clamp(150px,50%,600px)
&.banner
&.banner
top: 53px
.podsCount
@@ -41,7 +41,7 @@
table
width: 100%
margin-top: 20px
tbody
max-height: 70vh
overflow-y: auto

View File

@@ -1,7 +0,0 @@
@import "../../variables.module"
.spinnerContainer
display: flex
justify-content: center
margin-bottom: 10px

View File

@@ -0,0 +1,6 @@
const IP_ADDRESS_REGEX = /([0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3})(:([0-9]{1,5}))?/
export class Utils {
static isIpAddress = (address: string): boolean => IP_ADDRESS_REGEX.test(address)
}

View File

@@ -1,10 +1,11 @@
import TrafficViewer from './components/TrafficViewer/TrafficViewer';
import * as UI from "./components/UI"
import { StatusBar } from './components/UI';
import useWS,{DEFAULT_QUERY} from './hooks/useWS';
import {AnalyzeButton} from "./components/AnalyzeButton/AnalyzeButton"
import useWS, { DEFAULT_QUERY } from './hooks/useWS';
import { AnalyzeButton } from "./components/AnalyzeButton/AnalyzeButton"
import OasModal from './components/OasModal/OasModal';
import { ServiceMapModal } from './components/ServiceMapModal/ServiceMapModal';
export {UI,AnalyzeButton, StatusBar, OasModal}
export { useWS, DEFAULT_QUERY}
export { UI, AnalyzeButton, StatusBar, OasModal, ServiceMapModal }
export { useWS, DEFAULT_QUERY }
export default TrafficViewer;

View File

@@ -0,0 +1,8 @@
import { atom } from "recoil"
const leftOffTopAtom = atom({
key: "leftOffTopAtom",
default: null
})
export default leftOffTopAtom;

View File

@@ -1,3 +1,2 @@
import atom from "./atom";
export default atom
export default atom;

View File

@@ -48,7 +48,8 @@
"npm-link-shared": "^0.5.6",
"react-app-rewire-alias": "^1.1.7",
"react-dev-utils": "^12.0.0",
"recoil": "^0.5.2"
"recoil": "^0.5.2",
"react-error-overlay": "6.0.9"
},
"scripts": {
"start": "craco start",

View File

@@ -1,12 +1,12 @@
import { useState} from 'react';
import { useState } from 'react';
import './App.sass';
import {Header} from "./components/Header/Header";
import {TrafficPage} from "./components/Pages/TrafficPage/TrafficPage";
import { ServiceMapModal } from './components/ServiceMapModal/ServiceMapModal';
import {useRecoilState} from "recoil";
import { Header } from "./components/Header/Header";
import { TrafficPage } from "./components/Pages/TrafficPage/TrafficPage";
import { ServiceMapModal } from '@up9/mizu-common';
import { useRecoilState } from "recoil";
import serviceMapModalOpenAtom from "./recoil/serviceMapModalOpen";
import oasModalOpenAtom from './recoil/oasModalOpen/atom';
import {OasModal} from '@up9/mizu-common';
import { OasModal } from '@up9/mizu-common';
import Api from './helpers/api';
const api = Api.getInstance()
@@ -19,20 +19,20 @@ const App = () => {
return (
<div className="mizuApp">
<Header analyzeStatus={analyzeStatus} />
<TrafficPage setAnalyzeStatus={setAnalyzeStatus}/>
{window["isServiceMapEnabled"] && <ServiceMapModal
<Header analyzeStatus={analyzeStatus} />
<TrafficPage setAnalyzeStatus={setAnalyzeStatus} />
{window["isServiceMapEnabled"] && <ServiceMapModal
isOpen={serviceMapModalOpen}
onOpen={() => setServiceMapModalOpen(true)}
onClose={() => setServiceMapModalOpen(false)}
getServiceMapDataApi={api.serviceMapData} />}
{window["isOasEnabled"] && <OasModal
getOasServices={api.getOasServices}
getOasByService={api.getOasByService}
openModal={oasModalOpen}
handleCloseModal={() => setOasModalOpen(false)}
/>}
{window["isOasEnabled"] && <OasModal
getOasServices={api.getOasServices}
getOasByService={api.getOasByService}
openModal={oasModalOpen}
handleCloseModal={() => setOasModalOpen(false)}
/>}
</div>
</div>
);
}

View File

@@ -1,86 +0,0 @@
import {Button} from "@material-ui/core";
import React from "react";
import {UI} from "@up9/mizu-common";
import logo_up9 from "../assets/logo_up9.svg";
import {makeStyles} from "@material-ui/core/styles";
const useStyles = makeStyles(() => ({
tooltip: {
backgroundColor: "#3868dc",
color: "white",
fontSize: 13,
},
}));
interface AnalyseButtonProps {
analyzeStatus: any
}
export const AnalyzeButton: React.FC<AnalyseButtonProps> = ({analyzeStatus}) => {
const classes = useStyles();
const analysisMessage = analyzeStatus?.isRemoteReady ?
<span>
<table>
<tr>
<td>Status</td>
<td><b>Available</b></td>
</tr>
<tr>
<td>Messages</td>
<td><b>{analyzeStatus?.sentCount}</b></td>
</tr>
</table>
</span> :
analyzeStatus?.sentCount > 0 ?
<span>
<table>
<tr>
<td>Status</td>
<td><b>Processing</b></td>
</tr>
<tr>
<td>Messages</td>
<td><b>{analyzeStatus?.sentCount}</b></td>
</tr>
<tr>
<td colSpan={2}> Please allow a few minutes for the analysis to complete</td>
</tr>
</table>
</span> :
<span>
<table>
<tr>
<td>Status</td>
<td><b>Waiting for traffic</b></td>
</tr>
<tr>
<td>Messages</td>
<td><b>{analyzeStatus?.sentCount}</b></td>
</tr>
</table>
</span>
return ( <div>
<UI.Tooltip title={analysisMessage} isSimple classes={classes}>
<div>
<Button
style={{fontFamily: "system-ui",
fontWeight: 600,
fontSize: 12,
padding: 8}}
size={"small"}
variant="contained"
color="primary"
startIcon={<img style={{height: 24, maxHeight: "none", maxWidth: "none"}} src={logo_up9} alt={"up9"}/>}
disabled={!analyzeStatus?.isRemoteReady}
onClick={() => {
window.open(analyzeStatus?.remoteUrl)
}}>
Analysis
</Button>
</div>
</UI.Tooltip>
</div>);
}

View File

@@ -1,9 +1,9 @@
import React, {useEffect, useState} from "react";
import React, { useState } from "react";
import { Button } from "@material-ui/core";
import Api, { MizuWebsocketURL } from "../../../helpers/api";
import debounce from 'lodash/debounce';
import {useSetRecoilState, useRecoilState} from "recoil";
import {useCommonStyles} from "../../../helpers/commonStyle"
import { useRecoilState } from "recoil";
import { useCommonStyles } from "../../../helpers/commonStyle"
import serviceMapModalOpenAtom from "../../../recoil/serviceMapModalOpen";
import TrafficViewer from "@up9/mizu-common"
import "@up9/mizu-common/dist/index.css"
@@ -17,13 +17,13 @@ interface TrafficPageProps {
const api = Api.getInstance();
export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus}) => {
export const TrafficPage: React.FC<TrafficPageProps> = ({ setAnalyzeStatus }) => {
const commonClasses = useCommonStyles();
const setServiceMapModalOpen = useSetRecoilState(serviceMapModalOpenAtom);
const [serviceMapModalOpen, setServiceMapModalOpen] = useRecoilState(serviceMapModalOpenAtom);
const [openOasModal, setOpenOasModal] = useRecoilState(oasModalOpenAtom);
const [openWebSocket, setOpenWebSocket] = useState(true);
const trafficViewerApi = {...api}
const trafficViewerApi = { ...api }
const handleOpenOasModal = () => {
setOpenWebSocket(false)
@@ -36,37 +36,31 @@ const trafficViewerApi = {...api}
}, 500);
const actionButtons = (window["isOasEnabled"] || window["isServiceMapEnabled"]) &&
<div style={{ display: 'flex', height: "100%" }}>
{window["isOasEnabled"] && <Button
startIcon={<img className="custom" src={services} alt="services"></img>}
size="large"
variant="contained"
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
style={{ marginRight: 25, textTransform: 'unset' }}
onClick={handleOpenOasModal}>
OpenAPI Specs
</Button>}
{window["isServiceMapEnabled"] && <Button
startIcon={<img src={serviceMap} className="custom" alt="service-map" style={{marginRight:"8%"}}></img>}
size="large"
variant="contained"
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
onClick={openServiceMapModalDebounce}
style={{textTransform: 'unset'}}>
Service Map
</Button>}
</div>
useEffect(() => {
return () => {
//closeSocket()
}
},[])
<div style={{ display: 'flex', height: "100%" }}>
{window["isOasEnabled"] && <Button
startIcon={<img className="custom" src={services} alt="services"></img>}
size="large"
variant="contained"
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
style={{ marginRight: 25, textTransform: 'unset' }}
onClick={handleOpenOasModal}>
Service Catalog
</Button>}
{window["isServiceMapEnabled"] && <Button
startIcon={<img src={serviceMap} className="custom" alt="service-map" style={{ marginRight: "8%" }}></img>}
size="large"
variant="contained"
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
onClick={openServiceMapModalDebounce}
style={{ textTransform: 'unset' }}>
Service Map
</Button>}
</div>
return (
<>
<>
<TrafficViewer setAnalyzeStatus={setAnalyzeStatus} webSocketUrl={MizuWebsocketURL} isCloseWebSocket={!openWebSocket}
trafficViewerApiProp={trafficViewerApi} actionButtons={actionButtons} isShowStatusBar={!openOasModal} isDemoBannerView={false}/>
</>
trafficViewerApiProp={trafficViewerApi} actionButtons={actionButtons} isShowStatusBar={!(openOasModal || serviceMapModalOpen)} isDemoBannerView={false} />
</>
);
};

View File

@@ -1,31 +0,0 @@
@import "../../variables.module"
.legend-scale ul
margin-top: -29px
margin-left: -27px
padding: 0
float: left
list-style: none
li
display: block
float: left
width: 50px
margin-bottom: 6px
text-align: center
font-size: 80%
list-style: none
ul.legend-labels li span
display: block
float: left
height: 15px
width: 50px
.legend-source
font-size: 70%
color: #999
clear: both
a
color: #777

View File

@@ -1,223 +0,0 @@
import React, { useState, useEffect, useCallback } from "react";
import { Box, Fade, Modal, Backdrop, Button } from "@material-ui/core";
import { toast } from "react-toastify";
import Api from "../../helpers/api";
import spinnerStyle from '../style/Spinner.module.sass';
import './ServiceMapModal.sass';
import spinnerImg from '../assets/spinner.svg';
import Graph from "react-graph-vis";
import debounce from 'lodash/debounce';
import ServiceMapOptions from './ServiceMapOptions'
import { useCommonStyles } from "../../helpers/commonStyle";
import refresh from "../assets/refresh.svg";
import close from "../assets/close.svg";
import { TOAST_CONTAINER_ID } from "../../consts";
interface GraphData {
nodes: Node[];
edges: Edge[];
}
interface Node {
id: number;
value: number;
label: string;
title?: string;
color?: object;
}
interface Edge {
from: number;
to: number;
value: number;
label: string;
title?: string;
color?: object;
font?: object;
}
interface ServiceMapNode {
id: number;
name: string;
entry: Entry;
count: number;
}
interface ServiceMapEdge {
source: ServiceMapNode;
destination: ServiceMapNode;
count: number;
protocol: Protocol;
}
interface ServiceMapGraph {
nodes: ServiceMapNode[];
edges: ServiceMapEdge[];
}
interface Entry {
ip: string;
port: string;
name: string;
}
interface Protocol {
name: string;
abbr: string;
macro: string;
version: string;
backgroundColor: string;
foregroundColor: string;
fontSize: number;
referenceLink: string;
ports: string[];
priority: number;
}
interface ServiceMapModalProps {
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
}
const modalStyle = {
position: 'absolute',
top: '6%',
left: '50%',
transform: 'translate(-50%, 0%)',
width: '89vw',
height: '82vh',
bgcolor: 'background.paper',
borderRadius: '5px',
boxShadow: 24,
p: 4,
color: '#000',
};
const api = Api.getInstance();
export const ServiceMapModal: React.FC<ServiceMapModalProps> = ({ isOpen, onOpen, onClose }) => {
const commonClasses = useCommonStyles();
const [isLoading, setIsLoading] = useState<boolean>(true);
const [graphData, setGraphData] = useState<GraphData>({ nodes: [], edges: [] });
const [graphOptions, setGraphOptions] = useState(ServiceMapOptions);
const getServiceMapData = useCallback(async () => {
try {
setIsLoading(true)
const serviceMapData: ServiceMapGraph = await api.serviceMapData()
const newGraphData: GraphData = { nodes: [], edges: [] }
if (serviceMapData.nodes) {
newGraphData.nodes = serviceMapData.nodes.map<Node>(node => {
return {
id: node.id,
value: node.count,
label: (node.entry.name === "unresolved") ? node.name : `${node.entry.name} (${node.name})`,
title: "Count: " + node.name,
}
})
}
if (serviceMapData.edges) {
newGraphData.edges = serviceMapData.edges.map<Edge>(edge => {
return {
from: edge.source.id,
to: edge.destination.id,
value: edge.count,
label: edge.count.toString(),
color: {
color: edge.protocol.backgroundColor,
highlight: edge.protocol.backgroundColor
},
font: {
color: edge.protocol.backgroundColor,
strokeColor: edge.protocol.backgroundColor
},
}
})
}
setGraphData(newGraphData)
} catch (ex) {
toast.error("An error occurred while loading Mizu Service Map, see console for mode details", { containerId: TOAST_CONTAINER_ID });
console.error(ex);
} finally {
setIsLoading(false)
}
// eslint-disable-next-line
}, [isOpen])
useEffect(() => {
if(graphData?.nodes?.length === 0) return;
let options = {...graphOptions};
options.physics.barnesHut.avoidOverlap = graphData?.nodes?.length > 10 ? 0 : 1;
setGraphOptions(options);
// eslint-disable-next-line
},[graphData?.nodes?.length])
useEffect(() => {
getServiceMapData();
return () => setGraphData({ nodes: [], edges: [] })
}, [getServiceMapData])
const refreshServiceMap = debounce(() => {
getServiceMapData();
}, 500);
return (
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={isOpen}
onClose={onClose}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
style={{ overflow: 'auto' }}
>
<Fade in={isOpen}>
<Box sx={modalStyle}>
{isLoading && <div className={spinnerStyle.spinnerContainer}>
<img alt="spinner" src={spinnerImg} style={{ height: 50 }} />
</div>}
{!isLoading && <div style={{ height: "100%", width: "100%" }}>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<div>
<Button
startIcon={<img src={refresh} className="custom" alt="refresh" style={{ marginRight: "8%" }}/>}
size="medium"
variant="contained"
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
onClick={refreshServiceMap}
>
Refresh
</Button>
</div>
<img src={close} alt="close" onClick={() => onClose()} style={{ cursor: "pointer" }}/>
</div>
<Graph
graph={graphData}
options={graphOptions}
/>
<div className='legend-scale'>
<ul className='legend-labels'>
<li><span style={{ background: '#205cf5' }}/>HTTP</li>
<li><span style={{ background: '#244c5a' }}/>HTTP/2</li>
<li><span style={{ background: '#244c5a' }}/>gRPC</li>
<li><span style={{ background: '#ff6600' }}/>AMQP</li>
<li><span style={{ background: '#000000' }}/>KAFKA</li>
<li><span style={{ background: '#a41e11' }}/>REDIS</li>
</ul>
</div>
</div>}
</Box>
</Fade>
</Modal>
);
}

View File

@@ -1,174 +0,0 @@
const minNodeScaling = 10
const maxNodeScaling = 30
const minEdgeScaling = 1
const maxEdgeScaling = maxNodeScaling / 2
const minLabelScaling = 11
const maxLabelScaling = 16
const selectedNodeColor = "#0C0B1A"
const selectedNodeBorderColor = "#205CF5"
const selectedNodeLabelColor = "#205CF5"
const selectedEdgeLabelColor = "#205CF5"
const customScaling = (min, max, total, value) => {
if (max === min) {
return 0.5;
}
else {
const scale = 1 / (max - min);
return Math.max(0, (value - min) * scale);
}
}
const nodeSelected = (values, id, selected, hovering) => {
values.color = selectedNodeColor;
values.borderColor = selectedNodeBorderColor;
values.borderWidth = 4;
}
const nodeLabelSelected = (values, id, selected, hovering) => {
values.size = values.size + 1;
values.color = selectedNodeLabelColor;
values.strokeColor = selectedNodeLabelColor;
values.strokeWidth = 0.2
}
const edgeSelected = (values, id, selected, hovering) => {
values.opacity = 0.4;
values.width = values.width + 1;
}
const edgeLabelSelected = (values, id, selected, hovering) => {
values.size = values.size + 1;
values.color = selectedEdgeLabelColor;
values.strokeColor = selectedEdgeLabelColor;
values.strokeWidth = 0.2
}
const nodeOptions = {
shape: 'dot',
chosen: {
node: nodeSelected,
label: nodeLabelSelected,
},
color: {
background: '#494677',
border: selectedNodeColor,
},
font: {
color: selectedNodeColor,
size: 11, // px
face: 'Roboto',
background: '#FFFFFFBF',
strokeWidth: 0.2, // px
strokeColor: selectedNodeColor,
align: 'center',
multi: false,
},
// defines the node min and max sizes when zoom in/out, based on the node value
scaling: {
min: minNodeScaling,
max: maxNodeScaling,
// defines the label scaling size in px
label: {
enabled: true,
min: minLabelScaling,
max: maxLabelScaling,
maxVisible: maxLabelScaling,
drawThreshold: 5,
},
customScalingFunction: customScaling,
},
borderWidth: 2,
labelHighlightBold: true,
opacity: 1,
shadow: true,
}
const edgeOptions = {
chosen: {
edge: edgeSelected,
label: edgeLabelSelected,
},
dashes: false,
arrowStrikethrough: false,
arrows: {
to: {
enabled: true,
},
middle: {
enabled: false,
},
from: {
enabled: false,
}
},
smooth: {
enabled: true,
type: 'dynamic',
roundness: 1.0
},
font: {
color: '#000000',
size: 11, // px
face: 'Roboto',
background: '#FFFFFFCC',
strokeWidth: 0.2, // px
strokeColor: '#000000',
align: 'horizontal',
multi: false,
},
scaling: {
min: minEdgeScaling,
max: maxEdgeScaling,
label: {
enabled: true,
min: minLabelScaling,
max: maxLabelScaling,
maxVisible: maxLabelScaling,
drawThreshold: 5
},
customScalingFunction: customScaling,
},
labelHighlightBold: true,
selectionWidth: 1,
shadow: true,
}
const ServiceMapOptions = {
physics: {
enabled: true,
solver: 'barnesHut',
barnesHut: {
theta: 0.5,
gravitationalConstant: -2000,
centralGravity: 0.4,
springLength: 180,
springConstant: 0.04,
damping: 0.2,
avoidOverlap: 0
},
},
layout: {
hierarchical: false,
randomSeed: 1 // always on node 1
},
nodes: nodeOptions,
edges: edgeOptions,
autoResize: true,
interaction: {
selectable: true,
selectConnectedEdges: true,
multiselect: true,
dragNodes: true,
dragView: true,
hover: true,
hoverConnectedEdges: true,
zoomView: true,
zoomSpeed: 1,
},
};
export default ServiceMapOptions

View File

@@ -1,7 +0,0 @@
const dictionary = {
ctrlEnter : [{metaKey : true, code:"Enter"}, {ctrlKey:true, code:"Enter"}], // support Ctrl/command
enter : [{code:"Enter"}]
};
export default dictionary;

View File

@@ -25,21 +25,11 @@ export default class Api {
source = null;
}
serviceMapStatus = async () => {
const response = await client.get("/servicemap/status");
return response.data;
}
serviceMapData = async () => {
const response = await client.get(`/servicemap/get`);
return response.data;
}
serviceMapReset = async () => {
const response = await client.get(`/servicemap/reset`);
return response.data;
}
tapStatus = async () => {
const response = await client.get("/status/tap");
return response.data;

View File

@@ -1,5 +0,0 @@
export enum RouterRoutes {
LOGIN = "/login",
SETUP = "/setup",
SETTINGS = "/settings"
}

View File

@@ -1,38 +0,0 @@
import { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
const useKeyPress = (eventConfigs, callback, node = null) => {
// implement the callback ref pattern
const callbackRef = useRef(callback);
useLayoutEffect(() => {
callbackRef.current = callback;
});
// handle what happens on key press
const handleKeyPress = useCallback(
(event) => {
// check if one of the key is part of the ones we want
if (eventConfigs.some((eventConfig) => Object.keys(eventConfig).every(nameKey => eventConfig[nameKey] === event[nameKey]))) {
event.stopPropagation()
event.preventDefault();
callbackRef.current(event);
}
},
[eventConfigs]
);
useEffect(() => {
// target is either the provided node or the document
const targetNode = node ?? document;
// attach the event listener
targetNode &&
targetNode.addEventListener("keydown", handleKeyPress);
// remove the event listener
return () =>
targetNode &&
targetNode.removeEventListener("keydown", handleKeyPress);
}, [handleKeyPress, node]);
};
export default useKeyPress;

View File

@@ -1,8 +0,0 @@
import { atom } from "recoil"
const entPageAtom = atom({
key: "entPageAtom",
default: 0
})
export default entPageAtom

View File

@@ -1,11 +0,0 @@
import atom from "./atom";
enum Page {
Traffic,
Setup,
Login
}
export { Page };
export default atom;

View File

@@ -1,8 +0,0 @@
import { atom } from "recoil";
const entriesAtom = atom({
key: "entriesAtom",
default: []
});
export default entriesAtom;

View File

@@ -1,9 +0,0 @@
import { atom } from "recoil";
import {TappingStatusPod} from "./index";
const tappingStatusAtom = atom({
key: "tappingStatusAtom",
default: null as TappingStatusPod[]
});
export default tappingStatusAtom;

View File

@@ -1,22 +0,0 @@
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;

View File

@@ -1,17 +0,0 @@
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;