Make ScrollableFeed virtualized by replacing react-scrollable-feed with react-scrollable-feed-virtualized (#268)

* Make `ScrollableFeed` virtualized by replacing `react-scrollable-feed` with `react-scrollable-feed-virtualized`

* fix get new entries button

* Fix the not populated `Protocol` struct in case of `GetEntries` endpoint is called

Co-authored-by: Liraz Yehezkel <lirazy@up9.com>
This commit is contained in:
M. Mert Yıldıran 2021-09-13 16:18:43 +03:00 committed by GitHub
parent 30f07479cb
commit 616eccb2cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 175 additions and 22867 deletions

View File

@ -99,29 +99,35 @@ func (e *Emitting) Emit(item *OutputChannelItem) {
} }
type MizuEntry struct { type MizuEntry struct {
ID uint `gorm:"primarykey"` ID uint `gorm:"primarykey"`
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
ProtocolName string `json:"protocolKey" gorm:"column:protocolKey"` ProtocolName string `json:"protocolName" gorm:"column:protocolName"`
ProtocolVersion string `json:"protocolVersion" gorm:"column:protocolVersion"` ProtocolLongName string `json:"protocolLongName" gorm:"column:protocolLongName"`
Entry string `json:"entry,omitempty" gorm:"column:entry"` ProtocolAbbreviation string `json:"protocolAbbreviation" gorm:"column:protocolVersion"`
EntryId string `json:"entryId" gorm:"column:entryId"` ProtocolVersion string `json:"protocolVersion" gorm:"column:protocolVersion"`
Url string `json:"url" gorm:"column:url"` ProtocolBackgroundColor string `json:"protocolBackgroundColor" gorm:"column:protocolBackgroundColor"`
Method string `json:"method" gorm:"column:method"` ProtocolForegroundColor string `json:"protocolForegroundColor" gorm:"column:protocolForegroundColor"`
Status int `json:"status" gorm:"column:status"` ProtocolFontSize int8 `json:"protocolFontSize" gorm:"column:protocolFontSize"`
RequestSenderIp string `json:"requestSenderIp" gorm:"column:requestSenderIp"` ProtocolReferenceLink string `json:"protocolReferenceLink" gorm:"column:protocolReferenceLink"`
Service string `json:"service" gorm:"column:service"` Entry string `json:"entry,omitempty" gorm:"column:entry"`
Timestamp int64 `json:"timestamp" gorm:"column:timestamp"` EntryId string `json:"entryId" gorm:"column:entryId"`
ElapsedTime int64 `json:"elapsedTime" gorm:"column:elapsedTime"` Url string `json:"url" gorm:"column:url"`
Path string `json:"path" gorm:"column:path"` Method string `json:"method" gorm:"column:method"`
ResolvedSource string `json:"resolvedSource,omitempty" gorm:"column:resolvedSource"` Status int `json:"status" gorm:"column:status"`
ResolvedDestination string `json:"resolvedDestination,omitempty" gorm:"column:resolvedDestination"` RequestSenderIp string `json:"requestSenderIp" gorm:"column:requestSenderIp"`
SourceIp string `json:"sourceIp,omitempty" gorm:"column:sourceIp"` Service string `json:"service" gorm:"column:service"`
DestinationIp string `json:"destinationIp,omitempty" gorm:"column:destinationIp"` Timestamp int64 `json:"timestamp" gorm:"column:timestamp"`
SourcePort string `json:"sourcePort,omitempty" gorm:"column:sourcePort"` ElapsedTime int64 `json:"elapsedTime" gorm:"column:elapsedTime"`
DestinationPort string `json:"destinationPort,omitempty" gorm:"column:destinationPort"` Path string `json:"path" gorm:"column:path"`
IsOutgoing bool `json:"isOutgoing,omitempty" gorm:"column:isOutgoing"` ResolvedSource string `json:"resolvedSource,omitempty" gorm:"column:resolvedSource"`
EstimatedSizeBytes int `json:"-" gorm:"column:estimatedSizeBytes"` ResolvedDestination string `json:"resolvedDestination,omitempty" gorm:"column:resolvedDestination"`
SourceIp string `json:"sourceIp,omitempty" gorm:"column:sourceIp"`
DestinationIp string `json:"destinationIp,omitempty" gorm:"column:destinationIp"`
SourcePort string `json:"sourcePort,omitempty" gorm:"column:sourcePort"`
DestinationPort string `json:"destinationPort,omitempty" gorm:"column:destinationPort"`
IsOutgoing bool `json:"isOutgoing,omitempty" gorm:"column:isOutgoing"`
EstimatedSizeBytes int `json:"-" gorm:"column:estimatedSizeBytes"`
} }
type MizuEntryWrapper struct { type MizuEntryWrapper struct {
@ -162,11 +168,19 @@ type DataUnmarshaler interface {
} }
func (bed *BaseEntryDetails) UnmarshalData(entry *MizuEntry) error { func (bed *BaseEntryDetails) UnmarshalData(entry *MizuEntry) error {
entryUrl := entry.Url bed.Protocol = Protocol{
service := entry.Service Name: entry.ProtocolName,
LongName: entry.ProtocolLongName,
Abbreviation: entry.ProtocolAbbreviation,
Version: entry.ProtocolVersion,
BackgroundColor: entry.ProtocolBackgroundColor,
ForegroundColor: entry.ProtocolForegroundColor,
FontSize: entry.ProtocolFontSize,
ReferenceLink: entry.ProtocolReferenceLink,
}
bed.Id = entry.EntryId bed.Id = entry.EntryId
bed.Url = entryUrl bed.Url = entry.Url
bed.Service = service bed.Service = entry.Service
bed.Summary = entry.Path bed.Summary = entry.Path
bed.StatusCode = entry.Status bed.StatusCode = entry.Status
bed.Method = entry.Method bed.Method = entry.Method

View File

@ -267,25 +267,31 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, entryId string, resolve
request["url"] = summary request["url"] = summary
entryBytes, _ := json.Marshal(item.Pair) entryBytes, _ := json.Marshal(item.Pair)
return &api.MizuEntry{ return &api.MizuEntry{
ProtocolName: protocol.Name, ProtocolName: protocol.Name,
ProtocolVersion: protocol.Version, ProtocolLongName: protocol.LongName,
EntryId: entryId, ProtocolAbbreviation: protocol.Abbreviation,
Entry: string(entryBytes), ProtocolVersion: protocol.Version,
Url: fmt.Sprintf("%s%s", service, summary), ProtocolBackgroundColor: protocol.BackgroundColor,
Method: request["method"].(string), ProtocolForegroundColor: protocol.ForegroundColor,
Status: 0, ProtocolFontSize: protocol.FontSize,
RequestSenderIp: item.ConnectionInfo.ClientIP, ProtocolReferenceLink: protocol.ReferenceLink,
Service: service, EntryId: entryId,
Timestamp: item.Timestamp, Entry: string(entryBytes),
ElapsedTime: 0, Url: fmt.Sprintf("%s%s", service, summary),
Path: summary, Method: request["method"].(string),
ResolvedSource: resolvedSource, Status: 0,
ResolvedDestination: resolvedDestination, RequestSenderIp: item.ConnectionInfo.ClientIP,
SourceIp: item.ConnectionInfo.ClientIP, Service: service,
DestinationIp: item.ConnectionInfo.ServerIP, Timestamp: item.Timestamp,
SourcePort: item.ConnectionInfo.ClientPort, ElapsedTime: 0,
DestinationPort: item.ConnectionInfo.ServerPort, Path: summary,
IsOutgoing: item.ConnectionInfo.IsOutgoing, ResolvedSource: resolvedSource,
ResolvedDestination: resolvedDestination,
SourceIp: item.ConnectionInfo.ClientIP,
DestinationIp: item.ConnectionInfo.ServerIP,
SourcePort: item.ConnectionInfo.ClientPort,
DestinationPort: item.ConnectionInfo.ServerPort,
IsOutgoing: item.ConnectionInfo.IsOutgoing,
} }
} }

View File

@ -172,25 +172,31 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, entryId string, resolve
elapsedTime := item.Pair.Response.CaptureTime.Sub(item.Pair.Request.CaptureTime).Round(time.Millisecond).Milliseconds() elapsedTime := item.Pair.Response.CaptureTime.Sub(item.Pair.Request.CaptureTime).Round(time.Millisecond).Milliseconds()
entryBytes, _ := json.Marshal(item.Pair) entryBytes, _ := json.Marshal(item.Pair)
return &api.MizuEntry{ return &api.MizuEntry{
ProtocolName: protocol.Name, ProtocolName: protocol.Name,
ProtocolVersion: item.Protocol.Version, ProtocolLongName: protocol.LongName,
EntryId: entryId, ProtocolAbbreviation: protocol.Abbreviation,
Entry: string(entryBytes), ProtocolVersion: item.Protocol.Version,
Url: fmt.Sprintf("%s%s", service, path), ProtocolBackgroundColor: protocol.BackgroundColor,
Method: reqDetails["method"].(string), ProtocolForegroundColor: protocol.ForegroundColor,
Status: int(resDetails["status"].(float64)), ProtocolFontSize: protocol.FontSize,
RequestSenderIp: item.ConnectionInfo.ClientIP, ProtocolReferenceLink: protocol.ReferenceLink,
Service: service, EntryId: entryId,
Timestamp: item.Timestamp, Entry: string(entryBytes),
ElapsedTime: elapsedTime, Url: fmt.Sprintf("%s%s", service, path),
Path: path, Method: reqDetails["method"].(string),
ResolvedSource: resolvedSource, Status: int(resDetails["status"].(float64)),
ResolvedDestination: resolvedDestination, RequestSenderIp: item.ConnectionInfo.ClientIP,
SourceIp: item.ConnectionInfo.ClientIP, Service: service,
DestinationIp: item.ConnectionInfo.ServerIP, Timestamp: item.Timestamp,
SourcePort: item.ConnectionInfo.ClientPort, ElapsedTime: elapsedTime,
DestinationPort: item.ConnectionInfo.ServerPort, Path: path,
IsOutgoing: item.ConnectionInfo.IsOutgoing, ResolvedSource: resolvedSource,
ResolvedDestination: resolvedDestination,
SourceIp: item.ConnectionInfo.ClientIP,
DestinationIp: item.ConnectionInfo.ServerIP,
SourcePort: item.ConnectionInfo.ClientPort,
DestinationPort: item.ConnectionInfo.ServerPort,
IsOutgoing: item.ConnectionInfo.IsOutgoing,
} }
} }

