mirror of
https://codeberg.org/demostf/inspector.git
synced 2026-06-03 10:04:09 +02:00
filterbar
This commit is contained in:
parent
69d1318a43
commit
aa20564ccd
6 changed files with 291 additions and 26 deletions
|
|
@ -4,6 +4,7 @@ import {useDropzone} from 'react-dropzone'
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import {Header} from "./header";
|
import {Header} from "./header";
|
||||||
import {PacketDetails, PacketTable} from "./table";
|
import {PacketDetails, PacketTable} from "./table";
|
||||||
|
import {filterPacket, Search, SearchBar} from "./search";
|
||||||
|
|
||||||
let _style = require('../styles/style.css');
|
let _style = require('../styles/style.css');
|
||||||
|
|
||||||
|
|
@ -11,10 +12,11 @@ interface AppState {
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
header: Header | null,
|
header: Header | null,
|
||||||
packets: Packet[],
|
packets: Packet[],
|
||||||
prop_names: Map<number, { table: String, prop: String }>,
|
prop_names: Map<number, { table: string, prop: string }>,
|
||||||
class_names: Map<number, String>,
|
class_names: Map<number, string>,
|
||||||
active: Packet | null,
|
active: Packet | null,
|
||||||
activeIndex: number | null,
|
activeIndex: number | null,
|
||||||
|
search: Search,
|
||||||
}
|
}
|
||||||
|
|
||||||
class App extends Component<{}, AppState> {
|
class App extends Component<{}, AppState> {
|
||||||
|
|
@ -26,7 +28,15 @@ class App extends Component<{}, AppState> {
|
||||||
class_names: new Map(),
|
class_names: new Map(),
|
||||||
active: null,
|
active: null,
|
||||||
activeIndex: null,
|
activeIndex: null,
|
||||||
|
search: {
|
||||||
|
entity: 0,
|
||||||
|
filter: "",
|
||||||
|
classIds: [],
|
||||||
|
propIds: [],
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch = debounce((search: Search) => this.setState({search}), 500)
|
||||||
|
|
||||||
load(data: ArrayBuffer) {
|
load(data: ArrayBuffer) {
|
||||||
this.setState({loading: true});
|
this.setState({loading: true});
|
||||||
|
|
@ -65,6 +75,14 @@ class App extends Component<{}, AppState> {
|
||||||
worker.postMessage(data, [data]);
|
worker.postMessage(data, [data]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filteredPackets(): Packet[] {
|
||||||
|
if (this.state.search.filter || this.state.search.entity) {
|
||||||
|
return this.state.packets.filter(packet => filterPacket(packet, this.state.search));
|
||||||
|
} else {
|
||||||
|
return this.state.packets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.loading && this.state.header && this.state.packets.length) {
|
if (this.state.loading && this.state.header && this.state.packets.length) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -83,16 +101,20 @@ class App extends Component<{}, AppState> {
|
||||||
let active = <></>;
|
let active = <></>;
|
||||||
if (this.state.active) {
|
if (this.state.active) {
|
||||||
active = <div className="details"><PacketDetails packet={this.state.active}
|
active = <div className="details"><PacketDetails packet={this.state.active}
|
||||||
|
search={this.state.search}
|
||||||
prop_names={this.state.prop_names}
|
prop_names={this.state.prop_names}
|
||||||
class_names={this.state.class_names}/></div>
|
class_names={this.state.class_names}/></div>
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PacketTable packets={this.state.packets} class_names={this.state.class_names}
|
<SearchBar onSearch={this.onSearch} class_names={this.state.class_names} prop_names={this.state.prop_names}/>
|
||||||
|
<div className="packets">
|
||||||
|
<PacketTable packets={this.filteredPackets()} class_names={this.state.class_names}
|
||||||
activeIndex={this.state.activeIndex}
|
activeIndex={this.state.activeIndex}
|
||||||
prop_names={this.state.prop_names}
|
prop_names={this.state.prop_names}
|
||||||
onClick={(activeIndex, active) => this.setState({activeIndex, active})}/>
|
onClick={(activeIndex, active) => this.setState({activeIndex, active})}/>
|
||||||
{active}
|
{active}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -142,3 +164,11 @@ function DemoDropzone(
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function debounce(func: Function, timeout = 300){
|
||||||
|
let timer: any;
|
||||||
|
return (...args:any[]) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => { func.apply(this, args); }, timeout);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import {GameEventDefinition, Message, PacketEntity, SendPropValue} from "../parser";
|
import {EventInfo, GameEventDefinition, Message, PacketEntity, SendPropValue} from "../parser";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import {filterEntity, filterMessage, Search} from "../search";
|
||||||
|
|
||||||
export interface MessageInfoProps {
|
export interface MessageInfoProps {
|
||||||
msg: Message,
|
msg: Message,
|
||||||
prop_names: Map<number, { table: String, prop: String }>,
|
prop_names: Map<number, { table: String, prop: String }>,
|
||||||
class_names: Map<number, String>
|
class_names: Map<number, String>,
|
||||||
|
search: Search
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MessageInfo({msg, prop_names, class_names}: MessageInfoProps) {
|
export function MessageInfo({msg, prop_names, class_names, search}: MessageInfoProps) {
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case "Print":
|
case "Print":
|
||||||
return <>{msg.value}</>
|
return <>{msg.value}</>
|
||||||
|
|
@ -29,7 +31,8 @@ export function MessageInfo({msg, prop_names, class_names}: MessageInfoProps) {
|
||||||
case "GameEventList":
|
case "GameEventList":
|
||||||
return <>{msg.event_list.map(formatEventDefinition).map((str, i) => (<p key={i}>{str}</p>))}</>
|
return <>{msg.event_list.map(formatEventDefinition).map((str, i) => (<p key={i}>{str}</p>))}</>
|
||||||
case "PacketEntities":
|
case "PacketEntities":
|
||||||
let entities = msg.entities.map(entity => formatEntity(entity, prop_names, class_names)).map((str, i) => <p
|
let entities = filteredEntities(msg.entities, search).map(entity => formatEntity(entity, prop_names, class_names)).map((str, i) =>
|
||||||
|
<p
|
||||||
key={i}>{str}</p>);
|
key={i}>{str}</p>);
|
||||||
let deleted = <></>
|
let deleted = <></>
|
||||||
if (msg.removed_entities.length > 0) {
|
if (msg.removed_entities.length > 0) {
|
||||||
|
|
@ -44,7 +47,7 @@ export function MessageInfo({msg, prop_names, class_names}: MessageInfoProps) {
|
||||||
{deleted}
|
{deleted}
|
||||||
</>
|
</>
|
||||||
case "TempEntities":
|
case "TempEntities":
|
||||||
let events = msg.events.map(event => {
|
let events = filteredTempEntities(msg.events, search).map(event => {
|
||||||
let class_name = class_names.get(event.class_id);
|
let class_name = class_names.get(event.class_id);
|
||||||
let props = event.props.map(prop => {
|
let props = event.props.map(prop => {
|
||||||
let names = prop_names.get(prop.identifier);
|
let names = prop_names.get(prop.identifier);
|
||||||
|
|
@ -92,3 +95,25 @@ function formatEventDefinition(event: GameEventDefinition): string {
|
||||||
let values = event.entries.map(entry => `${entry.name}: ${entry.kind}`);
|
let values = event.entries.map(entry => `${entry.name}: ${entry.kind}`);
|
||||||
return `${event.event_type}{${values.join(', ')}}`;
|
return `${event.event_type}{${values.join(', ')}}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filteredEntities(entities: PacketEntity[], search: Search) {
|
||||||
|
if (search.filter || search.entity) {
|
||||||
|
return entities.filter(entities => {
|
||||||
|
return (search.entity == 0 || search.entity == entities.entity_index) &&
|
||||||
|
(search.filter.length < 3 || filterEntity(entities.server_class, entities.props, search))
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filteredTempEntities(entities: EventInfo[], search: Search) {
|
||||||
|
if (search.entity) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (search.filter) {
|
||||||
|
return entities.filter(entities => filterEntity(entities.class_id, entities.props, search));
|
||||||
|
} else {
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
www/src/search.css
Normal file
25
www/src/search.css
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
.search > form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin: 0 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 150px;
|
||||||
|
margin: 0 5px;
|
||||||
|
height: 24px;
|
||||||
|
border-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
border-bottom: 1px solid #888;
|
||||||
|
}
|
||||||
165
www/src/search.tsx
Normal file
165
www/src/search.tsx
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
import React, {ChangeEvent, Component} from "react";
|
||||||
|
|
||||||
|
import './search.css'
|
||||||
|
import {Message, Packet, SendProp, StringTable} from "./parser";
|
||||||
|
|
||||||
|
export interface Search {
|
||||||
|
filter: string,
|
||||||
|
entity: number,
|
||||||
|
propIds: number[],
|
||||||
|
classIds: number[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchBarProps {
|
||||||
|
onSearch: (search: Search) => void,
|
||||||
|
prop_names: Map<number, { table: string, prop: string }>,
|
||||||
|
class_names: Map<number, string>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchBarState {
|
||||||
|
filter: string,
|
||||||
|
entity: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SearchBar extends Component<SearchBarProps, SearchBarState> {
|
||||||
|
state: SearchBarState = {
|
||||||
|
filter: "",
|
||||||
|
entity: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
getSearch(): Search {
|
||||||
|
return {
|
||||||
|
filter: this.state.filter,
|
||||||
|
entity: this.state.entity,
|
||||||
|
propIds: filterPropNames(this.props.prop_names, this.state.filter),
|
||||||
|
classIds: filterClassNames(this.props.class_names, this.state.filter),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilter(event: ChangeEvent<HTMLInputElement>) {
|
||||||
|
let filter = event.target.value;
|
||||||
|
this.setState({filter});
|
||||||
|
setTimeout(() => this.props.onSearch(this.getSearch()), 1);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onEntity(event: ChangeEvent<HTMLInputElement>) {
|
||||||
|
let entity = parseInt(event.target.value, 10);
|
||||||
|
this.setState({entity});
|
||||||
|
setTimeout(() => this.props.onSearch(this.getSearch()), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="search">
|
||||||
|
<form>
|
||||||
|
<label className="filter">
|
||||||
|
Filter
|
||||||
|
<input onInput={this.onFilter.bind(this)}/>
|
||||||
|
</label>
|
||||||
|
<label className="entity">
|
||||||
|
Entity
|
||||||
|
<input onInput={this.onEntity.bind(this)} type="number"/>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function filterPacket(
|
||||||
|
packet: Packet,
|
||||||
|
search: Search,
|
||||||
|
): boolean {
|
||||||
|
switch (packet.type) {
|
||||||
|
case "Sigon":
|
||||||
|
case "Message":
|
||||||
|
return packet.messages.some(msg => filterMessage(msg, search))
|
||||||
|
case "SyncTick":
|
||||||
|
return false;
|
||||||
|
case "ConsoleCmd":
|
||||||
|
return search.entity == 0 && packet.command.includes(search.filter);
|
||||||
|
case "UserCmd":
|
||||||
|
return false;
|
||||||
|
case "DataTables":
|
||||||
|
return false;
|
||||||
|
case "Stop":
|
||||||
|
return false;
|
||||||
|
case "StringTables":
|
||||||
|
return search.entity == 0 && packet.tables.some(table => filterStringTable(table, search));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterPropNames(prop_names: Map<number, { table: string, prop: string }>, filter: string): number[] {
|
||||||
|
let ids = [];
|
||||||
|
for (let [id, {table, prop}] of prop_names.entries()) {
|
||||||
|
if (table.toLowerCase().includes(filter) || prop.toLowerCase().includes(filter)) {
|
||||||
|
ids.push(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterClassNames(class_names: Map<number, string>, filter: string): number[] {
|
||||||
|
let ids = [];
|
||||||
|
for (let [id, name] of class_names.entries()) {
|
||||||
|
if (name.toLowerCase().includes(filter)) {
|
||||||
|
ids.push(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterMessage(
|
||||||
|
message: Message,
|
||||||
|
search: Search,
|
||||||
|
): boolean {
|
||||||
|
switch (message.type) {
|
||||||
|
case "File":
|
||||||
|
return search.entity == 0 && message.file_name.includes(search.filter);
|
||||||
|
case "StringCmd":
|
||||||
|
return search.entity == 0 && message.command.includes(search.filter);
|
||||||
|
case "SetConVar":
|
||||||
|
return search.entity == 0 && message.vars.some(cvar => cvar.value.includes(search.filter) || cvar.key.includes(search.filter));
|
||||||
|
case "Print":
|
||||||
|
return search.entity == 0 && message.value.includes(search.filter);
|
||||||
|
case "ClassInfo":
|
||||||
|
return search.entity == 0 && message.entries.some(entry => entry.class_name.includes(search.filter) || entry.table_name.includes(search.filter));
|
||||||
|
case "CreateStringTable":
|
||||||
|
return search.entity == 0 && filterStringTable(message.table, search);
|
||||||
|
case "UpdateStringTable":
|
||||||
|
return search.entity == 0 && message.entries.some(([_index, entry]) => (entry.text && entry.text.includes(search.filter)));
|
||||||
|
case "SetView":
|
||||||
|
return search.entity == 0 && message.index === search.entity;
|
||||||
|
case "SayText2":
|
||||||
|
return search.entity == 0 && ((message.text && message.text.includes(search.filter)) || (message.from && message.from.includes(search.filter)));
|
||||||
|
case "Text":
|
||||||
|
return search.entity == 0 && message.text.includes(search.filter);
|
||||||
|
case "EntityMessage":
|
||||||
|
return search.entity == 0 && search.classIds.includes(message.class_id)
|
||||||
|
case "GameEvent":
|
||||||
|
return search.entity == 0 && message.event.type.includes(search.filter)
|
||||||
|
case "PacketEntities":
|
||||||
|
return message.removed_entities.includes(search.entity) || message.entities.some(entity => (search.entity == 0 || entity.entity_index == search.entity) && filterEntity(entity.server_class, entity.props, search))
|
||||||
|
case "TempEntities":
|
||||||
|
return search.entity == 0 && message.events.some(event => filterEntity(event.class_id, event.props, search))
|
||||||
|
case "GetCvarValue":
|
||||||
|
return search.entity == 0 && message.value.includes(search.filter);
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterEntity(class_id: number, props: SendProp[], search: Search): boolean {
|
||||||
|
return search.classIds.includes(class_id) || props.some(prop => search.propIds.includes(prop.identifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterStringTable(table: StringTable, search: Search): boolean {
|
||||||
|
if (table.name.includes(search.filter)) {
|
||||||
|
return true;
|
||||||
|
} else if (table.entries.some(([_index, entry]) => entry.text.includes(search.filter))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
@ -3,11 +3,12 @@ import {GameEventDefinition, Message, Packet, PacketEntity, SendPropValue, UserC
|
||||||
import {FixedSizeList as List} from 'react-window';
|
import {FixedSizeList as List} from 'react-window';
|
||||||
import {MessageInfo} from "./packets/message";
|
import {MessageInfo} from "./packets/message";
|
||||||
import {UserCmdDetails} from "./packets/usercmd";
|
import {UserCmdDetails} from "./packets/usercmd";
|
||||||
|
import {filterMessage, filterPacket, Search} from "./search";
|
||||||
|
|
||||||
interface TableProps {
|
interface TableProps {
|
||||||
packets: Packet[],
|
packets: Packet[],
|
||||||
prop_names: Map<number, { table: String, prop: String }>,
|
prop_names: Map<number, { table: string, prop: string }>,
|
||||||
class_names: Map<number, String>,
|
class_names: Map<number, string>,
|
||||||
onClick: (i: number, packet: Packet) => void,
|
onClick: (i: number, packet: Packet) => void,
|
||||||
activeIndex: number | null,
|
activeIndex: number | null,
|
||||||
}
|
}
|
||||||
|
|
@ -23,7 +24,7 @@ export function PacketTable({packets, prop_names, class_names, onClick, activeIn
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<List className="list" height={window.innerHeight} itemCount={packets.length} itemSize={30} width={210}>
|
<List className="list" height={window.innerHeight - 31} itemCount={packets.length} itemSize={30} width={210}>
|
||||||
{Row}
|
{Row}
|
||||||
</List>
|
</List>
|
||||||
</>
|
</>
|
||||||
|
|
@ -77,17 +78,26 @@ export function PacketRow({packet}: RowProps) {
|
||||||
|
|
||||||
interface DetailProps {
|
interface DetailProps {
|
||||||
packet: Packet,
|
packet: Packet,
|
||||||
prop_names: Map<number, { table: String, prop: String }>,
|
prop_names: Map<number, { table: string, prop: string }>,
|
||||||
class_names: Map<number, String>,
|
class_names: Map<number, string>,
|
||||||
|
search: Search,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PacketDetails({packet, prop_names, class_names}: DetailProps) {
|
function filteredMessages(messages: Message[], search: Search) {
|
||||||
|
if (search.filter || search.entity) {
|
||||||
|
return messages.filter(message => filterMessage(message, search));
|
||||||
|
} else {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PacketDetails({packet, prop_names, class_names, search}: DetailProps) {
|
||||||
switch (packet.type) {
|
switch (packet.type) {
|
||||||
case "Sigon":
|
case "Sigon":
|
||||||
case "Message":
|
case "Message":
|
||||||
let rows = packet.messages.map((message, y) => <tr key={y}>
|
let rows = filteredMessages(packet.messages, search).map((message, y) => <tr key={y}>
|
||||||
<td className="type">{message.type}</td>
|
<td className="type">{message.type}</td>
|
||||||
<td><MessageInfo msg={message} prop_names={prop_names} class_names={class_names}/></td>
|
<td><MessageInfo msg={message} prop_names={prop_names} class_names={class_names} search={search}/></td>
|
||||||
</tr>)
|
</tr>)
|
||||||
return (
|
return (
|
||||||
<table>
|
<table>
|
||||||
|
|
|
||||||
|
|
@ -10,19 +10,27 @@ span.type {
|
||||||
}
|
}
|
||||||
|
|
||||||
div.details {
|
div.details {
|
||||||
display: inline-block;
|
flex-grow: 1;
|
||||||
width: calc(100vw - 210px);
|
|
||||||
height: 100vh;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
width: calc(100vw - 210px);
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.list {
|
div.list {
|
||||||
display: inline-block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.packets {
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
|
|
@ -51,9 +59,11 @@ html, body, #root {
|
||||||
|
|
||||||
.prop_row {
|
.prop_row {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: #ccc;
|
background: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue