on master help sign in
Adding solaris hosting client + basic component for ship status
9a5e76dBrendan Hay
src/components/Buttons.js+4−7
@@ -24,11 +24,8 @@ export const OfflineButton = props => (
24 </OutButton>
24 </OutButton>
25);
25);
26
26
27−export const BootUrbitOSButton = props => (
27+export const HostingButton = props => (
28− <OutButton
29− href="https://urbit.org/docs/getting-started"
30− detail="Boot your computer"
28+ <ForwardButton detail="Boot your Urbit OS" {...props}>
31− {...props}>
32− Boot Urbit OS
29+ Boot your Urbit OS
33− </OutButton>
30+ </ForwardButton>
34);
31);
src/components/HostedShip.jsnew file+100
@@ -0,0 +1,100 @@
1+import React from 'react';
2+import cn from 'classnames';
3+import { Grid, ErrorText } from 'indigo-react';
4+
5+import BridgeForm from 'form/BridgeForm';
6+
7+import {
8+ OutButton,
9+ RestartButton,
10+ GenerateButton,
11+ ForwardButton,
12+} from './Buttons';
13+import ProgressButton from './ProgressButton';
14+
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,
25+
26+ checkShipExists,
27+ getShipEvents,
28+ createNewShip,
29+
30+ // additional from parent
31+ className,
32+}) {
33+ const canHost = true;
34+
35+ const renderPrimarySection = () => {
36+ if (notFound) {
37+ return (
38+ <Grid.Item full as={GenerateButton} onClick={createNewShip} success>
39+ Boot Urbit OS
40+ </Grid.Item>
41+ );
42+ } else if (running) {
43+ return (
44+ <Grid.Item full as={ForwardButton} onClick={getShipEvents} success>
45+ Connect
46+ </Grid.Item>
47+ );
48+ } else if (connecting) {
49+ 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
58+ </Grid.Item>
59+ );
60+ } else {
61+ return (
62+ <Grid.Item
63+ full
64+ as={RestartButton}
65+ onClick={checkShipExists}
66+ disabled={!canHost}
67+ loading={!canHost && unknown}>
68+ Check Hosting Status
69+ </Grid.Item>
70+ );
71+ }
72+ };
73+
74+ return (
75+ <Grid className={cn(className, 'mt1')}>
76+ <BridgeForm>
77+ {() => (
78+ <>
79+ {renderPrimarySection()}
80+
81+ {error && (
82+ <Grid.Item full as={ErrorText} className="mv1">
83+ {error.message}
84+ </Grid.Item>
85+ )}
86+
87+ {running && (
88+ <>
89+ <Grid.Item full as={OutButton}>
90+ Redirect to Urbit OS Dojo
91+ </Grid.Item>
92+ <Grid.Divider />
93+ </>
94+ )}
95+ </>
96+ )}
97+ </BridgeForm>
98+ </Grid>
99+ );
100+}
src/lib/SolarisClient.jsnew file+87
@@ -0,0 +1,87 @@
1+/* eslint-disable prettier/prettier */
2+
3+//
4+// Warning: this file is auto-generated.
5+//
6+
7+export default class SolarisClient {
8+ constructor(endpoint) {
9+ this.endpoint = endpoint;
10+ }
11+
12+ async getHealthz() {
13+ return await fetch(this.endpoint + '/healthz', {
14+ method: 'GET',
15+ headers: {
16+ 'Accept': 'application/json',
17+ },
18+ });
19+ }
20+
21+ async get() {
22+ return await fetch(this.endpoint + '/', {
23+ method: 'GET',
24+ headers: {
25+ 'Accept': 'application/json',
26+ },
27+ });
28+ }
29+
30+ async getImages() {
31+ return await fetch(this.endpoint + '/images', {
32+ method: 'GET',
33+ headers: {
34+ 'Accept': 'application/json',
35+ },
36+ });
37+ }
38+
39+ async getShips() {
40+ return await fetch(this.endpoint + '/ships', {
41+ method: 'GET',
42+ headers: {
43+ 'Accept': 'application/json',
44+ },
45+ });
46+ }
47+
48+ async getShipsByPatp(patp) {
49+ return await fetch(this.endpoint + '/ships/' + encodeURIComponent(patp) + '', {
50+ method: 'GET',
51+ headers: {
52+ 'Accept': 'application/json',
53+ },
54+ });
55+ }
56+
57+ async getShipsByPatpDetails(patp) {
58+ return await fetch(this.endpoint + '/ships/' + encodeURIComponent(patp) + '/details', {
59+ method: 'GET',
60+ headers: {
61+ 'Accept': 'application/json',
62+ },
63+ });
64+ }
65+
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+ });
73+ }
74+
75+ async postShips(data) {
76+ return await fetch(this.endpoint + '/ships', {
77+ method: 'POST',
78+ cache: 'no-cache',
79+ body: JSON.stringify(data),
80+ headers: {
81+ 'Accept': 'application/json',
82+ 'Content-Type': 'application/json',
83+ },
84+ });
85+ }
86+
87+}
src/lib/useHostedShip.jsnew file+138
@@ -0,0 +1,138 @@
1+import { useCallback, useState } from 'react';
2+import ob from 'urbit-ob';
3+
4+import SolarisClient from 'lib/SolarisClient';
5+import useDeepEqualReference from './useDeepEqualReference';
6+
7+const STATE = {
8+ NOT_FOUND: 'NOT_FOUND',
9+ RUNNING: 'RUNNING',
10+ BOOTING: 'BOOTING',
11+ CONNECTING: 'CONNECTING',
12+ CONNECTED: 'CONNECTED',
13+ UNKNOWN: 'UNKNOWN',
14+};
15+
16+export function useHostedShip(point, endpoint = 'http://localhost:3030') {
17+ const patp = ob.patp(point.value);
18+ const client = new SolarisClient(endpoint);
19+
20+ const [state, setState] = useState(STATE.UNKNOWN);
21+ const [events, setEvents] = useState([]);
22+ const [error, _setError] = useState();
23+
24+ const setError = useCallback(
25+ error => {
26+ _setError(error);
27+ setState(STATE.UNKNOWN);
28+ if (error) {
29+ console.error(error);
30+ }
31+ },
32+ [_setError]
33+ );
34+
35+ const addEvent = event => {
36+ setEvents(current => current.push(event));
37+ };
38+
39+ const notFound = state === STATE.NOT_FOUND;
40+ const running = state === STATE.RUNNING;
41+ const booting = state === STATE.BOOTING;
42+ const connecting = state === STATE.CONNECTING;
43+ const connected = state === STATE.CONNECTED;
44+ const unknown = state === STATE.UNKNOWN;
45+
46+ const checkShipExists = useCallback(async () => {
47+ try {
48+ setError(undefined);
49+
50+ const response = await client.getShipsByPatp(patp);
51+
52+ if (response.status === 404) {
53+ setState(STATE.NOT_FOUND);
54+ } else if (!response.ok) {
55+ setError(response.statusText);
56+ } else {
57+ const ship = await response.json();
58+
59+ if (ship.status === 'Running') {
60+ setState(STATE.RUNNING);
61+ } else {
62+ setError(ship);
63+ }
64+ }
65+ } catch (error) {
66+ setError(error);
67+ }
68+ }, [client, patp, setError]);
69+
70+ const getShipEvents = useCallback(async () => {
71+ try {
72+ setError(undefined);
73+ setState(STATE.CONNECTING);
74+
75+ const response = await client.getShipsByPatpEvents(patp);
76+
77+ if (!response.ok) {
78+ setError(response.statusText);
79+ } else {
80+ const reader = response.body.getReader();
81+
82+ reader.read().then(function yieldEvent({ done, value }) {
83+ setState(STATE.CONNECTED);
84+
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+ }
91+
92+ addEvent(JSON.parse(value));
93+
94+ // Read some more, and call this function again
95+ return reader.read().then(yieldEvent);
96+ });
97+ }
98+ } catch (error) {
99+ setError(error);
100+ }
101+ }, [client, patp, setError]);
102+
103+ const createNewShip = useCallback(async () => {
104+ try {
105+ setError(undefined);
106+ setState(STATE.BOOTING);
107+
108+ const response = await client.postShips({
109+ patp,
110+ key: '123',
111+ });
112+
113+ if (!response.ok) {
114+ setError(response.statusText);
115+ } else {
116+ checkShipExists();
117+ }
118+ } catch (error) {
119+ setError(error);
120+ }
121+ }, [checkShipExists, client, patp, setError]);
122+
123+ const values = useDeepEqualReference({
124+ error,
125+ notFound,
126+ running,
127+ booting,
128+ connecting,
129+ connected,
130+ unknown,
131+ events,
132+ checkShipExists,
133+ getShipEvents,
134+ createNewShip,
135+ });
136+
137+ return { ...values, bind: values };
138+}
src/views/Hosting.js+42−2
@@ -1,7 +1,47 @@
1−import React from 'react';
1+import React, { useCallback } from 'react';
2+import { Just } from 'folktale/maybe';
3+import { Grid } from 'indigo-react';
2
4
3import View from 'components/View';
5import 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';
11+import { usePointCursor } from 'store/pointCursor';
12+import { useWallet } from 'store/wallet';
13+import { useLocalRouter } from 'lib/LocalRouter';
14+import { useHostedShip } from 'lib/useHostedShip';
4
15
5export default function Hosting() {
16export default function Hosting() {
17+ const { pop } = useLocalRouter();
18+ const { pointCursor } = usePointCursor();
19+
20+ const { wallet } = useWallet();
21+ const point = need.point(pointCursor);
22+ const address = need.addressFromWallet(wallet);
23+
24+ const { bind } = useHostedShip(point);
25+
6− return <View />;
26+ return (
27+ <View pop={pop} inset>
28+ <Grid>
29+ <Passport
30+ point={Just(point)}
31+ address={Just(address)}
32+ animationMode={'slide'}
33+ />
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()} />
44+ </Grid>
45+ </View>
46+ );
7}
47}
src/views/Point.js+5−1
@@ -10,11 +10,11 @@ import { useWallet } from 'store/wallet';
10import View from 'components/View';
10import View from 'components/View';
11import Greeting from 'components/Greeting';
11import Greeting from 'components/Greeting';
12import Passport from 'components/Passport';
12import Passport from 'components/Passport';
13−import { ForwardButton } from 'components/Buttons';
14import Blinky from 'components/Blinky';
13import Blinky from 'components/Blinky';
15import BarGraph from 'components/BarGraph';
14import BarGraph from 'components/BarGraph';
16import Chip from 'components/Chip';
15import Chip from 'components/Chip';
17import InviteSigilList from 'components/InviteSigilList';
16import InviteSigilList from 'components/InviteSigilList';
17+import { ForwardButton, HostingButton } from 'components/Buttons';
18import CopyButton from 'components/CopyButton';
18import CopyButton from 'components/CopyButton';
19import { matchBlinky } from 'components/Blinky';
19import { matchBlinky } from 'components/Blinky';
20import DownloadSigilButton from 'components/DownloadSigilButton';
20import DownloadSigilButton from 'components/DownloadSigilButton';
@@ -91,6 +91,7 @@ export default function Point() {
91 const [showInviteForm, setShowInviteForm] = useState(false);
91 const [showInviteForm, setShowInviteForm] = useState(false);
92
92
93 const goBoot = useCallback(() => push(names.BOOT), [push, names]);
93 const goBoot = useCallback(() => push(names.BOOT), [push, names]);
94+ const goHosting = useCallback(() => push(names.HOSTING), [push, names]);
94
95
95 const inviteButton = (() => {
96 const inviteButton = (() => {
96 if (azimuth.getPointSize(point) === azimuth.PointSize.Star) {
97 if (azimuth.getPointSize(point) === azimuth.PointSize.Star) {
@@ -260,7 +261,10 @@ export default function Point() {
260 onClick={goBoot}>
261 onClick={goBoot}>
261 Boot Urbit OS
262 Boot Urbit OS
262 </Grid.Item>
263 </Grid.Item>
264+
265+ <Grid.Item full as={HostingButton} onClick={goHosting} />
263 <Grid.Divider />
266 <Grid.Divider />
267+
264 <Grid.Item
268 <Grid.Item
265 full
269 full
266 as={ForwardButton}
270 as={ForwardButton}