View File

@ -142,25 +142,31 @@ func (d dissecting) Analyze(item *api.OutputChannelItem, entryId string, resolve
elapsedTime := item.Pair.Response.CaptureTime.Sub(item.Pair.Request.CaptureTime).Round(time.Millisecond).Milliseconds() elapsedTime := item.Pair.Response.CaptureTime.Sub(item.Pair.Request.CaptureTime).Round(time.Millisecond).Milliseconds()
entryBytes, _ := json.Marshal(item.Pair) entryBytes, _ := json.Marshal(item.Pair)
return &api.MizuEntry{ return &api.MizuEntry{
ProtocolName: _protocol.Name, ProtocolName: _protocol.Name,
ProtocolVersion: _protocol.Version, ProtocolLongName: _protocol.LongName,
EntryId: entryId, ProtocolAbbreviation: _protocol.Abbreviation,
Entry: string(entryBytes), ProtocolVersion: _protocol.Version,
Url: fmt.Sprintf("%s%s", service, summary), ProtocolBackgroundColor: _protocol.BackgroundColor,
Method: apiNames[apiKey], ProtocolForegroundColor: _protocol.ForegroundColor,
Status: 0, ProtocolFontSize: _protocol.FontSize,
RequestSenderIp: item.ConnectionInfo.ClientIP, ProtocolReferenceLink: _protocol.ReferenceLink,
Service: service, EntryId: entryId,
Timestamp: item.Timestamp, Entry: string(entryBytes),
ElapsedTime: elapsedTime, Url: fmt.Sprintf("%s%s", service, summary),
Path: summary, Method: apiNames[apiKey],
ResolvedSource: resolvedSource, Status: 0,
ResolvedDestination: resolvedDestination, RequestSenderIp: item.ConnectionInfo.ClientIP,
SourceIp: item.ConnectionInfo.ClientIP, Service: service,
DestinationIp: item.ConnectionInfo.ServerIP, Timestamp: item.Timestamp,
SourcePort: item.ConnectionInfo.ClientPort, ElapsedTime: elapsedTime,
DestinationPort: item.ConnectionInfo.ServerPort, Path: summary,
IsOutgoing: item.ConnectionInfo.IsOutgoing, ResolvedSource: resolvedSource,
ResolvedDestination: resolvedDestination,
SourceIp: item.ConnectionInfo.ClientIP,
DestinationIp: item.ConnectionInfo.ServerIP,
SourcePort: item.ConnectionInfo.ClientPort,
DestinationPort: item.ConnectionInfo.ServerPort,
IsOutgoing: item.ConnectionInfo.IsOutgoing,
} }
} }

