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
This commit is contained in:
M. Mert Yıldıran 2021-11-30 15:27:10 +03:00 committed by GitHub
parent 9696ad9bad
commit 873f252544
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 107 additions and 63 deletions

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"mizuserver/pkg/models" "mizuserver/pkg/models"
"net/http" "net/http"
"strconv"
"sync" "sync"
"time" "time"
@ -94,6 +95,8 @@ func websocketHandler(w http.ResponseWriter, r *http.Request, eventHandlers Even
startTimeBytes, _ := models.CreateWebsocketStartTimeMessage(startTime) startTimeBytes, _ := models.CreateWebsocketStartTimeMessage(startTime)
SendToSocket(socketId, startTimeBytes) SendToSocket(socketId, startTimeBytes)
queryRecieved := false
for { for {
_, msg, err := ws.ReadMessage() _, msg, err := ws.ReadMessage()
if err != nil { if err != nil {
@ -101,65 +104,75 @@ func websocketHandler(w http.ResponseWriter, r *http.Request, eventHandlers Even
break break
} }
if !isTapper && !isQuerySet { if !queryRecieved {
query := string(msg) if !isTapper && !isQuerySet {
err = basenine.Validate(shared.BasenineHost, shared.BaseninePort, query) queryRecieved = true
if err != nil { query := string(msg)
toastBytes, _ := models.CreateWebsocketToastMessage(&models.ToastMessage{ err = basenine.Validate(shared.BasenineHost, shared.BaseninePort, query)
Type: "error", if err != nil {
AutoClose: 5000, toastBytes, _ := models.CreateWebsocketToastMessage(&models.ToastMessage{
Text: fmt.Sprintf("Syntax error: %s", err.Error()), Type: "error",
}) AutoClose: 5000,
SendToSocket(socketId, toastBytes) Text: fmt.Sprintf("Syntax error: %s", err.Error()),
break })
} 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)
} }
}
handleMetaChannel := func(c *basenine.Connection, meta chan []byte) { isQuerySet = true
for {
bytes := <-meta
if string(bytes) == basenine.CloseChannel { handleDataChannel := func(c *basenine.Connection, data chan []byte) {
return 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 { } 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"` Data int64 `json:"data"`
} }
type WebSocketFocusEntryMessage struct {
*shared.WebSocketMessageMetadata
Id int `json:"id"`
}
func CreateBaseEntryWebSocketMessage(base map[string]interface{}) ([]byte, error) { func CreateBaseEntryWebSocketMessage(base map[string]interface{}) ([]byte, error) {
message := &WebSocketEntryMessage{ message := &WebSocketEntryMessage{
WebSocketMessageMetadata: &shared.WebSocketMessageMetadata{ WebSocketMessageMetadata: &shared.WebSocketMessageMetadata{
@ -117,6 +122,16 @@ func CreateWebsocketStartTimeMessage(base int64) ([]byte, error) {
return json.Marshal(message) 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. // ExtendedHAR is the top level object of a HAR log.
type ExtendedHAR struct { type ExtendedHAR struct {
Log *ExtendedLog `json:"log"` Log *ExtendedLog `json:"log"`

View File

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

View File

@ -69,10 +69,10 @@ const EntrySummary: React.FC<any> = ({data, updateQuery}) => {
return <EntryItem return <EntryItem
key={entry.id} key={entry.id}
entry={entry} entry={entry}
focusedEntryId={null}
setFocusedEntryId={null} setFocusedEntryId={null}
style={{}} style={{}}
updateQuery={updateQuery} updateQuery={updateQuery}
forceSelect={false}
headingMode={true} headingMode={true}
/>; />;
}; };

View File

@ -1,4 +1,4 @@
import React, {useState} from "react"; import React from "react";
import styles from './EntryListItem.module.sass'; import styles from './EntryListItem.module.sass';
import StatusCode, {getClassification, StatusCodeClassification} from "../UI/StatusCode"; import StatusCode, {getClassification, StatusCodeClassification} from "../UI/StatusCode";
import Protocol, {ProtocolInterface} from "../UI/Protocol" import Protocol, {ProtocolInterface} from "../UI/Protocol"
@ -37,16 +37,16 @@ interface Rules {
interface EntryProps { interface EntryProps {
entry: Entry; entry: Entry;
focusedEntryId: string;
setFocusedEntryId: (id: string) => void; setFocusedEntryId: (id: string) => void;
style: object; style: object;
updateQuery: any; updateQuery: any;
forceSelect: boolean;
headingMode: boolean; headingMode: boolean;
} }
export const EntryItem: React.FC<EntryProps> = ({entry, setFocusedEntryId, style, updateQuery, forceSelect, headingMode}) => { export const EntryItem: React.FC<EntryProps> = ({entry, focusedEntryId, setFocusedEntryId, style, updateQuery, headingMode}) => {
const [isSelected, setIsSelected] = useState(!forceSelect ? false : true); const isSelected = focusedEntryId === entry.id.toString();
const classification = getClassification(entry.statusCode) const classification = getClassification(entry.statusCode)
const numberOfRules = entry.rules.numberOfRules const numberOfRules = entry.rules.numberOfRules
@ -125,7 +125,6 @@ export const EntryItem: React.FC<EntryProps> = ({entry, setFocusedEntryId, style
${isSelected && !rule && !contractEnabled ? styles.rowSelected : additionalRulesProperties}`} ${isSelected && !rule && !contractEnabled ? styles.rowSelected : additionalRulesProperties}`}
onClick={() => { onClick={() => {
if (!setFocusedEntryId) return; if (!setFocusedEntryId) return;
setIsSelected(!isSelected);
setFocusedEntryId(entry.id.toString()); setFocusedEntryId(entry.id.toString());
}} }}
style={{ style={{

View File

@ -119,20 +119,20 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
switch (message.messageType) { switch (message.messageType) {
case "entry": case "entry":
const entry = message.data; const entry = message.data;
var forceSelect = false; var focusThis = false;
if (!focusedEntryId) { if (!focusedEntryId) {
focusThis = true;
setFocusedEntryId(entry.id.toString()); setFocusedEntryId(entry.id.toString());
forceSelect = true;
} }
setEntriesBuffer([ setEntriesBuffer([
...entriesBuffer, ...entriesBuffer,
<EntryItem <EntryItem
key={entry.id} key={entry.id}
entry={entry} entry={entry}
focusedEntryId={focusThis ? entry.id.toString() : focusedEntryId}
setFocusedEntryId={setFocusedEntryId} setFocusedEntryId={setFocusedEntryId}
style={{}} style={{}}
updateQuery={updateQuery} updateQuery={updateQuery}
forceSelect={forceSelect}
headingMode={false} headingMode={false}
/> />
]); ]);
@ -166,6 +166,16 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
case "startTime": case "startTime":
setStartTime(message.data); setStartTime(message.data);
break; 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: default:
console.error(`unsupported websocket message type, Got: ${message.messageType}`) console.error(`unsupported websocket message type, Got: ${message.messageType}`)
} }
@ -191,6 +201,11 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
useEffect(() => { useEffect(() => {
if (!focusedEntryId) return; if (!focusedEntryId) return;
setSelectedEntryData(null); setSelectedEntryData(null);
if (ws.current.readyState === WebSocket.OPEN) {
ws.current.send(focusedEntryId);
}
(async () => { (async () => {
try { try {
const entryData = await api.getEntry(focusedEntryId); const entryData = await api.getEntry(focusedEntryId);