Compare commits

...

7 Commits

Author SHA1 Message Date
M. Mert Yıldıran
af557f7052 Add Queryable component to show a green add circle icon for the queryable UI elements (#512)
* Add `Queryable` component to show a green circle and use it in `EntryViewLine`

* Refactor `Queryable` component

* Use the `Queryable` component `EntryDetailed`

* Use the `Queryable` component `Summary`

* Instead of passing the style to `Queryable`, pass the children components directly

* Make `useTooltip = true` by default in `Queryable`

* Refactor a lot of styling to achieve using `Queryable` in `Protocol` component

* Migrate the last queryable elements in `EntryListItem` to `Queryable` component

* Fix some of the styling issues

* Make horizontal `Protocol` `Queryable` too

* Remove unnecessary child constants

* Revert some of the changes in 2a93f365f5

* Fix rest of the styling issues

* Fix one more styling issue

* Update the screenshots and text in the cheatsheet according to the change

* Use `let` not `var`

* Add missing dependencies to the React hook
2021-11-30 17:52:21 +03:00
M. Mert Yıldıran
b745f65971 Handle unexpected socket close and replace the default rlimit(100) filter with leftOff(-1) filter (#508)
* Handle unexpected socket close and replace the default `rlimit(100)` filter with `leftOff(-1)` filter

* Rename `dontClear` parameter to `resetEntriesBuffer` and remove negation
2021-11-30 16:30:18 +03:00
M. Mert Yıldıran
873f252544 Fix the selected entry behavior by propagating the focusedEntryId through WebSocket (before #452) TRA-3983 (#513)
* Revert the select entry behavior into its original state RACING! (before #452) [TRA-3983 alternative 3]

* Remove the remaining `forceSelect`(s)

* Add a missing `focusedEntryId` prop

* Fix the race condition

* Propagate the `focusedEntryId` through WebSocket to prevent racing
2021-11-30 15:27:10 +03:00
M. Mert Yıldıran
9696ad9bad Show the EntryItem as EntrySummary in EntryDetailed (#506) 2021-11-28 10:59:40 +03:00
M. Mert Yıldıran
a1bda0a6c3 Hide Encoding field if it's undefined or empty in the UI (#511) 2021-11-26 09:40:44 +03:00
M. Mert Yıldıran
a62842ac9f Add HTTP2 Over Cleartext (H2C) support (#510)
* Add HTTP2 Over Cleartext (H2C) support

* Remove a parameter which is a remnant of debugging
2021-11-25 20:36:13 +03:00
M. Mert Yıldıran
e667597e6e Rename URL field to Target URI in the UI to prevent confusion (#509) 2021-11-25 20:15:43 +03:00
30 changed files with 606 additions and 295 deletions

View File

@@ -42,8 +42,8 @@ RUN go build -ldflags="-s -w \
-X 'mizuserver/pkg/version.SemVer=${SEM_VER}'" -o mizuagent .
# Download Basenine executable, verify the sha1sum and move it to a directory in $PATH
ADD https://github.com/up9inc/basenine/releases/download/v0.2.11/basenine_linux_amd64 ./basenine_linux_amd64
ADD https://github.com/up9inc/basenine/releases/download/v0.2.11/basenine_linux_amd64.sha256 ./basenine_linux_amd64.sha256
ADD https://github.com/up9inc/basenine/releases/download/v0.2.12/basenine_linux_amd64 ./basenine_linux_amd64
ADD https://github.com/up9inc/basenine/releases/download/v0.2.12/basenine_linux_amd64.sha256 ./basenine_linux_amd64.sha256
RUN shasum -a 256 -c basenine_linux_amd64.sha256
RUN chmod +x ./basenine_linux_amd64

View File

@@ -16,7 +16,7 @@ require (
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/up9inc/basenine/client/go v0.0.0-20211121072216-04366911881c
github.com/up9inc/basenine/client/go v0.0.0-20211125004153-d0e8aec03fea
github.com/up9inc/mizu/shared v0.0.0
github.com/up9inc/mizu/tap v0.0.0
github.com/up9inc/mizu/tap/api v0.0.0

View File

@@ -450,8 +450,8 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/up9inc/basenine/client/go v0.0.0-20211121072216-04366911881c h1:GJsCVhDKjV/k3mNG255VN7hAQ7fxyNgX5T+VJyzoOQ0=
github.com/up9inc/basenine/client/go v0.0.0-20211121072216-04366911881c/go.mod h1:SvJGPoa/6erhUQV7kvHBwM/0x5LyO6XaG2lUaCaKiUI=
github.com/up9inc/basenine/client/go v0.0.0-20211125004153-d0e8aec03fea h1:GpJGO2PNTS/S0j2E1yqBi3ST/VXfokzuOrTQyaYgWnA=
github.com/up9inc/basenine/client/go v0.0.0-20211125004153-d0e8aec03fea/go.mod h1:SvJGPoa/6erhUQV7kvHBwM/0x5LyO6XaG2lUaCaKiUI=
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f h1:p4VB7kIXpOQvVn1ZaTIVp+3vuYAXFe3OJEvjbUYJLaA=
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"mizuserver/pkg/models"
"net/http"
"strconv"
"sync"
"time"
@@ -94,6 +95,8 @@ func websocketHandler(w http.ResponseWriter, r *http.Request, eventHandlers Even
startTimeBytes, _ := models.CreateWebsocketStartTimeMessage(startTime)
SendToSocket(socketId, startTimeBytes)
queryRecieved := false
for {
_, msg, err := ws.ReadMessage()
if err != nil {
@@ -101,65 +104,75 @@ func websocketHandler(w http.ResponseWriter, r *http.Request, eventHandlers Even
break
}
if !isTapper && !isQuerySet {
query := string(msg)
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()),
})
SendToSocket(socketId, toastBytes)
break
}
isQuerySet = true
handleDataChannel := func(c *basenine.Connection, data chan []byte) {
for {
bytes := <-data
if string(bytes) == basenine.CloseChannel {
return
}
var dataMap map[string]interface{}
err = json.Unmarshal(bytes, &dataMap)
base := dataMap["base"].(map[string]interface{})
base["id"] = uint(dataMap["id"].(float64))
baseEntryBytes, _ := models.CreateBaseEntryWebSocketMessage(base)
SendToSocket(socketId, baseEntryBytes)
if !queryRecieved {
if !isTapper && !isQuerySet {
queryRecieved = true
query := string(msg)
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()),
})
SendToSocket(socketId, toastBytes)
break
}
}
handleMetaChannel := func(c *basenine.Connection, meta chan []byte) {
for {
bytes := <-meta
isQuerySet = true
if string(bytes) == basenine.CloseChannel {
return
handleDataChannel := func(c *basenine.Connection, data chan []byte) {
for {
bytes := <-data
if string(bytes) == basenine.CloseChannel {
return
}
var dataMap map[string]interface{}
err = json.Unmarshal(bytes, &dataMap)
base := dataMap["base"].(map[string]interface{})
base["id"] = uint(dataMap["id"].(float64))
baseEntryBytes, _ := models.CreateBaseEntryWebSocketMessage(base)
SendToSocket(socketId, baseEntryBytes)
}
var metadata *basenine.Metadata
err = json.Unmarshal(bytes, &metadata)
if err != nil {
logger.Log.Debugf("Error recieving metadata: %v", err.Error())
}
metadataBytes, _ := models.CreateWebsocketQueryMetadataMessage(metadata)
SendToSocket(socketId, metadataBytes)
}
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 recieving metadata: %v", err.Error())
}
metadataBytes, _ := models.CreateWebsocketQueryMetadataMessage(metadata)
SendToSocket(socketId, metadataBytes)
}
}
go handleDataChannel(connection, data)
go handleMetaChannel(connection, meta)
connection.Query(query, data, meta)
} else {
eventHandlers.WebSocketMessage(socketId, msg)
}
go handleDataChannel(connection, data)
go handleMetaChannel(connection, meta)
connection.Query(query, data, meta)
} else {
eventHandlers.WebSocketMessage(socketId, msg)
id, err := strconv.Atoi(string(msg))
if err != nil {
continue
}
focusEntryBytes, _ := models.CreateWebsocketFocusEntry(id)
SendToSocket(socketId, focusEntryBytes)
}
}
}

View File

@@ -57,6 +57,11 @@ type WebSocketStartTimeMessage struct {
Data int64 `json:"data"`
}
type WebSocketFocusEntryMessage struct {
*shared.WebSocketMessageMetadata
Id int `json:"id"`
}
func CreateBaseEntryWebSocketMessage(base map[string]interface{}) ([]byte, error) {
message := &WebSocketEntryMessage{
WebSocketMessageMetadata: &shared.WebSocketMessageMetadata{
@@ -117,6 +122,16 @@ func CreateWebsocketStartTimeMessage(base int64) ([]byte, error) {
return json.Marshal(message)
}
func CreateWebsocketFocusEntry(id int) ([]byte, error) {
message := &WebSocketFocusEntryMessage{
WebSocketMessageMetadata: &shared.WebSocketMessageMetadata{
MessageType: shared.WebSocketMessageFocusEntry,
},
Id: id,
}
return json.Marshal(message)
}
// ExtendedHAR is the top level object of a HAR log.
type ExtendedHAR struct {
Log *ExtendedLog `json:"log"`

View File

@@ -37,8 +37,8 @@ COPY agent .
RUN go build -gcflags="all=-N -l" -o mizuagent .
# Download Basenine executable, verify the sha1sum and move it to a directory in $PATH
ADD https://github.com/up9inc/basenine/releases/download/v0.2.11/basenine_linux_amd64 ./basenine_linux_amd64
ADD https://github.com/up9inc/basenine/releases/download/v0.2.11/basenine_linux_amd64.sha256 ./basenine_linux_amd64.sha256
ADD https://github.com/up9inc/basenine/releases/download/v0.2.12/basenine_linux_amd64 ./basenine_linux_amd64
ADD https://github.com/up9inc/basenine/releases/download/v0.2.12/basenine_linux_amd64.sha256 ./basenine_linux_amd64.sha256
RUN shasum -a 256 -c basenine_linux_amd64.sha256
RUN chmod +x ./basenine_linux_amd64

View File

@@ -1,11 +1,12 @@
package shared
import (
"io/ioutil"
"strings"
"github.com/op/go-logging"
"github.com/up9inc/mizu/shared/logger"
"github.com/up9inc/mizu/tap/api"
"io/ioutil"
"strings"
"gopkg.in/yaml.v3"
)
@@ -21,6 +22,7 @@ const (
WebSocketMessageTypeToast WebSocketMessageType = "toast"
WebSocketMessageTypeQueryMetadata WebSocketMessageType = "queryMetadata"
WebSocketMessageTypeStartTime WebSocketMessageType = "startTime"
WebSocketMessageFocusEntry WebSocketMessageType = "focusEntry"
)
type Resources struct {

View File

@@ -7,6 +7,7 @@ import (
"io"
"io/ioutil"
"net/http"
"strings"
"github.com/up9inc/mizu/tap/api"
)
@@ -34,12 +35,13 @@ func handleHTTP2Stream(http2Assembler *Http2Assembler, tcpID *api.TcpID, superTi
switch messageHTTP1 := messageHTTP1.(type) {
case http.Request:
ident := fmt.Sprintf(
"%s->%s %s->%s %d",
"%s->%s %s->%s %d %s",
tcpID.SrcIP,
tcpID.DstIP,
tcpID.SrcPort,
tcpID.DstPort,
streamID,
"HTTP2",
)
item = reqResMatcher.registerRequest(ident, &messageHTTP1, superTimer.CaptureTime)
if item != nil {
@@ -53,12 +55,13 @@ func handleHTTP2Stream(http2Assembler *Http2Assembler, tcpID *api.TcpID, superTi
}
case http.Response:
ident := fmt.Sprintf(
"%s->%s %s->%s %d",
"%s->%s %s->%s %d %s",
tcpID.DstIP,
tcpID.SrcIP,
tcpID.DstPort,
tcpID.SrcPort,
streamID,
"HTTP2",
)
item = reqResMatcher.registerResponse(ident, &messageHTTP1, superTimer.CaptureTime)
if item != nil {
@@ -84,23 +87,30 @@ func handleHTTP2Stream(http2Assembler *Http2Assembler, tcpID *api.TcpID, superTi
return nil
}
func handleHTTP1ClientStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api.CounterPair, superTimer *api.SuperTimer, emitter api.Emitter, options *api.TrafficFilteringOptions) error {
req, err := http.ReadRequest(b)
func handleHTTP1ClientStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api.CounterPair, superTimer *api.SuperTimer, emitter api.Emitter, options *api.TrafficFilteringOptions) (switchingProtocolsHTTP2 bool, req *http.Request, err error) {
req, err = http.ReadRequest(b)
if err != nil {
return err
return
}
counterPair.Request++
body, err := ioutil.ReadAll(req.Body)
// Check HTTP2 upgrade - HTTP2 Over Cleartext (H2C)
if strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade") && strings.ToLower(req.Header.Get("Upgrade")) == "h2c" {
switchingProtocolsHTTP2 = true
}
var body []byte
body, err = ioutil.ReadAll(req.Body)
req.Body = io.NopCloser(bytes.NewBuffer(body)) // rewind
ident := fmt.Sprintf(
"%s->%s %s->%s %d",
"%s->%s %s->%s %d %s",
tcpID.SrcIP,
tcpID.DstIP,
tcpID.SrcPort,
tcpID.DstPort,
counterPair.Request,
"HTTP1",
)
item := reqResMatcher.registerRequest(ident, req, superTimer.CaptureTime)
if item != nil {
@@ -113,26 +123,34 @@ func handleHTTP1ClientStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api
}
filterAndEmit(item, emitter, options)
}
return nil
return
}
func handleHTTP1ServerStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api.CounterPair, superTimer *api.SuperTimer, emitter api.Emitter, options *api.TrafficFilteringOptions) error {
res, err := http.ReadResponse(b, nil)
func handleHTTP1ServerStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api.CounterPair, superTimer *api.SuperTimer, emitter api.Emitter, options *api.TrafficFilteringOptions) (switchingProtocolsHTTP2 bool, err error) {
var res *http.Response
res, err = http.ReadResponse(b, nil)
if err != nil {
return err
return
}
counterPair.Response++
body, err := ioutil.ReadAll(res.Body)
// Check HTTP2 upgrade - HTTP2 Over Cleartext (H2C)
if res.StatusCode == 101 && strings.Contains(strings.ToLower(res.Header.Get("Connection")), "upgrade") && strings.ToLower(res.Header.Get("Upgrade")) == "h2c" {
switchingProtocolsHTTP2 = true
}
var body []byte
body, err = ioutil.ReadAll(res.Body)
res.Body = io.NopCloser(bytes.NewBuffer(body)) // rewind
ident := fmt.Sprintf(
"%s->%s %s->%s %d",
"%s->%s %s->%s %d %s",
tcpID.DstIP,
tcpID.SrcIP,
tcpID.DstPort,
tcpID.SrcPort,
counterPair.Response,
"HTTP1",
)
item := reqResMatcher.registerResponse(ident, res, superTimer.CaptureTime)
if item != nil {
@@ -145,5 +163,5 @@ func handleHTTP1ServerStream(b *bufio.Reader, tcpID *api.TcpID, counterPair *api
}
filterAndEmit(item, emitter, options)
}
return nil
return
}

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"log"
"net/http"
"net/url"
"time"
@@ -85,7 +86,15 @@ func (d dissecting) Dissect(b *bufio.Reader, isClient bool, tcpID *api.TcpID, co
}
dissected := false
switchingProtocolsHTTP2 := false
for {
if switchingProtocolsHTTP2 {
switchingProtocolsHTTP2 = false
isHTTP2, err = checkIsHTTP2Connection(b, isClient)
prepareHTTP2Connection(b, isClient)
http2Assembler = createHTTP2Assembler(b)
}
if superIdentifier.Protocol != nil && superIdentifier.Protocol != &protocol {
return errors.New("Identified by another protocol")
}
@@ -99,15 +108,39 @@ func (d dissecting) Dissect(b *bufio.Reader, isClient bool, tcpID *api.TcpID, co
}
dissected = true
} else if isClient {
err = handleHTTP1ClientStream(b, tcpID, counterPair, superTimer, emitter, options)
var req *http.Request
switchingProtocolsHTTP2, req, err = handleHTTP1ClientStream(b, tcpID, counterPair, superTimer, emitter, options)
if err == io.EOF || err == io.ErrUnexpectedEOF {
break
} else if err != nil {
continue
}
dissected = true
// In case of an HTTP2 upgrade, duplicate the HTTP1 request into HTTP2 with stream ID 1
if switchingProtocolsHTTP2 {
ident := fmt.Sprintf(
"%s->%s %s->%s 1 %s",
tcpID.SrcIP,
tcpID.DstIP,
tcpID.SrcPort,
tcpID.DstPort,
"HTTP2",
)
item := reqResMatcher.registerRequest(ident, req, superTimer.CaptureTime)
if item != nil {
item.ConnectionInfo = &api.ConnectionInfo{
ClientIP: tcpID.SrcIP,
ClientPort: tcpID.SrcPort,
ServerIP: tcpID.DstIP,
ServerPort: tcpID.DstPort,
IsOutgoing: true,
}
filterAndEmit(item, emitter, options)
}
}
} else {
err = handleHTTP1ServerStream(b, tcpID, counterPair, superTimer, emitter, options)
switchingProtocolsHTTP2, err = handleHTTP1ServerStream(b, tcpID, counterPair, superTimer, emitter, options)
if err == io.EOF || err == io.ErrUnexpectedEOF {
break
} else if err != nil {
@@ -132,6 +165,8 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
reqDetails := request["details"].(map[string]interface{})
resDetails := response["details"].(map[string]interface{})
isRequestUpgradedH2C := false
for _, header := range reqDetails["headers"].([]interface{}) {
h := header.(map[string]interface{})
if h["name"] == "Host" {
@@ -143,13 +178,19 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
if h["name"] == ":path" {
path = h["value"].(string)
}
if h["name"] == "Upgrade" {
if h["value"].(string) == "h2c" {
isRequestUpgradedH2C = true
}
}
}
if resDetails["bodySize"].(float64) < 0 {
resDetails["bodySize"] = 0
}
if item.Protocol.Version == "2.0" {
if item.Protocol.Version == "2.0" && !isRequestUpgradedH2C {
service = authority
} else {
service = host
@@ -162,6 +203,7 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
}
request["url"] = reqDetails["url"].(string)
reqDetails["targetUri"] = reqDetails["url"]
reqDetails["path"] = path
reqDetails["summary"] = path
@@ -191,7 +233,7 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, resolvedSource string,
resDetails["statusText"] = grpcStatusCodes[statusCode]
}
if item.Protocol.Version == "2.0" {
if item.Protocol.Version == "2.0" && !isRequestUpgradedH2C {
reqDetails["url"] = path
request["url"] = path
}
@@ -269,9 +311,9 @@ func representRequest(request map[string]interface{}) (repRequest []interface{})
Selector: `request.method`,
},
{
Name: "URL",
Value: request["url"].(string),
Selector: `request.url`,
Name: "Target URI",
Value: request["targetUri"].(string),
Selector: `request.targetUri`,
},
{
Name: "Path",

View File

@@ -92,6 +92,6 @@ func splitIdent(ident string) []string {
}
func genKey(split []string) string {
key := fmt.Sprintf("%s:%s->%s:%s,%s", split[0], split[2], split[1], split[3], split[4])
key := fmt.Sprintf("%s:%s->%s:%s,%s%s", split[0], split[2], split[1], split[3], split[4], split[5])
return key
}

View File

@@ -1,9 +1,9 @@
import React from "react";
import EntryViewer from "./EntryDetailed/EntryViewer";
import {EntryItem} from "./EntryListItem/EntryListItem";
import {makeStyles} from "@material-ui/core";
import Protocol from "./UI/Protocol"
import StatusCode from "./UI/StatusCode";
import {Summary} from "./UI/Summary";
import Queryable from "./UI/Queryable";
const useStyles = makeStyles(() => ({
entryTitle: {
@@ -12,6 +12,7 @@ const useStyles = makeStyles(() => ({
maxHeight: 46,
alignItems: 'center',
marginBottom: 4,
marginLeft: 6,
padding: 2,
paddingBottom: 0
},
@@ -37,45 +38,49 @@ const EntryTitle: React.FC<any> = ({protocol, data, bodySize, elapsedTime, updat
const classes = useStyles();
const response = data.response;
return <div className={classes.entryTitle}>
<Protocol protocol={protocol} horizontal={true} updateQuery={null}/>
<Protocol protocol={protocol} horizontal={true} updateQuery={updateQuery}/>
<div style={{right: "30px", position: "absolute", display: "flex"}}>
{response && <div
className="queryable"
style={{margin: "0 18px", opacity: 0.5}}
onClick={() => {
updateQuery(`response.bodySize == ${bodySize}`)
}}
{response && <Queryable
query={`response.bodySize == ${bodySize}`}
updateQuery={updateQuery}
style={{margin: "0 18px"}}
displayIconOnMouseOver={true}
>
{formatSize(bodySize)}
</div>}
{response && <div
className="queryable"
style={{marginRight: 18, opacity: 0.5}}
onClick={() => {
updateQuery(`elapsedTime >= ${elapsedTime}`)
}}
<div
style={{opacity: 0.5}}
>
{formatSize(bodySize)}
</div>
</Queryable>}
{response && <Queryable
query={`elapsedTime >= ${elapsedTime}`}
updateQuery={updateQuery}
style={{marginRight: 18}}
displayIconOnMouseOver={true}
>
{Math.round(elapsedTime)}ms
</div>}
<div
style={{opacity: 0.5}}
>
{Math.round(elapsedTime)}ms
</div>
</Queryable>}
</div>
</div>;
};
const EntrySummary: React.FC<any> = ({data, updateQuery}) => {
const classes = useStyles();
const entry = data.base;
const response = data.response;
return <div className={classes.entrySummary}>
{response && "status" in response && <div style={{marginRight: 8}}>
<StatusCode statusCode={response.status} updateQuery={updateQuery}/>
</div>}
<div style={{flexGrow: 1, overflow: 'hidden'}}>
<Summary method={data.method} summary={data.summary} updateQuery={updateQuery}/>
</div>
</div>;
return <EntryItem
key={entry.id}
entry={entry}
focusedEntryId={null}
setFocusedEntryId={null}
style={{}}
updateQuery={updateQuery}
headingMode={true}
/>;
};
export const EntryDetailed: React.FC<EntryDetailedProps> = ({entryData, updateQuery}) => {

View File

@@ -27,7 +27,7 @@
font-weight: 600
font-size: .75rem
line-height: 1.2
margin: .3rem 0
margin-bottom: -2px
.dataKey
color: $blue-gray

View File

@@ -3,6 +3,7 @@ import React, {useState} from "react";
import {SyntaxHighlighter} from "../UI/SyntaxHighlighter/index";
import CollapsibleContainer from "../UI/CollapsibleContainer";
import FancyTextDisplay from "../UI/FancyTextDisplay";
import Queryable from "../UI/Queryable";
import Checkbox from "../UI/Checkbox";
import ProtobufDecoder from "protobuf-decoder";
@@ -15,23 +16,29 @@ interface EntryViewLineProps {
}
const EntryViewLine: React.FC<EntryViewLineProps> = ({label, value, updateQuery, selector, overrideQueryValue}) => {
let query: string;
if (!selector) {
query = "";
} else if (overrideQueryValue) {
query = `${selector} == ${overrideQueryValue}`;
} else if (typeof(value) == "string") {
query = `${selector} == "${JSON.stringify(value).slice(1, -1)}"`;
} else {
query = `${selector} == ${value}`;
}
return (label && <tr className={styles.dataLine}>
<td
className={`queryable ${styles.dataKey}`}
onClick={() => {
if (!selector) {
return
} else if (overrideQueryValue) {
updateQuery(`${selector} == ${overrideQueryValue}`)
} else if (typeof(value) === "string") {
updateQuery(`${selector} == "${JSON.stringify(value).slice(1, -1)}"`)
} else {
updateQuery(`${selector} == ${value}`)
}
}}
>
{label}
</td>
<td className={`${styles.dataKey}`}>
<Queryable
query={query}
updateQuery={updateQuery}
style={{float: "right", height: "0px"}}
iconStyle={{marginRight: "20px"}}
flipped={true}
displayIconOnMouseOver={true}
>
{label}
</Queryable>
</td>
<td>
<FancyTextDisplay
className={styles.dataValue}
@@ -53,9 +60,9 @@ interface EntrySectionCollapsibleTitleProps {
const EntrySectionCollapsibleTitle: React.FC<EntrySectionCollapsibleTitleProps> = ({title, color, isExpanded}) => {
return <div className={styles.title}>
<span className={`${styles.button} ${isExpanded ? styles.expanded : ''}`} style={{backgroundColor: color}}>
<div className={`${styles.button} ${isExpanded ? styles.expanded : ''}`} style={{backgroundColor: color}}>
{isExpanded ? '-' : '+'}
</span>
</div>
<span>{title}</span>
</div>
}
@@ -130,7 +137,7 @@ export const EntryBodySection: React.FC<EntryBodySectionProps> = ({
<table>
<tbody>
<EntryViewLine label={'Mime type'} value={contentType} updateQuery={updateQuery} selector={selector} overrideQueryValue={`r".*"`}/>
<EntryViewLine label={'Encoding'} value={encoding} updateQuery={updateQuery} selector={selector} overrideQueryValue={`r".*"`}/>
{encoding && <EntryViewLine label={'Encoding'} value={encoding} updateQuery={updateQuery} selector={selector} overrideQueryValue={`r".*"`}/>}
</tbody>
</table>

View File

@@ -4,6 +4,7 @@
font-family: "Source Sans Pro", Lucida Grande, Tahoma, sans-serif
height: calc(100% - 70px)
width: 100%
margin-top: 10px
h3,
h4

View File

@@ -19,7 +19,6 @@
.rowSelected
border: 1px $blue-color solid
margin-right: 3px
.ruleSuccessRow
background: #E8FFF1
@@ -52,7 +51,6 @@
white-space: nowrap
color: $secondary-font-color
padding-left: 4px
padding-top: 3px
padding-right: 10px
display: flex
font-size: 12px
@@ -70,8 +68,9 @@
flex-direction: column
overflow: hidden
padding-right: 10px
padding-left: 10px
flex-grow: 1
padding-top: 5px
margin-left: -6px
.separatorRight
display: flex

View File

@@ -1,8 +1,9 @@
import React, {useState} from "react";
import React from "react";
import styles from './EntryListItem.module.sass';
import StatusCode, {getClassification, StatusCodeClassification} from "../UI/StatusCode";
import Protocol, {ProtocolInterface} from "../UI/Protocol"
import {Summary} from "../UI/Summary";
import Queryable from "../UI/Queryable";
import ingoingIconSuccess from "../assets/ingoing-traffic-success.svg"
import ingoingIconFailure from "../assets/ingoing-traffic-failure.svg"
import ingoingIconNeutral from "../assets/ingoing-traffic-neutral.svg"
@@ -37,14 +38,16 @@ interface Rules {
interface EntryProps {
entry: Entry;
focusedEntryId: string;
setFocusedEntryId: (id: string) => void;
style: object;
updateQuery: any;
headingMode: boolean;
}
export const EntryItem: React.FC<EntryProps> = ({entry, setFocusedEntryId, style, updateQuery}) => {
export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocusedEntryId, style, updateQuery, headingMode}) => {
const [isSelected, setIsSelected] = useState(false);
const isSelected = focusedEntryId === entry.id.toString();
const classification = getClassification(entry.statusCode)
const numberOfRules = entry.rules.numberOfRules
@@ -122,37 +125,40 @@ export const EntryItem: React.FC<EntryProps> = ({entry, setFocusedEntryId, style
className={`${styles.row}
${isSelected && !rule && !contractEnabled ? styles.rowSelected : additionalRulesProperties}`}
onClick={() => {
setIsSelected(!isSelected);
if (!setFocusedEntryId) return;
setFocusedEntryId(entry.id.toString());
}}
style={{
border: isSelected ? `1px ${entry.protocol.backgroundColor} solid` : "1px transparent solid",
position: "absolute",
position: !headingMode ? "absolute" : "unset",
top: style['top'],
marginTop: style['marginTop'],
width: "calc(100% - 25px)",
marginTop: !headingMode ? style['marginTop'] : "10px",
width: !headingMode ? "calc(100% - 25px)" : "calc(100% - 18px)",
}}
>
<Protocol
{!headingMode ? <Protocol
protocol={entry.protocol}
horizontal={false}
updateQuery={updateQuery}
/>
/> : null}
{((entry.protocol.name === "http" && "statusCode" in entry) || entry.statusCode !== 0) && <div>
<StatusCode statusCode={entry.statusCode} updateQuery={updateQuery}/>
</div>}
<div className={styles.endpointServiceContainer}>
<Summary method={entry.method} summary={entry.summary} updateQuery={updateQuery}/>
<div className={styles.service}>
<span
title="Service Name"
className="queryable"
onClick={() => {
updateQuery(`service == "${entry.service}"`)
}}
<Queryable
query={`service == "${entry.service}"`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
style={{marginTop: "-4px"}}
>
{entry.service}
</span>
<span
title="Service Name"
>
{entry.service}
</span>
</Queryable>
</div>
</div>
{
@@ -170,74 +176,109 @@ export const EntryItem: React.FC<EntryProps> = ({entry, setFocusedEntryId, style
: ""
}
<div className={styles.separatorRight}>
<span
className={`queryable ${styles.tcpInfo} ${styles.ip}`}
title="Source IP"
onClick={() => {
updateQuery(`src.ip == "${entry.sourceIp}"`)
}}
<Queryable
query={`src.ip == "${entry.sourceIp}"`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={true}
iconStyle={{marginRight: "16px"}}
>
{entry.sourceIp}
</span>
<span className={`${styles.tcpInfo}`}>:</span>
<span
className={`queryable ${styles.tcpInfo} ${styles.port}`}
title="Source Port"
onClick={() => {
updateQuery(`src.port == "${entry.sourcePort}"`)
}}
<span
className={`${styles.tcpInfo} ${styles.ip}`}
title="Source IP"
>
{entry.sourceIp}
</span>
</Queryable>
<span className={`${styles.tcpInfo}`} style={{marginTop: "18px"}}>:</span>
<Queryable
query={`src.port == "${entry.sourcePort}"`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={true}
iconStyle={{marginTop: "28px"}}
>
{entry.sourcePort}
</span>
<span
className={`${styles.tcpInfo} ${styles.port}`}
title="Source Port"
>
{entry.sourcePort}
</span>
</Queryable>
{entry.isOutgoing ?
<img
src={outgoingIcon}
alt="Ingoing traffic"
title="Ingoing"
onClick={() => {
updateQuery(`outgoing == true`)
}}
/>
<Queryable
query={`outgoing == true`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={true}
iconStyle={{marginTop: "28px"}}
>
<img
src={outgoingIcon}
alt="Ingoing traffic"
title="Ingoing"
/>
</Queryable>
:
<img
src={ingoingIcon}
alt="Outgoing traffic"
title="Outgoing"
onClick={() => {
updateQuery(`outgoing == false`)
}}
/>
<Queryable
query={`outgoing == true`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={true}
iconStyle={{marginTop: "28px"}}
>
<img
src={ingoingIcon}
alt="Outgoing traffic"
title="Outgoing"
onClick={() => {
updateQuery(`outgoing == false`)
}}
/>
</Queryable>
}
<span
className={`queryable ${styles.tcpInfo} ${styles.ip}`}
title="Destination IP"
onClick={() => {
updateQuery(`dst.ip == "${entry.destinationIp}"`)
}}
<Queryable
query={`dst.ip == "${entry.destinationIp}"`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={false}
iconStyle={{marginTop: "28px"}}
>
{entry.destinationIp}
</span>
<span className={`${styles.tcpInfo}`}>:</span>
<span
className={`queryable ${styles.tcpInfo} ${styles.port}`}
title="Destination Port"
onClick={() => {
updateQuery(`dst.port == "${entry.destinationPort}"`)
}}
<span
className={`${styles.tcpInfo} ${styles.ip}`}
title="Destination IP"
>
{entry.destinationIp}
</span>
</Queryable>
<span className={`${styles.tcpInfo}`} style={{marginTop: "18px"}}>:</span>
<Queryable
query={`dst.port == "${entry.destinationPort}"`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={false}
>
{entry.destinationPort}
</span>
<span
className={`${styles.tcpInfo} ${styles.port}`}
title="Destination Port"
>
{entry.destinationPort}
</span>
</Queryable>
</div>
<div className={styles.timestamp}>
<span
title="Timestamp"
className="queryable"
onClick={() => {
updateQuery(`timestamp >= datetime("${new Date(+entry.timestamp)?.toLocaleString("en-US", {timeZone: 'UTC' })}")`)
}}
<Queryable
query={`timestamp >= datetime("${new Date(+entry.timestamp)?.toLocaleString("en-US", {timeZone: 'UTC' })}")`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={false}
>
{new Date(+entry.timestamp)?.toLocaleString("en-US")}
</span>
<span
title="Timestamp"
>
{new Date(+entry.timestamp)?.toLocaleString("en-US")}
</span>
</Queryable>
</div>
</div>
</>

View File

@@ -13,7 +13,7 @@ interface FiltersProps {
setQuery: any
backgroundColor: string
ws: any
openWebSocket: (query: string) => void;
openWebSocket: (query: string, resetEntriesBuffer: boolean) => void;
}
export const Filters: React.FC<FiltersProps> = ({query, setQuery, backgroundColor, ws, openWebSocket}) => {
@@ -33,7 +33,7 @@ interface QueryFormProps {
setQuery: any
backgroundColor: string
ws: any
openWebSocket: (query: string) => void;
openWebSocket: (query: string, resetEntriesBuffer: boolean) => void;
}
const style = {
@@ -63,8 +63,8 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
}
const handleSubmit = (e) => {
ws.close()
openWebSocket(query)
ws.close();
openWebSocket(query, true);
e.preventDefault();
}
@@ -226,7 +226,7 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
language="python"
/>
<Typography id="modal-modal-description">
By clicking the UI elements in both left-pane and right-pane, you can automatically select a field and update the query:
By clicking the plus icon that appears beside the queryable UI elements on hovering in both left-pane and right-pane, you can automatically select a field and update the query:
</Typography>
<img
src={filterUIExample1}
@@ -235,12 +235,12 @@ export const QueryForm: React.FC<QueryFormProps> = ({query, setQuery, background
title="Clicking to UI elements (left-pane)"
/>
<Typography id="modal-modal-description">
Such that; clicking this in left-pane, would append the query below:
Such that; clicking this icon in left-pane, would append the query below:
</Typography>
<SyntaxHighlighter
isWrapped={false}
showLineNumbers={false}
code={`and service == "http://carts.sock-shop"`}
code={`and service == "carts.sock-shop"`}
language="python"
/>
<Typography id="modal-modal-description">

View File

@@ -21,7 +21,7 @@ const useLayoutStyles = makeStyles(() => ({
padding: "12px 24px",
borderRadius: 4,
marginTop: 15,
background: variables.headerBackgoundColor,
background: variables.headerBackgroundColor,
},
viewer: {
@@ -66,6 +66,7 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
const [queriedCurrent, setQueriedCurrent] = useState(0);
const [queriedTotal, setQueriedTotal] = useState(0);
const [leftOff, setLeftOff] = useState(0);
const [startTime, setStartTime] = useState(0);
@@ -100,16 +101,30 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
const listEntry = useRef(null);
const openWebSocket = (query) => {
setFocusedEntryId(null);
setEntries([]);
setEntriesBuffer([]);
const openWebSocket = (query: string, resetEntriesBuffer: boolean) => {
if (resetEntriesBuffer) {
setFocusedEntryId(null);
setEntries([]);
setEntriesBuffer([]);
} else {
setEntriesBuffer(entries);
}
ws.current = new WebSocket(MizuWebsocketURL);
ws.current.onopen = () => {
ws.current.send(query)
setConnection(ConnectionStatus.Connected);
ws.current.send(query);
}
ws.current.onclose = () => {
setConnection(ConnectionStatus.Closed);
}
ws.current.onerror = (event) => {
console.error("WebSocket error:", event);
if (query) {
openWebSocket(`(${query}) and leftOff(${leftOff})`, false);
} else {
openWebSocket(`leftOff(${leftOff})`, false);
}
}
ws.current.onclose = () => setConnection(ConnectionStatus.Closed);
}
if (ws.current) {
@@ -119,15 +134,21 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
switch (message.messageType) {
case "entry":
const entry = message.data;
if (!focusedEntryId) setFocusedEntryId(entry.id.toString());
var focusThis = false;
if (!focusedEntryId) {
focusThis = true;
setFocusedEntryId(entry.id.toString());
}
setEntriesBuffer([
...entriesBuffer,
<EntryItem
key={entry.id}
entry={entry}
focusedEntryId={focusThis ? entry.id.toString() : focusedEntryId}
setFocusedEntryId={setFocusedEntryId}
style={{}}
updateQuery={updateQuery}
headingMode={false}
/>
]);
break
@@ -155,11 +176,22 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
case "queryMetadata":
setQueriedCurrent(message.data.current);
setQueriedTotal(message.data.total);
setLeftOff(message.data.leftOff);
setEntries(entriesBuffer);
break;
case "startTime":
setStartTime(message.data);
break;
case "focusEntry":
// To achieve selecting only one entry, render all elements in the buffer
// with the current `focusedEntryId` value.
entriesBuffer.forEach((entry: any, i: number) => {
entriesBuffer[i] = React.cloneElement(entry, {
focusedEntryId: focusedEntryId
});
})
setEntries(entriesBuffer);
break;
default:
console.error(`unsupported websocket message type, Got: ${message.messageType}`)
}
@@ -168,7 +200,7 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
useEffect(() => {
(async () => {
openWebSocket("rlimit(100)");
openWebSocket("leftOff(-1)", true);
try{
const tapStatusResponse = await api.tapStatus();
setTappingStatus(tapStatusResponse);
@@ -185,21 +217,28 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
useEffect(() => {
if (!focusedEntryId) return;
setSelectedEntryData(null);
if (ws.current.readyState === WebSocket.OPEN) {
ws.current.send(focusedEntryId);
}
(async () => {
try {
const entryData = await api.getEntry(focusedEntryId);
setSelectedEntryData(entryData);
} catch (error) {
toast[error.response.data.type](`Entry[${focusedEntryId}]: ${error.response.data.msg}`, {
position: "bottom-right",
theme: "colored",
autoClose: error.response.data.autoClose,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
if (error.response) {
toast[error.response.data.type](`Entry[${focusedEntryId}]: ${error.response.data.msg}`, {
position: "bottom-right",
theme: "colored",
autoClose: error.response.data.autoClose,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
});
}
console.error(error);
}
})()
@@ -209,8 +248,11 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
if (connection === ConnectionStatus.Connected) {
ws.current.close();
} else {
openWebSocket(query);
setConnection(ConnectionStatus.Connected);
if (query) {
openWebSocket(`(${query}) and leftOff(${leftOff})`, false);
} else {
openWebSocket(`leftOff(${leftOff})`, false);
}
}
}

View File

@@ -1,5 +1,6 @@
import React from "react";
import styles from './style/Protocol.module.sass';
import Queryable from "./Queryable";
export interface ProtocolInterface {
name: string
@@ -22,34 +23,45 @@ interface ProtocolProps {
const Protocol: React.FC<ProtocolProps> = ({protocol, horizontal, updateQuery}) => {
if (horizontal) {
return <a target="_blank" rel="noopener noreferrer" href={protocol.referenceLink}>
return <Queryable
query={protocol.macro}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
>
<a target="_blank" rel="noopener noreferrer" href={protocol.referenceLink}>
<span
className={`${styles.base} ${styles.horizontal}`}
style={{
backgroundColor: protocol.backgroundColor,
color: protocol.foregroundColor,
fontSize: 13,
}}
title={protocol.abbr}
>
{protocol.longName}
</span>
</a>
</Queryable>
} else {
return <Queryable
query={protocol.macro}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
flipped={false}
iconStyle={{marginTop: "48px"}}
>
<span
className={`${styles.base} ${styles.horizontal}`}
className={`${styles.base} ${styles.vertical}`}
style={{
backgroundColor: protocol.backgroundColor,
color: protocol.foregroundColor,
fontSize: 13,
fontSize: protocol.fontSize,
}}
title={protocol.abbr}
title={protocol.longName}
>
{protocol.longName}
{protocol.abbr}
</span>
</a>
} else {
return <span
className={`${styles.base} ${styles.vertical}`}
style={{
backgroundColor: protocol.backgroundColor,
color: protocol.foregroundColor,
fontSize: protocol.fontSize,
}}
title={protocol.longName}
onClick={() => {
updateQuery(protocol.macro)
}}
>
{protocol.abbr}
</span>
</Queryable>
}
};

View File

@@ -0,0 +1,62 @@
import React, { useEffect, useState } from 'react';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import AddCircleIcon from '@material-ui/icons/AddCircle';
import './style/Queryable.sass';
interface Props {
query: string,
updateQuery: any,
style?: object,
iconStyle?: object,
className?: string,
useTooltip?: boolean,
displayIconOnMouseOver?: boolean,
flipped?: boolean,
}
const Queryable: React.FC<Props> = ({query, updateQuery, style, iconStyle, className, useTooltip= true, displayIconOnMouseOver = false, flipped = false, children}) => {
const [showAddedNotification, setAdded] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);
const onCopy = () => {
setAdded(true)
};
useEffect(() => {
let timer;
if (showAddedNotification) {
updateQuery(query);
timer = setTimeout(() => {
setAdded(false);
}, 1000);
}
return () => clearTimeout(timer);
}, [showAddedNotification, query, updateQuery]);
const addButton = query ? <CopyToClipboard text={query} onCopy={onCopy}>
<span
className={`Queryable-Icon`}
title={`Add "${query}" to the filter`}
style={iconStyle}
>
<AddCircleIcon fontSize="small" color="inherit"></AddCircleIcon>
{showAddedNotification && <span className={'Queryable-AddNotifier'}>Added</span>}
</span>
</CopyToClipboard> : null;
return (
<div
className={`Queryable-Container displayIconOnMouseOver ${className ? className : ''} ${displayIconOnMouseOver ? 'displayIconOnMouseOver ' : ''}`}
style={style}
onMouseOver={ e => setShowTooltip(true)}
onMouseLeave={ e => setShowTooltip(false)}
>
{flipped && addButton}
{children}
{!flipped && addButton}
{useTooltip && showTooltip && <span className={'Queryable-Tooltip'}>{query}</span>}
</div>
);
};
export default Queryable;

View File

@@ -1,5 +1,6 @@
import React from "react";
import styles from './style/StatusCode.module.sass';
import Queryable from "./Queryable";
export enum StatusCodeClassification {
SUCCESS = "success",
@@ -16,15 +17,19 @@ const StatusCode: React.FC<EntryProps> = ({statusCode, updateQuery}) => {
const classification = getClassification(statusCode)
return <span
title="Status Code"
className={`queryable ${styles[classification]} ${styles.base}`}
onClick={() => {
updateQuery(`response.status == ${statusCode}`)
}}
return <Queryable
query={`response.status == ${statusCode}`}
updateQuery={updateQuery}
style={{minWidth: "68px"}}
displayIconOnMouseOver={true}
>
{statusCode}
</span>
<span
title="Status Code"
className={`${styles[classification]} ${styles.base}`}
>
{statusCode}
</span>
</Queryable>
};
export function getClassification(statusCode: number): string {

View File

@@ -1,6 +1,7 @@
import miscStyles from "./style/misc.module.sass";
import React from "react";
import styles from './style/Summary.module.sass';
import Queryable from "./Queryable";
interface SummaryProps {
method: string
@@ -9,24 +10,28 @@ interface SummaryProps {
}
export const Summary: React.FC<SummaryProps> = ({method, summary, updateQuery}) => {
return <div className={styles.container}>
{method && <span
title="Method"
className={`queryable ${miscStyles.protocol} ${miscStyles.method}`}
onClick={() => {
updateQuery(`method == "${method}"`)
}}
{method && <Queryable
query={`method == "${method}"`}
className={`${miscStyles.protocol} ${miscStyles.method}`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
>
{method}
</span>}
{summary && <div
title="Summary"
className={`queryable ${styles.summary}`}
onClick={() => {
updateQuery(`summary == "${summary}"`)
}}
<span>
{method}
</span>
</Queryable>}
{summary && <Queryable
query={`summary == "${summary}"`}
updateQuery={updateQuery}
displayIconOnMouseOver={true}
>
{summary}
</div>}
<div
className={`${styles.summary}`}
>
{summary}
</div>
</Queryable>}
</div>
};

View File

@@ -0,0 +1,48 @@
.Queryable-Container
display: flex
align-items: center
&.displayIconOnMouseOver
.Queryable-Icon
opacity: 0
width: 0px
pointer-events: none
&:hover
.Queryable-Icon
opacity: 1
pointer-events: all
.Queryable-Icon
height: 22px
width: 22px
cursor: pointer
color: #27AE60
&:hover
background-color: rgba(255, 255, 255, 0.06)
border-radius: 4px
color: #1E884B
.Queryable-AddNotifier
background-color: #1E884B
font-weight: normal
padding: 2px 5px
border-radius: 4px
position: absolute
transform: translate(0, 10%)
color: white
z-index: 1000
font-size: 11px
.Queryable-Tooltip
background-color: #1E884B
font-weight: normal
padding: 2px 5px
border-radius: 4px
position: absolute
transform: translate(0, -80%)
color: white
z-index: 1000
font-size: 11px

View File

@@ -3,6 +3,4 @@
align-items: center
.summary
text-overflow: ellipsis
overflow: hidden
white-space: nowrap

View File

@@ -11,13 +11,14 @@
&.method
margin-right: 10px
height: 12px
&.filterPlate
border-color: #bcc6dd20
color: #a0b2ff
font-size: 10px
.noSelect
.noSelect
-webkit-touch-callout: none
-webkit-user-select: none
-khtml-user-select: none

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -114,4 +114,4 @@
.playPauseIcon
cursor: pointer
margin-right: 15px
height: 30px
height: 30px

View File

@@ -22,11 +22,6 @@ code
.uppercase
text-transform: uppercase
.queryable
cursor: pointer
&:hover
text-decoration: underline
/****
* Button
***/

View File

@@ -11,7 +11,7 @@ $blue-gray: #494677;
:export {
mainBackgroundColor: $main-background-color;
headerBackgoundColor: $header-background-color;
headerBackgroundColor: $header-background-color;
fontColor: $font-color;
secondaryFontColor: $secondary-font-color;
blueColor: $blue-color;