dbug: implement debug dashboard

This commit is contained in:
Fang 2020-05-08 14:58:40 +02:00
parent ac494a265e
commit 959884c9cd
No known key found for this signature in database
GPG Key ID: EB035760C1BBA972
26 changed files with 9257 additions and 0 deletions

716
pkg/arvo/app/dbug.hoon Normal file
View File

@ -0,0 +1,716 @@
:: dbug: debug dashboard server
::
/- spider
/+ server, default-agent, verb, dbug
::
|%
+$ state-0 [%0 passcode=(unit @t)]
+$ card card:agent:gall
--
::
=| state-0
=* state -
::
%+ verb |
%- agent:dbug
^- agent:gall
=<
|_ =bowl:gall
+* this .
do ~(. +> bowl)
def ~(. (default-agent this %|) bowl)
::
++ on-init
^- (quip card _this)
:_ this
[%pass /connect %arvo %e %connect [~ /'~debug'] dap.bowl]~
::
++ on-save !>(state)
::
++ on-load
|= old=vase
^- (quip card _this)
[~ this(state !<(state-0 old))]
::
++ on-watch
|= =path
^- (quip card _this)
?. ?=([%http-response *] path)
(on-watch:def path)
[~ this]
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?: ?=(%noun mark)
?> (team:title [our src]:bowl)
[~ this(passcode !<((unit @t) vase))]
?. ?=(%handle-http-request mark)
(on-poke:def mark vase)
=+ !<([eyre-id=@ta =inbound-request:eyre] vase)
:: !!
:_ this
%+ give-simple-payload:app:server eyre-id
%+ authorize-http-request:do inbound-request
handle-http-request:do
::
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
?. ?=([%e %bound *] sign-arvo)
(on-arvo:def wire sign-arvo)
~? !accepted.sign-arvo
[dap.bowl "bind rejected!" binding.sign-arvo]
[~ this]
::
++ on-peek on-peek:def
++ on-leave on-leave:def
++ on-agent on-agent:def
++ on-fail on-fail:def
--
::
|_ =bowl:gall
::
:: serving
::
++ authorize-http-request
=, server
:: if no passcode configured, only allow host ship to view
::
?~ passcode require-authorization:app
|= $: =inbound-request:eyre
handler=$-(inbound-request:eyre simple-payload:http)
==
?: authenticated.inbound-request
(handler inbound-request)
:: else, allow randos access,
:: on the condition they provide a correct ?passcode= url parameter
::
=; pass=(unit @t)
?: =(passcode pass)
(handler inbound-request)
(require-authorization:app inbound-request handler)
=/ from-url=(unit @t)
=- (~(get by -) 'passcode')
%- ~(gas by *(map @t @t))
args:(parse-request-line url.request.inbound-request)
?^ from-url from-url
:: try the referer field instead
::
=/ ref-url=(unit @t)
(get-header:http 'referer' header-list.request.inbound-request)
?~ ref-url ~
?~ (find "passcode={(trip u.passcode)}" (trip u.ref-url)) ~
passcode
::
++ handle-http-request
=, server
|= =inbound-request:eyre
^- simple-payload:http
=/ =request-line
%- parse-request-line
url.request.inbound-request
=* req-head header-list.request.inbound-request
::TODO handle POST
?. ?=(%'GET' method.request.inbound-request)
not-found:gen
(handle-get-request req-head request-line)
::
++ handle-get-request
=, server
|= [headers=header-list:http request-line]
^- simple-payload:http
=? site ?=([%'~debug' *] site) t.site
?~ ext
$(ext `%html, site [%index ~]) ::NOTE hack
:: if not json, serve static file
::
?. ?=([~ %json] ext)
=/ file=(unit octs)
(get-file-at /app/debug site u.ext)
?~ file not-found:gen
?+ u.ext not-found:gen
%html (html-response:gen u.file)
%js (js-response:gen u.file)
%css (css-response:gen u.file)
%png (png-response:gen u.file)
==
:: get data matching the json and convert it
::
=; json=(unit json)
?~ json not-found:gen
%- json-response:gen
=, html
(as-octt:mimes (en-json u.json))
=, enjs:format
?+ site ~
:: /app.json: {appname: running, ...}
::
[%app ~]
%- some
%- pairs
%+ turn all:apps
|= app=term
[app b+(running:apps app)]
::
:: /app/[appname].json: {state: }
::
[%app @ ~]
=* app i.t.site
::TODO ?. (dbugable:apps app) ~
%- some
%- pairs
:~ :- 'state'
(tank (sell (state:apps app)))
::
:- 'subscriptions'
%- pairs
=+ (subscriptions:apps app)
|^ ~['in'^(incoming in) 'out'^(outgoing out)]
::
++ incoming
|= =bitt:gall
^- json
:- %a
%+ turn ~(tap by bitt)
|= [d=duct [s=^ship p=^path]]
%- pairs
~! (ship s)
:~ 'duct'^a+(turn d path)
'ship'^(ship s)
'path'^(path p)
==
::
++ outgoing
|= =boat:gall
^- json
:- %a
%+ turn ~(tap by boat)
|= [[w=wire s=^ship t=term] [a=? p=^path]]
%- pairs
:~ 'wire'^(path w)
'ship'^(ship s)
'app'^s+t
'acked'^b+a
'path'^(path p)
==
--
==
::
:: /spider.json
::
[%spider %threads ~]
%- some
:: turn flat stack descriptors into object (tree) representing stacks
::
|^ (tree-to-json build-thread-tree)
::
+$ tree
$~ ~
(map tid:spider tree)
::
++ build-thread-tree
%+ roll tree:threads
|= [stack=(list tid:spider) =tree]
?~ stack tree
%+ ~(put by tree) i.stack
%_ $
stack t.stack
tree (~(gut by tree) i.stack ~)
==
::
++ tree-to-json
|= =tree
o+(~(run by tree) tree-to-json)
--
::
:: /azimuth/status
::
:: /ames/peer.json
::
[%ames %peer ~]
=/ [known=(list [^ship *]) alien=(list [^ship *])]
%+ skid ~(tap by peers:v-ames)
|= [^ship kind=?(%alien %known)]
?=(%known kind)
%- some
%- pairs
::NOTE would do (cork head ship) but can't get that to compile...
:~ 'known'^a+(turn (turn known head) ship)
'alien'^a+(turn (turn alien head) ship)
==
::
:: /ames/peer/[shipname].json
::
[%ames %peer @ ~]
=/ who=^ship
(rash i.t.t.site fed:ag)
%- some
=, v-ames
(peer-to-json (peer who))
::
:: /behn/timers.json
::
[%behn %timers ~]
%- some
:- %a
%+ turn timers:v-behn
|= [date=@da =duct]
%- pairs
:~ 'date'^(time date)
'duct'^a+(turn duct path)
==
::
:: /clay/commits.json
::
[%clay %commits ~]
(some commits-json:v-clay)
::
:: /eyre/bindings.json
::
[%eyre %bindings ~]
%- some
:- %a
%+ turn bindings:v-eyre
=, eyre
|= [binding =duct =action]
%- pairs
:~ 'location'^s+(cat 3 (fall site '*') (spat path))
::
:- 'action'
:- %s
?+ -.action -.action
%gen :((cury cat 3) '+' (spat [desk path]:generator.action))
%app (cat 3 ':' app.action)
==
==
==
::
++ get-file-at
|= [base=path file=path ext=@ta]
^- (unit octs)
?. ?=(?(%html %css %js %png) ext)
~
=/ =path
:* (scot %p our.bowl)
q.byk.bowl
(scot %da now.bowl)
(snoc (weld base file) ext)
==
?. .^(? %cu path) ~
%- some
%- as-octs:mimes:html
.^(@ %cx path)
::
:: applications
::
++ apps
|%
++ all
^- (list term)
%+ murn
(scry (list path) %ct %home /app)
|= =path
^- (unit term)
?. ?=([%app @ %hoon ~] path) ~
`i.t.path
::
++ running
|= app=term
(scry ? %gu app ~)
::
++ dbugable
|= app=term
^- ?
!! ::TODO how to check if it supports the /dbug scries?
::
++ state
|= app=term
(scry-dbug vase app /dbug/state)
::
++ subscriptions
=, gall
|= app=term
^- [out=boat in=bitt]
(scry-dbug ,[boat bitt] app /dbug/subscriptions)
::
++ scry-dbug
|* [=mold app=term =path]
(scry mold %gx app (snoc `^path`path %noun))
::
::TODO but why? we can't tell if it's on or not
++ poke-verb-toggle
|= app=term
^- card
(poke /verb/[app] app %verb !>(%loud))
--
::
:: threads
::
++ threads
|%
::NOTE every (list tid:spider) represents a stack,
:: with a unique tid at the end
++ tree
(scry (list (list tid:spider)) %gx %spider /tree/noun)
::
++ poke-kill
|= =tid:spider
^- card
(poke /spider/kill/[tid] %spider %spider-stop !>([tid |]))
--
::
:: ames
::
++ v-ames
|%
++ peers
(scry (map ship ?(%alien %known)) %a %peers ~)
::
++ peer
|= who=ship
(scry ship-state:ames %a %peer /(scot %p who))
::
++ peer-to-json
=, ames
=, enjs:format
|= =ship-state
|^ ^- json
%+ frond -.ship-state
?- -.ship-state
%alien (alien +.ship-state)
%known (known +.ship-state)
==
::
++ alien
|= alien-agenda
%- pairs
:~ 'messages'^(numb (lent messages))
'packets'^(numb ~(wyt in packets))
'heeds'^(set-array heeds from-duct)
==
::
:: json for known peer is structures to closely match the peer-state type.
:: where an index is specified, the array is generally sorted by those.
::
:: { life: 123,
:: route: { direct: true, lane: 'something' },
:: qos: { kind: 'status', last-contact: 123456 }, // ms timestamp
:: snd: [
:: { bone: 123, // index
:: duct: ['/paths', ...]
:: current: 123,
:: next: 123,
:: unsent-messages: [123, ...], // size in bytes
:: queued-message-acks: [{
:: message-num: 123, // index
:: ack: 'ok'
:: }, ...],
:: packet-pump-state: {
:: next-wake: 123456, // ms timestamp
:: live: [{
:: message-num: 123, // index
:: fragment-num: 123, // index
:: num-fragments: 123,
:: last-sent: 123456, // ms timestamp
:: retries: 123,
:: skips: 123
:: }, ...],
:: metrics: {
:: rto: 123, // seconds
:: rtt: 123, // seconds
:: rttvar: 123,
:: ssthresh: 123,
:: num-live: 123,
:: cwnd: 123,
:: counter: 123
:: }
:: }
:: },
:: ...],
:: rcv: [
:: { bone: 123, // index
:: duct: ['/paths', ...] // index
:: last-acked: 123,
:: last-heard: 123,
:: pending-vane-ack: [123, ...],
:: live-messages: [{
:: message-num: 123, // index
:: num-received: 122,
:: num-fragments: 123,
:: fragments: [123, ...]
:: }, ...],
:: nax: [123, ...]
:: },
:: ...],
:: nax: [{
:: bone: 123, // index
:: duct: ['/paths', ...],
:: message-num: 123
:: }, ...],
:: heeds: [['/paths', ...] ...]
:: }
::
++ known
|= peer-state
%- pairs
:~ 'life'^(numb life)
::
:- 'route'
%+ maybe route
|= [direct=? =lane]
%- pairs
:~ 'direct'^b+direct
'lane'^?-(-.lane %& (ship p.lane), %| s+(scot %x p.lane))
==
::
:- 'qos'
%- pairs
:~ 'kind'^s+-.qos
'last-contact'^(time last-contact.qos)
==
::
:- 'snd'
:- %a
%+ turn (sort ~(tap by snd) aor) :: sort by bone
(cury snd-with-bone ossuary)
::
:- 'rcv'
:- %a
%+ turn (sort ~(tap by rcv) aor) :: sort by bone
(cury rcv-with-bone ossuary)
::
:- 'nax'
:- %a
%+ turn (sort ~(tap in nax) aor) :: sort by bone
|= [=bone =message-num]
%- pairs
:* 'message-num'^(numb message-num)
(bone-to-pairs bone ossuary)
==
::
'heeds'^(set-array heeds from-duct)
==
::
++ snd-with-bone
|= [=ossuary =bone message-pump-state]
^- json
%- pairs
:* 'current'^(numb current)
'next'^(numb next)
::
:- 'unsent-messages' :: as byte sizes
(set-array unsent-messages (cork (cury met 3) numb))
::
'unsent-fragments'^(numb (lent unsent-fragments)) :: as lent
::
:- 'queued-message-acks'
:- %a
%+ turn (sort ~(tap by queued-message-acks) aor) :: sort by msg nr
|= [=message-num =ack]
%- pairs
:~ 'message-num'^(numb message-num)
'ack'^s+-.ack
==
::
:- 'packet-pump-state'
%- pairs
=, packet-pump-state
:~ 'next-wake'^(maybe next-wake time)
::
:- 'live'
:- %a
%+ turn (sort ~(tap in live) aor) :: sort by msg nr & frg nr
|= [live-packet-key live-packet-val]
%- pairs
:~ 'message-num'^(numb message-num)
'fragment-num'^(numb fragment-num)
'num-fragments'^(numb num-fragments)
'last-sent'^(time last-sent)
'retries'^(numb retries)
'skips'^(numb skips)
==
::
:- 'metrics'
%- pairs
=, metrics
:~ 'rto'^(numb (div rto ~s1)) ::TODO milliseconds?
'rtt'^(numb (div rtt ~s1))
'rttvar'^(numb (div rttvar ~s1))
'ssthresh'^(numb ssthresh)
'num-live'^(numb num-live)
'cwnd'^(numb cwnd)
'counter'^(numb counter)
==
==
::
(bone-to-pairs bone ossuary)
==
::
++ rcv-with-bone
|= [=ossuary =bone message-sink-state]
^- json
%- pairs
:* 'last-acked'^(numb last-acked)
'last-heard'^(numb last-heard)
::
:- 'pending-vane-ack'
=- a+(turn - numb)
(sort (turn ~(tap in pending-vane-ack) head) aor) :: sort by msg #
::
:- 'live-messages'
:- %a
%+ turn (sort ~(tap by live-messages) aor) :: sort by msg #
|= [=message-num partial-rcv-message]
%- pairs
:~ 'message-num'^(numb message-num)
'num-received'^(numb num-received)
'num-fragments'^(numb num-fragments)
'fragments'^(set-array ~(key by fragments) numb)
==
::
'nax'^(set-array nax numb)
::
(bone-to-pairs bone ossuary)
==
::
++ bone-to-pairs
|= [=bone ossuary]
^- (list [@t json])
:~ 'bone'^(numb bone)
'duct'^(from-duct (~(gut by by-bone) bone ~))
==
::
++ maybe
|* [unit=(unit) enjs=$-(* json)]
^- json
?~ unit ~
(enjs u.unit)
::
++ set-array
|* [set=(set) enjs=$-(* json)]
^- json
a+(turn ~(tap in set) enjs)
::
++ from-duct
|= =duct
a+(turn duct path)
--
--
::
:: behn
::
++ v-behn
|%
++ timers
(scry ,(list [date=@da =duct]) %b %timers ~)
--
::
:: clay
::
::TODO depends on new clay changes (%s care)
++ v-clay
=, clay
|%
++ start-path /(scot %p our.bowl)/home/(scot %da now.bowl)
::
+$ commit
[=tako parents=(list tako) children=(list tako) wen=@da content-hash=@uvI]
::
++ commits-json
^- json
=+ .^(desks=(set desk) %cd start-path)
=/ heads=(list [tako desk])
%+ turn ~(tap in desks)
|= =desk
=+ .^(=dome %cv /(scot %p our.bowl)/[desk]/(scot %da now.bowl))
=/ =tako (~(got by hit.dome) let.dome)
[tako desk]
=/ yakis=(set yaki)
%- silt
^- (list yaki)
%- zing
%+ turn heads
|= [=tako =desk]
(trace-tako tako)
=/ commits=(list commit) (yakis-to-commits ~(tap in yakis))
=, enjs:format
%: pairs
head+(pairs (turn heads |=([=tako =desk] (scot %uv tako)^s+desk)))
commits+(commits-to-json commits)
~
==
::
++ yakis-to-commits
|= yakis=(list yaki)
^- (list commit)
%+ turn yakis
|= =yaki
:* r.yaki p.yaki
=/ candidates
%+ turn
(skim yakis |=(can=^yaki (lien p.can |=(=tako =(r.yaki tako)))))
|= can=^yaki
r.can
~(tap in (silt candidates))
t.yaki
.^(@uvI %cs (weld start-path /hash/(scot %uv r.yaki)))
==
::
++ trace-tako
|= =tako
~+
^- (list yaki)
=+ .^(=yaki %cs (weld start-path /yaki/(scot %uv tako)))
:- yaki
(zing (turn p.yaki trace-tako))
::
++ commits-to-json
|= commits=(list commit)
^- json
:- %a
%+ turn
%+ sort commits
|= [a=commit b=commit]
(gte wen.a wen.b)
|= =commit
(commit-to-json commit)
::
++ commit-to-json
|= =commit
^- json
=, enjs:format
%: pairs
'commitHash'^(tako-to-json tako.commit)
parents+a+(turn parents.commit tako-to-json)
children+a+(turn children.commit tako-to-json)
'contentHash'^(tako-to-json content-hash.commit)
~
==
::
++ tako-to-json
|= =tako
^- json
s+(scot %uv tako)
--
::
:: eyre
::
++ v-eyre
|%
++ bindings
=, eyre
(scry ,(list [=binding =duct =action]) %e %bindings ~)
--
::
:: helpers
::
++ poke
|= [=wire app=term =mark =vase]
^- card
[%pass wire %agent [our.bowl app] %poke mark vase]
::
++ scry
|* [=mold care=term =desk =path]
.^(mold care (scot %p our.bowl) desk (scot %da now.bowl) path)
--

View File

@ -0,0 +1,183 @@
var gulp = require('gulp');
var cssimport = require('gulp-cssimport');
var rollup = require('gulp-better-rollup');
var cssnano = require('cssnano');
var postcss = require('gulp-postcss');
var sucrase = require('@sucrase/gulp-plugin');
var minify = require('gulp-minify');
var rename = require('gulp-rename');
var del = require('del');
var resolve = require('rollup-plugin-node-resolve');
var commonjs = require('rollup-plugin-commonjs');
var rootImport = require('rollup-plugin-root-import');
var globals = require('rollup-plugin-node-globals');
/***
Main config options
***/
var urbitrc = require('../urbitrc');
/***
End main config options
***/
gulp.task('css-bundle', function() {
let plugins = [
cssnano()
];
return gulp
.src('src/index.css')
.pipe(cssimport())
.pipe(postcss(plugins))
.pipe(gulp.dest('../../arvo/app/debug/css'));
});
gulp.task('jsx-transform', function(cb) {
return gulp.src('src/**/*.js')
.pipe(sucrase({
transforms: ['jsx']
}))
.pipe(gulp.dest('dist'));
});
gulp.task('tile-jsx-transform', function(cb) {
return gulp.src('tile/**/*.js')
.pipe(sucrase({
transforms: ['jsx']
}))
.pipe(gulp.dest('dist'));
});
gulp.task('js-imports', function(cb) {
return gulp.src('dist/index.js')
.pipe(rollup({
plugins: [
commonjs({
namedExports: {
'node_modules/react/index.js': [ 'Component', 'createRef', 'createElement', 'useState', 'useRef', 'useEffect', 'Fragment' ],
'node_modules/react-is/index.js': [ 'isValidElementType' ],
}
}),
rootImport({
root: `${__dirname}/dist/js`,
useEntry: 'prepend',
extensions: '.js'
}),
globals(),
resolve()
]
}, 'umd'))
.on('error', function(e){
console.log(e);
cb();
})
.pipe(gulp.dest('../../arvo/app/debug/js/'))
.on('end', cb);
});
gulp.task('tile-js-imports', function(cb) {
return gulp.src('dist/tile.js')
.pipe(rollup({
plugins: [
commonjs({
namedExports: {
'node_modules/react/index.js': [ 'Component' ],
}
}),
rootImport({
root: `${__dirname}/dist/js`,
useEntry: 'prepend',
extensions: '.js'
}),
globals(),
resolve()
]
}, 'umd'))
.on('error', function(e){
console.log(e);
cb();
})
.pipe(gulp.dest('../../arvo/app/debug/js/'))
.on('end', cb);
});
gulp.task('js-minify', function () {
return gulp.src('../../arvo/app/debug/js/index.js')
.pipe(minify())
.pipe(gulp.dest('../../arvo/app/debug/js/'));
});
gulp.task('tile-js-minify', function () {
return gulp.src('../../arvo/app/debug/js/tile.js')
.pipe(minify())
.pipe(gulp.dest('../../arvo/app/debug/js/'));
});
gulp.task('rename-index-min', function() {
return gulp.src('../../arvo/app/debug/js/index-min.js')
.pipe(rename('index.js'))
.pipe(gulp.dest('../../arvo/app/debug/js/'))
});
gulp.task('rename-tile-min', function() {
return gulp.src('../../arvo/app/debug/js/tile-min.js')
.pipe(rename('tile.js'))
.pipe(gulp.dest('../../arvo/app/debug/js/'))});
gulp.task('clean-min', function() {
return del(['../../arvo/app/debug/js/index-min.js', '../../arvo/app/debug/js/tile-min.js'], {force: true})
});
gulp.task('urbit-copy', function () {
let ret = gulp.src('../../arvo/**/*');
urbitrc.URBIT_PIERS.forEach(function(pier) {
ret = ret.pipe(gulp.dest(pier));
});
return ret;
});
gulp.task('js-bundle-dev', gulp.series('jsx-transform', 'js-imports'));
gulp.task('tile-js-bundle-dev', gulp.series('tile-jsx-transform', 'tile-js-imports'));
gulp.task('js-bundle-prod', gulp.series('jsx-transform', 'js-imports', 'js-minify'))
gulp.task('tile-js-bundle-prod',
gulp.series('tile-jsx-transform', 'tile-js-imports', 'tile-js-minify'));
gulp.task('bundle-dev',
gulp.series(
gulp.parallel(
'css-bundle',
'js-bundle-dev',
'tile-js-bundle-dev'
),
'urbit-copy'
)
);
gulp.task('bundle-prod',
gulp.series(
gulp.parallel(
'css-bundle',
'js-bundle-prod',
'tile-js-bundle-prod',
),
'rename-index-min',
'rename-tile-min',
'clean-min',
'urbit-copy'
)
);
gulp.task('default', gulp.series('bundle-dev'));
gulp.task('watch', gulp.series('default', function() {
gulp.watch('tile/**/*.js', gulp.parallel('tile-js-bundle-dev'));
gulp.watch('src/**/*.js', gulp.parallel('js-bundle-dev'));
gulp.watch('src/**/*.css', gulp.parallel('css-bundle'));
gulp.watch('../../arvo/**/*', gulp.parallel('urbit-copy'));
}));

6543
pkg/interface/dbug/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
{
"name": "urbit-apps",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@sucrase/gulp-plugin": "^2.0.0",
"cssnano": "^4.1.10",
"gulp": "^4.0.0",
"gulp-better-rollup": "^4.0.1",
"gulp-cssimport": "^7.0.0",
"gulp-minify": "^3.1.0",
"gulp-postcss": "^8.0.0",
"gulp-rename": "^1.4.0",
"moment": "^2.24.0",
"rollup": "^1.6.0",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-root-import": "^0.2.3",
"sucrase": "^3.8.0"
},
"dependencies": {
"@gitgraph/react": "^1.5.4",
"classnames": "^2.2.6",
"del": "^5.1.0",
"lodash": "^4.17.11",
"mousetrap": "^1.6.3",
"react": "^16.5.2",
"react-dom": "^16.8.6",
"react-router-dom": "^5.0.0",
"urbit-ob": "^5.0.0",
"urbit-sigil-js": "^1.3.2"
},
"resolutions": {
"natives": "1.1.3"
}
}

View File

@ -0,0 +1,217 @@
html, body {
height: 100%;
width: 100%;
-webkit-font-smoothing: antialiased;
overflow: hidden;
}
p, h1, h2, h3, h4, h5, h6, a, input, textarea, button {
margin-block-end: unset;
margin-block-start: unset;
-webkit-margin-before: unset;
-webkit-margin-after: unset;
font-family: Inter, sans-serif;
padding: 0;
}
button, summary {
cursor: pointer;
}
h2 {
font-weight: 400;
}
a {
color: #000;
text-decoration: none;
}
.inter {
font-family: Inter, sans-serif;
}
.clamp-3 {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.clamp-message {
max-width: calc(100% - 36px - 1.5rem);
}
.clamp-attachment {
overflow: scroll;
max-height: 10em;
max-width: 100%;
}
.lh-16 {
line-height: 16px;
}
.mono {
font-family: "Source Code Pro", monospace;
}
.list-ship {
line-height: 2.2;
}
.bg-welcome-green {
background-color: #ECF6F2;
}
.c-default {
cursor: default;
}
.m0a {
margin: 0 auto;
}
.mix-blend-diff {
mix-blend-mode: difference;
}
.focus-b--black:focus {
border-color: #000;
}
.embed-container {
position: relative;
height: 0;
overflow: hidden;
padding-bottom: 56.25%;
}
.embed-container iframe, .embed-container object, .embed-container embed {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* spinner */
.spin-active {
animation: spin 2s infinite;
}
@keyframes spin {
0% {transform: rotate(0deg);}
25% {transform: rotate(90deg);}
50% {transform: rotate(180deg);}
75% {transform: rotate(270deg);}
100% {transform: rotate(360deg);}
}
/* toggler checkbox */
.toggle::after {
content: "";
height: 12px;
width: 12px;
background: white;
position: absolute;
top: 2px;
left: 2px;
border-radius: 100%;
}
.toggle.checked::after {
left: 14px;
}
/* responsive */
@media all and (max-width: 34.375em) {
.dn-s {
display: none;
}
.flex-basis-100-s, .flex-basis-full-s {
flex-basis: 100%;
}
.h-100-m-40-s {
height: calc(100% - 40px);
}
.black-s {
color: #000;
}
}
@media all and (min-width: 34.375em) {
.db-ns {
display: block;
}
.flex-basis-30-ns {
flex-basis: 30vw;
}
.h-100-m-40-ns {
height: calc(100% - 40px);
}
}
@media all and (prefers-color-scheme: dark) {
body {
background-color: #333;
}
.bg-black-d {
background-color: black;
}
.white-d {
color: white;
}
.gray1-d {
color: #4d4d4d;
}
.gray2-d {
color: #7f7f7f;
}
.gray3-d {
color: #b1b2b3;
}
.gray4-d {
color: #e6e6e6;
}
.bg-gray0-d {
background-color: #333;
}
.bg-gray1-d {
background-color: #4d4d4d;
}
.b--gray0-d {
border-color: #333;
}
.b--gray1-d {
border-color: #4d4d4d;
}
.b--gray2-d {
border-color: #7f7f7f;
}
.b--white-d {
border-color: #fff;
}
.bb-d {
border-bottom-width: 1px;
border-bottom-style: solid;
}
.invert-d {
filter: invert(1);
}
.o-60-d {
opacity: .6;
}
.focus-b--white-d:focus {
border-color: #fff;
}
a {
color: #fff;
}
.hover-bg-gray1-d:hover {
background-color: #4d4d4d;
}
}

View File

@ -0,0 +1,63 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
src: url("https://media.urbit.org/fonts/Inter-Regular.woff2") format("woff2");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
src: url("https://media.urbit.org/fonts/Inter-Italic.woff2") format("woff2");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
src: url("https://media.urbit.org/fonts/Inter-Bold.woff2") format("woff2");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
src: url("https://media.urbit.org/fonts/Inter-BoldItalic.woff2") format("woff2");
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-extralight.woff");
font-weight: 200;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-light.woff");
font-weight: 300;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff");
font-weight: 400;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-medium.woff");
font-weight: 500;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-semibold.woff");
font-weight: 600;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-bold.woff");
font-weight: 700;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,4 @@
@import "css/indigo-static.css";
@import "css/fonts.css";
@import "css/custom.css";

View File

@ -0,0 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Root } from '/components/root';
import { api } from '/api';
import { store } from '/store';
import { subscription } from "/subscription";
api.setAuthTokens({
ship: window.ship
});
subscription.start();
ReactDOM.render((
<Root />
), document.querySelectorAll("#root")[0]);

View File

@ -0,0 +1,210 @@
import React from 'react';
import ReactDOM from 'react-dom';
import _ from 'lodash';
import { stringToTa } from '/lib/util';
import { store } from '/store';
import moment from 'moment';
class UrbitApi {
setAuthTokens(authTokens) {
this.authTokens = authTokens;
this.bindPaths = [];
this.bind = this.bind.bind(this);
}
bind(path, method, ship = this.authTokens.ship, app, success, fail, quit) {
this.bindPaths = _.uniq([...this.bindPaths, path]);
window.subscriptionId = window.urb.subscribe(ship, app, path,
(err) => {
fail(err);
},
(event) => {
success({
data: event,
from: {
ship,
path
}
});
},
(qui) => {
quit(qui);
});
}
action(appl, mark, data) {
return new Promise((resolve, reject) => {
window.urb.poke(ship, appl, mark, data,
(json) => {
resolve(json);
},
(err) => {
reject(err);
});
});
}
dbugAction(data) {
return this.action("dbug", "dbug-action", data);
}
bindToVerb(app) {
return this.bind('/verb/events', 'PUT', this.authTokens.ship, app,
(result) => {
console.log('verb result', result);
result.data.app = app;
store.handleEvent({data: { local: { verbResult: result.data }}});
},
() => {
store.handleEvent({data: { local: { verbStatus: {
app: app,
msg: 'failed to establish verb connection to ' + app
}}}});
},
() => {
store.handleEvent({data: { local: { verbStatus: {
app: app,
msg: 'verb connection to ' + app + ' was dropped'
}}}});
}
);
}
getJson(path, localTransform, onFail) {
let source = '/~debug' + path + '.json';
const query = window.location.href.split('?')[1];
if (query) source = source + '?' + query;
fetch(source)
.then((response) => {
if (!response.ok) {
console.error('Network response not ok');
onFail();
} else {
return response.json();
}
})
.then((data) => {
store.handleEvent({data: { local: localTransform(data) }});
})
.catch((error) => {
console.error(`JSON fetch on ${source} failed:`, error);
onFail();
});
}
wrapLocal(name) {
return (data) => {
console.log('got data', data);
let e = {};
e[name] = data;
e['status'] = null; // clear previous status
return e;
};
}
showStatus(what) {
return () => {
store.handleEvent({data: { local: { 'status': what }}});
};
}
// apps
getApps() {
//TODO onfail render "failed to fetch apps"
//TODO generic "fail" local event that prints to status bar?
this.getJson('/app',
this.wrapLocal('apps'),
this.showStatus('error fetching apps')
);
}
getAppDetails(app) {
this.getJson('/app/'+app, (data) => {
data.app = app;
return this.wrapLocal('app')(data);
},
() => { // on fail
store.handleEvent({data: { local: { 'appFailed': app } }});
}
);
}
// spider
getThreads() {
console.log('getting threads');
this.getJson('/spider/threads',
this.wrapLocal('threads'),
this.showStatus('error fetching threads')
);
}
// ames
getPeers() {
this.getJson('/ames/peer',
this.wrapLocal('amesPeers'),
this.showStatus('error fetching ames peers')
);
}
getPeer(who) {
this.getJson(`/ames/peer/${who}`, (data) => {
data.who = who;
return this.wrapLocal('amesPeer')(data);
},
this.showStatus('error fetching ames details for ' + who)
);
}
// behn
getTimers() {
this.getJson('/behn/timers',
this.wrapLocal('behnTimers'),
this.showStatus('error fetching behn timers')
);
}
// clay
getCommits() {
this.getJson('/clay/commits',
this.wrapLocal('clayCommits'),
this.showStatus('error fetching clay commits')
);
}
// eyre
getBindings() {
this.getJson('/eyre/bindings',
this.wrapLocal('eyreBindings'),
this.showStatus('error fetching eyre bindings')
);
}
// local
sidebarToggle() {
let sidebarBoolean = true;
if (store.state.sidebarShown === true) {
sidebarBoolean = false;
}
store.handleEvent({
data: {
local: {
'sidebarToggle': sidebarBoolean
}
}
});
}
}
export let api = new UrbitApi();
window.api = api;

View File

@ -0,0 +1,8 @@
import React, { Component } from 'react';
import { MessageScreen } from '/components/lib/message-screen';
export class LoadingScreen extends Component {
render() {
return (<MessageScreen text="Loading..."/>);
}
}

View File

@ -0,0 +1,111 @@
import React, { Component } from 'react';
import { BrowserRouter, Switch, Route, Link } from "react-router-dom";
import classnames from 'classnames';
import _ from 'lodash';
import { api } from '/api';
import { subscription } from '/subscription';
import { store } from '/store';
import { Skeleton } from '/components/skeleton';
import { MessageScreen } from '/components/lib/message-screen';
import { Apps } from '/views/apps';
import { Spider } from '/views/spider';
import { Ames } from '/views/ames';
import { Behn } from '/views/behn';
import { Clay } from '/views/clay';
import { Eyre } from '/views/eyre';
import { makeRoutePath } from '../lib/util';
export class Root extends Component {
constructor(props) {
super(props);
this.state = store.state;
store.setStateHandler(this.setState.bind(this));
console.log('built root');
}
componentDidMount() {
// preload spinner asset
new Image().src = "/~debug/img/Spinner.png";
}
render() {
const { state } = this;
return (
<BrowserRouter><Switch>
<Route exact path="/~debug"
render={(props) => {
return (
<Skeleton status={state.status} selected="">
<MessageScreen text="select a component on the left" />
</Skeleton>
);
}}
/>
<Route exact path={makeRoutePath('apps')}
render={(props) => {
return (
<Skeleton status={state.status} selected="apps">
<Apps apps={state.apps} {...props}/>
</Skeleton>
);
}}
/>
<Route exact path={makeRoutePath('spider')}
render={(props) => {
return (
<Skeleton status={state.status} selected="spider">
<Spider threads={state.threads} {...props}/>
</Skeleton>
);
}}
/>
<Route exact path={makeRoutePath('ames')}
render={(props) => {
return (
<Skeleton status={state.status} selected="ames">
<Ames peers={state.peers} {...props}/>
</Skeleton>
);
}}
/>
<Route exact path={makeRoutePath('behn')}
render={(props) => {
return (
<Skeleton status={state.status} selected="behn">
<Behn timers={state.timers} {...props}/>
</Skeleton>
);
}}
/>
<Route exact path={makeRoutePath('clay')}
render={(props) => {
return (
<Skeleton status={state.status} selected="clay">
<Clay commits={state.commits} {...props}/>
</Skeleton>
);
}}
/>
<Route exact path={makeRoutePath('eyre')}
render={(props) => {
return (
<Skeleton status={state.status} selected="eyre">
<Eyre bindings={state.bindings} {...props}/>
</Skeleton>
);
}}
/>
</Switch></BrowserRouter>
)
}
}

View File

@ -0,0 +1,48 @@
import React, { Component } from 'react';
export class SearchableList extends Component {
// expected props:
// items: [{key: 'some key', jsx: <w/e>}, ...]
// placeholder: ''
constructor(props) {
super(props);
this.state = {
query: ''
};
this.updateQuery = this.updateQuery.bind(this);
}
updateQuery(event) {
this.setState({ query: event.target.value });
}
render() {
const { state, props } = this;
const searchBar = (
<input
type="text"
placeholder={props.placeholder}
onChange={this.updateQuery}
value={state.query}
style={{border: '1px solid black'}}
/>
);
let items = props.items.filter(item => {
return (state.query === '') || item.key.includes(state.query);
})
if (items.length === 0) {
items = 'none';
} else {
items = items.map(item => (<div style={{marginTop: '4px'}}>{item.jsx}</div>));
}
return (<div style={{border: '1px solid grey', padding: '4px'}}>
<div>{searchBar}</div>
<div>{items}</div>
</div>);
}
}

View File

@ -0,0 +1,81 @@
import React, { Component } from 'react';
import { Link } from "react-router-dom";
import classnames from 'classnames';
import { HeaderBar } from './lib/header-bar';
import { ChannelsSidebar } from './lib/channel-sidebar';
import { makeRoutePath } from '../lib/util';
class SidebarItem extends Component {
render() {
const { props } = this;
let selectedClass = (props.selected)
? "bg-gray5 bg-gray1-d"
: "pointer hover-bg-gray5 hover-bg-gray1-d";
return (
<Link to={makeRoutePath(props.what, true)} key="what">
<div className={"w-100 v-mid f9 ph4 z1 pv1 " + selectedClass}>
<p className="f9 dib">{props.what}</p>
</div>
</Link>
);
}
}
export class Skeleton extends Component {
render() {
const { props } = this;
let items = [
'apps',
'spider',
'ames',
'behn',
//TODO 'clay',
'eyre'
];
items = items.map(what => {
return (<SidebarItem what={what} selected={props.selected === what}/>);
});
let rightPanelHide = this.props.rightPanelHide
? "dn-s" : "";
const status = props.status
? (<div style={{
position: 'absolute', right: '16px', bottom: '16px',
padding: '8px', border: '1px solid #e22'
}}>
{props.status}
</div>)
: null;
return (
<div className="absolute h-100 w-100 mono">
<div className="cf w-100 h-100 flex">
<div className="bn br-m br-l br-xl b--gray4 b--gray1-d lh-copy h-100 flex-shrink-0 mw5-m mw5-l mw5-xl pt3 pt0-m pt0-l pt0-xl relative">
<a className="db dn-m dn-l dn-xl f8 pb3 pl3" href="/"> Landscape</a>
<div className="overflow-y-scroll h-100">
<div className="w-100 bg-transparent">
<Link
className="dib f9 pointer green2 gray4-d pa4"
to={"/~chat/join/~/~dopzod/urbit-help"}>
Get help
</Link>
</div>
{items}
</div>
</div>
{status}
<div className={"h-100 w-100 flex-auto overflow-scroll relative " + rightPanelHide} style={{
flexGrow: 1,
padding: '8px'
}}>
{this.props.children}
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,37 @@
import React, { Component } from 'react';
export class Summary extends Component {
// expected props:
// id: 'id'
// summary: <jsx>
// details: <jsx>
// onOpen: function(id)
// onClose: function(id)
constructor(props) {
super(props);
this.onToggle = this.onToggle.bind(this);
}
onToggle(event) {
console.log('toggle for', this.props.id);
if (event.target.open) {
if (this.props.onOpen) this.props.onOpen(this.props.id);
} else {
if (this.props.onClose) this.props.onClose(this.props.id);
}
}
render() {
const { props } = this;
return (
<details onToggle={this.onToggle} {...props} style={{border: '1px solid black', padding: '4px', ...props.style}}>
<summary>
{props.summary}
</summary>
<div style={{borderTop: '1px solid black'}}>{props.details}</div>
</details>
)
}
}

View File

@ -0,0 +1,32 @@
import _ from 'lodash';
import classnames from 'classnames';
export function makeRoutePath(resource, includeQuery = false) {
let query = window.location.href.split('?')[1];
if (includeQuery && query) {
query = '?' + query;
} else {
query = '';
}
return '/~debug/' + resource + query;
}
export function msToDa(ms, mil) {
const d = new Date(ms);
var fil = function(n) {
return n >= 10 ? n : "0" + n;
};
return (
`~${d.getUTCFullYear()}.` +
`${(d.getUTCMonth() + 1)}.` +
`${fil(d.getUTCDate())}..` +
`${fil(d.getUTCHours())}.` +
`${fil(d.getUTCMinutes())}.` +
`${fil(d.getUTCSeconds())}` +
`${mil ? "..0000" : ""}`
);
}
export function renderDuct(duct) {
return duct.reduce((a, b) => a + b + ' ', '');
}

View File

@ -0,0 +1,142 @@
import _ from 'lodash';
export class LocalReducer {
reduce(json, state) {
const data = _.get(json, 'local', false);
if (data) {
this.status(data, state);
//
this.apps(data, state);
this.app(data, state);
this.appFailed(data, state);
this.verbResult(data, state);
this.verbStatus(data, state);
//
this.threads(data, state);
//
this.amesPeers(data, state);
this.amesPeer(data, state);
//
this.behnTimers(data, state);
//
this.clayCommits(data, state);
//
this.eyreBindings(data, state);
}
}
status(obj, state) {
const data = _.get(obj, 'status', false);
if (data) {
state.status = data;
}
}
// apps
apps(obj, state) {
const data = _.get(obj, 'apps', false);
if (data) {
state.apps = data;
}
}
app(obj, state) {
const data = _.get(obj, 'app', false);
if (data) {
state.apps[data.app] = data;
}
}
appFailed(obj, state) {
const data = _.get(obj, 'appFailed', false);
if (data) {
console.log('loading app deets failed', data);
state.apps[data] = { noDebug: true };
}
}
verbResult(obj, state) {
const data = _.get(obj, 'verbResult', false);
if (data) {
if (!state.apps[data.app]) state.apps[data.app] = {};
if (!state.apps[data.app].events) state.apps[data.app].events = [];
let msg = 'some event';
if (data['on-init']) msg = '+on-init';
if (data['on-load']) msg = '+on-load';
if (data['on-poke']) msg = '+on-poke with mark ' + data['on-poke'];
if (data['on-watch']) msg = '+on-watch at path ' + data['on-watch'];
if (data['on-leave']) msg = '+on-leave on path ' + data['on-leave'];
if (data['on-agent']) msg = '+on-agent at wire ' + data['on-agent'].wire +
' with sign ' + data['on-agent'].sign;
if (data['on-arvo']) msg = '+on-arvo at wire ' + data['on-arvo'].wire +
' from vane ' + data['on-arvo'].vane +
' with sign ' + data['on-arvo'].sign;
if (data['on-fail']) msg = '+on-fail on ' + data['on-fail'];
state.apps[data.app].events.push(msg);
}
}
verbStatus(obj, state) {
const data = _.get(obj, 'verbStatus', false);
if (data) {
if (!state.apps[data.app]) state.apps[data.app] = {};
if (!state.apps[data.app].events) state.apps[data.app].events = [];
state.apps[data.app].events.push(data.msg);
}
}
// spider
threads(obj, state) {
const data = _.get(obj, 'threads', false);
if (data) {
state.threads = data;
}
}
// ames
amesPeers(obj, state) {
const data = _.get(obj, 'amesPeers', false);
if (data) {
state.peers.known = data.known;
state.peers.alien = data.alien;
}
}
amesPeer(obj, state) {
const data = _.get(obj, 'amesPeer', false);
if (data) {
state.peers.deets[data.who] = data;
}
}
// behn
behnTimers(obj, state) {
const data = _.get(obj, 'behnTimers', false);
if (data) {
state.timers = data;
}
}
// clay
clayCommits(obj, state) {
const data = _.get(obj, 'clayCommits', false);
if (data) {
console.log('clay comms', data);
state.commits = data;
}
}
// eyre
eyreBindings(obj, state) {
const data = _.get(obj, 'eyreBindings', false);
if (data) {
state.bindings = data;
}
}
}

View File

@ -0,0 +1,45 @@
import { InitialReducer } from '/reducers/initial';
import { LocalReducer } from '/reducers/local.js';
import _ from 'lodash';
class Store {
constructor() {
this.state = {
status: null,
apps: {},
threads: {},
peers: { known: [], alien: [], deets: {}},
timers: [],
commits: [],
bindings: [],
sidebarShown: true
};
this.initialReducer = new InitialReducer();
this.localReducer = new LocalReducer();
this.setState = () => {};
}
setStateHandler(setState) {
this.setState = setState;
}
handleEvent(data) {
let json;
if (data.data) {
json = data.data;
} else {
json = data;
}
console.log('event', json);
this.initialReducer.reduce(json, this.state);
this.localReducer.reduce(json, this.state);
this.setState(this.state);
}
}
export let store = new Store();
window.store = store;

View File

@ -0,0 +1,31 @@
import { api } from '/api';
import { store } from '/store';
export class Subscription {
start() {
if (api.authTokens) {
//
} else {
console.error("~~~ ERROR: Must set api.authTokens before operation ~~~");
}
}
handleEvent(diff) {
store.handleEvent(diff);
}
handleError(err) {
console.error(err);
}
handleQuitSilently(quit) {
// no-op
}
handleQuitAndResubscribe(quit) {
// TODO: resubscribe
}
}
export let subscription = new Subscription();

View File

@ -0,0 +1,297 @@
import React, { Component } from 'react';
import { Spinner } from '../components/lib/icons/icon-spinner';
import { Subscriptions } from '../components/subscriptions';
import { Route, Link } from 'react-router-dom';
import { msToDa, renderDuct } from '../lib/util';
import urbitOb from 'urbit-ob';
import { Summary } from '../components/summary';
import { SearchableList } from '../components/searchable-list';
export class Ames extends Component {
constructor(props) {
super(props);
this.state = {
opened: new Set()
};
this.loadPeers = this.loadPeers.bind(this);
this.loadPeerDetails = this.loadPeerDetails.bind(this);
}
componentDidMount() {
const { known, alien } = this.props.peers;
if (known.length === 0 && alien.length === 0) {
this.loadPeers();
}
}
componentDidUpdate(prevProps, prevState) {
const { props, state } = this;
//
}
loadPeers() {
api.getPeers();
}
loadPeerDetails(who) {
api.getPeer(who);
}
renderDucts(ducts) {
const items = ducts.map(duct => {
return {
key: duct.join(' '),
jsx: (<div>{renderDuct(duct)}</div>)
}
});
return <SearchableList placeholder="duct" items={items}/>
}
//TODO use classes for styling?
render() {
const { props, state } = this;
const { known, alien, deets } = props.peers;
const renderDetails = (who) => {
const peer = deets[who];
console.log('deets', props.peers, deets, peer, who);
if (!peer) {
return 'Loading...';
} else if (peer.alien) {
return (<>
Pending messages: {peer.alien.messages}
Pending packets: {peer.alien.packets}
Heeds: {this.renderDucts(peer.alien.heeds)}
</>);
} else if (peer.known) {
const p = peer.known;
const status = (<>
<h4 style={{marginTop: '1em'}}>status</h4>
<table><tbody>
<tr>
<td class="inter">Life</td>
<td>{p.life}</td>
</tr>
<tr>
<td class="inter">Route</td>
<td>
{ p.route
? `${p.route.direct ? '' : 'in'}direct, on lane ${p.route.lane}`
: 'none'
}
</td>
</tr>
<tr>
<td class="inter">QoS</td>
<td>
{p.qos.kind},
last contact {msToDa(p.qos['last-contact'])}
</td>
</tr>
</tbody></table>
</>);
const sndItems = p.snd.map(snd => {
const unsent = snd['unsent-messages'].reduce((a, b) => {
return a + b + ' bytes, ';
}, 'unsent msg sizes: ');
const queuedAcks = snd['queued-message-acks'].map(qa => {
return {key: qa['message-num'], jsx: (
qa['message-num'] + ': ' + qa.ack
)};
});
const m = snd['packet-pump-state'].metrics;
const pumpMetrics = (<>
<table><tbody>
<tr class="inter">
<td>rto</td>
<td>rtt</td>
<td>rttvar</td>
<td>ssthresh</td>
<td>num-live</td>
<td>cwnd</td>
<td>counter</td>
</tr>
<tr>
<td>{m.rto}</td>
<td>{m.rtt}</td>
<td>{m.rttvar}</td>
<td>{m.ssthresh}</td>
<td>{m['num-live']}</td>
<td>{m.cwnd}</td>
<td>{m.counter}</td>
</tr>
</tbody></table>
</>);
const liveItems = snd['packet-pump-state'].live.map(live => {
return {key: live['message-num']+','+live['fragment-num'], jsx: (
<table><tbody>
<tr>
<td>message-num</td>
<td>fragment-num</td>
<td>num-fragments</td>
<td>last-sent</td>
<td>retries</td>
<td>skips</td>
</tr>
<tr>
<td>{live['message-num']}</td>
<td>{live['fragment-num']}</td>
<td>{live['num-fragments']}</td>
<td>{msToDa(live['last-sent'])}</td>
<td>{live.retries}</td>
<td>{live.skips}</td>
</tr>
</tbody></table>
)};
});
const live = (
<SearchableList placeholder="msg-num,frag-num" items={liveItems} />
);
const summary = (<>
{renderDuct(snd.duct)}
<table><tbody>
<tr class="inter">
<td>bone</td>
<td>current</td>
<td>next</td>
<td>next wake</td>
<td>total unsent</td>
</tr>
<tr>
<td>{snd.bone}</td>
<td>{snd.current}</td>
<td>{snd.next}</td>
<td>{msToDa(snd['packet-pump-state']['next-wake'])}</td>
<td>
{snd['unsent-messages'].reduce((a,b) => a+b, 0)} bytes
({snd['unsent-messages'].length} messages)
</td>
</tr>
</tbody></table>
</>);
const details = (<>
{pumpMetrics}
{unsent}
{queuedAcks}
{live}
</>);
return {key: snd.bone + ', ' + renderDuct(snd.duct), jsx: (
<Summary summary={summary} details={details} />
)};
});
const snd = (<>
<h4 style={{marginTop: '1em'}}>snd</h4>
<SearchableList placeholder="bone, duct" items={sndItems} />
</>);
const rcvItems = p.rcv.map(rcv => {
console.log('rcv', rcv);
const pendingVaneAcks = rcv['pending-vane-ack'].reduce((a, b) => {
return a + b + ', ';
}, 'pending vane acks: ');
const nax = rcv.nax.reduce((a, b) => {
return a + b + ', ';
}, 'nacks: ');
const liveItems = rcv['live-messages'].map(live => {
return {key: live['message-num'], jsx: (<>
Message #{live['message-num']}<br/>
{live['num-received']} out of {live['num-fragments']} fragments received:<br/>
{live.fragments.reduce((a, b) => a + b + ', ', '')}
</>)};
});
const liveMessages = (<>
Live messages:<br/>
<SearchableList placeholder="message num" items={liveItems} />
</>);
const summary = (<>
{renderDuct(rcv.duct)}
<table><tbody>
<tr>
<td>bone</td>
<td>last-acked</td>
<td>last-heard</td>
</tr>
<tr>
<td>{rcv.bone}</td>
<td>{rcv['last-acked']}</td>
<td>{rcv['last-heard']}</td>
</tr>
</tbody></table>
</>);
const details = (<>
{pendingVaneAcks}<br/>
{nax}<br/>
{liveMessages}
</>);
return {key: rcv.bone + ', ' + renderDuct(rcv.duct), jsx: (
<Summary summary={summary} details={details} />
)};
});
const rcv = (<>
<h4 style={{marginTop: '1em'}}>rcv</h4>
<SearchableList placeholder="bone, duct" items={rcvItems} />
</>);
const naxItems = p.nax.map(nack => {
return {key: nack.bone, jsx: (
<div>
bone {nack.bone}, message #{nack['message-num']}, duct:<br/>
{renderDuct(nack.duct)}
</div>
)};
});
const nax = (<>
<h4 style={{marginTop: '1em'}}>nax</h4>
<SearchableList placeholder="bone" items={naxItems} />
</>);
const heeds = (<>
<h4 style={{marginTop: '1em'}}>heeds</h4>
{this.renderDucts(p.heeds)}
</>);
return (<>
{status}
{snd}
{rcv}
{nax}
{heeds}
</>);
} else {
console.log('weird peer', peer);
return '???';
}
}
const knownItems = known.map(who => {
return {key: '~'+who, jsx: (<Summary
id={who}
summary={'~'+who + ' (known)'}
details={renderDetails(who)}
onOpen={this.loadPeerDetails}
/>)};
});
const alienItems = alien.map(who => {
return {key: '~'+who, jsx: (<Summary
id={who}
summary={'~'+who + ' (alien)'}
details={renderDetails(who)}
onOpen={this.loadPeerDetails}
/>)};
});
const items = [...knownItems, ...alienItems];
return (
<SearchableList placeholder="ship name" items={items} />
);
}
}

View File

@ -0,0 +1,104 @@
import React, { Component } from 'react';
import { Spinner } from '../components/lib/icons/icon-spinner';
import { Subscriptions } from '../components/subscriptions';
import { Route, Link } from 'react-router-dom';
import { makeRoutePath, isPatTa, deSig } from '../lib/util';
import urbitOb from 'urbit-ob';
import { SearchableList } from '../components/searchable-list';
import { Summary } from '../components/summary';
export class Apps extends Component {
constructor(props) {
super(props);
this.state = {};
this.loadApps = this.loadApps.bind(this);
this.loadAppDetails = this.loadAppDetails.bind(this);
}
componentDidMount() {
if (Object.keys(this.props.apps).length === 0) {
this.loadApps();
}
}
componentDidUpdate(prevProps, prevState) {
const { props, state } = this;
//
}
loadApps() {
api.getApps();
}
loadAppDetails(app) {
api.getAppDetails(app);
}
//TODO use classes for styling?
render() {
const { props, state } = this;
console.log('render', props.apps, Object.keys(props.apps));
const apps = Object.keys(props.apps).sort().map(app => {
const appData = props.apps[app];
const haveDeets = (typeof appData === 'object');
const running = haveDeets
? true
: appData;
const runStyle = running
? {borderLeft: '3px solid green'}
: {borderLeft: '3px solid grey'}
let deets = null;
if (!haveDeets) {
deets = running
? "Loading..."
: "App not running.";
} else if (appData.noDebug) {
deets = "App doesn't use /lib/dbug";
} else {
const data = appData;
const events = (data.events || []).map(e => {
return {key: e, jsx: (<>
{e}<br/>
</>)};
})
deets = (<>
<div style={{maxHeight: '500px', overflow: 'scroll'}}>
<pre>{data.state.join('\n')}</pre>
</div>
<div>
<Subscriptions {...data.subscriptions} />
</div>
<div>
<button onClick={()=>{api.bindToVerb(app)}}>listen to verb</button>
<SearchableList placeholder="event description" items={events} />
</div>
</>)
}
const onOpen = running
? this.loadAppDetails
: null;
return {key: app, jsx: (
<Summary id={app} summary={'%'+app} details={deets} onOpen={onOpen} style={runStyle} />
)};
});
return (
<div
className={
"h-100 w-100 pa3 pt4 overflow-x-hidden " +
"bg-gray0-d white-d flex flex-column"
}>
<div className="w-100 dn-m dn-l dn-xl inter pt1 pb6 f8">
refresh?
</div>
<SearchableList placeholder="app name" items={apps} />
</div>
);
}
}

View File

@ -0,0 +1,56 @@
import React, { Component } from 'react';
import { Spinner } from '../components/lib/icons/icon-spinner';
import { Subscriptions } from '../components/subscriptions';
import { Route, Link } from 'react-router-dom';
import { msToDa, renderDuct } from '../lib/util';
import urbitOb from 'urbit-ob';
import { SearchableList } from '../components/searchable-list';
export class Behn extends Component {
constructor(props) {
super(props);
this.state = {};
this.loadTimers = this.loadTimers.bind(this);
}
componentDidMount() {
const { timers } = this.props;
if (timers.length === 0) {
this.loadTimers();
}
}
componentDidUpdate(prevProps, prevState) {
const { props, state } = this;
//
}
loadTimers() {
api.getTimers();
}
//TODO use classes for styling?
render() {
const { props, state } = this;
const items = props.timers.map(timer => {
const duct = renderDuct(timer.duct);
return {key: duct, jsx: (<div class="flex">
<div class="flex-auto" style={{maxWidth: '50%'}}>
{msToDa(timer.date)}
</div>
<div class="flex-auto" style={{maxWidth: '50%'}}>
{duct}
</div>
</div>)};
});
return (
<table><tbody>
<SearchableList placeholder="duct" items={items} />
</tbody></table>
);
}
}

View File

@ -0,0 +1,143 @@
import { Gitgraph, templateExtend, TemplateName } from "@gitgraph/react";
import React, { Component } from 'react';
import { BrowserRouter, Route } from "react-router-dom";
import _ from 'lodash';
export class Clay extends Component {
constructor(props) {
super(props);
this.clickety = this.clickety.bind(this);
this.clickCommit = this.clickCommit.bind(this);
this.submit = this.submit.bind(this);
this.graph = this.graph.bind(this);
this.template = templateExtend(TemplateName.Metro, {
branch: {
lineWidth: 6,
},
commit: {
spacing: 40,
dot: {
size: 10,
},
message: {
displayHash: false,
displayAuthor: false,
font: "normal 16pt monospace",
}
},
});
}
componentDidMount() {
api.getCommits();
}
clickety() {
let { commits, gitgraph } = this.props;
if ( !commits.commits ) return;
let commitMap = {};
commits.commits.forEach(commit => {
commitMap[commit.commitHash] = commit;
});
let data = commits.commits.map(com => {
console.log(com.commitHash,commits.head);
let ref = [];
if (com.commitHash in commits.head) {
ref = ["HEAD", commits.head[com.commitHash]];
}
return {
refs: ref,
hash: com.commitHash.slice(2), // lop off 0v for more unique hash
parents: com.parents.map(par => {return par.slice(2);}),
onMessageClick: this.clickCommit,
subject: "commit: " +
com.commitHash.slice(-5) +
", content: " +
com.contentHash.slice(-5) +
", parents: " +
com.parents.map(par => {return par.slice(-5);}),
author: {
name: "me",
email: "me",
timestamp: 1500000000000,
} } });
gitgraph.import(data);
}
clickCommit(commit, args) {
console.log("click", commit);
let val = commit.refs.slice(-1)[0];
if (!val) {
return
} else if (this.bobDesk.value == "") {
this.bobDesk.value = val;
} else {
this.aliDesk.value = val;
}
}
submit() {
//TODO hook up
api.pottery( {
ali: this.aliDesk.value,
bob: this.bobDesk.value,
germ: this.germ.value,
});
}
graph(gitgraph) {
this.setState({gitgraph: gitgraph});
}
render() {
let textAreaClasses =
"f7 mono ba bg-gray0-d white-d pa3 mb2 db " +
"focus-b--black focus-b--white-d b--gray3 b--gray2-d nowrap "
const inputs = (<>
<textarea
ref={ e => { this.bobDesk = e; } }
className={textAreaClasses}
placeholder="target desk"
spellCheck="false"
rows={1}
/>
<textarea
ref={ e => { this.aliDesk = e; } }
className={textAreaClasses}
placeholder="source desk"
spellCheck="false"
rows={1}
/>
<select
ref={ e => { this.germ = e; } }
className={textAreaClasses}>
<option value="mate">%mate: conflict if changed same lines</option>
<option value="meet">%meet: conflict if changed same files</option>
<option value="meld">%meld: annotate conflicts</option>
<option value="fine">%fine: fast-forward (requires ancestor)</option>
<option value="this">%this: use target desk's data</option>
<option value="that">%that: use source desk's data</option>
<option value="init">%init: start new desk (danger!)</option>
</select>
<button
className={textAreaClasses}
onClick={this.submit}>
Merge!
</button>
</>);
this.clickety();
return (
<div className="cf w-100 flex flex-column pa4 ba-m ba-l ba-xl b--gray2 br1 h-100 h-100-minus-40-m h-100-minus-40-l h-100-minus-40-xl f9 white-d">
<Gitgraph options={{template: this.template}}>{this.graph}</Gitgraph>
</div>
);
}
}

View File

@ -0,0 +1,56 @@
import React, { Component } from 'react';
import { Spinner } from '../components/lib/icons/icon-spinner';
import { Subscriptions } from '../components/subscriptions';
import { Route, Link } from 'react-router-dom';
import { msToDa, renderDuct } from '../lib/util';
import urbitOb from 'urbit-ob';
import { SearchableList } from '../components/searchable-list';
export class Eyre extends Component {
constructor(props) {
super(props);
this.state = {};
this.loadBindings = this.loadBindings.bind(this);
}
componentDidMount() {
const { bindings } = this.props;
if (bindings.length === 0) {
this.loadBindings();
}
}
componentDidUpdate(prevProps, prevState) {
const { props, state } = this;
//
}
loadBindings() {
api.getBindings();
}
//TODO use classes for styling?
render() {
const { props, state } = this;
const items = props.bindings.map(binding => {
return {key: binding.location + ' ' + binding.action, jsx: (<div class="flex">
<div class="flex-auto" style={{maxWidth: '50%'}}>
{binding.location}
</div>
<div class="flex-auto" style={{maxWidth: '50%'}}>
{binding.action}
</div>
</div>)};
});
return (
<table><tbody>
<SearchableList placeholder="binding" items={items} />
</tbody></table>
);
}
}

View File

@ -0,0 +1,59 @@
import React, { Component } from 'react';
import { Spinner } from '../components/lib/icons/icon-spinner';
import { Subscriptions } from '../components/subscriptions';
import { Route, Link } from 'react-router-dom';
import { msToDa, renderDuct } from '../lib/util';
import urbitOb from 'urbit-ob';
import { SearchableList } from '../components/searchable-list';
export class Spider extends Component {
constructor(props) {
super(props);
this.state = {};
this.loadThreads = this.loadThreads.bind(this);
this.renderThreads = this.renderThreads.bind(this);
this.killThread = this.killThread.bind(this);
}
componentDidMount() {
const { threads } = this.props;
if (Object.keys(threads).length === 0) {
this.loadThreads();
}
}
componentDidUpdate(prevProps, prevState) {
const { props, state } = this;
//
}
loadThreads() {
api.getThreads();
}
killThread(tid) {
api.killThread(tid);
}
renderThreads(threads) {
console.log('rendering threads', threads);
return Object.keys(threads).map(thread => {
const kids = this.renderThreads(threads[thread]);
return (<>
<div>
<button style={{margin: '4px'}} onClick={()=>{this.killThread(thread)}}>kill</button>
{thread}
</div>
<div style={{paddingLeft: '16px'}}>{kids}</div>
</>);
});
}
render() {
if (Object.keys(this.props.threads).length === 0)
return 'no running threads';
return this.renderThreads(this.props.threads);
}
}

View File

@ -0,0 +1,11 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import _ from 'lodash';
export default class DebugTile extends Component {
render() {
return null;
}
}
window.debugTile = DebugTile;