on master help sign in
Adding links to/from hosting and visualising ship states
commitf872505e39eb56023e6ab81f784bac117bf1a62b authorBrendan Hay <brendan.g.hay@gmail.com> dateTue 10 Dec 2019, 09:37 (7 years ago) parent9a5e76d
6 files changed+269−155
src/components/HostedShip.js+106−62 src/lib/SolarisClient.js+33−14 src/lib/useHostedShip.js+78−57 src/style/hosted-ship.scss+14 src/views/Admin/AdminNetworkingKeys.js+2−2 src/views/Hosting.js+36−20
src/components/HostedShip.js+106−62
@@ -1,100 +1,144 @@
11import React from 'react';
2−import cn from 'classnames';
32import { Grid, ErrorText } from 'indigo-react';
43
4+import cn from 'classnames';
5+
56import BridgeForm from 'form/BridgeForm';
67
7−import {
8− OutButton,
9− RestartButton,
10− GenerateButton,
11− ForwardButton,
12−} from './Buttons';
13−import ProgressButton from './ProgressButton';
8+import { GenerateButton, ForwardButton } from './Buttons';
149
15−export default function HostedShip({
16− // from useHostedShip.bind
17− error,
18− notFound,
19− running,
20− booting,
21− connecting,
22− connected,
23− unknown,
24− events,
10+import 'style/hosted-ship.scss';
2511
26− checkShipExists,
27− getShipEvents,
28− createNewShip,
12+export default function HostedShip({
13+ // useHostedShip
14+ ship,
2915
3016 // additional from parent
3117 className,
3218}) {
33− const canHost = true;
34−
35− const renderPrimarySection = () => {
36− if (notFound) {
19+ const renderStatus = () => {
20+ if (ship.missing || ship.booting) {
3721 return (
38− <Grid.Item full as={GenerateButton} onClick={createNewShip} success>
22+ <Grid.Item
23+ full
24+ as={GenerateButton}
25+ solid
26+ success
27+ disabled={ship.booting}
28+ loading={ship.booting}
29+ onClick={ship.create}>
3930 Boot Urbit OS
4031 </Grid.Item>
4132 );
42− } else if (running) {
33+ } else if (ship.running || ship.connecting) {
4334 return (
44− <Grid.Item full as={ForwardButton} onClick={getShipEvents} success>
35+ <Grid.Item
36+ full
37+ as={ForwardButton}
38+ solid
39+ success
40+ disabled={ship.connecting}
41+ loading={ship.connecting}
42+ onClick={ship.getEvents}>
4543 Connect
4644 </Grid.Item>
4745 );
48− } else if (connecting) {
46+ } else if (ship.connected) {
4947 return (
50− <Grid.Item full as={ProgressButton} success disabled>
51− Connecting
52− </Grid.Item>
53− );
54− } else if (booting) {
55− return (
56− <Grid.Item full as={ProgressButton} success disabled>
57− Booting
48+ <Grid.Item
49+ full
50+ as={GenerateButton}
51+ solid
52+ success
53+ onClick={ship.getStatus}>
54+ Disconnect
5855 </Grid.Item>
5956 );
60− } else {
57+ } else if (ship.unknown || ship.querying) {
6158 return (
6259 <Grid.Item
6360 full
64− as={RestartButton}
65− onClick={checkShipExists}
66− disabled={!canHost}
67− loading={!canHost && unknown}>
68− Check Hosting Status
61+ as={GenerateButton}
62+ accessory={ship.error ? '↺' : '->'}
63+ solid
64+ success
65+ disabled={ship.querying}
66+ loading={ship.querying}
67+ onClick={ship.getStatus}>
68+ Query
6969 </Grid.Item>
7070 );
7171 }
7272 };
7373
74+ const renderEvents = (title, detail, events) => {
75+ return (
76+ events.length > 0 && (
77+ <>
78+ <Grid.Item
79+ full
80+ as={ForwardButton}
81+ accessory=""
82+ disabled
83+ detail={detail}>
84+ {title}
85+ </Grid.Item>
86+ <Grid.Item full>
87+ <ul className="hosting-events">
88+ {events.map(event => (
89+ <li>{event}</li>
90+ ))}
91+ </ul>
92+ </Grid.Item>
93+ </>
94+ )
95+ );
96+ };
97+
7498 return (
75− <Grid className={cn(className, 'mt1')}>
76− <BridgeForm>
77− {() => (
78− <>
79− {renderPrimarySection()}
99+ <>
100+ <Grid className={cn(className, 'mt1')}>
101+ <BridgeForm>
102+ {() => (
103+ <>
104+ {ship.error && (
105+ <Grid.Item full as={ErrorText} className="mv1">
106+ {ship.error.message}
107+ </Grid.Item>
108+ )}
109+ {renderStatus()}
110+ {!ship.unknown && (
111+ <Grid.Item
112+ full
113+ as={ForwardButton}
114+ accessory=""
115+ disabled
116+ detail={ship.state}
117+ detailClassName="mono">
118+ Ship Status
119+ </Grid.Item>
120+ )}
121+ </>
122+ )}
123+ </BridgeForm>
80124
81− {error && (
82− <Grid.Item full as={ErrorText} className="mv1">
83− {error.message}
84− </Grid.Item>
125+ {ship.connected && (
126+ <>
127+ <Grid.Divider />
128+ {renderEvents('Run', 'Terminal events', ship.runEvents)}
129+ {renderEvents(
130+ 'New',
131+ 'Pier creation and boot events',
132+ ship.newEvents
85133 )}
86−
87− {running && (
88− <>
89− <Grid.Item full as={OutButton}>
90− Redirect to Urbit OS Dojo
91− </Grid.Item>
92− <Grid.Divider />
93− </>
134+ {renderEvents(
135+ 'Sys',
136+ 'Hosting platform events',
137+ ship.sysEvents.map(e => e.object.message)
94138 )}
95139 </>
96140 )}
97− </BridgeForm>
98− </Grid>
141+ </Grid>
142+ </>
99143 );
100144}
src/lib/SolarisClient.js+33−14
@@ -9,8 +9,12 @@ export default class SolarisClient {
99 this.endpoint = endpoint;
1010 }
1111
12+ getHealthzUrl() {
13+ return (this.endpoint + '/healthz');
14+ }
15+
1216 async getHealthz() {
13− return await fetch(this.endpoint + '/healthz', {
17+ return await fetch(this.getHealthzUrl(), {
1418 method: 'GET',
1519 headers: {
1620 'Accept': 'application/json',
@@ -18,8 +22,12 @@ export default class SolarisClient {
1822 });
1923 }
2024
25+ getUrl() {
26+ return (this.endpoint + '/');
27+ }
28+
2129 async get() {
22− return await fetch(this.endpoint + '/', {
30+ return await fetch(this.getUrl(), {
2331 method: 'GET',
2432 headers: {
2533 'Accept': 'application/json',
@@ -27,8 +35,12 @@ export default class SolarisClient {
2735 });
2836 }
2937
38+ getImagesUrl() {
39+ return (this.endpoint + '/images');
40+ }
41+
3042 async getImages() {
31− return await fetch(this.endpoint + '/images', {
43+ return await fetch(this.getImagesUrl(), {
3244 method: 'GET',
3345 headers: {
3446 'Accept': 'application/json',
@@ -36,8 +48,12 @@ export default class SolarisClient {
3648 });
3749 }
3850
51+ getShipsUrl() {
52+ return (this.endpoint + '/ships');
53+ }
54+
3955 async getShips() {
40− return await fetch(this.endpoint + '/ships', {
56+ return await fetch(this.getShipsUrl(), {
4157 method: 'GET',
4258 headers: {
4359 'Accept': 'application/json',
@@ -45,8 +61,12 @@ export default class SolarisClient {
4561 });
4662 }
4763
64+ getShipsByPatpUrl(patp) {
65+ return (this.endpoint + '/ships/' + encodeURIComponent(patp) + '');
66+ }
67+
4868 async getShipsByPatp(patp) {
49− return await fetch(this.endpoint + '/ships/' + encodeURIComponent(patp) + '', {
69+ return await fetch(this.getShipsByPatpUrl(patp), {
5070 method: 'GET',
5171 headers: {
5272 'Accept': 'application/json',
@@ -54,8 +74,12 @@ export default class SolarisClient {
5474 });
5575 }
5676
77+ getShipsByPatpDetailsUrl(patp) {
78+ return (this.endpoint + '/ships/' + encodeURIComponent(patp) + '/details');
79+ }
80+
5781 async getShipsByPatpDetails(patp) {
58− return await fetch(this.endpoint + '/ships/' + encodeURIComponent(patp) + '/details', {
82+ return await fetch(this.getShipsByPatpDetailsUrl(patp), {
5983 method: 'GET',
6084 headers: {
6185 'Accept': 'application/json',
@@ -63,17 +87,12 @@ export default class SolarisClient {
6387 });
6488 }
6589
66− async getShipsByPatpEvents(patp) {
67− return await fetch(this.endpoint + '/ships/' + encodeURIComponent(patp) + '/events', {
68− method: 'GET',
69− headers: {
70− 'Accept': 'application/json',
71− },
72− });
90+ postShipsUrl() {
91+ return (this.endpoint + '/ships');
7392 }
7493
7594 async postShips(data) {
76− return await fetch(this.endpoint + '/ships', {
95+ return await fetch(this.postShipsUrl(), {
7796 method: 'POST',
7897 cache: 'no-cache',
7998 body: JSON.stringify(data),
src/lib/useHostedShip.js+78−57
@@ -1,62 +1,73 @@
11import { useCallback, useState } from 'react';
2−import ob from 'urbit-ob';
3−
4−import SolarisClient from 'lib/SolarisClient';
5−import useDeepEqualReference from './useDeepEqualReference';
62
73const STATE = {
8− NOT_FOUND: 'NOT_FOUND',
4+ // container states
5+ UNKNOWN: 'UNKNOWN',
6+ MISSING: 'MISSING',
97 RUNNING: 'RUNNING',
8+ PENDING: 'PENDING',
9+ // api states
10+ QUERYING: 'QUERYING',
1011 BOOTING: 'BOOTING',
1112 CONNECTING: 'CONNECTING',
13+ // connection states
1214 CONNECTED: 'CONNECTED',
13− UNKNOWN: 'UNKNOWN',
1415};
1516
16−export function useHostedShip(point, endpoint = 'http://localhost:3030') {
17− const patp = ob.patp(point.value);
18− const client = new SolarisClient(endpoint);
19−
17+export function useHostedShip(client, patp) {
2018 const [state, setState] = useState(STATE.UNKNOWN);
21− const [events, setEvents] = useState([]);
22− const [error, _setError] = useState();
2319
20+ const [sysEvents, setSysEvents] = useState([]);
21+ const [newEvents, setNewEvents] = useState([]);
22+ const [runEvents, setRunEvents] = useState([]);
23+
24+ const [error, _setError] = useState();
2425 const setError = useCallback(
2526 error => {
2627 _setError(error);
27− setState(STATE.UNKNOWN);
2828 if (error) {
29+ setState(STATE.UNKNOWN);
2930 console.error(error);
3031 }
3132 },
32− [_setError]
33+ [_setError, setState]
3334 );
3435
35− const addEvent = event => {
36− setEvents(current => current.push(event));
37− };
38−
39− const notFound = state === STATE.NOT_FOUND;
36+ const unknown = state === STATE.UNKNOWN;
37+ const missing = state === STATE.MISSING;
4038 const running = state === STATE.RUNNING;
39+ const pending = state === STATE.PENDING;
40+ const querying = state === STATE.QUERYING;
4141 const booting = state === STATE.BOOTING;
4242 const connecting = state === STATE.CONNECTING;
4343 const connected = state === STATE.CONNECTED;
44− const unknown = state === STATE.UNKNOWN;
4544
46− const checkShipExists = useCallback(async () => {
45+ const resetEvents = useCallback(() => {
46+ setSysEvents([]);
47+ setNewEvents([]);
48+ setRunEvents([]);
49+ }, []);
50+
51+ const getStatus = useCallback(async () => {
4752 try {
4853 setError(undefined);
4954
55+ resetEvents();
56+
57+ setState(STATE.QUERYING);
58+
5059 const response = await client.getShipsByPatp(patp);
5160
5261 if (response.status === 404) {
53− setState(STATE.NOT_FOUND);
62+ setState(STATE.MISSING);
5463 } else if (!response.ok) {
5564 setError(response.statusText);
5665 } else {
5766 const ship = await response.json();
5867
59− if (ship.status === 'Running') {
68+ if (ship.status === 'Pending') {
69+ setState(STATE.PENDING);
70+ } else if (ship.status === 'Running') {
6071 setState(STATE.RUNNING);
6172 } else {
6273 setError(ship);
@@ -65,44 +76,47 @@ export function useHostedShip(point, endpoint = 'http://localhost:3030') {
6576 } catch (error) {
6677 setError(error);
6778 }
68− }, [client, patp, setError]);
79+ }, [client, patp, resetEvents, setError]);
6980
70− const getShipEvents = useCallback(async () => {
81+ const getEvents = useCallback(() => {
7182 try {
7283 setError(undefined);
73− setState(STATE.CONNECTING);
7484
75− const response = await client.getShipsByPatpEvents(patp);
85+ resetEvents();
7686
77− if (!response.ok) {
78− setError(response.statusText);
79− } else {
80− const reader = response.body.getReader();
87+ setState(STATE.CONNECTING);
88+
89+ const url = client.getShipsByPatpUrl(patp) + '/events';
90+ const source = new EventSource(url);
8191
82− reader.read().then(function yieldEvent({ done, value }) {
83− setState(STATE.CONNECTED);
92+ source.onopen = () => setState(STATE.CONNECTED);
93+ // source.onerror = () => setState(STATE.UNKNOWN);
94+ // source.onmessage = console.log;
8495
85− // Result objects contain two properties:
86− // done - true if the stream has already given you all its data.
87− // value - some data. Always undefined when done is true.
88− if (done) {
89− return;
90− }
96+ source.addEventListener('sys', event => {
97+ setSysEvents(previous => [...previous, JSON.parse(event.data)]);
98+ });
9199
92− addEvent(JSON.parse(value));
100+ source.addEventListener('new', event => {
101+ if (event.data !== '') {
102+ setNewEvents(previous => [...previous, event.data]);
103+ }
104+ });
93105
94− // Read some more, and call this function again
95− return reader.read().then(yieldEvent);
96− });
97− }
106+ source.addEventListener('run', event => {
107+ if (event.data !== '') {
108+ setRunEvents(previous => [...previous, event.data]);
109+ }
110+ });
98111 } catch (error) {
99112 setError(error);
100113 }
101− }, [client, patp, setError]);
114+ }, [setError, resetEvents, client, patp, setState]);
102115
103− const createNewShip = useCallback(async () => {
116+ const create = useCallback(async () => {
104117 try {
105118 setError(undefined);
119+
106120 setState(STATE.BOOTING);
107121
108122 const response = await client.postShips({
@@ -113,26 +127,33 @@ export function useHostedShip(point, endpoint = 'http://localhost:3030') {
113127 if (!response.ok) {
114128 setError(response.statusText);
115129 } else {
116− checkShipExists();
130+ getEvents();
117131 }
118132 } catch (error) {
119133 setError(error);
120134 }
121− }, [checkShipExists, client, patp, setError]);
135+ }, [setError, client, patp, getEvents]);
122136
123− const values = useDeepEqualReference({
137+ return {
124138 error,
125− notFound,
139+ unknown,
140+ missing,
126141 running,
142+ pending,
143+ querying,
127144 booting,
128145 connecting,
129146 connected,
130− unknown,
131− events,
132− checkShipExists,
133− getShipEvents,
134− createNewShip,
135− });
136147
137− return { ...values, bind: values };
148+ sysEvents,
149+ newEvents,
150+ runEvents,
151+
152+ getStatus,
153+ getEvents,
154+ create,
155+
156+ // debugging
157+ state,
158+ };
138159}
src/style/hosted-ship.scssnew file+14
@@ -0,0 +1,14 @@
1+.hosting-events {
2+ margin: 0;
3+ padding: 2px;
4+ background: #000;
5+ list-style: none;
6+ font-family: monospace;
7+
8+ li {
9+ margin: 1px;
10+ padding: 2px;
11+ background: #111;
12+ color: #26ca41;
13+ }
14+}
src/views/Admin/AdminNetworkingKeys.js+2−2
@@ -26,7 +26,7 @@ import { addHexPrefix } from 'lib/wallet';
2626import useKeyfileGenerator from 'lib/useKeyfileGenerator';
2727
2828import ViewHeader from 'components/ViewHeader';
29−import { BootUrbitOSButton } from 'components/Buttons';
29+import { HostingButton } from 'components/Buttons';
3030import FooterButton from 'components/FooterButton';
3131import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
3232import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
@@ -417,7 +417,7 @@ export default function AdminNetworkingKeys() {
417417
418418 {completed && (
419419 <>
420− <Grid.Item full as={BootUrbitOSButton} />
420+ <Grid.Item full as={HostingButton} onReturn={() => pop()} />
421421 <Grid.Divider />
422422 </>
423423 )}
src/views/Hosting.js+36−20
@@ -1,27 +1,34 @@
1−import React, { useCallback } from 'react';
2−import { Just } from 'folktale/maybe';
1+import React from 'react';
32import { Grid } from 'indigo-react';
3+import { Just } from 'folktale/maybe';
4+import ob from 'urbit-ob';
45
5−import View from 'components/View';
6−import Passport from 'components/Passport';
7−import { OutButton } from 'components/Buttons';
8−import HostedShip from 'components/HostedShip';
9−
10−import * as need from 'lib/need';
116import { usePointCursor } from 'store/pointCursor';
127import { useWallet } from 'store/wallet';
8+
9+import * as need from 'lib/need';
1310import { useLocalRouter } from 'lib/LocalRouter';
1411import { useHostedShip } from 'lib/useHostedShip';
12+import SolarisClient from 'lib/SolarisClient';
13+
14+import View from 'components/View';
15+import Passport from 'components/Passport';
16+import { OutButton, ForwardButton } from 'components/Buttons';
17+import HostedShip from 'components/HostedShip';
18+import Footer from 'components/Footer';
1519
1620export default function Hosting() {
1721 const { pop } = useLocalRouter();
22+
1823 const { pointCursor } = usePointCursor();
24+ const point = need.point(pointCursor);
25+ const patp = ob.patp(point);
1926
2027 const { wallet } = useWallet();
21− const point = need.point(pointCursor);
2228 const address = need.addressFromWallet(wallet);
2329
24− const { bind } = useHostedShip(point);
30+ const client = new SolarisClient('https://localhost:3030');
31+ const ship = useHostedShip(client, patp);
2532
2633 return (
2734 <View pop={pop} inset>
@@ -31,17 +38,26 @@ export default function Hosting() {
3138 address={Just(address)}
3239 animationMode={'slide'}
3340 />
34−
35− <Grid.Item
36− full
37− as={OutButton}
38− href="https://urbit.org/docs/getting-started"
39− detail="Run your own Urbit OS">
40− Hosting Instructions
41− </Grid.Item>
42−
43− <Grid.Item full as={HostedShip} {...bind} onReturn={() => pop()} />
41+ <Grid.Item full as={HostedShip} ship={ship} onReturn={() => pop()} />
42+ <Grid.Divider />
4443 </Grid>
44+
45+ <Footer>
46+ <Grid>
47+ <Grid.Divider />
48+ <Grid.Item full as={ForwardButton} disabled={!ship.running}>
49+ Redirect to Urbit OS Dojo
50+ </Grid.Item>
51+ <Grid.Divider />
52+ <Grid.Item
53+ full
54+ as={OutButton}
55+ href="https://urbit.org/docs/getting-started"
56+ detail="Run your own Urbit OS">
57+ Hosting Instructions
58+ </Grid.Item>
59+ </Grid>
60+ </Footer>
4561 </View>
4662 );
4763}