Joystream Stats 2 lat temu
rodzic
commit
60da7c09b2

+ 3 - 2
package.json

@@ -17,6 +17,7 @@
     "i18next": "^20.4.0",
     "i18next-browser-languagedetector": "^6.1.2",
     "interactjs": "^1.10.2",
+    "moment": "^2.29.1",
     "pako": "^2.0.4",
     "react": "^17.0.1",
     "react-beautiful-dnd": "^13.1.0",
@@ -59,7 +60,6 @@
     ]
   },
   "devDependencies": {
-    "typescript": "^4.4.0",
     "@types/bootstrap": "^5.0.1",
     "@types/jest": "^26.0.15",
     "@types/node": "^12.0.0",
@@ -71,6 +71,7 @@
     "@types/react-calendar-timeline": "^0.26.3",
     "@types/react-dom": "^16.9.8",
     "@types/react-router-dom": "^5.1.6",
-    "@types/styled-components": "^5.1.12"
+    "@types/styled-components": "^5.1.12",
+    "typescript": "^4.4.3"
   }
 }

+ 45 - 21
src/App.tsx

@@ -50,7 +50,7 @@ class App extends React.Component<IProps, IState> {
   async updateStatus(api: ApiPromise, id: number): Promise<Status> {
     console.debug(`#${id}: Updating status`);
     //this.updateActiveProposals();
-    getTokenomics().then((tokenomics) => this.save(`tokenomics`, tokenomics));
+    //getTokenomics().then((tokenomics) => this.save(`tokenomics`, tokenomics));
 
     let { status, councils } = this.state;
     //status.election = await updateElection(api);
@@ -253,6 +253,8 @@ class App extends React.Component<IProps, IState> {
           toggleFooter={this.toggleFooter}
           toggleStar={this.toggleStar}
           getMember={this.getMember}
+          save={this.save}
+          hidden={this.state.hidden}
           {...this.state}
         />
 
@@ -282,18 +284,20 @@ class App extends React.Component<IProps, IState> {
   async handleBlock(api: ApiPromise, header: Header) {
     let { status } = this.state;
     const id = header.number.toNumber();
-    const isEven = id / 50 === Math.floor(id / 50);
-    if (isEven || status.block?.id + 50 < id) this.updateStatus(api, id);
+
+    //const isEven = id / 50 === Math.floor(id / 50);
+    //if (isEven || status.block?.id + 50 < id) this.updateStatus(api, id);
     if (this.state.blocks.find((b) => b.id === id)) return;
+
     const timestamp = (await api.query.timestamp.now()).toNumber();
-    const duration = status.block ? timestamp - status.block.timestamp : 6000;
+    //const duration = status.block ? timestamp - status.block.timestamp : 6000;
     const hash = await getBlockHash(api, id);
     const events = (await getEvents(api, hash)).map((e) => {
       const { section, method, data } = e.event;
       return { blockId: id, section, method, data: data.toHuman() };
     });
-    status.block = { id, timestamp, duration, events };
-    console.debug(`new finalized head`, status.block);
+    status.block = { id, timestamp, events };
+    console.info(`new finalized head`, status.block);
     this.save("status", status);
     this.save("blocks", this.state.blocks.concat(status.block));
   }
@@ -322,25 +326,41 @@ class App extends React.Component<IProps, IState> {
   }
 
   async syncBlocks(api:ApiPromise) {
-    const syncAll = true
     const head = this.state.blocks.reduce((max, b) => b.id > max ? b.id : max, 0)
     console.log(`Syncing block events from ${head}`)
-
+    let missing = []
     for (let id = head ; id > 0 ; --id) {
-      if (!syncAll) return
-      if (this.state.blocks.find(block=> block.id === id)) continue
-      
+      if (!this.state.blocks.find(block=> block.id === id)) missing.push(id)
+    }
+    if (!this.state.syncEvents) return
+    const maxWorkers = 5
+    let slots = []
+    for (let s = 0 ; s < maxWorkers ; ++s) { slots[s] = s }
+    slots.map(async (slot) =>{
+      while (this.state.syncEventsl && missing.length) {
+        const id = slot < maxWorkers /2 ? missing.pop() : missing.shift()
+        await this.syncBlock(api, id, slot)
+      }
+      console.debug(`Slot ${slot} idle.`)
+      return true
+    })
+  }
+
+  async syncBlock(api:ApiPromise, id: number, slot: number) {
+     try {
       const hash = await getBlockHash(api, id);
       const events = (await getEvents(api, hash)).map((e) => {
         const { section, method, data } = e.event;
         return { blockId: id, section, method, data: data.toHuman() };
-      });
+      }).filter(e=> e.method !== 'ExtrinsicSuccess')
       const timestamp = (await api.query.timestamp.now.at(hash)).toNumber();
-      const duration = 6000 // TODO update later
-      const block = { id, timestamp, duration, events };
-      console.debug(`synced block`, block);
+      //const duration = 6000 // TODO update later
+      const block = { id, timestamp, events };
+      console.debug(`worker ${slot}: synced block`, block);
       this.save("blocks", this.state.blocks.concat(block));
-    }
+     } catch (e) {
+      console.error(`Failed to get block ${id}: ${e.message}`)
+     }
   }
 
   save(key: string, data: any) {
@@ -351,6 +371,8 @@ class App extends React.Component<IProps, IState> {
     } catch (e) {
       const size = value.length / 1024;
       console.warn(`Failed to save ${key} (${size.toFixed()} KB)`, e.message);
+      if (key === 'blocks') this.load(key)
+      else this.setState({syncEvents:false})
     }
     return data;
   }
@@ -362,9 +384,11 @@ class App extends React.Component<IProps, IState> {
       const size = data.length;
       if (size > 10240)
         console.debug(` -${key}: ${(size / 1024).toFixed(1)} KB`);
-      const value = JSON.parse(data) || [];
-      this.setState({ [key]: value });
-      return value;
+	let loaded = JSON.parse(data)
+	if (key === 'blocks') loaded = loaded.map(({id,timestamp,events}) => {
+         return {id,timestamp,events: events.filter(e=> e.method !== 'ExtrinsicSuccess')}
+	})
+      this.setState({ [key]: loaded  });
     } catch (e) {
       console.warn(`Failed to load ${key}`, e);
     }
@@ -372,12 +396,12 @@ class App extends React.Component<IProps, IState> {
 
   async loadData() {
     console.debug(`Loading data`);
-    "status members assets providers councils council election workers categories channels proposals posts threads openings tokenomics transactions reports validators nominators staches stakes rewardPoints stars blocks"
+    "status members assets providers councils council election workers categories channels proposals posts threads openings tokenomics transactions reports validators nominators staches stakes rewardPoints stars blocks hidden"
       .split(" ")
       .map((key) => this.load(key));
     getTokenomics().then((tokenomics) => this.save(`tokenomics`, tokenomics));
     //bootstrap(this.save); // axios requests
-    this.updateCouncils();
+    //this.updateCouncils();
   }
 
   componentDidMount() {

+ 2 - 1
src/components/Dashboard/index.tsx

@@ -5,7 +5,8 @@ interface IProps extends IState {
 }
 
 const Dashboard = (props: IProps) => {
-  return <Events selectEvent={props.selectEvent} blocks={props.blocks} />
+  const { save, hidden, selectEvent, blocks} = props
+  return <Events save={save} hidden={hidden} selectEvent={selectEvent} blocks={blocks} />
 };
 
 export default Dashboard;

+ 68 - 23
src/components/Events/index.tsx

@@ -1,63 +1,108 @@
-import { useState } from "react";
+import { useState, useMemo } from "react";
 import { Badge, Button } from "react-bootstrap";
 import { IState } from "../../types";
+import moment from 'moment'
 
 interface IProps extends IState {
   blocks: { id: number; events: any }[];
 }
 
-const Events = (props: IProps) => {
-  const [hidden,setHidden] = useState(['ExtrinsicSuccess'])
-  const { blocks, selectEvent } = props;
-
-  // all methods and sections
-  const methods = []
+const getMethodsAndSections = (blocks) => {
   const sections = []
+  const methods = []
   blocks.forEach(block=> block.events?.forEach(event => {
-    if (!methods.includes(event.method)) methods.push(event.method)
     if (!sections.includes(event.section)) sections.push(event.section)
+    if (!methods.includes(event.method)) methods.push(event.method)
   }))
+  return [sections, methods]
+}
 
-  // filter hidden methods and sections
-  const filteredBlocks = blocks
-    .filter((b) => b.timestamp > 164804000000)
-    .filter(b=> b.events?.filter(e=> !hidden.includes(e.section) && !hidden.includes(e.method)).length)
+const applyFilter = (event, filter) => {
+    if (!filter.length) return true
+    if (event.section.includes(filter) || event.method.includes(filter)) return true
+    if (JSON.stringify(event.data).includes(filter)) return true
+    return false
+}
+
+const filterBlocks = (blocks, filter, hidden) => blocks.filter((b) => b.timestamp > 164804000000)
+    .filter(b=> b.events?.filter(e=> !hidden.includes(e.section) && !hidden.includes(e.method) && applyFilter(e, filter)).length)
     .sort((a, b) => b.id - a.id)
 
+const getDays = (blocks) => {
+  const days = {}
+  blocks.forEach(b => {
+    const day = moment(b.timestamp).format('MMM D YYYY')
+    if (!days[day]) days[day] = []
+    days[day].push(b)
+  })
+  return days
+}
+
+const Events = (props: IProps) => {
+  //console.debug(`hidden event sections and methods`, props.hidden)
+  const [hidden,setHidden] = useState(props.hidden)
+  const [filter,setFilter] = useState('')
+  const { blocks, save, selectEvent } = props;
+  const head = blocks.reduce((max, b)=> b.id > max ? b.id : max, 0)
+  
+  const [sections, methods] = useMemo(() => getMethodsAndSections(blocks), [blocks])
+  const filteredBlocks = useMemo(() => filterBlocks(blocks, filter, hidden), [blocks, filter, hidden])
+  const days = useMemo(() => getDays(filteredBlocks), [filteredBlocks])
+
+  const handleChange = (e) => setFilter(e.target.value)
   const toggleHide = (item) => {
-     if (hidden.includes(item)) setHidden(hidden.filter(h=> h !== item))
-     else setHidden(hidden.concat(item))
+     if (hidden.includes(item)) setHidden(save('hidden', hidden.filter(h=> h !== item)))
+     else setHidden(save('hidden',hidden.concat(item)))     
   }
 
   return (
     <div className="p-3 text-light">
+    <div className="box text-left">
+      <div>    
+        <b className="col-1">Search</b> <input type='text' name='filter' value={filter} onChange={handleChange} size={50} />
+ 	<span className="ml-2">
+	  {blocks.length} of {head} blocks synced.
+	</span>
+      </div>
       <div>
-        <div>Synced {blocks.length} blocks.</div>
-        <b>Sections</b>
+        <b className="col-1">Sections</b>
         {sections.map((s,i)=> <Button variant={hidden.includes(s) ? 'outline-dark' : 'dark'} className='btn-sm p-1 m-1 mr-1' key={i} onClick={()=>toggleHide(s)} title={`Click to ${hidden.includes(s) ? `show` : `hide`}`}>{s}</Button>)}
       </div>
       <div>
-        <b>Methods</b>
+        <b className="col-1">Methods</b>
         {methods.map((m,i)=> <Button variant={hidden.includes(m) ? 'outline-dark' : 'dark'} className='btn-sm p-1 m-1 mr-1' key={i} onClick={()=>toggleHide(m)} title={`Click to ${hidden.includes(m) ? `show` : `hide`}`}>{m}</Button>)}
       </div>
-      <div className='mt-2'>
-     {filteredBlocks.map((block) => (
-          <div key={block.id} className="d-flex flex-wrap px-2">
-            #{block.id}
-            {block.events?.filter(e=> !hidden.includes(e.section) && !hidden.includes(e.method)).map((event, index: number) => (
+     </div>
+
+     {Object.keys(days).map((day) =>
+     <div className='mt-2 ml-2 p-1'>
+      <h2 className='col-2 text-right' onClick={() => toggleHide(day)}>{day}</h2>
+      {hidden.includes(day) ? <div/> : (
+       <div className='mt-2'>
+        {days[day].sort((a,b)=>b.id-a.id).map((block) => (
+          <div key={block.id} className="d-flex flex-row px-2">
+           <b className="col-1 text-right">#{block.id}</b>
+	   {moment(block.timestamp).format('HH:mm:ss')}
+           <div key={block.id} className="col-8 d-flex flex-wrap px-2">	    
+            {block.events?.filter(e=> !hidden.includes(e.section) && !hidden.includes(e.method) && applyFilter(e, filter))
+	      .map((event, index: number) => (
               <Badge
                 key={index}
                 variant="success"
-                className="ml-1"
+                className="ml-1 mb-1"
                 title={JSON.stringify(event.data)}
                 onClick={() => selectEvent(event)}
               >
                 {event.section}.{event.method}
               </Badge>
             ))}
+	    </div>
           </div>
         ))}
+       </div>
+      )}       
       </div>
+     )}
     </div>
   );
 };

+ 1 - 0
src/components/Modals/Event.tsx

@@ -36,6 +36,7 @@ const Event = (props) => {
 
 const ObjectValues = (props) => {
   const { object } = props;
+  if (!object) return <div/>
   if (typeof object !== "object") return <div>{object}</div>;
   return Object.keys(object).map((key) => (
     <div key={key} className="d-flex flex-row justify-content-between">

+ 2 - 0
src/state.ts

@@ -29,6 +29,8 @@ export const initialState = {
   stashes: [],
   stars: {},
   hideFooter: true,
+  hidden: ['ExtrinsicSuccess'],
+  syncEvents: true,
   showStatus: false,
   editKpi: false,
   status: { era: 0, block: { id: 0, era: 0, timestamp: 0, duration: 6 } },

+ 5 - 0
yarn.lock

@@ -8803,6 +8803,11 @@ moment@^2.0.0, moment@^2.24.0:
   resolved "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz"
   integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
 
+moment@^2.29.1:
+  version "2.29.1"
+  resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
+  integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
+
 move-concurrently@^1.0.1:
   version "1.0.1"
   resolved "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz"