Compare commits

..

21 Commits

Author SHA1 Message Date
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
lirazyehezkel
59ad8d8fad TLS icon position (#956)
* TLS icon position

* cr fix
2022-03-31 11:26:58 +03:00
Nimrod Gilboa Markevich
a49443f101 Set the entry namespace to the source namespace if the destination is not resolved (#950)
* Set the entry namespace to the source namespace if the destination is not resolved
* Overwrite src namespace with dst namespace only if dst non-empty
2022-03-30 15:40:21 +03:00
lirazyehezkel
2427955aa4 Avoid overlap only for service map including under 10 services 2022-03-30 15:30:09 +03:00
David Levanon
27a73e21fb Read from service mesh network namespaces upon update (#944) 2022-03-30 13:56:37 +03:00
Igor Gov
8eeb0e54c9 Changing unit tests workflow timeout to 30 minutes 2022-03-30 11:52:47 +03:00
Andrey Pokhilko
97db24aeba OAS: rework data feeding + sampleIDs (#917)
* Call OAS feeder

* Don't call old OAS code

* Rework calls

* Work on it

* Put back rules

* Make it compile

* start thinking of test

* Compiles

* Save

* Fixes

* Save

* Fixing

* Trying to fake conn

* add timeout

* Test timeout

* Fix tests

* Only build OAS for HTTP entries

* Remove some dead code

* Adding SampleIDs

* Cosmetics

* lint

* Revert rename

* Sample ID for content

* Cleanuo

* Add more sample IDs

* Checking hypothesis

* Move assignment place a bit

* Cosmetics

* Update test.yml

Co-authored-by: undera <undera@undera-old-desktop.home>
Co-authored-by: Igor Gov <iggvrv@gmail.com>
2022-03-30 11:14:25 +03:00
RamiBerm
63cf7ac34e Refactor entries controller logic (#949)
* wip

* Update entries_controller.go and entries_provider.go

* Update entries_controller.go

* change entries provider into a struct + interface

* Update entries_provider.go

* Update entries_provider.go
2022-03-29 18:30:19 +03:00
Igor Gov
e867b7d0f1 Build ui-common part of CI (#914)
* Build ui-common always locally
2022-03-29 14:14:52 +03:00
86 changed files with 1902 additions and 1594 deletions

View File

@@ -43,7 +43,7 @@ jobs:
with: with:
status: ${{ job.status }} status: ${{ job.status }}
notification_title: 'Mizu {workflow} has {status_message}' 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}>' footer: 'Linked Repo <{repo_url}|{repo}>'
notify_when: 'failure' notify_when: 'failure'
env: env:

View File

@@ -45,7 +45,7 @@ jobs:
- name: Check modified files - name: Check modified files
id: modified_files id: modified_files
run: devops/check_modified_files.sh agent/ shared/ tap/ ui/ Dockerfile run: devops/check_modified_files.sh agent/ shared/ tap/ ui/ ui-common/ Dockerfile
- name: Set up Docker Buildx - name: Set up Docker Buildx
if: steps.modified_files.outputs.matched == 'true' if: steps.modified_files.outputs.matched == 'true'

View File

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

View File

@@ -18,6 +18,7 @@ jobs:
run-unit-tests: run-unit-tests:
name: Unit Tests name: Unit Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20
steps: steps:
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@@ -1,12 +1,23 @@
ARG BUILDARCH=amd64 ARG BUILDARCH=amd64
ARG TARGETARCH=amd64 ARG TARGETARCH=amd64
### Front-end common
FROM node:16 AS front-end-common
WORKDIR /app/ui-build
COPY ui-common/package.json .
COPY ui-common/package-lock.json .
RUN npm i
COPY ui-common .
RUN npm pack
### Front-end ### Front-end
FROM node:16 AS front-end FROM node:16 AS front-end
WORKDIR /app/ui-build WORKDIR /app/ui-build
COPY ui/package.json ui/package-lock.json ./ COPY ui/package.json ui/package-lock.json ./
COPY --from=front-end-common ["/app/ui-build/up9-mizu-common-0.0.0.tgz", "."]
RUN npm i RUN npm i
COPY ui . COPY ui .
RUN npm run build RUN npm run build

View File

@@ -18,6 +18,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/up9inc/mizu/agent/pkg/dependency" "github.com/up9inc/mizu/agent/pkg/dependency"
"github.com/up9inc/mizu/agent/pkg/elastic" "github.com/up9inc/mizu/agent/pkg/elastic"
"github.com/up9inc/mizu/agent/pkg/entries"
"github.com/up9inc/mizu/agent/pkg/middlewares" "github.com/up9inc/mizu/agent/pkg/middlewares"
"github.com/up9inc/mizu/agent/pkg/models" "github.com/up9inc/mizu/agent/pkg/models"
"github.com/up9inc/mizu/agent/pkg/oas" "github.com/up9inc/mizu/agent/pkg/oas"
@@ -370,5 +371,8 @@ func handleIncomingMessageAsTapper(socketConnection *websocket.Conn) {
func initializeDependencies() { func initializeDependencies() {
dependency.RegisterGenerator(dependency.ServiceMapGeneratorDependency, func() interface{} { return servicemap.GetDefaultServiceMapInstance() }) dependency.RegisterGenerator(dependency.ServiceMapGeneratorDependency, func() interface{} { return servicemap.GetDefaultServiceMapInstance() })
dependency.RegisterGenerator(dependency.OasGeneratorDependency, func() interface{} { return oas.GetDefaultOasGeneratorInstance() }) dependency.RegisterGenerator(dependency.OasGeneratorDependency, func() interface{} { return oas.GetDefaultOasGeneratorInstance(nil) })
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

@@ -5,6 +5,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/up9inc/mizu/agent/pkg/models"
"os" "os"
"path" "path"
"sort" "sort"
@@ -19,8 +20,6 @@ import (
"github.com/up9inc/mizu/agent/pkg/servicemap" "github.com/up9inc/mizu/agent/pkg/servicemap"
"github.com/up9inc/mizu/agent/pkg/models"
"github.com/up9inc/mizu/agent/pkg/oas"
"github.com/up9inc/mizu/agent/pkg/resolver" "github.com/up9inc/mizu/agent/pkg/resolver"
"github.com/up9inc/mizu/agent/pkg/utils" "github.com/up9inc/mizu/agent/pkg/utils"
@@ -140,20 +139,6 @@ func startReadingChannel(outputItems <-chan *tapApi.OutputChannelItem, extension
rules, _, _ := models.RunValidationRulesState(*harEntry, mizuEntry.Destination.Name) rules, _, _ := models.RunValidationRulesState(*harEntry, mizuEntry.Destination.Name)
mizuEntry.Rules = rules mizuEntry.Rules = rules
} }
entryWSource := oas.EntryWithSource{
Entry: *harEntry,
Source: mizuEntry.Source.Name,
Destination: mizuEntry.Destination.Name,
Id: mizuEntry.Id,
}
if entryWSource.Destination == "" {
entryWSource.Destination = mizuEntry.Destination.IP + ":" + mizuEntry.Destination.Port
}
oasGenerator := dependency.GetInstance(dependency.OasGeneratorDependency).(oas.OasGeneratorSink)
oasGenerator.PushEntry(&entryWSource)
} }
data, err := json.Marshal(mizuEntry) data, err := json.Marshal(mizuEntry)
@@ -183,6 +168,7 @@ func resolveIP(connectionInfo *tapApi.ConnectionInfo) (resolvedSource string, re
} }
} else { } else {
resolvedSource = resolvedSourceObject.FullAddress resolvedSource = resolvedSourceObject.FullAddress
namespace = resolvedSourceObject.Namespace
} }
unresolvedDestination := fmt.Sprintf("%s:%s", connectionInfo.ServerIP, connectionInfo.ServerPort) unresolvedDestination := fmt.Sprintf("%s:%s", connectionInfo.ServerIP, connectionInfo.ServerPort)
@@ -194,7 +180,11 @@ func resolveIP(connectionInfo *tapApi.ConnectionInfo) (resolvedSource string, re
} }
} else { } else {
resolvedDestination = resolvedDestinationObject.FullAddress resolvedDestination = resolvedDestinationObject.FullAddress
namespace = resolvedDestinationObject.Namespace // Overwrite namespace (if it was set according to the source)
// Only overwrite if non-empty
if resolvedDestinationObject.Namespace != "" {
namespace = resolvedDestinationObject.Namespace
}
} }
} }
return resolvedSource, resolvedDestination, namespace return resolvedSource, resolvedDestination, namespace

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 package api
import ( import (
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"sync" "sync"
"time" "time"
"github.com/up9inc/mizu/agent/pkg/models"
"github.com/up9inc/mizu/agent/pkg/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
basenine "github.com/up9inc/basenine/client/go" "github.com/up9inc/mizu/agent/pkg/models"
"github.com/up9inc/mizu/shared" "github.com/up9inc/mizu/agent/pkg/utils"
"github.com/up9inc/mizu/shared/logger" "github.com/up9inc/mizu/shared/logger"
tapApi "github.com/up9inc/mizu/tap/api" tapApi "github.com/up9inc/mizu/tap/api"
) )
@@ -25,9 +21,9 @@ func InitExtensionsMap(ref map[string]*tapApi.Extension) {
} }
type EventHandlers interface { type EventHandlers interface {
WebSocketConnect(socketId int, isTapper bool) WebSocketConnect(c *gin.Context, socketId int, isTapper bool)
WebSocketDisconnect(socketId int, isTapper bool) WebSocketDisconnect(socketId int, isTapper bool)
WebSocketMessage(socketId int, message []byte) WebSocketMessage(socketId int, isTapper bool, message []byte)
} }
type SocketConnection struct { type SocketConnection struct {
@@ -62,11 +58,11 @@ func init() {
func WebSocketRoutes(app *gin.Engine, eventHandlers EventHandlers) { func WebSocketRoutes(app *gin.Engine, eventHandlers EventHandlers) {
SocketGetBrowserHandler = func(c *gin.Context) { SocketGetBrowserHandler = func(c *gin.Context) {
websocketHandler(c.Writer, c.Request, eventHandlers, false) websocketHandler(c, eventHandlers, false)
} }
SocketGetTapperHandler = func(c *gin.Context) { SocketGetTapperHandler = func(c *gin.Context) {
websocketHandler(c.Writer, c.Request, eventHandlers, true) websocketHandler(c, eventHandlers, true)
} }
app.GET("/ws", func(c *gin.Context) { 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) { func websocketHandler(c *gin.Context, eventHandlers EventHandlers, isTapper bool) {
ws, err := websocketUpgrader.Upgrade(w, r, nil) ws, err := websocketUpgrader.Upgrade(c.Writer, c.Request, nil)
if err != 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 return
} }
@@ -93,30 +89,11 @@ func websocketHandler(w http.ResponseWriter, r *http.Request, eventHandlers Even
websocketIdsLock.Unlock() 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() { defer func() {
socketCleanup(socketId, connectedWebsockets[socketId]) 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) startTimeBytes, _ := models.CreateWebsocketStartTimeMessage(utils.StartTime)
@@ -124,127 +101,32 @@ func websocketHandler(w http.ResponseWriter, r *http.Request, eventHandlers Even
logger.Log.Error(err) logger.Log.Error(err)
} }
var params WebSocketParams
for { for {
_, msg, err := ws.ReadMessage() _, msg, err := ws.ReadMessage()
if err != nil { if err != nil {
if _, ok := err.(*websocket.CloseError); ok { 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 { } 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 break
} }
if !isTapper && !isQuerySet { eventHandlers.WebSocketMessage(socketId, isTapper, msg)
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)
}
} }
} }
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 { func SendToSocket(socketId int, message []byte) error {
socketObj := connectedWebsockets[socketId] socketObj := connectedWebsockets[socketId]
if socketObj == nil { if socketObj == nil {
return fmt.Errorf("Socket %v is disconnected", socketId) return fmt.Errorf("socket %v is disconnected", socketId)
} }
var sent = false var sent = false
time.AfterFunc(time.Second*5, func() { time.AfterFunc(time.Second*5, func() {
if !sent { if !sent {
logger.Log.Error("Socket timed out") logger.Log.Error("socket timed out")
socketCleanup(socketId, socketObj) socketCleanup(socketId, socketObj)
} }
}) })
@@ -255,7 +137,20 @@ func SendToSocket(socketId int, message []byte) error {
sent = true sent = true
if err != nil { 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 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 package api
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt"
"sync" "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/models"
"github.com/up9inc/mizu/agent/pkg/providers"
"github.com/up9inc/mizu/agent/pkg/providers/tappedPods" "github.com/up9inc/mizu/agent/pkg/providers/tappedPods"
"github.com/up9inc/mizu/agent/pkg/providers/tappers" "github.com/up9inc/mizu/agent/pkg/providers/tappers"
"github.com/up9inc/mizu/agent/pkg/up9" "github.com/up9inc/mizu/agent/pkg/up9"
@@ -17,7 +18,11 @@ import (
"github.com/up9inc/mizu/shared/logger" "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 tapperClientSocketUUIDs = make([]int, 0)
var socketListLock = sync.Mutex{} var socketListLock = sync.Mutex{}
@@ -30,7 +35,7 @@ func init() {
go up9.UpdateAnalyzeStatus(BroadcastToBrowserClients) go up9.UpdateAnalyzeStatus(BroadcastToBrowserClients)
} }
func (h *RoutesEventHandlers) WebSocketConnect(socketId int, isTapper bool) { func (h *RoutesEventHandlers) WebSocketConnect(_ *gin.Context, socketId int, isTapper bool) {
if isTapper { if isTapper {
logger.Log.Infof("Websocket event - Tapper connected, socket ID: %d", socketId) logger.Log.Infof("Websocket event - Tapper connected, socket ID: %d", socketId)
tappers.Connected() 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) logger.Log.Infof("Websocket event - Browser socket connected, socket ID: %d", socketId)
socketListLock.Lock() socketListLock.Lock()
browserClientSocketUUIDs = append(browserClientSocketUUIDs, socketId) browserClients[socketId] = &BrowserClient{}
socketListLock.Unlock() socketListLock.Unlock()
BroadcastTappedPodsStatus() BroadcastTappedPodsStatus()
@@ -63,13 +68,16 @@ func (h *RoutesEventHandlers) WebSocketDisconnect(socketId int, isTapper bool) {
} else { } else {
logger.Log.Infof("Websocket event - Browser socket disconnected, socket ID: %d", socketId) logger.Log.Infof("Websocket event - Browser socket disconnected, socket ID: %d", socketId)
socketListLock.Lock() socketListLock.Lock()
removeSocketUUIDFromBrowserSlice(socketId) if browserClients[socketId] != nil && browserClients[socketId].dataStreamCancelFunc != nil {
browserClients[socketId].dataStreamCancelFunc()
}
delete(browserClients, socketId)
socketListLock.Unlock() socketListLock.Unlock()
} }
} }
func BroadcastToBrowserClients(message []byte) { func BroadcastToBrowserClients(message []byte) {
for _, socketId := range browserClientSocketUUIDs { for socketId := range browserClients {
go func(socketId int) { go func(socketId int) {
if err := SendToSocket(socketId, message); err != nil { if err := SendToSocket(socketId, message); err != nil {
logger.Log.Error(err) 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 var socketMessageBase shared.WebSocketMessageMetadata
err := json.Unmarshal(message, &socketMessageBase) err := json.Unmarshal(message, &socketMessageBase)
if err != nil { 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) logger.Log.Infof("Could not unmarshal message of message type %s %v", socketMessageBase.MessageType, err)
} else { } else {
// NOTE: This is where the message comes back from the intermediate WebSocket to code. // NOTE: This is where the message comes back from the intermediate WebSocket to code.
h.SocketOutChannel <- tappedEntryMessage.Data socketOutChannel <- tappedEntryMessage.Data
} }
case shared.WebSocketMessageTypeUpdateStatus: case shared.WebSocketMessageTypeUpdateStatus:
var statusMessage shared.WebSocketStatusMessage var statusMessage shared.WebSocketStatusMessage
@@ -110,15 +144,7 @@ func (h *RoutesEventHandlers) WebSocketMessage(_ int, message []byte) {
if err != nil { if err != nil {
logger.Log.Infof("Could not unmarshal message of message type %s %v", socketMessageBase.MessageType, err) logger.Log.Infof("Could not unmarshal message of message type %s %v", socketMessageBase.MessageType, err)
} else { } else {
BroadcastToBrowserClients(message) broadcastMessageFunc(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)
} }
default: default:
logger.Log.Infof("Received socket message of type %s for which no handlers are defined", socketMessageBase.MessageType) 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) { func removeSocketUUIDFromTapperSlice(uuidToRemove int) {
newUUIDSlice := make([]int, 0, len(tapperClientSocketUUIDs)) newUUIDSlice := make([]int, 0, len(tapperClientSocketUUIDs))
for _, uuid := range tapperClientSocketUUIDs { for _, uuid := range tapperClientSocketUUIDs {

View File

@@ -1,25 +1,20 @@
package controllers package controllers
import ( import (
"encoding/json"
"net/http" "net/http"
"strconv" "strconv"
"time"
"github.com/up9inc/mizu/agent/pkg/app" "github.com/up9inc/mizu/agent/pkg/dependency"
"github.com/up9inc/mizu/agent/pkg/har" "github.com/up9inc/mizu/agent/pkg/entries"
"github.com/up9inc/mizu/agent/pkg/models" "github.com/up9inc/mizu/agent/pkg/models"
"github.com/up9inc/mizu/agent/pkg/validation" "github.com/up9inc/mizu/agent/pkg/validation"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
basenine "github.com/up9inc/basenine/client/go"
"github.com/up9inc/mizu/shared"
"github.com/up9inc/mizu/shared/logger" "github.com/up9inc/mizu/shared/logger"
tapApi "github.com/up9inc/mizu/tap/api"
) )
func Error(c *gin.Context, err error) bool { func HandleEntriesError(c *gin.Context, err error) bool {
if err != nil { if err != nil {
logger.Log.Errorf("Error getting entry: %v", err) logger.Log.Errorf("Error getting entry: %v", err)
_ = c.Error(err) _ = c.Error(err)
@@ -49,45 +44,18 @@ func GetEntries(c *gin.Context) {
entriesRequest.TimeoutMs = 3000 entriesRequest.TimeoutMs = 3000
} }
data, meta, err := basenine.Fetch(shared.BasenineHost, shared.BaseninePort, entriesProvider := dependency.GetInstance(dependency.EntriesProvider).(entries.EntriesProvider)
entriesRequest.LeftOff, entriesRequest.Direction, entriesRequest.Query, entries, metadata, err := entriesProvider.GetEntries(entriesRequest)
entriesRequest.Limit, time.Duration(entriesRequest.TimeoutMs)*time.Millisecond) if !HandleEntriesError(c, err) {
if err != nil { baseEntries := make([]interface{}, 0)
c.JSON(http.StatusInternalServerError, validationError) for _, entry := range entries {
} baseEntries = append(baseEntries, entry.Base)
response := &models.EntriesResponse{}
var dataSlice []interface{}
for _, row := range data {
var entry *tapApi.Entry
err = json.Unmarshal(row, &entry)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": true,
"type": "error",
"autoClose": "5000",
"msg": string(row),
})
return // exit
} }
c.JSON(http.StatusOK, models.EntriesResponse{
extension := app.ExtensionsMap[entry.Protocol.Name] Data: baseEntries,
base := extension.Dissector.Summarize(entry) Meta: metadata,
})
dataSlice = append(dataSlice, base)
} }
var metadata *basenine.Metadata
err = json.Unmarshal(meta, &metadata)
if err != nil {
logger.Log.Debugf("Error recieving metadata: %v", err.Error())
}
response.Data = dataSlice
response.Meta = metadata
c.JSON(http.StatusOK, response)
} }
func GetEntry(c *gin.Context) { func GetEntry(c *gin.Context) {
@@ -102,54 +70,11 @@ func GetEntry(c *gin.Context) {
} }
id, _ := strconv.Atoi(c.Param("id")) id, _ := strconv.Atoi(c.Param("id"))
var entry *tapApi.Entry
bytes, err := basenine.Single(shared.BasenineHost, shared.BaseninePort, id, singleEntryRequest.Query)
if Error(c, err) {
return // exit
}
err = json.Unmarshal(bytes, &entry)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": true,
"type": "error",
"autoClose": "5000",
"msg": string(bytes),
})
return // exit
}
extension := app.ExtensionsMap[entry.Protocol.Name] entriesProvider := dependency.GetInstance(dependency.EntriesProvider).(entries.EntriesProvider)
base := extension.Dissector.Summarize(entry) entry, err := entriesProvider.GetEntry(singleEntryRequest, id)
var representation []byte
representation, err = extension.Dissector.Represent(entry.Request, entry.Response)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": true,
"type": "error",
"autoClose": "5000",
"msg": err.Error(),
})
return // exit
}
var rules []map[string]interface{} if !HandleEntriesError(c, err) {
var isRulesEnabled bool c.JSON(http.StatusOK, entry)
if entry.Protocol.Name == "http" {
harEntry, _ := har.NewEntry(entry.Request, entry.Response, entry.StartTime, entry.ElapsedTime)
_, rulesMatched, _isRulesEnabled := models.RunValidationRulesState(*harEntry, entry.Destination.Name)
isRulesEnabled = _isRulesEnabled
inrec, _ := json.Marshal(rulesMatched)
if err := json.Unmarshal(inrec, &rules); err != nil {
logger.Log.Error(err)
}
} }
c.JSON(http.StatusOK, tapApi.EntryWrapper{
Protocol: entry.Protocol,
Representation: string(representation),
Data: entry,
Base: base,
Rules: rules,
IsRulesEnabled: isRulesEnabled,
})
} }

View File

@@ -1,8 +1,12 @@
package controllers package controllers
import ( import (
"bytes"
basenine "github.com/up9inc/basenine/client/go"
"net"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"time"
"github.com/up9inc/mizu/agent/pkg/dependency" "github.com/up9inc/mizu/agent/pkg/dependency"
"github.com/up9inc/mizu/agent/pkg/oas" "github.com/up9inc/mizu/agent/pkg/oas"
@@ -11,39 +15,55 @@ import (
) )
func TestGetOASServers(t *testing.T) { func TestGetOASServers(t *testing.T) {
dependency.RegisterGenerator(dependency.OasGeneratorDependency, func() interface{} { return oas.GetDefaultOasGeneratorInstance() }) recorder, c := getRecorderAndContext()
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
oas.GetDefaultOasGeneratorInstance().Start()
oas.GetDefaultOasGeneratorInstance().GetServiceSpecs().Store("some", oas.NewGen("some"))
GetOASServers(c) GetOASServers(c)
t.Logf("Written body: %s", recorder.Body.String()) t.Logf("Written body: %s", recorder.Body.String())
} }
func TestGetOASAllSpecs(t *testing.T) { func TestGetOASAllSpecs(t *testing.T) {
dependency.RegisterGenerator(dependency.OasGeneratorDependency, func() interface{} { return oas.GetDefaultOasGeneratorInstance() }) recorder, c := getRecorderAndContext()
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
oas.GetDefaultOasGeneratorInstance().Start()
oas.GetDefaultOasGeneratorInstance().GetServiceSpecs().Store("some", oas.NewGen("some"))
GetOASAllSpecs(c) GetOASAllSpecs(c)
t.Logf("Written body: %s", recorder.Body.String()) t.Logf("Written body: %s", recorder.Body.String())
} }
func TestGetOASSpec(t *testing.T) { func TestGetOASSpec(t *testing.T) {
dependency.RegisterGenerator(dependency.OasGeneratorDependency, func() interface{} { return oas.GetDefaultOasGeneratorInstance() }) recorder, c := getRecorderAndContext()
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
oas.GetDefaultOasGeneratorInstance().Start()
oas.GetDefaultOasGeneratorInstance().GetServiceSpecs().Store("some", oas.NewGen("some"))
c.Params = []gin.Param{{Key: "id", Value: "some"}} c.Params = []gin.Param{{Key: "id", Value: "some"}}
GetOASSpec(c) GetOASSpec(c)
t.Logf("Written body: %s", recorder.Body.String()) t.Logf("Written body: %s", recorder.Body.String())
} }
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 }
func getRecorderAndContext() (*httptest.ResponseRecorder, *gin.Context) {
dummyConn := new(basenine.Connection)
dummyConn.Conn = fakeConn{
sendBuffer: bytes.NewBufferString("\n"),
receiveBuffer: bytes.NewBufferString("\n"),
}
dependency.RegisterGenerator(dependency.OasGeneratorDependency, func() interface{} {
return oas.GetDefaultOasGeneratorInstance(dummyConn)
})
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
oas.GetDefaultOasGeneratorInstance(dummyConn).Start()
oas.GetDefaultOasGeneratorInstance(dummyConn).GetServiceSpecs().Store("some", oas.NewGen("some"))
return recorder, c
}

View File

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

View File

@@ -5,4 +5,7 @@ type DependencyContainerType string
const ( const (
ServiceMapGeneratorDependency = "ServiceMapGeneratorDependency" ServiceMapGeneratorDependency = "ServiceMapGeneratorDependency"
OasGeneratorDependency = "OasGeneratorDependency" OasGeneratorDependency = "OasGeneratorDependency"
EntriesProvider = "EntriesProvider"
EntriesSocketStreamer = "EntriesSocketStreamer"
EntryStreamerSocketConnector = "EntryStreamerSocketConnector"
) )

View File

@@ -0,0 +1,98 @@
package entries
import (
"encoding/json"
"time"
basenine "github.com/up9inc/basenine/client/go"
"github.com/up9inc/mizu/agent/pkg/app"
"github.com/up9inc/mizu/agent/pkg/har"
"github.com/up9inc/mizu/agent/pkg/models"
"github.com/up9inc/mizu/shared"
"github.com/up9inc/mizu/shared/logger"
tapApi "github.com/up9inc/mizu/tap/api"
)
type EntriesProvider interface {
GetEntries(entriesRequest *models.EntriesRequest) ([]*tapApi.EntryWrapper, *basenine.Metadata, error)
GetEntry(singleEntryRequest *models.SingleEntryRequest, entryId int) (*tapApi.EntryWrapper, error)
}
type BasenineEntriesProvider struct{}
func (e *BasenineEntriesProvider) GetEntries(entriesRequest *models.EntriesRequest) ([]*tapApi.EntryWrapper, *basenine.Metadata, error) {
data, meta, err := basenine.Fetch(shared.BasenineHost, shared.BaseninePort,
entriesRequest.LeftOff, entriesRequest.Direction, entriesRequest.Query,
entriesRequest.Limit, time.Duration(entriesRequest.TimeoutMs)*time.Millisecond)
if err != nil {
return nil, nil, err
}
var dataSlice []*tapApi.EntryWrapper
for _, row := range data {
var entry *tapApi.Entry
err = json.Unmarshal(row, &entry)
if err != nil {
return nil, nil, err
}
extension := app.ExtensionsMap[entry.Protocol.Name]
base := extension.Dissector.Summarize(entry)
dataSlice = append(dataSlice, &tapApi.EntryWrapper{
Protocol: entry.Protocol,
Data: entry,
Base: base,
})
}
var metadata *basenine.Metadata
err = json.Unmarshal(meta, &metadata)
if err != nil {
logger.Log.Debugf("Error recieving metadata: %v", err.Error())
}
return dataSlice, metadata, nil
}
func (e *BasenineEntriesProvider) GetEntry(singleEntryRequest *models.SingleEntryRequest, entryId int) (*tapApi.EntryWrapper, error) {
var entry *tapApi.Entry
bytes, err := basenine.Single(shared.BasenineHost, shared.BaseninePort, entryId, singleEntryRequest.Query)
if err != nil {
return nil, err
}
err = json.Unmarshal(bytes, &entry)
if err != nil {
return nil, err
}
extension := app.ExtensionsMap[entry.Protocol.Name]
base := extension.Dissector.Summarize(entry)
var representation []byte
representation, err = extension.Dissector.Represent(entry.Request, entry.Response)
if err != nil {
return nil, err
}
var rules []map[string]interface{}
var isRulesEnabled bool
if entry.Protocol.Name == "http" {
harEntry, _ := har.NewEntry(entry.Request, entry.Response, entry.StartTime, entry.ElapsedTime)
_, rulesMatched, _isRulesEnabled := models.RunValidationRulesState(*harEntry, entry.Destination.Name)
isRulesEnabled = _isRulesEnabled
inrec, _ := json.Marshal(rulesMatched)
if err := json.Unmarshal(inrec, &rules); err != nil {
logger.Log.Error(err)
}
}
return &tapApi.EntryWrapper{
Protocol: entry.Protocol,
Representation: string(representation),
Data: entry,
Base: base,
Rules: rules,
IsRulesEnabled: isRulesEnabled,
}, nil
}

View File

@@ -67,23 +67,23 @@ func fileSize(fname string) int64 {
return fi.Size() return fi.Size()
} }
func feedEntries(fromFiles []string, isSync bool) (count int, err error) { func feedEntries(fromFiles []string, isSync bool, gen *defaultOasGenerator) (count uint, err error) {
badFiles := make([]string, 0) badFiles := make([]string, 0)
cnt := 0 cnt := uint(0)
for _, file := range fromFiles { for _, file := range fromFiles {
logger.Log.Info("Processing file: " + file) logger.Log.Info("Processing file: " + file)
ext := strings.ToLower(filepath.Ext(file)) ext := strings.ToLower(filepath.Ext(file))
eCnt := 0 eCnt := uint(0)
switch ext { switch ext {
case ".har": case ".har":
eCnt, err = feedFromHAR(file, isSync) eCnt, err = feedFromHAR(file, isSync, gen)
if err != nil { if err != nil {
logger.Log.Warning("Failed processing file: " + err.Error()) logger.Log.Warning("Failed processing file: " + err.Error())
badFiles = append(badFiles, file) badFiles = append(badFiles, file)
continue continue
} }
case ".ldjson": case ".ldjson":
eCnt, err = feedFromLDJSON(file, isSync) eCnt, err = feedFromLDJSON(file, isSync, gen)
if err != nil { if err != nil {
logger.Log.Warning("Failed processing file: " + err.Error()) logger.Log.Warning("Failed processing file: " + err.Error())
badFiles = append(badFiles, file) badFiles = append(badFiles, file)
@@ -102,7 +102,7 @@ func feedEntries(fromFiles []string, isSync bool) (count int, err error) {
return cnt, nil return cnt, nil
} }
func feedFromHAR(file string, isSync bool) (int, error) { func feedFromHAR(file string, isSync bool, gen *defaultOasGenerator) (uint, error) {
fd, err := os.Open(file) fd, err := os.Open(file)
if err != nil { if err != nil {
panic(err) panic(err)
@@ -121,16 +121,16 @@ func feedFromHAR(file string, isSync bool) (int, error) {
return 0, err return 0, err
} }
cnt := 0 cnt := uint(0)
for _, entry := range harDoc.Log.Entries { for _, entry := range harDoc.Log.Entries {
cnt += 1 cnt += 1
feedEntry(&entry, "", isSync, file) feedEntry(&entry, "", file, gen, cnt)
} }
return cnt, nil return cnt, nil
} }
func feedEntry(entry *har.Entry, source string, isSync bool, file string) { func feedEntry(entry *har.Entry, source string, file string, gen *defaultOasGenerator, cnt uint) {
entry.Comment = file entry.Comment = file
if entry.Response.Status == 302 { if entry.Response.Status == 302 {
logger.Log.Debugf("Dropped traffic entry due to permanent redirect status: %s", entry.StartedDateTime) logger.Log.Debugf("Dropped traffic entry due to permanent redirect status: %s", entry.StartedDateTime)
@@ -145,15 +145,11 @@ func feedEntry(entry *har.Entry, source string, isSync bool, file string) {
logger.Log.Errorf("Failed to parse entry URL: %v, err: %v", entry.Request.URL, err) logger.Log.Errorf("Failed to parse entry URL: %v, err: %v", entry.Request.URL, err)
} }
ews := EntryWithSource{Entry: *entry, Source: source, Destination: u.Host, Id: uint(0)} ews := EntryWithSource{Entry: *entry, Source: source, Destination: u.Host, Id: cnt}
if isSync { gen.handleHARWithSource(&ews)
GetDefaultOasGeneratorInstance().entriesChan <- ews // blocking variant, right?
} else {
GetDefaultOasGeneratorInstance().PushEntry(&ews)
}
} }
func feedFromLDJSON(file string, isSync bool) (int, error) { func feedFromLDJSON(file string, isSync bool, gen *defaultOasGenerator) (uint, error) {
fd, err := os.Open(file) fd, err := os.Open(file)
if err != nil { if err != nil {
panic(err) panic(err)
@@ -165,7 +161,7 @@ func feedFromLDJSON(file string, isSync bool) (int, error) {
var meta map[string]interface{} var meta map[string]interface{}
buf := strings.Builder{} buf := strings.Builder{}
cnt := 0 cnt := uint(0)
source := "" source := ""
for { for {
substr, isPrefix, err := reader.ReadLine() substr, isPrefix, err := reader.ReadLine()
@@ -196,7 +192,7 @@ func feedFromLDJSON(file string, isSync bool) (int, error) {
logger.Log.Warningf("Failed decoding entry: %s", line) logger.Log.Warningf("Failed decoding entry: %s", line)
} else { } else {
cnt += 1 cnt += 1
feedEntry(&entry, source, isSync, file) feedEntry(&entry, source, file, gen, cnt)
} }
} }
} }

View File

@@ -3,10 +3,13 @@ package oas
import ( import (
"context" "context"
"encoding/json" "encoding/json"
basenine "github.com/up9inc/basenine/client/go"
"github.com/up9inc/mizu/agent/pkg/har"
"github.com/up9inc/mizu/shared"
"github.com/up9inc/mizu/tap/api"
"net/url" "net/url"
"sync" "sync"
"github.com/up9inc/mizu/agent/pkg/har"
"github.com/up9inc/mizu/shared/logger" "github.com/up9inc/mizu/shared/logger"
) )
@@ -15,10 +18,6 @@ var (
instance *defaultOasGenerator instance *defaultOasGenerator
) )
type OasGeneratorSink interface {
PushEntry(entryWithSource *EntryWithSource)
}
type OasGenerator interface { type OasGenerator interface {
Start() Start()
Stop() Stop()
@@ -32,12 +31,20 @@ type defaultOasGenerator struct {
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
serviceSpecs *sync.Map serviceSpecs *sync.Map
entriesChan chan EntryWithSource dbConn *basenine.Connection
} }
func GetDefaultOasGeneratorInstance() *defaultOasGenerator { func GetDefaultOasGeneratorInstance(conn *basenine.Connection) *defaultOasGenerator {
syncOnce.Do(func() { syncOnce.Do(func() {
instance = NewDefaultOasGenerator() if conn == nil {
c, err := basenine.NewConnection(shared.BasenineHost, shared.BaseninePort)
if err != nil {
panic(err)
}
conn = c
}
instance = NewDefaultOasGenerator(conn)
logger.Log.Debug("OAS Generator Initialized") logger.Log.Debug("OAS Generator Initialized")
}) })
return instance return instance
@@ -50,7 +57,6 @@ func (g *defaultOasGenerator) Start() {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
g.cancel = cancel g.cancel = cancel
g.ctx = ctx g.ctx = ctx
g.entriesChan = make(chan EntryWithSource, 100) // buffer up to 100 entries for OAS processing
g.serviceSpecs = &sync.Map{} g.serviceSpecs = &sync.Map{}
g.started = true g.started = true
go g.runGenerator() go g.runGenerator()
@@ -70,80 +76,117 @@ func (g *defaultOasGenerator) IsStarted() bool {
} }
func (g *defaultOasGenerator) runGenerator() { func (g *defaultOasGenerator) runGenerator() {
// Make []byte channels to recieve the data and the meta
dataChan := make(chan []byte)
metaChan := make(chan []byte)
g.dbConn.Query("", dataChan, metaChan)
for { for {
select { select {
case <-g.ctx.Done(): case <-g.ctx.Done():
logger.Log.Infof("OAS Generator was canceled") logger.Log.Infof("OAS Generator was canceled")
return return
case entryWithSource, ok := <-g.entriesChan: case metaBytes, ok := <-metaChan:
if !ok {
logger.Log.Infof("OAS Generator - meta channel closed")
break
}
logger.Log.Debugf("Meta: %s", metaBytes)
case dataBytes, ok := <-dataChan:
if !ok { if !ok {
logger.Log.Infof("OAS Generator - entries channel closed") logger.Log.Infof("OAS Generator - entries channel closed")
break break
} }
entry := entryWithSource.Entry
u, err := url.Parse(entry.Request.URL) logger.Log.Debugf("Data: %s", dataBytes)
e := new(api.Entry)
err := json.Unmarshal(dataBytes, e)
if err != nil { if err != nil {
logger.Log.Errorf("Failed to parse entry URL: %v, err: %v", entry.Request.URL, err)
}
val, found := g.serviceSpecs.Load(entryWithSource.Destination)
var gen *SpecGen
if !found {
gen = NewGen(u.Scheme + "://" + entryWithSource.Destination)
g.serviceSpecs.Store(entryWithSource.Destination, gen)
} else {
gen = val.(*SpecGen)
}
opId, err := gen.feedEntry(entryWithSource)
if err != nil {
txt, suberr := json.Marshal(entry)
if suberr == nil {
logger.Log.Debugf("Problematic entry: %s", txt)
}
logger.Log.Warningf("Failed processing entry: %s", err)
continue continue
} }
g.handleEntry(e)
logger.Log.Debugf("Handled entry %s as opId: %s", entry.Request.URL, opId) // TODO: set opId back to entry?
} }
} }
} }
func (g *defaultOasGenerator) handleEntry(mizuEntry *api.Entry) {
if mizuEntry.Protocol.Name == "http" {
entry, err := har.NewEntry(mizuEntry.Request, mizuEntry.Response, mizuEntry.StartTime, mizuEntry.ElapsedTime)
if err != nil {
logger.Log.Warningf("Failed to turn MizuEntry %d into HAR Entry: %s", mizuEntry.Id, err)
return
}
dest := mizuEntry.Destination.Name
if dest == "" {
dest = mizuEntry.Destination.IP + ":" + mizuEntry.Destination.Port
}
entryWSource := &EntryWithSource{
Entry: *entry,
Source: mizuEntry.Source.Name,
Destination: dest,
Id: mizuEntry.Id,
}
g.handleHARWithSource(entryWSource)
} else {
logger.Log.Debugf("OAS: Unsupported protocol in entry %d: %s", mizuEntry.Id, mizuEntry.Protocol.Name)
}
}
func (g *defaultOasGenerator) handleHARWithSource(entryWSource *EntryWithSource) {
entry := entryWSource.Entry
gen := g.getGen(entryWSource.Destination, entry.Request.URL)
opId, err := gen.feedEntry(entryWSource)
if err != nil {
txt, suberr := json.Marshal(entry)
if suberr == nil {
logger.Log.Debugf("Problematic entry: %s", txt)
}
logger.Log.Warningf("Failed processing entry %d: %s", entryWSource.Id, err)
return
}
logger.Log.Debugf("Handled entry %d as opId: %s", entryWSource.Id, opId) // TODO: set opId back to entry?
}
func (g *defaultOasGenerator) getGen(dest string, urlStr string) *SpecGen {
u, err := url.Parse(urlStr)
if err != nil {
logger.Log.Errorf("Failed to parse entry URL: %v, err: %v", urlStr, err)
}
val, found := g.serviceSpecs.Load(dest)
var gen *SpecGen
if !found {
gen = NewGen(u.Scheme + "://" + dest)
g.serviceSpecs.Store(dest, gen)
} else {
gen = val.(*SpecGen)
}
return gen
}
func (g *defaultOasGenerator) Reset() { func (g *defaultOasGenerator) Reset() {
g.serviceSpecs = &sync.Map{} g.serviceSpecs = &sync.Map{}
} }
func (g *defaultOasGenerator) PushEntry(entryWithSource *EntryWithSource) {
if !g.started {
return
}
select {
case g.entriesChan <- *entryWithSource:
default:
logger.Log.Warningf("OAS Generator - entry wasn't sent to channel because the channel has no buffer or there is no receiver")
}
}
func (g *defaultOasGenerator) GetServiceSpecs() *sync.Map { func (g *defaultOasGenerator) GetServiceSpecs() *sync.Map {
return g.serviceSpecs return g.serviceSpecs
} }
func NewDefaultOasGenerator() *defaultOasGenerator { func NewDefaultOasGenerator(c *basenine.Connection) *defaultOasGenerator {
return &defaultOasGenerator{ return &defaultOasGenerator{
started: false, started: false,
ctx: nil, ctx: nil,
cancel: nil, cancel: nil,
serviceSpecs: nil, serviceSpecs: nil,
entriesChan: nil, dbConn: c,
} }
} }
type EntryWithSource struct {
Source string
Destination string
Entry har.Entry
Id uint
}

View File

@@ -0,0 +1,36 @@
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)
if err != nil {
panic(err)
}
ews := &EntryWithSource{
Destination: "some",
Entry: *e,
}
gen.handleHARWithSource(ews)
g, ok := gen.serviceSpecs.Load("some")
if !ok {
panic("Failed")
}
sg := g.(*SpecGen)
spec, err := sg.GetSpec()
if err != nil {
panic(err)
}
specText, _ := json.Marshal(spec)
t.Log(string(specText))
}

View File

@@ -28,6 +28,13 @@ const CountersTotal = "x-counters-total"
const CountersPerSource = "x-counters-per-source" const CountersPerSource = "x-counters-per-source"
const SampleId = "x-sample-entry" const SampleId = "x-sample-entry"
type EntryWithSource struct {
Source string
Destination string
Entry har.Entry
Id uint
}
type reqResp struct { // hello, generics in Go type reqResp struct { // hello, generics in Go
Req *har.Request Req *har.Request
Resp *har.Response Resp *har.Response
@@ -60,7 +67,7 @@ func (g *SpecGen) StartFromSpec(oas *openapi.OpenAPI) {
g.tree = new(Node) g.tree = new(Node)
for pathStr, pathObj := range oas.Paths.Items { for pathStr, pathObj := range oas.Paths.Items {
pathSplit := strings.Split(string(pathStr), "/") pathSplit := strings.Split(string(pathStr), "/")
g.tree.getOrSet(pathSplit, pathObj) g.tree.getOrSet(pathSplit, pathObj, 0)
// clean "last entry timestamp" markers from the past // clean "last entry timestamp" markers from the past
for _, pathAndOp := range g.tree.listOps() { for _, pathAndOp := range g.tree.listOps() {
@@ -69,11 +76,11 @@ func (g *SpecGen) StartFromSpec(oas *openapi.OpenAPI) {
} }
} }
func (g *SpecGen) feedEntry(entryWithSource EntryWithSource) (string, error) { func (g *SpecGen) feedEntry(entryWithSource *EntryWithSource) (string, error) {
g.lock.Lock() g.lock.Lock()
defer g.lock.Unlock() defer g.lock.Unlock()
opId, err := g.handlePathObj(&entryWithSource) opId, err := g.handlePathObj(entryWithSource)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -219,7 +226,7 @@ func (g *SpecGen) handlePathObj(entryWithSource *EntryWithSource) (string, error
} else { } else {
split = strings.Split(urlParsed.Path, "/") split = strings.Split(urlParsed.Path, "/")
} }
node := g.tree.getOrSet(split, new(openapi.PathObj)) node := g.tree.getOrSet(split, new(openapi.PathObj), entryWithSource.Id)
opObj, err := handleOpObj(entryWithSource, node.pathObj) opObj, err := handleOpObj(entryWithSource, node.pathObj)
if opObj != nil { if opObj != nil {
@@ -242,12 +249,12 @@ func handleOpObj(entryWithSource *EntryWithSource, pathObj *openapi.PathObj) (*o
return nil, nil return nil, nil
} }
err = handleRequest(&entry.Request, opObj, isSuccess) err = handleRequest(&entry.Request, opObj, isSuccess, entryWithSource.Id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = handleResponse(&entry.Response, opObj, isSuccess) err = handleResponse(&entry.Response, opObj, isSuccess, entryWithSource.Id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -257,6 +264,8 @@ func handleOpObj(entryWithSource *EntryWithSource, pathObj *openapi.PathObj) (*o
return nil, err return nil, err
} }
setSampleID(&opObj.Extensions, entryWithSource.Id)
return opObj, nil return opObj, nil
} }
@@ -329,15 +338,10 @@ func handleCounters(opObj *openapi.Operation, success bool, entryWithSource *Ent
return err return err
} }
err = opObj.Extensions.SetExtension(SampleId, entryWithSource.Id)
if err != nil {
return err
}
return nil return nil
} }
func handleRequest(req *har.Request, opObj *openapi.Operation, isSuccess bool) error { func handleRequest(req *har.Request, opObj *openapi.Operation, isSuccess bool, sampleId uint) error {
// TODO: we don't handle the situation when header/qstr param can be defined on pathObj level. Also the path param defined on opObj // TODO: we don't handle the situation when header/qstr param can be defined on pathObj level. Also the path param defined on opObj
urlParsed, err := url.Parse(req.URL) urlParsed, err := url.Parse(req.URL)
if err != nil { if err != nil {
@@ -361,7 +365,7 @@ func handleRequest(req *har.Request, opObj *openapi.Operation, isSuccess bool) e
IsIgnored: func(name string) bool { return false }, IsIgnored: func(name string) bool { return false },
GeneralizeName: func(name string) string { return name }, GeneralizeName: func(name string) string { return name },
} }
handleNameVals(qstrGW, &opObj.Parameters, false) handleNameVals(qstrGW, &opObj.Parameters, false, sampleId)
hdrGW := nvParams{ hdrGW := nvParams{
In: openapi.InHeader, In: openapi.InHeader,
@@ -369,7 +373,7 @@ func handleRequest(req *har.Request, opObj *openapi.Operation, isSuccess bool) e
IsIgnored: isHeaderIgnored, IsIgnored: isHeaderIgnored,
GeneralizeName: strings.ToLower, GeneralizeName: strings.ToLower,
} }
handleNameVals(hdrGW, &opObj.Parameters, true) handleNameVals(hdrGW, &opObj.Parameters, true, sampleId)
if isSuccess { if isSuccess {
reqBody, err := getRequestBody(req, opObj) reqBody, err := getRequestBody(req, opObj)
@@ -378,12 +382,14 @@ func handleRequest(req *har.Request, opObj *openapi.Operation, isSuccess bool) e
} }
if reqBody != nil { if reqBody != nil {
setSampleID(&reqBody.Extensions, sampleId)
if req.PostData.Text == "" { if req.PostData.Text == "" {
reqBody.Required = false reqBody.Required = false
} else { } else {
reqCtype, _ := getReqCtype(req) reqCtype, _ := getReqCtype(req)
reqMedia, err := fillContent(reqResp{Req: req}, reqBody.Content, reqCtype) reqMedia, err := fillContent(reqResp{Req: req}, reqBody.Content, reqCtype, sampleId)
if err != nil { if err != nil {
return err return err
} }
@@ -395,18 +401,20 @@ func handleRequest(req *har.Request, opObj *openapi.Operation, isSuccess bool) e
return nil return nil
} }
func handleResponse(resp *har.Response, opObj *openapi.Operation, isSuccess bool) error { func handleResponse(resp *har.Response, opObj *openapi.Operation, isSuccess bool, sampleId uint) error {
// TODO: we don't support "default" response // TODO: we don't support "default" response
respObj, err := getResponseObj(resp, opObj, isSuccess) respObj, err := getResponseObj(resp, opObj, isSuccess)
if err != nil { if err != nil {
return err return err
} }
handleRespHeaders(resp.Headers, respObj) setSampleID(&respObj.Extensions, sampleId)
handleRespHeaders(resp.Headers, respObj, sampleId)
respCtype := getRespCtype(resp) respCtype := getRespCtype(resp)
respContent := respObj.Content respContent := respObj.Content
respMedia, err := fillContent(reqResp{Resp: resp}, respContent, respCtype) respMedia, err := fillContent(reqResp{Resp: resp}, respContent, respCtype, sampleId)
if err != nil { if err != nil {
return err return err
} }
@@ -414,7 +422,7 @@ func handleResponse(resp *har.Response, opObj *openapi.Operation, isSuccess bool
return nil return nil
} }
func handleRespHeaders(reqHeaders []har.Header, respObj *openapi.ResponseObj) { func handleRespHeaders(reqHeaders []har.Header, respObj *openapi.ResponseObj, sampleId uint) {
visited := map[string]*openapi.HeaderObj{} visited := map[string]*openapi.HeaderObj{}
for _, pair := range reqHeaders { for _, pair := range reqHeaders {
if isHeaderIgnored(pair.Name) { if isHeaderIgnored(pair.Name) {
@@ -436,6 +444,8 @@ func handleRespHeaders(reqHeaders []har.Header, respObj *openapi.ResponseObj) {
logger.Log.Warningf("Failed to add example to a parameter: %s", err) logger.Log.Warningf("Failed to add example to a parameter: %s", err)
} }
visited[nameGeneral] = param visited[nameGeneral] = param
setSampleID(&param.Extensions, sampleId)
} }
// maintain "required" flag // maintain "required" flag
@@ -456,13 +466,15 @@ func handleRespHeaders(reqHeaders []har.Header, respObj *openapi.ResponseObj) {
} }
} }
func fillContent(reqResp reqResp, respContent openapi.Content, ctype string) (*openapi.MediaType, error) { func fillContent(reqResp reqResp, respContent openapi.Content, ctype string, sampleId uint) (*openapi.MediaType, error) {
content, found := respContent[ctype] content, found := respContent[ctype]
if !found { if !found {
respContent[ctype] = &openapi.MediaType{} respContent[ctype] = &openapi.MediaType{}
content = respContent[ctype] content = respContent[ctype]
} }
setSampleID(&content.Extensions, sampleId)
var text string var text string
var isBinary bool var isBinary bool
if reqResp.Req != nil { if reqResp.Req != nil {
@@ -474,10 +486,10 @@ func fillContent(reqResp reqResp, respContent openapi.Content, ctype string) (*o
if !isBinary && text != "" { if !isBinary && text != "" {
var exampleMsg []byte var exampleMsg []byte
// try treating it as json // try treating it as json
any, isJSON := anyJSON(text) anyVal, isJSON := anyJSON(text)
if isJSON { if isJSON {
// re-marshal with forced indent // re-marshal with forced indent
if msg, err := json.MarshalIndent(any, "", "\t"); err != nil { if msg, err := json.MarshalIndent(anyVal, "", "\t"); err != nil {
panic("Failed to re-marshal value, super-strange") panic("Failed to re-marshal value, super-strange")
} else { } else {
exampleMsg = msg exampleMsg = msg

View File

@@ -6,6 +6,7 @@ import (
"os" "os"
"regexp" "regexp"
"strings" "strings"
"sync"
"testing" "testing"
"time" "time"
@@ -19,7 +20,7 @@ import (
// if started via env, write file into subdir // if started via env, write file into subdir
func outputSpec(label string, spec *openapi.OpenAPI, t *testing.T) string { func outputSpec(label string, spec *openapi.OpenAPI, t *testing.T) string {
content, err := json.MarshalIndent(spec, "", "\t") content, err := json.MarshalIndent(spec, "", " ")
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -48,14 +49,16 @@ func TestEntries(t *testing.T) {
t.Log(err) t.Log(err)
t.FailNow() t.FailNow()
} }
GetDefaultOasGeneratorInstance().Start()
loadStartingOAS("test_artifacts/catalogue.json", "catalogue") gen := NewDefaultOasGenerator(nil)
loadStartingOAS("test_artifacts/trcc.json", "trcc-api-service") gen.serviceSpecs = new(sync.Map)
loadStartingOAS("test_artifacts/catalogue.json", "catalogue", gen.serviceSpecs)
loadStartingOAS("test_artifacts/trcc.json", "trcc-api-service", gen.serviceSpecs)
go func() { go func() {
for { for {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
GetDefaultOasGeneratorInstance().GetServiceSpecs().Range(func(key, val interface{}) bool { gen.serviceSpecs.Range(func(key, val interface{}) bool {
svc := key.(string) svc := key.(string)
t.Logf("Getting spec for %s", svc) t.Logf("Getting spec for %s", svc)
gen := val.(*SpecGen) gen := val.(*SpecGen)
@@ -68,16 +71,14 @@ func TestEntries(t *testing.T) {
} }
}() }()
cnt, err := feedEntries(files, true) cnt, err := feedEntries(files, true, gen)
if err != nil { if err != nil {
t.Log(err) t.Log(err)
t.Fail() t.Fail()
} }
waitQueueProcessed()
svcs := strings.Builder{} svcs := strings.Builder{}
GetDefaultOasGeneratorInstance().GetServiceSpecs().Range(func(key, val interface{}) bool { gen.serviceSpecs.Range(func(key, val interface{}) bool {
gen := val.(*SpecGen) gen := val.(*SpecGen)
svc := key.(string) svc := key.(string)
svcs.WriteString(svc + ",") svcs.WriteString(svc + ",")
@@ -99,7 +100,7 @@ func TestEntries(t *testing.T) {
return true return true
}) })
GetDefaultOasGeneratorInstance().GetServiceSpecs().Range(func(key, val interface{}) bool { gen.serviceSpecs.Range(func(key, val interface{}) bool {
svc := key.(string) svc := key.(string)
gen := val.(*SpecGen) gen := val.(*SpecGen)
spec, err := gen.GetSpec() spec, err := gen.GetSpec()
@@ -123,20 +124,18 @@ func TestEntries(t *testing.T) {
} }
func TestFileSingle(t *testing.T) { func TestFileSingle(t *testing.T) {
GetDefaultOasGeneratorInstance().Start() gen := NewDefaultOasGenerator(nil)
GetDefaultOasGeneratorInstance().Reset() gen.serviceSpecs = new(sync.Map)
// loadStartingOAS() // loadStartingOAS()
file := "test_artifacts/params.har" file := "test_artifacts/params.har"
files := []string{file} files := []string{file}
cnt, err := feedEntries(files, true) cnt, err := feedEntries(files, true, gen)
if err != nil { if err != nil {
logger.Log.Warning("Failed processing file: " + err.Error()) logger.Log.Warning("Failed processing file: " + err.Error())
t.Fail() t.Fail()
} }
waitQueueProcessed() gen.serviceSpecs.Range(func(key, val interface{}) bool {
GetDefaultOasGeneratorInstance().GetServiceSpecs().Range(func(key, val interface{}) bool {
svc := key.(string) svc := key.(string)
gen := val.(*SpecGen) gen := val.(*SpecGen)
spec, err := gen.GetSpec() spec, err := gen.GetSpec()
@@ -189,18 +188,7 @@ func TestFileSingle(t *testing.T) {
logger.Log.Infof("Processed entries: %d", cnt) logger.Log.Infof("Processed entries: %d", cnt)
} }
func waitQueueProcessed() { func loadStartingOAS(file string, label string, specs *sync.Map) {
for {
time.Sleep(100 * time.Millisecond)
queue := len(GetDefaultOasGeneratorInstance().entriesChan)
logger.Log.Infof("Queue: %d", queue)
if queue < 1 {
break
}
}
}
func loadStartingOAS(file string, label string) {
fd, err := os.Open(file) fd, err := os.Open(file)
if err != nil { if err != nil {
panic(err) panic(err)
@@ -222,12 +210,14 @@ func loadStartingOAS(file string, label string) {
gen := NewGen(label) gen := NewGen(label)
gen.StartFromSpec(doc) gen.StartFromSpec(doc)
GetDefaultOasGeneratorInstance().GetServiceSpecs().Store(label, gen) specs.Store(label, gen)
} }
func TestEntriesNegative(t *testing.T) { func TestEntriesNegative(t *testing.T) {
gen := NewDefaultOasGenerator(nil)
gen.serviceSpecs = new(sync.Map)
files := []string{"invalid"} files := []string{"invalid"}
_, err := feedEntries(files, false) _, err := feedEntries(files, false, gen)
if err == nil { if err == nil {
t.Logf("Should have failed") t.Logf("Should have failed")
t.Fail() t.Fail()
@@ -235,8 +225,10 @@ func TestEntriesNegative(t *testing.T) {
} }
func TestEntriesPositive(t *testing.T) { func TestEntriesPositive(t *testing.T) {
gen := NewDefaultOasGenerator(nil)
gen.serviceSpecs = new(sync.Map)
files := []string{"test_artifacts/params.har"} files := []string{"test_artifacts/params.har"}
_, err := feedEntries(files, false) _, err := feedEntries(files, false, gen)
if err != nil { if err != nil {
t.Logf("Failed") t.Logf("Failed")
t.Fail() t.Fail()

View File

@@ -21,9 +21,11 @@
"description": "Successful call with status 200", "description": "Successful call with status 200",
"content": { "content": {
"application/json": { "application/json": {
"example": null "example": null,
"x-sample-entry": 4
} }
} },
"x-sample-entry": 4
} }
}, },
"x-counters-per-source": { "x-counters-per-source": {
@@ -45,7 +47,7 @@
"sumDuration": 0 "sumDuration": 0
}, },
"x-last-seen-ts": 1567750580.04, "x-last-seen-ts": 1567750580.04,
"x-sample-entry": 0 "x-sample-entry": 4
} }
}, },
"/appears-twice": { "/appears-twice": {
@@ -58,9 +60,11 @@
"description": "Successful call with status 200", "description": "Successful call with status 200",
"content": { "content": {
"application/json": { "application/json": {
"example": null "example": null,
"x-sample-entry": 6
} }
} },
"x-sample-entry": 6
} }
}, },
"x-counters-per-source": { "x-counters-per-source": {
@@ -82,7 +86,7 @@
"sumDuration": 1 "sumDuration": 1
}, },
"x-last-seen-ts": 1567750581.74, "x-last-seen-ts": 1567750581.74,
"x-sample-entry": 0 "x-sample-entry": 6
} }
}, },
"/body-optional": { "/body-optional": {
@@ -94,8 +98,11 @@
"200": { "200": {
"description": "Successful call with status 200", "description": "Successful call with status 200",
"content": { "content": {
"": {} "": {
} "x-sample-entry": 12
}
},
"x-sample-entry": 12
} }
}, },
"x-counters-per-source": { "x-counters-per-source": {
@@ -117,14 +124,16 @@
"sumDuration": 0.01 "sumDuration": 0.01
}, },
"x-last-seen-ts": 1567750581.75, "x-last-seen-ts": 1567750581.75,
"x-sample-entry": 0, "x-sample-entry": 12,
"requestBody": { "requestBody": {
"description": "Generic request body", "description": "Generic request body",
"content": { "content": {
"application/json": { "application/json": {
"example": "{\"key\", \"val\"}" "example": "{\"key\", \"val\"}",
"x-sample-entry": 11
} }
} },
"x-sample-entry": 12
} }
} }
}, },
@@ -137,8 +146,11 @@
"200": { "200": {
"description": "Successful call with status 200", "description": "Successful call with status 200",
"content": { "content": {
"": {} "": {
} "x-sample-entry": 13
}
},
"x-sample-entry": 13
} }
}, },
"x-counters-per-source": { "x-counters-per-source": {
@@ -160,15 +172,17 @@
"sumDuration": 0 "sumDuration": 0
}, },
"x-last-seen-ts": 1567750581.75, "x-last-seen-ts": 1567750581.75,
"x-sample-entry": 0, "x-sample-entry": 13,
"requestBody": { "requestBody": {
"description": "Generic request body", "description": "Generic request body",
"content": { "content": {
"": { "": {
"example": "body exists" "example": "body exists",
"x-sample-entry": 13
} }
}, },
"required": true "required": true,
"x-sample-entry": 13
} }
} }
}, },
@@ -182,9 +196,11 @@
"description": "Successful call with status 200", "description": "Successful call with status 200",
"content": { "content": {
"": { "": {
"example": {} "example": {},
"x-sample-entry": 9
} }
} },
"x-sample-entry": 9
} }
}, },
"x-counters-per-source": { "x-counters-per-source": {
@@ -206,7 +222,7 @@
"sumDuration": 0 "sumDuration": 0
}, },
"x-last-seen-ts": 1567750582.74, "x-last-seen-ts": 1567750582.74,
"x-sample-entry": 0, "x-sample-entry": 9,
"requestBody": { "requestBody": {
"description": "Generic request body", "description": "Generic request body",
"content": { "content": {
@@ -233,10 +249,12 @@
} }
} }
}, },
"example": "--BOUNDARY\r\nContent-Disposition: form-data; name=\"file\"; filename=\"metadata.json\"\r\nContent-Type: application/json\r\n\r\n{\"functions\": 123}\r\n--BOUNDARY\r\nContent-Disposition: form-data; name=\"path\"\r\n\r\n/content/components\r\n--BOUNDARY--\r\n" "example": "--BOUNDARY\r\nContent-Disposition: form-data; name=\"file\"; filename=\"metadata.json\"\r\nContent-Type: application/json\r\n\r\n{\"functions\": 123}\r\n--BOUNDARY\r\nContent-Disposition: form-data; name=\"path\"\r\n\r\n/content/components\r\n--BOUNDARY--\r\n",
"x-sample-entry": 9
} }
}, },
"required": true "required": true,
"x-sample-entry": 9
} }
} }
}, },
@@ -249,8 +267,11 @@
"200": { "200": {
"description": "Successful call with status 200", "description": "Successful call with status 200",
"content": { "content": {
"": {} "": {
} "x-sample-entry": 8
}
},
"x-sample-entry": 8
} }
}, },
"x-counters-per-source": { "x-counters-per-source": {
@@ -272,7 +293,7 @@
"sumDuration": 1 "sumDuration": 1
}, },
"x-last-seen-ts": 1567750581.74, "x-last-seen-ts": 1567750581.74,
"x-sample-entry": 0, "x-sample-entry": 8,
"requestBody": { "requestBody": {
"description": "Generic request body", "description": "Generic request body",
"content": { "content": {
@@ -312,10 +333,12 @@
} }
} }
}, },
"example": "agent-id=ade\u0026callback-url=\u0026token=sometoken" "example": "agent-id=ade\u0026callback-url=\u0026token=sometoken",
"x-sample-entry": 8
} }
}, },
"required": true "required": true,
"x-sample-entry": 8
} }
} }
}, },
@@ -331,8 +354,11 @@
"200": { "200": {
"description": "Successful call with status 200", "description": "Successful call with status 200",
"content": { "content": {
"": {} "": {
} "x-sample-entry": 14
}
},
"x-sample-entry": 14
} }
}, },
"x-counters-per-source": { "x-counters-per-source": {
@@ -354,7 +380,7 @@
"sumDuration": 0 "sumDuration": 0
}, },
"x-last-seen-ts": 1567750582, "x-last-seen-ts": 1567750582,
"x-sample-entry": 0 "x-sample-entry": 14
}, },
"parameters": [ "parameters": [
{ {
@@ -369,7 +395,8 @@
"example #0": { "example #0": {
"value": "234324" "value": "234324"
} }
} },
"x-sample-entry": 14
} }
] ]
}, },
@@ -385,8 +412,11 @@
"200": { "200": {
"description": "Successful call with status 200", "description": "Successful call with status 200",
"content": { "content": {
"": {} "": {
} "x-sample-entry": 18
}
},
"x-sample-entry": 18
} }
}, },
"x-counters-per-source": { "x-counters-per-source": {
@@ -408,7 +438,7 @@
"sumDuration": 9.53e-7 "sumDuration": 9.53e-7
}, },
"x-last-seen-ts": 1567750582.00, "x-last-seen-ts": 1567750582.00,
"x-sample-entry": 0 "x-sample-entry": 18
}, },
"parameters": [ "parameters": [
{ {
@@ -436,7 +466,8 @@
"example #4": { "example #4": {
"value": "prefix-gibberish-afterwards" "value": "prefix-gibberish-afterwards"
} }
} },
"x-sample-entry": 19
} }
] ]
}, },
@@ -452,8 +483,11 @@
"200": { "200": {
"description": "Successful call with status 200", "description": "Successful call with status 200",
"content": { "content": {
"": {} "": {
} "x-sample-entry": 15
}
},
"x-sample-entry": 15
} }
}, },
"x-counters-per-source": { "x-counters-per-source": {
@@ -475,7 +509,7 @@
"sumDuration": 0 "sumDuration": 0
}, },
"x-last-seen-ts": 1567750582.00, "x-last-seen-ts": 1567750582.00,
"x-sample-entry": 0 "x-sample-entry": 15
}, },
"parameters": [ "parameters": [
{ {
@@ -503,7 +537,8 @@
"example #4": { "example #4": {
"value": "prefix-gibberish-afterwards" "value": "prefix-gibberish-afterwards"
} }
} },
"x-sample-entry": 19
} }
] ]
}, },
@@ -519,8 +554,11 @@
"200": { "200": {
"description": "Successful call with status 200", "description": "Successful call with status 200",
"content": { "content": {
"": {} "": {
} "x-sample-entry": 16
}
},
"x-sample-entry": 16
} }
}, },
"x-counters-per-source": { "x-counters-per-source": {
@@ -542,7 +580,7 @@
"sumDuration": 0 "sumDuration": 0
}, },
"x-last-seen-ts": 1567750582.00, "x-last-seen-ts": 1567750582.00,
"x-sample-entry": 0 "x-sample-entry": 16
}, },
"parameters": [ "parameters": [
{ {
@@ -570,7 +608,8 @@
"example #4": { "example #4": {
"value": "prefix-gibberish-afterwards" "value": "prefix-gibberish-afterwards"
} }
} },
"x-sample-entry": 19
} }
] ]
}, },
@@ -586,8 +625,11 @@
"200": { "200": {
"description": "Successful call with status 200", "description": "Successful call with status 200",
"content": { "content": {
"": {} "": {
} "x-sample-entry": 19
}
},
"x-sample-entry": 19
} }
}, },
"x-counters-per-source": { "x-counters-per-source": {
@@ -609,7 +651,7 @@
"sumDuration": 0 "sumDuration": 0
}, },
"x-last-seen-ts": 1567750582.00, "x-last-seen-ts": 1567750582.00,
"x-sample-entry": 0 "x-sample-entry": 19
}, },
"parameters": [ "parameters": [
{ {
@@ -624,7 +666,8 @@
"example #0": { "example #0": {
"value": "23421" "value": "23421"
} }
} },
"x-sample-entry": 19
}, },
{ {
"name": "parampatternId", "name": "parampatternId",
@@ -651,7 +694,8 @@
"example #4": { "example #4": {
"value": "prefix-gibberish-afterwards" "value": "prefix-gibberish-afterwards"
} }
} },
"x-sample-entry": 19
} }
] ]
}, },
@@ -665,9 +709,11 @@
"description": "Successful call with status 200", "description": "Successful call with status 200",
"content": { "content": {
"application/json": { "application/json": {
"example": null "example": null,
"x-sample-entry": 3
} }
} },
"x-sample-entry": 3
} }
}, },
"x-counters-per-source": { "x-counters-per-source": {
@@ -689,7 +735,7 @@
"sumDuration": 0 "sumDuration": 0
}, },
"x-last-seen-ts": 1567750579.74, "x-last-seen-ts": 1567750579.74,
"x-sample-entry": 0 "x-sample-entry": 3
}, },
"parameters": [ "parameters": [
{ {
@@ -707,7 +753,8 @@
"example #1": { "example #1": {
"value": "<UUID4>" "value": "<UUID4>"
} }
} },
"x-sample-entry": 3
} }
] ]
}, },
@@ -720,8 +767,11 @@
"200": { "200": {
"description": "Successful call with status 200", "description": "Successful call with status 200",
"content": { "content": {
"text/html": {} "text/html": {
} "x-sample-entry": 1
}
},
"x-sample-entry": 1
} }
}, },
"x-counters-per-source": { "x-counters-per-source": {
@@ -743,7 +793,7 @@
"sumDuration": 0 "sumDuration": 0
}, },
"x-last-seen-ts": 1567750483.86, "x-last-seen-ts": 1567750483.86,
"x-sample-entry": 0 "x-sample-entry": 1
}, },
"parameters": [ "parameters": [
{ {
@@ -761,7 +811,8 @@
"example #1": { "example #1": {
"value": "<UUID4>" "value": "<UUID4>"
} }
} },
"x-sample-entry": 3
} }
] ]
}, },
@@ -775,9 +826,11 @@
"description": "Successful call with status 200", "description": "Successful call with status 200",
"content": { "content": {
"application/json": { "application/json": {
"example": null "example": null,
"x-sample-entry": 2
} }
} },
"x-sample-entry": 2
} }
}, },
"x-counters-per-source": { "x-counters-per-source": {
@@ -799,7 +852,7 @@
"sumDuration": 0 "sumDuration": 0
}, },
"x-last-seen-ts": 1567750578.74, "x-last-seen-ts": 1567750578.74,
"x-sample-entry": 0 "x-sample-entry": 2
}, },
"parameters": [ "parameters": [
{ {
@@ -817,7 +870,8 @@
"example #1": { "example #1": {
"value": "<UUID4>" "value": "<UUID4>"
} }
} },
"x-sample-entry": 3
} }
] ]
} }

