on master help sign in
Adding links to/from hosting and visualising ship states
f872505Brendan Hay
src/components/HostedShip.js+106−62
@@ -1,100 +1,144 @@
1import React from 'react';
1import React from 'react';
2−import cn from 'classnames';
3import { Grid, ErrorText } from 'indigo-react';
2import { Grid, ErrorText } from 'indigo-react';
4
3
4+import cn from 'classnames';
5+
5import BridgeForm from 'form/BridgeForm';
6import BridgeForm from 'form/BridgeForm';
6
7
7−import {
8− OutButton,
9− RestartButton,
10− GenerateButton,
8+import { GenerateButton, ForwardButton } from './Buttons';
11− ForwardButton,
12−} from './Buttons';
13−import ProgressButton from './ProgressButton';
14
9
15−export default function HostedShip({
10+import 'style/hosted-ship.scss';
16− // from useHostedShip.bind
17− error,
18− notFound,
19− running,
20− booting,
21− connecting,
22− connected,
23− unknown,
24− events,
25
11
26− checkShipExists,
12+export default function HostedShip({
27− getShipEvents,
13+ // useHostedShip
28− createNewShip,
14+ ship,
29
15
30 // additional from parent
16 // additional from parent
31 className,
17 className,
32}) {
18}) {
33− const canHost = true;
19+ const renderStatus = () => {
34−
20+ if (ship.missing || ship.booting) {
35− const renderPrimarySection = () => {
36− if (notFound) {
37 return (
21 return (
22+ <Grid.Item
23+ full
38− <Grid.Item full as={GenerateButton} onClick={createNewShip} success>
24+ as={GenerateButton}
25+ solid
26+ success
27+ disabled={ship.booting}
28+ loading={ship.booting}
29+ onClick={ship.create}>
39 Boot Urbit OS
30 Boot Urbit OS
40 </Grid.Item>
31 </Grid.Item>
41 );
32 );
42− } else if (running) {
33+ } else if (ship.running || ship.connecting) {
43 return (
34 return (
35+ <Grid.Item
36+ full
44− <Grid.Item full as={ForwardButton} onClick={getShipEvents} success>
37+ as={ForwardButton}
38+ solid
39+ success
40+ disabled={ship.connecting}
41+ loading={ship.connecting}
42+ onClick={ship.getEvents}>
45 Connect
43 Connect
46 </Grid.Item>
44 </Grid.Item>
47 );
45 );
48− } else if (connecting) {
46+ } else if (ship.connected) {
49 return (
47 return (
50− <Grid.Item full as={ProgressButton} success disabled>
48+ <Grid.Item
51− Connecting
49+ full
52− </Grid.Item>
50+ as={GenerateButton}
53− );
51+ solid
54− } else if (booting) {
52+ success
55− return (
53+ onClick={ship.getStatus}>
56− <Grid.Item full as={ProgressButton} success disabled>
54+ Disconnect
57− Booting
58 </Grid.Item>
55 </Grid.Item>
59 );
56 );
60− } else {
57+ } else if (ship.unknown || ship.querying) {
61 return (
58 return (
62 <Grid.Item
59 <Grid.Item
63 full
60 full
64− as={RestartButton}
61+ as={GenerateButton}
65− onClick={checkShipExists}
62+ accessory={ship.error ? '↺' : '->'}
63+ solid
64+ success
66− disabled={!canHost}
65+ disabled={ship.querying}
67− loading={!canHost && unknown}>
66+ loading={ship.querying}
68− Check Hosting Status
67+ onClick={ship.getStatus}>
68+ Query
69 </Grid.Item>
69 </Grid.Item>
70 );
70 );
71 }
71 }
72 };
72 };
73
73
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+
74 return (
98 return (
99+ <>
75− <Grid className={cn(className, 'mt1')}>
100+ <Grid className={cn(className, 'mt1')}>
76− <BridgeForm>
101+ <BridgeForm>
77− {() => (
102+ {() => (
78− <>
103+ <>
79− {renderPrimarySection()}
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>
80
124
81− {error && (
125+ {ship.connected && (
82− <Grid.Item full as={ErrorText} className="mv1">
126+ <>
83− {error.message}
127+ <Grid.Divider />
84− </Grid.Item>
128+ {renderEvents('Run', 'Terminal events', ship.runEvents)}
129+ {renderEvents(
130+ 'New',
131+ 'Pier creation and boot events',
132+ ship.newEvents
85 )}
133 )}
86−
134+ {renderEvents(
87− {running && (
135+ 'Sys',
88− <>
136+ 'Hosting platform events',
89− <Grid.Item full as={OutButton}>
137+ ship.sysEvents.map(e => e.object.message)
90− Redirect to Urbit OS Dojo
91− </Grid.Item>
92− <Grid.Divider />
93− </>
94 )}
138 )}
95 </>
139 </>
96 )}
140 )}
97− </BridgeForm>
141+ </Grid>
98− </Grid>
142+ </>
99 );
143 );
100}
144}
src/lib/SolarisClient.js+33−14
@@ -9,8 +9,12 @@ export default class SolarisClient {
9 this.endpoint = endpoint;
9 this.endpoint = endpoint;
10 }
10 }
11
11
12+ getHealthzUrl() {
13+ return (this.endpoint + '/healthz');
14+ }
15+
12 async getHealthz() {
16 async getHealthz() {
13− return await fetch(this.endpoint + '/healthz', {
17+ return await fetch(this.getHealthzUrl(), {
14 method: 'GET',
18 method: 'GET',
15 headers: {
19 headers: {
16 'Accept': 'application/json',
20 'Accept': 'application/json',
@@ -18,8 +22,12 @@ export default class SolarisClient {
18 });
22 });
19 }
23 }
20
24
25+ getUrl() {
26+ return (this.endpoint + '/');
27+ }
28+
21 async get() {
29 async get() {
22− return await fetch(this.endpoint + '/', {
30+ return await fetch(this.getUrl(), {
23 method: 'GET',
31 method: 'GET',
24 headers: {
32 headers: {
25 'Accept': 'application/json',
33 'Accept': 'application/json',
@@ -27,8 +35,12 @@ export default class SolarisClient {
27 });
35 });
28 }
36 }
29
37
38+ getImagesUrl() {
39+ return (this.endpoint + '/images');
40+ }
41+
30 async getImages() {
42 async getImages() {
31− return await fetch(this.endpoint + '/images', {
43+ return await fetch(this.getImagesUrl(), {
32 method: 'GET',
44 method: 'GET',
33 headers: {
45 headers: {
34 'Accept': 'application/json',
46 'Accept': 'application/json',
@@ -36,8 +48,12 @@ export default class SolarisClient {
36 });
48 });
37 }
49 }
38
50
51+ getShipsUrl() {
52+ return (this.endpoint + '/ships');
53+ }
54+
39 async getShips() {
55 async getShips() {
40− return await fetch(this.endpoint + '/ships', {
56+ return await fetch(this.getShipsUrl(), {
41 method: 'GET',
57 method: 'GET',
42 headers: {
58 headers: {
43 'Accept': 'application/json',
59 'Accept': 'application/json',
@@ -45,8 +61,12 @@ export default class SolarisClient {
45 });
61 });
46 }
62 }
47
63
64+ getShipsByPatpUrl(patp) {
65+ return (this.endpoint + '/ships/' + encodeURIComponent(patp) + '');
66+ }
67+
48 async getShipsByPatp(patp) {
68 async getShipsByPatp(patp) {
49− return await fetch(this.endpoint + '/ships/' + encodeURIComponent(patp) + '', {
69+ return await fetch(this.getShipsByPatpUrl(patp), {
50 method: 'GET',
70 method: 'GET',
51 headers: {
71 headers: {
52 'Accept': 'application/json',
72 'Accept': 'application/json',
@@ -54,8 +74,12 @@ export default class SolarisClient {
54 });
74 });
55 }
75 }
56
76
77+ getShipsByPatpDetailsUrl(patp) {
78+ return (this.endpoint + '/ships/' + encodeURIComponent(patp) + '/details');
79+ }
80+
57 async getShipsByPatpDetails(patp) {
81 async getShipsByPatpDetails(patp) {
58− return await fetch(this.endpoint + '/ships/' + encodeURIComponent(patp) + '/details', {
82+ return await fetch(this.getShipsByPatpDetailsUrl(patp), {
59 method: 'GET',
83 method: 'GET',
60 headers: {
84 headers: {
61 'Accept': 'application/json',
85 'Accept': 'application/json',
@@ -63,17 +87,12 @@ export default class SolarisClient {
63 });
87 });
64 }
88 }
65
89
66− async getShipsByPatpEvents(patp) {
90+ postShipsUrl() {
67− return await fetch(this.endpoint + '/ships/' + encodeURIComponent(patp) + '/events', {
91+ return (this.endpoint + '/ships');
68− method: 'GET',
69− headers: {
70− 'Accept': 'application/json',
71− },
72− });
73 }
92 }
74
93
75 async postShips(data) {
94 async postShips(data) {
76− return await fetch(this.endpoint + '/ships', {
95+ return await fetch(this.postShipsUrl(), {
77 method: 'POST',
96 method: 'POST',
78 cache: 'no-cache',
97 cache: 'no-cache',
79 body: JSON.stringify(data),
98 body: JSON.stringify(data),
src/lib/useHostedShip.js+78−57
@@ -1,62 +1,73 @@
1import { useCallback, useState } from 'react';
1import { useCallback, useState } from 'react';
2−import ob from 'urbit-ob';
3−
4−import SolarisClient from 'lib/SolarisClient';
5−import useDeepEqualReference from './useDeepEqualReference';
6
2
7const STATE = {
3const STATE = {
8− NOT_FOUND: 'NOT_FOUND',
4+ // container states
5+ UNKNOWN: 'UNKNOWN',
6+ MISSING: 'MISSING',
9 RUNNING: 'RUNNING',
7 RUNNING: 'RUNNING',
8+ PENDING: 'PENDING',
9+ // api states
10+ QUERYING: 'QUERYING',
10 BOOTING: 'BOOTING',
11 BOOTING: 'BOOTING',
11 CONNECTING: 'CONNECTING',
12 CONNECTING: 'CONNECTING',
13+ // connection states
12 CONNECTED: 'CONNECTED',
14 CONNECTED: 'CONNECTED',
13− UNKNOWN: 'UNKNOWN',
14};
15};
15
16
16−export function useHostedShip(point, endpoint = 'http://localhost:3030') {
17+export function useHostedShip(client, patp) {
17− const patp = ob.patp(point.value);
18− const client = new SolarisClient(endpoint);
19−
20 const [state, setState] = useState(STATE.UNKNOWN);
18 const [state, setState] = useState(STATE.UNKNOWN);
21− const [events, setEvents] = useState([]);
22− const [error, _setError] = useState();
23
19
20+ const [sysEvents, setSysEvents] = useState([]);
21+ const [newEvents, setNewEvents] = useState([]);
22+ const [runEvents, setRunEvents] = useState([]);
23+
24+ const [error, _setError] = useState();
24 const setError = useCallback(
25 const setError = useCallback(
25 error => {
26 error => {
26 _setError(error);
27 _setError(error);
27− setState(STATE.UNKNOWN);
28 if (error) {
28 if (error) {
29+ setState(STATE.UNKNOWN);
29 console.error(error);
30 console.error(error);
30 }
31 }
31 },
32 },
32− [_setError]
33+ [_setError, setState]
33 );
34 );
34
35
35− const addEvent = event => {
36+ const unknown = state === STATE.UNKNOWN;
36− setEvents(current => current.push(event));
37− };
38−
39− const notFound = state === STATE.NOT_FOUND;
37+ const missing = state === STATE.MISSING;
40 const running = state === STATE.RUNNING;
38 const running = state === STATE.RUNNING;
39+ const pending = state === STATE.PENDING;
40+ const querying = state === STATE.QUERYING;
41 const booting = state === STATE.BOOTING;
41 const booting = state === STATE.BOOTING;
42 const connecting = state === STATE.CONNECTING;
42 const connecting = state === STATE.CONNECTING;
43 const connected = state === STATE.CONNECTED;
43 const connected = state === STATE.CONNECTED;
44− const unknown = state === STATE.UNKNOWN;
45
44
45+ const resetEvents = useCallback(() => {
46+ setSysEvents([]);
47+ setNewEvents([]);
48+ setRunEvents([]);
49+ }, []);
50+
46− const checkShipExists = useCallback(async () => {
51+ const getStatus = useCallback(async () => {
47 try {
52 try {
48 setError(undefined);
53 setError(undefined);
49
54
55+ resetEvents();
56+
57+ setState(STATE.QUERYING);
58+
50 const response = await client.getShipsByPatp(patp);
59 const response = await client.getShipsByPatp(patp);
51
60
52 if (response.status === 404) {
61 if (response.status === 404) {
53− setState(STATE.NOT_FOUND);
62+ setState(STATE.MISSING);
54 } else if (!response.ok) {
63 } else if (!response.ok) {
55 setError(response.statusText);
64 setError(response.statusText);
56 } else {
65 } else {
57 const ship = await response.json();
66 const ship = await response.json();
58
67
59− if (ship.status === 'Running') {
68+ if (ship.status === 'Pending') {
69+ setState(STATE.PENDING);
70+ } else if (ship.status === 'Running') {
60 setState(STATE.RUNNING);
71 setState(STATE.RUNNING);
61 } else {
72 } else {
62 setError(ship);
73 setError(ship);
@@ -65,44 +76,47 @@ export function useHostedShip(point, endpoint = 'http://localhost:3030') {
65 } catch (error) {
76 } catch (error) {
66 setError(error);
77 setError(error);
67 }
78 }
68− }, [client, patp, setError]);
79+ }, [client, patp, resetEvents, setError]);
69
80
70− const getShipEvents = useCallback(async () => {
81+ const getEvents = useCallback(() => {
71 try {
82 try {
72 setError(undefined);
83 setError(undefined);
73− setState(STATE.CONNECTING);
74
84
75− const response = await client.getShipsByPatpEvents(patp);
85+ resetEvents();
76
86
77− if (!response.ok) {
87+ setState(STATE.CONNECTING);
78− setError(response.statusText);
88+
79− } else {
89+ const url = client.getShipsByPatpUrl(patp) + '/events';
80− const reader = response.body.getReader();
90+ const source = new EventSource(url);
81
91
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;
84
95
85− // Result objects contain two properties:
96+ source.addEventListener('sys', event => {
86− // done - true if the stream has already given you all its data.
97+ setSysEvents(previous => [...previous, JSON.parse(event.data)]);
87− // value - some data. Always undefined when done is true.
88− if (done) {
89− return;
90− }
98+ });
91
99
92− addEvent(JSON.parse(value));
100+ source.addEventListener('new', event => {
101+ if (event.data !== '') {
102+ setNewEvents(previous => [...previous, event.data]);
103+ }
104+ });
93
105
94− // Read some more, and call this function again
106+ source.addEventListener('run', event => {
95− return reader.read().then(yieldEvent);
107+ if (event.data !== '') {
108+ setRunEvents(previous => [...previous, event.data]);
96− });
109+ }
97− }
110+ });
98 } catch (error) {
111 } catch (error) {
99 setError(error);
112 setError(error);
100 }
113 }
101− }, [client, patp, setError]);
114+ }, [setError, resetEvents, client, patp, setState]);
102
115
103− const createNewShip = useCallback(async () => {
116+ const create = useCallback(async () => {
104 try {
117 try {
105 setError(undefined);
118 setError(undefined);
119+
106 setState(STATE.BOOTING);
120 setState(STATE.BOOTING);
107
121
108 const response = await client.postShips({
122 const response = await client.postShips({
@@ -113,26 +127,33 @@ export function useHostedShip(point, endpoint = 'http://localhost:3030') {
113 if (!response.ok) {
127 if (!response.ok) {
114 setError(response.statusText);
128 setError(response.statusText);
115 } else {
129 } else {
116− checkShipExists();
130+ getEvents();
117 }
131 }
118 } catch (error) {
132 } catch (error) {
119 setError(error);
133 setError(error);
120 }
134 }
121− }, [checkShipExists, client, patp, setError]);
135+ }, [setError, client, patp, getEvents]);
122
136
123− const values = useDeepEqualReference({
137+ return {
124 error,
138 error,
125− notFound,
139+ unknown,
140+ missing,
126 running,
141 running,
142+ pending,
143+ querying,
127 booting,
144 booting,
128 connecting,
145 connecting,
129 connected,
146 connected,
130− unknown,
131− events,
132− checkShipExists,
133− getShipEvents,
134− createNewShip,
135− });
136
147
137− return { ...values, bind: values };
148+ sysEvents,
149+ newEvents,
150+ runEvents,
151+
152+ getStatus,
153+ getEvents,
154+ create,
155+
156+ // debugging
157+ state,
158+ };
138}
159}
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';
26import useKeyfileGenerator from 'lib/useKeyfileGenerator';
26import useKeyfileGenerator from 'lib/useKeyfileGenerator';
27
27
28import ViewHeader from 'components/ViewHeader';
28import ViewHeader from 'components/ViewHeader';
29−import { BootUrbitOSButton } from 'components/Buttons';
29+import { HostingButton } from 'components/Buttons';
30import FooterButton from 'components/FooterButton';
30import FooterButton from 'components/FooterButton';
31import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
31import DownloadKeyfileButton from 'components/DownloadKeyfileButton';
32import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
32import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
@@ -417,7 +417,7 @@ export default function AdminNetworkingKeys() {
417
417
418 {completed && (
418 {completed && (
419 <>
419 <>
420− <Grid.Item full as={BootUrbitOSButton} />
420+ <Grid.Item full as={HostingButton} onReturn={() => pop()} />
421 <Grid.Divider />
421 <Grid.Divider />
422 </>
422 </>
423 )}
423 )}
src/views/Hosting.js+36−20
@@ -1,27 +1,34 @@
1−import React, { useCallback } from 'react';
1+import React from 'react';
2−import { Just } from 'folktale/maybe';
3import { Grid } from 'indigo-react';
2import { Grid } from 'indigo-react';
3+import { Just } from 'folktale/maybe';
4+import ob from 'urbit-ob';
4
5
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';
11import { usePointCursor } from 'store/pointCursor';
6import { usePointCursor } from 'store/pointCursor';
12import { useWallet } from 'store/wallet';
7import { useWallet } from 'store/wallet';
8+
9+import * as need from 'lib/need';
13import { useLocalRouter } from 'lib/LocalRouter';
10import { useLocalRouter } from 'lib/LocalRouter';
14import { useHostedShip } from 'lib/useHostedShip';
11import { 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';
15
19
16export default function Hosting() {
20export default function Hosting() {
17 const { pop } = useLocalRouter();
21 const { pop } = useLocalRouter();
22+
18 const { pointCursor } = usePointCursor();
23 const { pointCursor } = usePointCursor();
24+ const point = need.point(pointCursor);
25+ const patp = ob.patp(point);
19
26
20 const { wallet } = useWallet();
27 const { wallet } = useWallet();
21− const point = need.point(pointCursor);
22 const address = need.addressFromWallet(wallet);
28 const address = need.addressFromWallet(wallet);
23
29
30+ const client = new SolarisClient('https://localhost:3030');
24− const { bind } = useHostedShip(point);
31+ const ship = useHostedShip(client, patp);
25
32
26 return (
33 return (
27 <View pop={pop} inset>
34 <View pop={pop} inset>
@@ -31,17 +38,26 @@ export default function Hosting() {
31 address={Just(address)}
38 address={Just(address)}
32 animationMode={'slide'}
39 animationMode={'slide'}
33 />
40 />
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 />
44 </Grid>
43 </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>
45 </View>
61 </View>
46 );
62 );
47}
63}