22792
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,8 +12,8 @@
"@types/node": "^12.20.10", "@types/node": "^12.20.10",
"@types/react": "^17.0.3", "@types/react": "^17.0.3",
"@types/react-dom": "^17.0.3", "@types/react-dom": "^17.0.3",
"jsonpath": "^1.1.1",
"axios": "^0.21.1", "axios": "^0.21.1",
"jsonpath": "^1.1.1",
"node-sass": "^5.0.0", "node-sass": "^5.0.0",
"numeral": "^2.0.6", "numeral": "^2.0.6",
"protobuf-decoder": "^0.1.0", "protobuf-decoder": "^0.1.0",
@ -21,7 +21,7 @@
"react-copy-to-clipboard": "^5.0.3", "react-copy-to-clipboard": "^5.0.3",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"react-scrollable-feed": "^1.3.0", "react-scrollable-feed-virtualized": "^1.4.2",
"react-syntax-highlighter": "^15.4.3", "react-syntax-highlighter": "^15.4.3",
"typescript": "^4.2.4", "typescript": "^4.2.4",
"web-vitals": "^1.1.1" "web-vitals": "^1.1.1"

View File

@ -2,7 +2,7 @@ import {EntryItem} from "./EntryListItem/EntryListItem";
import React, {useCallback, useEffect, useMemo, useRef, useState} from "react"; import React, {useCallback, useEffect, useMemo, useRef, useState} from "react";
import styles from './style/EntriesList.module.sass'; import styles from './style/EntriesList.module.sass';
import spinner from './assets/spinner.svg'; import spinner from './assets/spinner.svg';
import ScrollableFeed from "react-scrollable-feed"; import ScrollableFeedVirtualized from "react-scrollable-feed-virtualized";
import {StatusType} from "./Filters"; import {StatusType} from "./Filters";
import Api from "../helpers/api"; import Api from "../helpers/api";
import down from "./assets/downImg.svg"; import down from "./assets/downImg.svg";
@ -77,9 +77,6 @@ export const EntriesList: React.FC<EntriesListProps> = ({entries, setEntries, fo
} }
setIsLoadingTop(false); setIsLoadingTop(false);
const newEntries = [...data, ...entries]; const newEntries = [...data, ...entries];
if(newEntries.length >= 1000) {
newEntries.splice(1000);
}
setEntries(newEntries); setEntries(newEntries);
if(scrollTo) { if(scrollTo) {
@ -100,10 +97,6 @@ export const EntriesList: React.FC<EntriesListProps> = ({entries, setEntries, fo
} }
scrollTo = document.getElementById(filteredEntries?.[filteredEntries.length -1]?.id); scrollTo = document.getElementById(filteredEntries?.[filteredEntries.length -1]?.id);
let newEntries = [...entries, ...data]; let newEntries = [...entries, ...data];
if(newEntries.length >= 1000) {
setNoMoreDataTop(false);
newEntries = newEntries.slice(-1000);
}
setEntries(newEntries); setEntries(newEntries);
if(scrollTo) { if(scrollTo) {
scrollTo.scrollIntoView({behavior: "smooth"}); scrollTo.scrollIntoView({behavior: "smooth"});
@ -116,19 +109,20 @@ export const EntriesList: React.FC<EntriesListProps> = ({entries, setEntries, fo
{isLoadingTop && <div className={styles.spinnerContainer}> {isLoadingTop && <div className={styles.spinnerContainer}>
<img alt="spinner" src={spinner} style={{height: 25}}/> <img alt="spinner" src={spinner} style={{height: 25}}/>
</div>} </div>}
<ScrollableFeed ref={scrollableRef} onScroll={(isAtBottom) => onScrollEvent(isAtBottom)}> <ScrollableFeedVirtualized ref={scrollableRef} itemHeight={48} marginTop={10} onScroll={(isAtBottom) => onScrollEvent(isAtBottom)}>
{noMoreDataTop && !connectionOpen && <div id="noMoreDataTop" className={styles.noMoreDataAvailable}>No more data available</div>} {noMoreDataTop && !connectionOpen && <div id="noMoreDataTop" className={styles.noMoreDataAvailable}>No more data available</div>}
{filteredEntries.map(entry => <EntryItem key={entry.id} {filteredEntries.map(entry => <EntryItem key={entry.id}
entry={entry} entry={entry}
setFocusedEntryId={setFocusedEntryId} setFocusedEntryId={setFocusedEntryId}
isSelected={focusedEntryId === entry.id}/>)} isSelected={focusedEntryId === entry.id}
{!connectionOpen && !noMoreDataBottom && <div className={styles.fetchButtonContainer}> style={{}}/>)}
<div className={styles.styledButton} onClick={() => getNewEntries()}>Fetch more entries</div> </ScrollableFeedVirtualized>
</div>} {!connectionOpen && !noMoreDataBottom && <div className={styles.fetchButtonContainer}>
</ScrollableFeed> <div className={styles.styledButton} onClick={() => getNewEntries()}>Fetch more entries</div>
<button type="button" </div>}
className={`${styles.btnLive} ${scrollableList ? styles.showButton : styles.hideButton}`} <button type="button"
onClick={(_) => scrollableRef.current.scrollToBottom()}> className={`${styles.btnLive} ${scrollableList ? styles.showButton : styles.hideButton}`}
onClick={(_) => scrollableRef.current.jumpToBottom()}>
<img alt="down" src={down} /> <img alt="down" src={down} />
</button> </button>
</div> </div>

View File

@ -23,7 +23,7 @@ interface Entry {
sourcePort: string, sourcePort: string,
destinationIp: string, destinationIp: string,
destinationPort: string, destinationPort: string,
isOutgoing?: boolean; isOutgoing?: boolean;
latency: number; latency: number;
rules: Rules; rules: Rules;
} }
@ -38,9 +38,10 @@ interface EntryProps {
entry: Entry; entry: Entry;
setFocusedEntryId: (id: string) => void; setFocusedEntryId: (id: string) => void;
isSelected?: boolean; isSelected?: boolean;
style: object;
} }
export const EntryItem: React.FC<EntryProps> = ({entry, setFocusedEntryId, isSelected}) => { export const EntryItem: React.FC<EntryProps> = ({entry, setFocusedEntryId, isSelected, style}) => {
const classification = getClassification(entry.statusCode) const classification = getClassification(entry.statusCode)
let ingoingIcon; let ingoingIcon;
let outgoingIcon; let outgoingIcon;
@ -103,7 +104,13 @@ export const EntryItem: React.FC<EntryProps> = ({entry, setFocusedEntryId, isSel
className={`${styles.row} className={`${styles.row}
${isSelected ? styles.rowSelected : backgroundColor}`} ${isSelected ? styles.rowSelected : backgroundColor}`}
onClick={() => setFocusedEntryId(entry.id)} onClick={() => setFocusedEntryId(entry.id)}
style={{border: isSelected ? `1px ${entry.protocol.backgroundColor} solid` : "1px transparent solid"}} style={{
border: isSelected ? `1px ${entry.protocol.backgroundColor} solid` : "1px transparent solid",
position: "absolute",
top: style['top'],
marginTop: style['marginTop'],
width: "calc(100% - 25px)",
}}
> >
<Protocol protocol={entry.protocol} horizontal={false}/> <Protocol protocol={entry.protocol} horizontal={false}/>
{((entry.protocol.name === "http" && "statusCode" in entry) || entry.statusCode !== 0) && <div> {((entry.protocol.name === "http" && "statusCode" in entry) || entry.statusCode !== 0) && <div>

View File

@ -86,10 +86,6 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus, onTLS
} }
if (!focusedEntryId) setFocusedEntryId(entry.id) if (!focusedEntryId) setFocusedEntryId(entry.id)
let newEntries = [...entries]; let newEntries = [...entries];
if (entries.length === 1000) {
newEntries = newEntries.splice(1);
setNoMoreDataTop(false);
}
setEntries([...newEntries, entry]) setEntries([...newEntries, entry])
if(listEntry.current) { if(listEntry.current) {
if(isScrollable(listEntry.current.firstChild)) { if(isScrollable(listEntry.current.firstChild)) {

View File

@ -14,7 +14,6 @@
flex-direction: column flex-direction: column
overflow: hidden overflow: hidden
flex-grow: 1 flex-grow: 1
padding-top: 20px
.footer .footer
display: flex display: flex