View File

@@ -20,7 +20,7 @@ type Node struct {
children []*Node children []*Node
} }
func (n *Node) getOrSet(path NodePath, existingPathObj *openapi.PathObj) (node *Node) { func (n *Node) getOrSet(path NodePath, existingPathObj *openapi.PathObj, sampleId uint) (node *Node) {
if existingPathObj == nil { if existingPathObj == nil {
panic("Invalid function call") panic("Invalid function call")
} }
@@ -70,6 +70,10 @@ func (n *Node) getOrSet(path NodePath, existingPathObj *openapi.PathObj) (node *
} }
} }
if node.pathParam != nil {
setSampleID(&node.pathParam.Extensions, sampleId)
}
// add example if it's a gibberish chunk // add example if it's a gibberish chunk
if node.pathParam != nil && !chunkIsParam { if node.pathParam != nil && !chunkIsParam {
exmp := &node.pathParam.Examples exmp := &node.pathParam.Examples
@@ -85,7 +89,7 @@ func (n *Node) getOrSet(path NodePath, existingPathObj *openapi.PathObj) (node *
// TODO: eat up trailing slash, in a smart way: node.pathObj!=nil && path[1]=="" // TODO: eat up trailing slash, in a smart way: node.pathObj!=nil && path[1]==""
if len(path) > 1 { if len(path) > 1 {
return node.getOrSet(path[1:], existingPathObj) return node.getOrSet(path[1:], existingPathObj, sampleId)
} else if node.pathObj == nil { } else if node.pathObj == nil {
node.pathObj = existingPathObj node.pathObj = existingPathObj
} }

View File

@@ -20,10 +20,10 @@ func TestTree(t *testing.T) {
} }
tree := new(Node) tree := new(Node)
for _, tc := range testCases { for i, tc := range testCases {
split := strings.Split(tc.inp, "/") split := strings.Split(tc.inp, "/")
pathObj := new(openapi.PathObj) pathObj := new(openapi.PathObj)
node := tree.getOrSet(split, pathObj) node := tree.getOrSet(split, pathObj, uint(i))
fillPathParams(node, pathObj) fillPathParams(node, pathObj)

View File

@@ -115,7 +115,7 @@ type nvParams struct {
GeneralizeName func(name string) string GeneralizeName func(name string) string
} }
func handleNameVals(gw nvParams, params **openapi.ParameterList, checkIgnore bool) { func handleNameVals(gw nvParams, params **openapi.ParameterList, checkIgnore bool, sampleId uint) {
visited := map[string]*openapi.ParameterObj{} visited := map[string]*openapi.ParameterObj{}
for _, pair := range gw.Pairs { for _, pair := range gw.Pairs {
if (checkIgnore && gw.IsIgnored(pair.Name)) || pair.Name == "" { if (checkIgnore && gw.IsIgnored(pair.Name)) || pair.Name == "" {
@@ -137,6 +137,8 @@ func handleNameVals(gw nvParams, params **openapi.ParameterList, checkIgnore boo
logger.Log.Warningf("Failed to add example to a parameter: %s", err) logger.Log.Warningf("Failed to add example to a parameter: %s", err)
} }
visited[nameGeneral] = param visited[nameGeneral] = param
setSampleID(&param.Extensions, sampleId)
} }
// maintain "required" flag // maintain "required" flag
@@ -474,3 +476,15 @@ func intersectSliceWithMap(required []string, names map[string]struct{}) []strin
} }
return required return required
} }
func setSampleID(extensions *openapi.Extensions, id uint) {
if id > 0 {
if *extensions == nil {
*extensions = openapi.Extensions{}
}
err := (extensions).SetExtension(SampleId, id)
if err != nil {
logger.Log.Warningf("Failed to set sample ID: %s", err)
}
}
}

View File

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

View File

@@ -227,10 +227,11 @@ func (s *defaultServiceMap) GetNodes() []ServiceMapNode {
var nodes []ServiceMapNode var nodes []ServiceMapNode
for i, n := range s.graph.Nodes { for i, n := range s.graph.Nodes {
nodes = append(nodes, ServiceMapNode{ nodes = append(nodes, ServiceMapNode{
Id: n.id, Id: n.id,
Name: string(i), Name: string(i),
Entry: n.entry, Resolved: n.entry.Name != UnresolvedNodeName,
Count: n.count, Entry: n.entry,
Count: n.count,
}) })
} }
return nodes return nodes
@@ -243,16 +244,18 @@ func (s *defaultServiceMap) GetEdges() []ServiceMapEdge {
for _, p := range s.graph.Edges[u][v].data { for _, p := range s.graph.Edges[u][v].data {
edges = append(edges, ServiceMapEdge{ edges = append(edges, ServiceMapEdge{
Source: ServiceMapNode{ Source: ServiceMapNode{
Id: s.graph.Nodes[u].id, Id: s.graph.Nodes[u].id,
Name: string(u), Name: string(u),
Entry: s.graph.Nodes[u].entry, Entry: s.graph.Nodes[u].entry,
Count: s.graph.Nodes[u].count, Resolved: s.graph.Nodes[u].entry.Name != UnresolvedNodeName,
Count: s.graph.Nodes[u].count,
}, },
Destination: ServiceMapNode{ Destination: ServiceMapNode{
Id: s.graph.Nodes[v].id, Id: s.graph.Nodes[v].id,
Name: string(v), Name: string(v),
Entry: s.graph.Nodes[v].entry, Entry: s.graph.Nodes[v].entry,
Count: s.graph.Nodes[v].count, Resolved: s.graph.Nodes[v].entry.Name != UnresolvedNodeName,
Count: s.graph.Nodes[v].count,
}, },
Count: p.count, Count: p.count,
Protocol: p.protocol, Protocol: p.protocol,

View File

@@ -53,7 +53,16 @@ func representMapSliceAsTable(mapSlice []interface{}, selectorPrefix string) (re
h := item.(map[string]interface{}) h := item.(map[string]interface{})
key := h["name"].(string) key := h["name"].(string)
value := h["value"] 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: case reflect.Slice:
fallthrough fallthrough
case reflect.Array: case reflect.Array:

View File

@@ -8,6 +8,7 @@ require (
github.com/segmentio/kafka-go v0.4.27 github.com/segmentio/kafka-go v0.4.27
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.6.1
github.com/up9inc/mizu/tap/api v0.0.0 github.com/up9inc/mizu/tap/api v0.0.0
golang.org/x/text v0.3.0
) )
require ( 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/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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/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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

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

View File

@@ -112,7 +112,7 @@ func UpdateTapTargets(newTapTargets []v1.Pod) {
tapTargets = newTapTargets tapTargets = newTapTargets
packetSourceManager.UpdatePods(tapTargets) packetSourceManager.UpdatePods(tapTargets, !*nodefrag, mainPacketInputChan)
if tlsTapperInstance != nil { if tlsTapperInstance != nil {
if err := tlstapper.UpdateTapTargets(tlsTapperInstance, &tapTargets, *procfs); err != nil { if err := tlstapper.UpdateTapTargets(tlsTapperInstance, &tapTargets, *procfs); err != nil {
@@ -198,12 +198,8 @@ func initializePacketSources() error {
} }
var err error var err error
if packetSourceManager, err = source.NewPacketSourceManager(*procfs, *fname, *iface, *servicemesh, tapTargets, behaviour); err != nil { packetSourceManager, err = source.NewPacketSourceManager(*procfs, *fname, *iface, *servicemesh, tapTargets, behaviour, !*nodefrag, mainPacketInputChan)
return err return err
} else {
packetSourceManager.ReadPackets(!*nodefrag, mainPacketInputChan)
return nil
}
} }
func initializePassiveTapper(opts *TapOpts, outputItems chan *api.OutputChannelItem) (*tcpStreamMap, *tcpAssembler) { func initializePassiveTapper(opts *TapOpts, outputItems chan *api.OutputChannelItem) (*tcpStreamMap, *tcpAssembler) {

View File

@@ -24,7 +24,7 @@ type PacketSourceManager struct {
} }
func NewPacketSourceManager(procfs string, filename string, interfaceName string, func NewPacketSourceManager(procfs string, filename string, interfaceName string,
mtls bool, pods []v1.Pod, behaviour TcpPacketSourceBehaviour) (*PacketSourceManager, error) { mtls bool, pods []v1.Pod, behaviour TcpPacketSourceBehaviour, ipdefrag bool, packets chan<- TcpPacketInfo) (*PacketSourceManager, error) {
hostSource, err := newHostPacketSource(filename, interfaceName, behaviour) hostSource, err := newHostPacketSource(filename, interfaceName, behaviour)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -43,7 +43,7 @@ func NewPacketSourceManager(procfs string, filename string, interfaceName string
behaviour: behaviour, behaviour: behaviour,
} }
sourceManager.UpdatePods(pods) go hostSource.readPackets(ipdefrag, packets)
return sourceManager, nil return sourceManager, nil
} }
@@ -64,16 +64,16 @@ func newHostPacketSource(filename string, interfaceName string,
return source, nil return source, nil
} }
func (m *PacketSourceManager) UpdatePods(pods []v1.Pod) { func (m *PacketSourceManager) UpdatePods(pods []v1.Pod, ipdefrag bool, packets chan<- TcpPacketInfo) {
if m.config.mtls { if m.config.mtls {
m.updateMtlsPods(m.config.procfs, pods, m.config.interfaceName, m.config.behaviour) m.updateMtlsPods(m.config.procfs, pods, m.config.interfaceName, m.config.behaviour, ipdefrag, packets)
} }
m.setBPFFilter(pods) m.setBPFFilter(pods)
} }
func (m *PacketSourceManager) updateMtlsPods(procfs string, pods []v1.Pod, func (m *PacketSourceManager) updateMtlsPods(procfs string, pods []v1.Pod,
interfaceName string, behaviour TcpPacketSourceBehaviour) { interfaceName string, behaviour TcpPacketSourceBehaviour, ipdefrag bool, packets chan<- TcpPacketInfo) {
relevantPids := m.getRelevantPids(procfs, pods) relevantPids := m.getRelevantPids(procfs, pods)
logger.Log.Infof("Updating mtls pods (new: %v) (current: %v)", relevantPids, m.sources) logger.Log.Infof("Updating mtls pods (new: %v) (current: %v)", relevantPids, m.sources)
@@ -90,6 +90,7 @@ func (m *PacketSourceManager) updateMtlsPods(procfs string, pods []v1.Pod,
source, err := newNetnsPacketSource(procfs, pid, interfaceName, behaviour) source, err := newNetnsPacketSource(procfs, pid, interfaceName, behaviour)
if err == nil { if err == nil {
go source.readPackets(ipdefrag, packets)
m.sources[pid] = source m.sources[pid] = source
} }
} }
@@ -153,12 +154,6 @@ func (m *PacketSourceManager) setBPFFilter(pods []v1.Pod) {
} }
} }
func (m *PacketSourceManager) ReadPackets(ipdefrag bool, packets chan<- TcpPacketInfo) {
for _, src := range m.sources {
go src.readPackets(ipdefrag, packets)
}
}
func (m *PacketSourceManager) Close() { func (m *PacketSourceManager) Close() {
for _, src := range m.sources { for _, src := range m.sources {
src.close() src.close()

View File

@@ -1,12 +1,12 @@
{ {
"name": "@up9/mizu-common", "name": "@up9/mizu-common",
"version": "1.0.135", "version": "0.0.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@up9/mizu-common", "name": "@up9/mizu-common",
"version": "1.0.135", "version": "0.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@craco/craco": "^6.4.3", "@craco/craco": "^6.4.3",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@up9/mizu-common", "name": "@up9/mizu-common",
"version": "1.0.145", "version": "0.0.0",
"description": "Made with create-react-library", "description": "Made with create-react-library",
"author": "", "author": "",
"license": "MIT", "license": "MIT",
@@ -49,6 +49,7 @@
"node-fetch": "^3.1.1", "node-fetch": "^3.1.1",
"numeral": "^2.0.6", "numeral": "^2.0.6",
"protobuf-decoder": "^0.1.0", "protobuf-decoder": "^0.1.0",
"react-resizable": "^3.0.4",
"react-graph-vis": "^1.0.7", "react-graph-vis": "^1.0.7",
"react-lowlight": "^3.0.0", "react-lowlight": "^3.0.0",
"react-router-dom": "^6.2.1", "react-router-dom": "^6.2.1",
@@ -90,4 +91,4 @@
"files": [ "files": [
"dist" "dist"
] ]
} }

View File

@@ -42,10 +42,10 @@ const OasModal = ({ openModal, handleCloseModal, getOasServices, getOasByService
try { try {
const data = await getOasByService(selectedService ? selectedService : oasServices[0]); const data = await getOasByService(selectedService ? selectedService : oasServices[0]);
setSelectedServiceSpec(data); setSelectedServiceSpec(data);
} catch (e) { } catch (e) {
toast.error("Error occurred while fetching service OAS spec"); toast.error("Error occurred while fetching service OAS spec");
console.error(e); console.error(e);
} }
}; };
useEffect(() => { useEffect(() => {
@@ -61,7 +61,7 @@ const OasModal = ({ openModal, handleCloseModal, getOasServices, getOasByService
useEffect(() => { useEffect(() => {
onSelectedOASService(null); onSelectedOASService(null);
},[oasServices]) }, [oasServices])
return ( return (
<Modal <Modal
@@ -80,28 +80,28 @@ const OasModal = ({ openModal, handleCloseModal, getOasServices, getOasByService
<div className={style.boxContainer}> <div className={style.boxContainer}>
<div className={style.selectHeader}> <div className={style.selectHeader}>
<div><img src={openApiLogo} alt="openAPI" className={style.openApilogo} /></div> <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>
<div style={{ cursor: "pointer" }}> <div style={{ cursor: "pointer" }}>
<img src={closeIcon} alt="close" onClick={handleCloseModal} /> <img src={closeIcon} alt="close" onClick={handleCloseModal} />
</div> </div>
</div> </div>
<div className={style.selectContainer} > <div className={style.selectContainer} >
<FormControl> <FormControl>
<Select <Select
labelId="service-select-label" labelId="service-select-label"
id="service-select" id="service-select"
value={selectedServiceName} value={selectedServiceName}
onChangeCb={onSelectedOASService} onChangeCb={onSelectedOASService}
> >
{oasServices.map((service) => ( {oasServices.map((service) => (
<MenuItem key={service} value={service}> <MenuItem key={service} value={service}>
{service} {service}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
</FormControl> </FormControl>
</div> </div>
<div className={style.borderLine}></div> <div className={style.borderLine}></div>
<div className={style.redoc}> <div className={style.redoc}>
{selectedServiceSpec && <RedocStandalone {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 {EntryItem} from "./EntryListItem/EntryListItem";
import down from "assets/downImg.svg"; import down from "assets/downImg.svg";
import spinner from 'assets/spinner.svg'; import spinner from 'assets/spinner.svg';
import {RecoilState, useRecoilState, useRecoilValue} from "recoil"; import {RecoilState, useRecoilState, useRecoilValue, useSetRecoilState} from "recoil";
import entriesAtom from "../../recoil/entries"; import entriesAtom from "../../recoil/entries";
import queryAtom from "../../recoil/query"; import queryAtom from "../../recoil/query";
import TrafficViewerApiAtom from "../../recoil/TrafficViewerApi"; import TrafficViewerApiAtom from "../../recoil/TrafficViewerApi";
import TrafficViewerApi from "./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 { interface EntriesListProps {
listEntryREF: any; listEntryREF: any;
onSnapBrokenEvent: () => void; onSnapBrokenEvent: () => void;
isSnappedToBottom: boolean; isSnappedToBottom: boolean;
setIsSnappedToBottom: any; setIsSnappedToBottom: any;
queriedCurrent: number; noMoreDataTop: boolean;
setQueriedCurrent: any; setNoMoreDataTop: (flag: boolean) => void;
queriedTotal: number; openWebSocket: (query: string, resetEntries: boolean) => void;
setQueriedTotal: any; scrollableRef: any;
startTime: number; ws: any;
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;
} }
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 [entries, setEntries] = useRecoilState(entriesAtom);
const query = useRecoilValue(queryAtom); const query = useRecoilValue(queryAtom);
const isWsConnectionClosed = ws?.current?.readyState !== WebSocket.OPEN; 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 [loadMoreTop, setLoadMoreTop] = useState(false);
const [isLoadingTop, setIsLoadingTop] = useState(false); const [isLoadingTop, setIsLoadingTop] = useState(false);
const [queriedTotal, setQueriedTotal] = useState(0);
const [startTime, setStartTime] = useState(0);
const [truncatedTimestamp, setTruncatedTimestamp] = useState(0);
useEffect(() => { const leftOffBottom = entries.length > 0 ? entries[entries.length - 1].id : -1;
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 memoizedEntries = useMemo(() => { useEffect(() => {
return entries; const list = document.getElementById('list').firstElementChild;
},[entries]); list.addEventListener('scroll', (e) => {
const el: any = e.target;
const getOldEntries = useCallback(async () => { if (el.scrollTop === 0) {
setLoadMoreTop(true);
} else {
setNoMoreDataTop(false);
setLoadMoreTop(false); setLoadMoreTop(false);
if (leftOffTop === null || leftOffTop <= 0) { }
return; });
} }, [setLoadMoreTop, setNoMoreDataTop]);
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);
let scrollTo: boolean; const memoizedEntries = useMemo(() => {
if (data.meta.leftOff === 0) { return entries;
setNoMoreDataTop(true); }, [entries]);
scrollTo = false;
} else {
scrollTo = true;
}
setIsLoadingTop(false);
const newEntries = [...data.data.reverse(), ...entries]; const getOldEntries = useCallback(async () => {
setEntries(newEntries); 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); let scrollTo: boolean;
setQueriedTotal(data.meta.total); if (data.meta.leftOff === 0) {
setTruncatedTimestamp(data.meta.truncatedTimestamp); setNoMoreDataTop(true);
scrollTo = false;
} else {
scrollTo = true;
}
setIsLoadingTop(false);
if (scrollTo) { const newEntries = [...data.data.reverse(), ...entries];
scrollableRef.current.scrollToIndex(data.data.length - 1); setEntries(newEntries);
}
},[setLoadMoreTop, setIsLoadingTop, entries, setEntries, query, setNoMoreDataTop, leftOffTop, setLeftOffTop, queriedCurrent, setQueriedCurrent, setQueriedTotal, setTruncatedTimestamp, scrollableRef]);
useEffect(() => { setQueriedTotal(data.meta.total);
if(!isWsConnectionClosed || !loadMoreTop || noMoreDataTop) return; setTruncatedTimestamp(data.meta.truncatedTimestamp);
getOldEntries();
}, [loadMoreTop, noMoreDataTop, getOldEntries, isWsConnectionClosed]);
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> useEffect(() => {
<div className={styles.list}> if (!isWsConnectionClosed || !loadMoreTop || noMoreDataTop) return;
<div id="list" ref={listEntryREF} className={styles.list}> getOldEntries();
{isLoadingTop && <div className={styles.spinnerContainer}> }, [loadMoreTop, noMoreDataTop, getOldEntries, isWsConnectionClosed]);
<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}> const scrollbarVisible = scrollableRef.current?.childWrapperRef.current.clientHeight > scrollableRef.current?.wrapperRef.current.clientHeight;
<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> if (ws.current) {
</React.Fragment>; 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 Protocol from "../UI/Protocol"
import Queryable from "../UI/Queryable"; import Queryable from "../UI/Queryable";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { RecoilState, useRecoilState, useRecoilValue } from "recoil"; import { RecoilState, useRecoilValue } from "recoil";
import focusedEntryIdAtom from "../../recoil/focusedEntryId"; import focusedEntryIdAtom from "../../recoil/focusedEntryId";
import trafficViewerApi from "../../recoil/TrafficViewerApi";
import TrafficViewerApi from "./TrafficViewerApi"; import TrafficViewerApi from "./TrafficViewerApi";
import TrafficViewerApiAtom from "../../recoil/TrafficViewerApi/atom"; import TrafficViewerApiAtom from "../../recoil/TrafficViewerApi/atom";
import queryAtom from "../../recoil/query/atom"; import queryAtom from "../../recoil/query/atom";
import useWindowDimensions, { useRequestTextByWidth } from "../../hooks/WindowDimensionsHook"; import useWindowDimensions, { useRequestTextByWidth } from "../../hooks/WindowDimensionsHook";
import { TOAST_CONTAINER_ID } from "../../configs/Consts"; import { TOAST_CONTAINER_ID } from "../../configs/Consts";
import spinner from "assets/spinner.svg";
const useStyles = makeStyles(() => ({ const useStyles = makeStyles(() => ({
entryTitle: { entryTitle: {
@@ -105,12 +105,13 @@ export const EntryDetailed = () => {
const focusedEntryId = useRecoilValue(focusedEntryIdAtom); const focusedEntryId = useRecoilValue(focusedEntryIdAtom);
const trafficViewerApi = useRecoilValue(TrafficViewerApiAtom as RecoilState<TrafficViewerApi>) const trafficViewerApi = useRecoilValue(TrafficViewerApiAtom as RecoilState<TrafficViewerApi>)
const query = useRecoilValue(queryAtom); const query = useRecoilValue(queryAtom);
const [isLoading, setIsLoading] = useState(false);
const [entryData, setEntryData] = useState(null); const [entryData, setEntryData] = useState(null);
useEffect(() => { useEffect(() => {
if (!focusedEntryId) return; if (!focusedEntryId) return;
setEntryData(null); setEntryData(null);
setIsLoading(true);
(async () => { (async () => {
try { try {
const entryData = await trafficViewerApi.getEntry(focusedEntryId, query); const entryData = await trafficViewerApi.getEntry(focusedEntryId, query);
@@ -125,20 +126,23 @@ export const EntryDetailed = () => {
}); });
} }
console.error(error); console.error(error);
} finally {
setIsLoading(false);
} }
})(); })();
// eslint-disable-next-line // eslint-disable-next-line
}, [focusedEntryId]); }, [focusedEntryId]);
return <React.Fragment> 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} protocol={entryData.protocol}
data={entryData.data} data={entryData.data}
elapsedTime={entryData.data.elapsedTime} elapsedTime={entryData.data.elapsedTime}
/>} />}
{entryData && <EntrySummary entry={entryData.base} />} {!isLoading && entryData && <EntrySummary entry={entryData.base} />}
<React.Fragment> <React.Fragment>
{entryData && <EntryViewer {!isLoading && entryData && <EntryViewer
representation={entryData.representation} representation={entryData.representation}
isRulesEnabled={entryData.isRulesEnabled} isRulesEnabled={entryData.isRulesEnabled}
rulesMatched={entryData.rulesMatched} rulesMatched={entryData.rulesMatched}

View File

@@ -66,8 +66,10 @@
margin-top: -60px margin-top: -60px
.capture img .capture img
height: 20px height: 14px
z-index: 1000 z-index: 1000
margin-top: 12px
margin-left: -2px
.endpointServiceContainer .endpointServiceContainer
display: flex display: flex
@@ -76,6 +78,7 @@
padding-right: 10px padding-right: 10px
padding-top: 4px padding-top: 4px
flex-grow: 1 flex-grow: 1
padding-left: 10px
.separatorRight .separatorRight
display: flex display: flex

View File

@@ -140,8 +140,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, style, headingMode}) =>
const isStatusCodeEnabled = ((entry.proto.name === "http" && "status" in entry) || entry.status !== 0); const isStatusCodeEnabled = ((entry.proto.name === "http" && "status" in entry) || entry.status !== 0);
let endpointServiceContainer = "10px";
if (!isStatusCodeEnabled) endpointServiceContainer = "20px";
return <React.Fragment> return <React.Fragment>
<div <div
@@ -178,7 +176,7 @@ export const EntryItem: React.FC<EntryProps> = ({entry, style, headingMode}) =>
{isStatusCodeEnabled && <div> {isStatusCodeEnabled && <div>
<StatusCode statusCode={entry.status} statusQuery={entry.statusQuery}/> <StatusCode statusCode={entry.status} statusQuery={entry.statusQuery}/>
</div>} </div>}
<div className={styles.endpointServiceContainer} style={{paddingLeft: endpointServiceContainer}}> <div className={styles.endpointServiceContainer}>
<Summary method={entry.method} methodQuery={entry.methodQuery} summary={entry.summary} summaryQuery={entry.summaryQuery}/> <Summary method={entry.method} methodQuery={entry.methodQuery} summary={entry.summary} summaryQuery={entry.summaryQuery}/>
<div className={styles.resolvedName}> <div className={styles.resolvedName}>
<Queryable <Queryable

View File

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

View File

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

View File

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

@@ -54,7 +54,7 @@ const Protocol: React.FC<ProtocolProps> = ({protocol, horizontal}) => {
backgroundColor: protocol.backgroundColor, backgroundColor: protocol.backgroundColor,
color: protocol.foregroundColor, color: protocol.foregroundColor,
fontSize: protocol.fontSize, fontSize: protocol.fontSize,
marginRight: "-20px", marginRight: "-6px",
}} }}
title={protocol.longName} title={protocol.longName}
> >

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; isDemoBannerView: boolean;
} }
export const StatusBar = ({isDemoBannerView}) => { export const StatusBar: React.FC<StatusBarProps> = ({isDemoBannerView}) => {
const tappingStatus = useRecoilValue(tappingStatusAtom); const tappingStatus = useRecoilValue(tappingStatusAtom);
const [expandedBar, setExpandedBar] = useState(false); const [expandedBar, setExpandedBar] = useState(false);
const {uniqueNamespaces, amountOfPods, amountOfTappedPods, amountOfUntappedPods} = useRecoilValue(tappingStatusDetails); 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"> return <div className={`${isDemoBannerView ? `${style.banner}` : ''} ${style.statusBar} ${(expandedBar ? `${style.expandedStatusBar}` : "")}`} onMouseOver={() => setExpandedBar(true)} onMouseLeave={() => setExpandedBar(false)} data-cy="expandedStatusBar">
<div className={style.podsCount}> <div className={style.podsCount}>
{tappingStatus.some(pod => !pod.isTapped) && <img src={warningIcon} alt="warning"/>} {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}> {tappingStatus.map(pod => <tr key={pod.name}>
<td style={{width: "40%"}}>{pod.name}</td> <td style={{width: "40%"}}>{pod.name}</td>
<td style={{width: "40%"}}>{pod.namespace}</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>)} </tr>)}
</tbody> </tbody>
</table> </table>

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 { StatusBar } from "./StatusBar";
import CustomModal from "./CustomModal"; import CustomModal from "./CustomModal";
import { InformationIcon } from "./InformationIcon"; import { InformationIcon } from "./InformationIcon";
import SelectList from "./SelectList";
import NoDataMessage from "./NoDataMessage";
export {LoadingOverlay,Select,Tabs,Tooltip,Checkbox,CustomModal,InformationIcon} export { LoadingOverlay, Select, Tabs, Tooltip, Checkbox, CustomModal, InformationIcon, SelectList, NoDataMessage }
export {StatusBar} export { StatusBar }

View File

@@ -1,2 +1,8 @@
.flex .linkStyle
display: flex 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 .spinnerContainer
display: flex display: flex
justify-content: center justify-content: center
margin-bottom: 10px margin-bottom: 10px

View File

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

View File

@@ -9,7 +9,7 @@
text-align: center text-align: center
line-height: 22px line-height: 22px
font-weight: 600 font-weight: 600
margin-left: 8px margin-left: 3px
.neutral .neutral
background: gray background: gray

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 TrafficViewer from './components/TrafficViewer/TrafficViewer';
import * as UI from "./components/UI" import * as UI from "./components/UI"
import { StatusBar } from './components/UI'; import { StatusBar } from './components/UI';
import useWS,{DEFAULT_QUERY} from './hooks/useWS'; import useWS, { DEFAULT_QUERY } from './hooks/useWS';
import {AnalyzeButton} from "./components/AnalyzeButton/AnalyzeButton" import { AnalyzeButton } from "./components/AnalyzeButton/AnalyzeButton"
import OasModal from './components/OasModal/OasModal'; import OasModal from './components/OasModal/OasModal';
import { ServiceMapModal } from './components/ServiceMapModal/ServiceMapModal';
export {UI,AnalyzeButton, StatusBar, OasModal} export { UI, AnalyzeButton, StatusBar, OasModal, ServiceMapModal }
export { useWS, DEFAULT_QUERY} export { useWS, DEFAULT_QUERY }
export default TrafficViewer; 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"; import atom from "./atom";
export default atom;
export default atom

View File

@@ -13,7 +13,7 @@
"@types/jest": "^26.0.22", "@types/jest": "^26.0.22",
"@types/node": "^12.20.10", "@types/node": "^12.20.10",
"@uiw/react-textarea-code-editor": "^1.4.12", "@uiw/react-textarea-code-editor": "^1.4.12",
"@up9/mizu-common": "1.0.145", "@up9/mizu-common": "file:up9-mizu-common-0.0.0.tgz",
"axios": "^0.25.0", "axios": "^0.25.0",
"core-js": "^3.20.2", "core-js": "^3.20.2",
"craco-babel-loader": "^1.0.3", "craco-babel-loader": "^1.0.3",
@@ -48,7 +48,8 @@
"npm-link-shared": "^0.5.6", "npm-link-shared": "^0.5.6",
"react-app-rewire-alias": "^1.1.7", "react-app-rewire-alias": "^1.1.7",
"react-dev-utils": "^12.0.0", "react-dev-utils": "^12.0.0",
"recoil": "^0.5.2" "recoil": "^0.5.2",
"react-error-overlay": "6.0.9"
}, },
"scripts": { "scripts": {
"start": "craco start", "start": "craco start",

View File

@@ -1,12 +1,12 @@
import { useState} from 'react'; import { useState } from 'react';
import './App.sass'; import './App.sass';
import {Header} from "./components/Header/Header"; import { Header } from "./components/Header/Header";
import {TrafficPage} from "./components/Pages/TrafficPage/TrafficPage"; import { TrafficPage } from "./components/Pages/TrafficPage/TrafficPage";
import { ServiceMapModal } from './components/ServiceMapModal/ServiceMapModal'; import { ServiceMapModal } from '@up9/mizu-common';
import {useRecoilState} from "recoil"; import { useRecoilState } from "recoil";
import serviceMapModalOpenAtom from "./recoil/serviceMapModalOpen"; import serviceMapModalOpenAtom from "./recoil/serviceMapModalOpen";
import oasModalOpenAtom from './recoil/oasModalOpen/atom'; import oasModalOpenAtom from './recoil/oasModalOpen/atom';
import {OasModal} from '@up9/mizu-common'; import { OasModal } from '@up9/mizu-common';
import Api from './helpers/api'; import Api from './helpers/api';
const api = Api.getInstance() const api = Api.getInstance()
@@ -19,20 +19,20 @@ const App = () => {
return ( return (
<div className="mizuApp"> <div className="mizuApp">
<Header analyzeStatus={analyzeStatus} /> <Header analyzeStatus={analyzeStatus} />
<TrafficPage setAnalyzeStatus={setAnalyzeStatus}/> <TrafficPage setAnalyzeStatus={setAnalyzeStatus} />
{window["isServiceMapEnabled"] && <ServiceMapModal {window["isServiceMapEnabled"] && <ServiceMapModal
isOpen={serviceMapModalOpen} isOpen={serviceMapModalOpen}
onOpen={() => setServiceMapModalOpen(true)} onOpen={() => setServiceMapModalOpen(true)}
onClose={() => setServiceMapModalOpen(false)} onClose={() => setServiceMapModalOpen(false)}
getServiceMapDataApi={api.serviceMapData} />}
{window["isOasEnabled"] && <OasModal
getOasServices={api.getOasServices}
getOasByService={api.getOasByService}
openModal={oasModalOpen}
handleCloseModal={() => setOasModalOpen(false)}
/>} />}
{window["isOasEnabled"] && <OasModal </div>
getOasServices={api.getOasServices}
getOasByService={api.getOasByService}
openModal={oasModalOpen}
handleCloseModal={() => setOasModalOpen(false)}
/>}
</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 { Button } from "@material-ui/core";
import Api, { MizuWebsocketURL } from "../../../helpers/api"; import Api, { MizuWebsocketURL } from "../../../helpers/api";
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import {useSetRecoilState, useRecoilState} from "recoil"; import { useRecoilState } from "recoil";
import {useCommonStyles} from "../../../helpers/commonStyle" import { useCommonStyles } from "../../../helpers/commonStyle"
import serviceMapModalOpenAtom from "../../../recoil/serviceMapModalOpen"; import serviceMapModalOpenAtom from "../../../recoil/serviceMapModalOpen";
import TrafficViewer from "@up9/mizu-common" import TrafficViewer from "@up9/mizu-common"
import "@up9/mizu-common/dist/index.css" import "@up9/mizu-common/dist/index.css"
@@ -17,13 +17,13 @@ interface TrafficPageProps {
const api = Api.getInstance(); const api = Api.getInstance();
export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus}) => { export const TrafficPage: React.FC<TrafficPageProps> = ({ setAnalyzeStatus }) => {
const commonClasses = useCommonStyles(); const commonClasses = useCommonStyles();
const setServiceMapModalOpen = useSetRecoilState(serviceMapModalOpenAtom); const [serviceMapModalOpen, setServiceMapModalOpen] = useRecoilState(serviceMapModalOpenAtom);
const [openOasModal, setOpenOasModal] = useRecoilState(oasModalOpenAtom); const [openOasModal, setOpenOasModal] = useRecoilState(oasModalOpenAtom);
const [openWebSocket, setOpenWebSocket] = useState(true); const [openWebSocket, setOpenWebSocket] = useState(true);
const trafficViewerApi = {...api} const trafficViewerApi = { ...api }
const handleOpenOasModal = () => { const handleOpenOasModal = () => {
setOpenWebSocket(false) setOpenWebSocket(false)
@@ -36,37 +36,31 @@ const trafficViewerApi = {...api}
}, 500); }, 500);
const actionButtons = (window["isOasEnabled"] || window["isServiceMapEnabled"]) && const actionButtons = (window["isOasEnabled"] || window["isServiceMapEnabled"]) &&
<div style={{ display: 'flex', height: "100%" }}> <div style={{ display: 'flex', height: "100%" }}>
{window["isOasEnabled"] && <Button {window["isOasEnabled"] && <Button
startIcon={<img className="custom" src={services} alt="services"></img>} startIcon={<img className="custom" src={services} alt="services"></img>}
size="large" size="large"
variant="contained" variant="contained"
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton} className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
style={{ marginRight: 25, textTransform: 'unset' }} style={{ marginRight: 25, textTransform: 'unset' }}
onClick={handleOpenOasModal}> onClick={handleOpenOasModal}>
OpenAPI Specs Service Catalog
</Button>} </Button>}
{window["isServiceMapEnabled"] && <Button {window["isServiceMapEnabled"] && <Button
startIcon={<img src={serviceMap} className="custom" alt="service-map" style={{marginRight:"8%"}}></img>} startIcon={<img src={serviceMap} className="custom" alt="service-map" style={{ marginRight: "8%" }}></img>}
size="large" size="large"
variant="contained" variant="contained"
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton} className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
onClick={openServiceMapModalDebounce} onClick={openServiceMapModalDebounce}
style={{textTransform: 'unset'}}> style={{ textTransform: 'unset' }}>
Service Map Service Map
</Button>} </Button>}
</div> </div>
useEffect(() => {
return () => {
//closeSocket()
}
},[])
return ( return (
<> <>
<TrafficViewer setAnalyzeStatus={setAnalyzeStatus} webSocketUrl={MizuWebsocketURL} isCloseWebSocket={!openWebSocket} <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,214 +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 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(() => {
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%" }}></img>}
size="medium"
variant="contained"
className={commonClasses.outlinedButton + " " + commonClasses.imagedButton}
onClick={refreshServiceMap}
>
Refresh
</Button>
</div>
<img src={close} alt="close" onClick={() => onClose()} style={{ cursor: "pointer" }}></img>
</div>
<Graph
graph={graphData}
options={ServiceMapOptions}
/>
<div className='legend-scale'>
<ul className='legend-labels'>
<li><span style={{ background: '#205cf5' }}></span>HTTP</li>
<li><span style={{ background: '#244c5a' }}></span>HTTP/2</li>
<li><span style={{ background: '#244c5a' }}></span>gRPC</li>
<li><span style={{ background: '#ff6600' }}></span>AMQP</li>
<li><span style={{ background: '#000000' }}></span>KAFKA</li>
<li><span style={{ background: '#a41e11' }}></span>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: 1
},
},
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; source = null;
} }
serviceMapStatus = async () => {
const response = await client.get("/servicemap/status");
return response.data;
}
serviceMapData = async () => { serviceMapData = async () => {
const response = await client.get(`/servicemap/get`); const response = await client.get(`/servicemap/get`);
return response.data; return response.data;
} }
serviceMapReset = async () => {
const response = await client.get(`/servicemap/reset`);
return response.data;
}
tapStatus = async () => { tapStatus = async () => {
const response = await client.get("/status/tap"); const response = await client.get("/status/tap");
return response.data; 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;