@transitive-sdk/utils-web 0.10.3 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/hooks.jsx +79 -64
- package/client/react-web-component/index.js +12 -0
- package/client/shared.jsx +84 -20
- package/dist/utils-web.js +1530 -1
- package/dist/utils-web.js.map +4 -4
- package/docs/client.md +60 -19
- package/esbuild.js +3 -3
- package/package.json +1 -1
package/client/hooks.jsx
CHANGED
|
@@ -10,15 +10,17 @@ log.setLevel('info');
|
|
|
10
10
|
log.setLevel('debug'); // #DEBUG
|
|
11
11
|
|
|
12
12
|
/** Hook for using MqttSync in React.
|
|
13
|
-
@returns {object} An object `{data, mqttSync, ready, StatusComponent, status}`
|
|
14
|
-
where:
|
|
15
|
-
`data` is a reactive data source in React containing all the data received by
|
|
16
|
-
mqttsync,
|
|
17
|
-
`mqttSync` is the MqttSync object itself,
|
|
18
|
-
`ready` indicates when mqttSync is ready to be used (connected and received
|
|
19
|
-
successfully subscribed to mqtt system heartbeats)
|
|
20
|
-
|
|
21
|
-
export const useMqttSync = ({jwt, id, mqttUrl}) => {
|
|
13
|
+
* @returns {object} An object `{data, mqttSync, ready, StatusComponent, status}`
|
|
14
|
+
* where:
|
|
15
|
+
* `data` is a reactive data source in React containing all the data received by
|
|
16
|
+
* mqttsync,
|
|
17
|
+
* `mqttSync` is the MqttSync object itself,
|
|
18
|
+
* `ready` indicates when mqttSync is ready to be used (connected and received
|
|
19
|
+
* successfully subscribed to mqtt system heartbeats)
|
|
20
|
+
*/
|
|
21
|
+
export const useMqttSync = ({jwt, id, mqttUrl, appReact}) => {
|
|
22
|
+
const { useState, useRef, useEffect } = appReact || React;
|
|
23
|
+
|
|
22
24
|
const [status, setStatus] = useState('connecting');
|
|
23
25
|
const [mqttSync, setMqttSync] = useState();
|
|
24
26
|
const [data, setData] = useState({});
|
|
@@ -74,49 +76,53 @@ export const useMqttSync = ({jwt, id, mqttUrl}) => {
|
|
|
74
76
|
};
|
|
75
77
|
|
|
76
78
|
/** Hook for using Transitive in React. Connects to MQTT, establishes sync, and
|
|
77
|
-
exposes reactive `data` state variable. */
|
|
78
|
-
export const useTransitive =
|
|
79
|
+
* exposes reactive `data` state variable. */
|
|
80
|
+
export const useTransitive =
|
|
81
|
+
({jwt, id, host, ssl, capability, versionNS, appReact}) => {
|
|
79
82
|
|
|
80
|
-
|
|
83
|
+
const [scope, capabilityName] = capability.split('/');
|
|
81
84
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
const { device } = decodeJWT(jwt);
|
|
86
|
+
const prefixPath = [id, device, scope, capabilityName];
|
|
87
|
+
const prefix = pathToTopic(prefixPath);
|
|
88
|
+
const prefixPathVersion = [...prefixPath, versionNS];
|
|
89
|
+
const prefixVersion = pathToTopic(prefixPathVersion);
|
|
87
90
|
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
const mqttUrl = `${ssl && JSON.parse(ssl) ? 'wss' : 'ws'}://mqtt.${host}`;
|
|
92
|
+
const fromMqttSync = useMqttSync({ jwt, id, mqttUrl, appReact });
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
};
|
|
94
|
+
return {...fromMqttSync, device, prefixPath, prefix, prefixPathVersion,
|
|
95
|
+
prefixVersion};
|
|
96
|
+
};
|
|
94
97
|
|
|
95
98
|
|
|
96
99
|
/** Subscribe to MqttSync topics using the provided JWT. This will
|
|
97
|
-
automatically find which version of the capability named in the JWT is running
|
|
98
|
-
on the device of the JWT and get the data for that version.
|
|
99
|
-
|
|
100
|
-
Example usage (with webrtc-video):
|
|
101
|
-
|
|
102
|
-
```js
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
@param {object} options An object containing:
|
|
110
|
-
`JWT`: A list of subtopics of the capability named in the JWT.
|
|
111
|
-
|
|
112
|
-
@returns {object} An object `{data, mqttSync, ready, agentStatus, topicData}`
|
|
113
|
-
where:
|
|
114
|
-
|
|
115
|
-
heartbeat and runningPackages, and
|
|
116
|
-
|
|
100
|
+
* automatically find which version of the capability named in the JWT is running
|
|
101
|
+
* on the device of the JWT and get the data for that version.
|
|
102
|
+
*
|
|
103
|
+
* Example usage (with webrtc-video):
|
|
104
|
+
*
|
|
105
|
+
* ```js
|
|
106
|
+
* const { agentStatus, topicData } = useTopics({ jwt, topics: [
|
|
107
|
+
* '/options/videoSource',
|
|
108
|
+
* '/stats/+/log/'
|
|
109
|
+
* ]});
|
|
110
|
+
* ```
|
|
111
|
+
*
|
|
112
|
+
* @param {object} options An object containing:
|
|
113
|
+
* `JWT`: A list of subtopics of the capability named in the JWT.
|
|
114
|
+
* `topics`: A list of subtopics of the capability named in the JWT.
|
|
115
|
+
* @returns {object} An object `{data, mqttSync, ready, agentStatus, topicData}`
|
|
116
|
+
* where:
|
|
117
|
+
* `agentStatus` is the `status` field of the running robot agent, including
|
|
118
|
+
* heartbeat and runningPackages, and
|
|
119
|
+
* `topicData` is the data for the selected topics of the capability
|
|
117
120
|
*/
|
|
118
121
|
export const useTopics = ({jwt, host = 'transitiverobotics.com', ssl = true,
|
|
119
|
-
topics = []}) => {
|
|
122
|
+
topics = [], appReact}) => {
|
|
123
|
+
|
|
124
|
+
log.debug({appReact});
|
|
125
|
+
const { useState, useEffect } = appReact || React;
|
|
120
126
|
|
|
121
127
|
// We need to make sure we don't resubscribe (below) when this function
|
|
122
128
|
// is called with the same content of `topics` but a different object.
|
|
@@ -132,7 +138,7 @@ export const useTopics = ({jwt, host = 'transitiverobotics.com', ssl = true,
|
|
|
132
138
|
const agentPrefix = `/${id}/${device}/@transitive-robotics/_robot-agent/+/status`;
|
|
133
139
|
|
|
134
140
|
const {mqttSync, data, status, ready, StatusComponent} =
|
|
135
|
-
useMqttSync({jwt, id, mqttUrl: `ws${ssl ? 's' : ''}://mqtt.${host}
|
|
141
|
+
useMqttSync({jwt, id, mqttUrl: `ws${ssl ? 's' : ''}://mqtt.${host}`, appReact});
|
|
136
142
|
|
|
137
143
|
useEffect(() => {
|
|
138
144
|
if (ready) {
|
|
@@ -170,20 +176,21 @@ export const useTopics = ({jwt, host = 'transitiverobotics.com', ssl = true,
|
|
|
170
176
|
const listeners = {};
|
|
171
177
|
const loadedModules = {};
|
|
172
178
|
/** Hook to load a Transitive capability. Besides loading the custom element,
|
|
173
|
-
this hook also returns any functions and objects the component exports in
|
|
174
|
-
`loadedModule`. Example:
|
|
175
|
-
```js
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
```
|
|
179
|
+
* this hook also returns any functions and objects the component exports in
|
|
180
|
+
* `loadedModule`. Example:
|
|
181
|
+
* ```js
|
|
182
|
+
* const {loaded, loadedModule} = useCapability({
|
|
183
|
+
* capability: '@transitive-robotics/terminal',
|
|
184
|
+
* name: 'mock-device',
|
|
185
|
+
* userId: 'user123',
|
|
186
|
+
* deviceId: 'd_mydevice123',
|
|
187
|
+
* });
|
|
188
|
+
* ```
|
|
183
189
|
*/
|
|
184
190
|
export const useCapability = ({ capability, name, userId, deviceId,
|
|
185
|
-
host = 'transitiverobotics.com', ssl = true
|
|
191
|
+
host = 'transitiverobotics.com', ssl = true, appReact
|
|
186
192
|
}) => {
|
|
193
|
+
const { useState, useEffect } = appReact || React;
|
|
187
194
|
|
|
188
195
|
const [returns, setReturns] = useState({ loaded: false });
|
|
189
196
|
|
|
@@ -206,7 +213,8 @@ export const useCapability = ({ capability, name, userId, deviceId,
|
|
|
206
213
|
if (listeners[name]) {
|
|
207
214
|
log.debug('already loading');
|
|
208
215
|
// get notified when loading completes
|
|
209
|
-
|
|
216
|
+
listeners[name].push(done);
|
|
217
|
+
return;
|
|
210
218
|
}
|
|
211
219
|
listeners[name] = [done];
|
|
212
220
|
|
|
@@ -215,15 +223,22 @@ export const useCapability = ({ capability, name, userId, deviceId,
|
|
|
215
223
|
// filename without extension as we'll try multiple
|
|
216
224
|
const fileBasename = `${baseUrl}/running/${capability}/dist/${name}`;
|
|
217
225
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
+
/* Since some users use webpack and webpack is stupid, we need to use
|
|
227
|
+
this magic comment for it to ignore these (remote) requests, see:
|
|
228
|
+
https://webpack.js.org/api/module-methods/#webpackignore. */
|
|
229
|
+
import(/* webpackIgnore: true */
|
|
230
|
+
`${fileBasename}.esm.js?${params.toString()}`).then(
|
|
231
|
+
esm => notifyListeners('loaded esm', esm),
|
|
232
|
+
error => {
|
|
233
|
+
log.warn(`No ESM module found for ${name}, loading iife`, error);
|
|
234
|
+
import(/* webpackIgnore: true */
|
|
235
|
+
`${fileBasename}.js?${params.toString()}`).then(
|
|
236
|
+
iife => notifyListeners('loaded iife', iife),
|
|
237
|
+
error => log.error(`Failed to load ${name} iife`, error));
|
|
238
|
+
});
|
|
226
239
|
}, [capability, name, userId, deviceId]);
|
|
227
240
|
|
|
228
241
|
return returns;
|
|
229
242
|
};
|
|
243
|
+
|
|
244
|
+
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const React = require('react');
|
|
2
2
|
const ReactDOM = require('react-dom');
|
|
3
|
+
// const { createRoot } = require('react-dom/client'); // react 18; wip
|
|
3
4
|
const retargetEvents = require('react-shadow-dom-retarget-events');
|
|
4
5
|
const getStyleElementsFromReactWebComponentStyleLoader = require('./getStyleElementsFromReactWebComponentStyleLoader');
|
|
5
6
|
const extractAttributes = require('./extractAttributes');
|
|
@@ -69,6 +70,17 @@ module.exports = {
|
|
|
69
70
|
self.callConstructorHook();
|
|
70
71
|
self.callLifeCycleHook('connectedCallback');
|
|
71
72
|
});
|
|
73
|
+
|
|
74
|
+
// WIP: trying to upgrade to react 18, which lacks a render callback
|
|
75
|
+
// --> See https://github.com/reactwg/react-18/discussions/5 on how to
|
|
76
|
+
// do this in react 18
|
|
77
|
+
// createRoot(mountPoint).render(
|
|
78
|
+
// // This is where we instantiate the actual component (in its wrapper)
|
|
79
|
+
// React.createElement(wrapper, extractAttributes(self)),
|
|
80
|
+
// );
|
|
81
|
+
// self.instance = this;
|
|
82
|
+
// self.callConstructorHook();
|
|
83
|
+
// self.callLifeCycleHook('connectedCallback');
|
|
72
84
|
}
|
|
73
85
|
|
|
74
86
|
disconnectedCallback() {
|
package/client/shared.jsx
CHANGED
|
@@ -105,17 +105,17 @@ export const Timer = ({duration, onTimeout, onStart, setOnDisconnect, children})
|
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
/** Dynamically load and use the Transitive web component specified in the JWT.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
```
|
|
113
|
-
<TransitiveCapability jwt={jwt}
|
|
114
|
-
myconfig={{a: 1, b: 2}}
|
|
115
|
-
onData={(data) => setData(data)}
|
|
116
|
-
onclick={() => { console.log('custom click handler'); }}
|
|
117
|
-
|
|
118
|
-
```
|
|
108
|
+
* Embedding Transitive components this way also enables the use of functional
|
|
109
|
+
* and object properties, which get lost when using the custom element (Web
|
|
110
|
+
* Component) because HTML attributes are strings.
|
|
111
|
+
* Example:
|
|
112
|
+
* ```jsx
|
|
113
|
+
* <TransitiveCapability jwt={jwt}
|
|
114
|
+
* myconfig={{a: 1, b: 2}}
|
|
115
|
+
* onData={(data) => setData(data)}
|
|
116
|
+
* onclick={() => { console.log('custom click handler'); }}
|
|
117
|
+
* />
|
|
118
|
+
* ```
|
|
119
119
|
*/
|
|
120
120
|
export const TransitiveCapability = ({
|
|
121
121
|
jwt, host = 'transitiverobotics.com', ssl = true, ...config
|
|
@@ -154,11 +154,11 @@ export const TransitiveCapability = ({
|
|
|
154
154
|
|
|
155
155
|
|
|
156
156
|
/** A simple error boundary. Usage:
|
|
157
|
-
```jsx
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
```
|
|
157
|
+
* ```jsx
|
|
158
|
+
* <ErrorBoundary message="Something went wrong">
|
|
159
|
+
* <SomeFlakyComponent />
|
|
160
|
+
* </ErrorBoundary>
|
|
161
|
+
* ```
|
|
162
162
|
*/
|
|
163
163
|
export class ErrorBoundary extends React.Component {
|
|
164
164
|
constructor(props) {
|
|
@@ -182,6 +182,70 @@ export class ErrorBoundary extends React.Component {
|
|
|
182
182
|
};
|
|
183
183
|
|
|
184
184
|
|
|
185
|
+
export const CapabilityContext = React.createContext({});
|
|
186
|
+
|
|
187
|
+
/* Only used internally: the actual context provider, given the loaded module */
|
|
188
|
+
const LoadedCapabilityContextProvider = (props) => {
|
|
189
|
+
const {children, jwt, id, host, ssl, loadedModule} = props;
|
|
190
|
+
|
|
191
|
+
const context = loadedModule.provideContext?.({
|
|
192
|
+
jwt, id, host, ssl, appReact: React
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return <CapabilityContext.Provider value={{ ...context }}>
|
|
196
|
+
{children}
|
|
197
|
+
</CapabilityContext.Provider>;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Context provider for capabilities. Use this to access the front-end API
|
|
202
|
+
* provided by some capabilities. Example:
|
|
203
|
+
* ```jsx
|
|
204
|
+
* <CapabilityContextProvider jwt={jwt}>
|
|
205
|
+
* <MyROSComponent />
|
|
206
|
+
* </CapabilityContextProvider>
|
|
207
|
+
* ```
|
|
208
|
+
* where `jwt` is a JWT for a capability that exposes a front-end API. Then use
|
|
209
|
+
* `useContext` in `MyROSComponent` to get the exposed data and functions, e.g.:
|
|
210
|
+
* ```jsx
|
|
211
|
+
* const MyROSComponent = () => {
|
|
212
|
+
* const { ready, subscribe, data } = useContext(CapabilityContext);
|
|
213
|
+
* // When ready, subscribe to the `/odom` topic in ROS1
|
|
214
|
+
* useEffect(() => { ready && subscribe(1, '/odom'); }, [ready]);
|
|
215
|
+
* return <pre>{JSON.stringify(data, true, 2)}</pre>;
|
|
216
|
+
* }
|
|
217
|
+
* ```
|
|
218
|
+
* Where `ready`, `subscribe`, and `data` are reactive variables and functions
|
|
219
|
+
* exposed by the capability of the provided JWT. In this example, the latest
|
|
220
|
+
* message from the subscribed ROS topics will be available in the capabilities
|
|
221
|
+
* namespace in `data`.
|
|
222
|
+
* @param {object} props
|
|
223
|
+
*/
|
|
224
|
+
export const CapabilityContextProvider =
|
|
225
|
+
({children, jwt, host = undefined, ssl = undefined}) => {
|
|
226
|
+
|
|
227
|
+
const {id, device, capability} = decodeJWT(jwt);
|
|
228
|
+
const type = device == '_fleet' ? 'fleet' : 'device';
|
|
229
|
+
const capName = capability.split('/')[1];
|
|
230
|
+
const name = `${capName}-${type}`;
|
|
231
|
+
|
|
232
|
+
const {loaded, loadedModule} = useCapability({
|
|
233
|
+
capability,
|
|
234
|
+
name,
|
|
235
|
+
userId: id,
|
|
236
|
+
deviceId: device,
|
|
237
|
+
appReact: React,
|
|
238
|
+
host,
|
|
239
|
+
ssl
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (!loadedModule) return <div>Loading {capability}</div>;
|
|
243
|
+
return <LoadedCapabilityContextProvider {...{jwt, id, host, ssl, loadedModule}}>
|
|
244
|
+
{children}
|
|
245
|
+
</LoadedCapabilityContextProvider>;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
|
|
185
249
|
/* whether or not the given react component allows refs, i.e., is either
|
|
186
250
|
* a functional component wrapped with forwardRef or a class component */
|
|
187
251
|
const componentPermitsRefs = (Component) =>
|
|
@@ -190,10 +254,10 @@ const componentPermitsRefs = (Component) =>
|
|
|
190
254
|
|
|
191
255
|
|
|
192
256
|
/** Create a WebComponent from the given react component and name that is
|
|
193
|
-
|
|
194
|
-
```js
|
|
195
|
-
|
|
196
|
-
```
|
|
257
|
+
* reactive to all attributes. Used in web capabilities. Example:
|
|
258
|
+
* ```js
|
|
259
|
+
* createWebComponent(Diagnostics, 'health-monitoring-device', TR_PKG_VERSION);
|
|
260
|
+
* ```
|
|
197
261
|
*/
|
|
198
262
|
export const createWebComponent = (Component, name, version = '0.0.0',
|
|
199
263
|
options = {}) => {
|