@openremote/or-dashboard-builder 1.2.0-snapshot.20240616202404 → 1.2.0-snapshot.20240819101332
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/dist/umd/index.js +593 -497
- package/dist/umd/index.orbundle.js +294 -195
- package/dist/umd/index.orbundle.js.LICENSE.txt +2 -2
- package/lib/index.js +4 -4
- package/lib/index.js.map +1 -1
- package/lib/or-dashboard-tree.js +7 -2
- package/lib/or-dashboard-tree.js.map +1 -1
- package/lib/or-dashboard-widgetcontainer.d.ts +4 -2
- package/lib/or-dashboard-widgetcontainer.js +2 -2
- package/lib/or-dashboard-widgetcontainer.js.map +1 -1
- package/lib/service/widget-service.js +1 -1
- package/lib/service/widget-service.js.map +1 -1
- package/lib/settings/gateway-settings.d.ts +19 -0
- package/lib/settings/gateway-settings.js +46 -0
- package/lib/settings/gateway-settings.js.map +1 -0
- package/lib/settings/gauge-settings.d.ts +0 -1
- package/lib/settings/gauge-settings.js +8 -8
- package/lib/settings/gauge-settings.js.map +1 -1
- package/lib/settings/image-settings.js +1 -1
- package/lib/settings/image-settings.js.map +1 -1
- package/lib/widgets/gateway-widget.d.ts +84 -0
- package/lib/widgets/gateway-widget.js +47 -0
- package/lib/widgets/gateway-widget.js.map +1 -0
- package/package.json +6 -6
- package/src/index.ts +33 -8
- package/src/or-dashboard-tree.ts +5 -0
- package/src/or-dashboard-widgetcontainer.ts +17 -6
- package/src/service/widget-service.ts +7 -1
- package/src/settings/gateway-settings.ts +144 -0
- package/src/settings/gauge-settings.ts +21 -5
- package/src/settings/image-settings.ts +1 -1
- package/src/widgets/gateway-widget.ts +386 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import {customElement, state} from "lit/decorators.js";
|
|
2
|
+
import {OrWidget, WidgetManifest} from "../util/or-widget";
|
|
3
|
+
import {css, html, PropertyValues, TemplateResult} from "lit";
|
|
4
|
+
import {WidgetSettings} from "../util/widget-settings";
|
|
5
|
+
import {WidgetConfig} from "../util/widget-config";
|
|
6
|
+
import {GatewaySettings} from "../settings/gateway-settings";
|
|
7
|
+
import {InputType, OrInputChangedEvent} from "@openremote/or-mwc-components/or-mwc-input";
|
|
8
|
+
import {GatewayTunnelInfo, GatewayTunnelInfoType} from "@openremote/model";
|
|
9
|
+
import manager from "@openremote/core";
|
|
10
|
+
import {when} from "lit/directives/when.js";
|
|
11
|
+
import {i18next} from "@openremote/or-translate";
|
|
12
|
+
import {showSnackbar} from "@openremote/or-mwc-components/or-mwc-snackbar";
|
|
13
|
+
|
|
14
|
+
const styling = css`
|
|
15
|
+
#gateway-widget-wrapper {
|
|
16
|
+
height: 100%;
|
|
17
|
+
display: flex;
|
|
18
|
+
justify-content: center;
|
|
19
|
+
align-items: center;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#gateway-widget-container {
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
gap: 8px;
|
|
28
|
+
max-height: 48px;
|
|
29
|
+
flex-wrap: wrap;
|
|
30
|
+
position: relative;
|
|
31
|
+
}
|
|
32
|
+
`
|
|
33
|
+
|
|
34
|
+
export interface GatewayWidgetConfig extends WidgetConfig {
|
|
35
|
+
gatewayId?: string;
|
|
36
|
+
type: GatewayTunnelInfoType;
|
|
37
|
+
target: string;
|
|
38
|
+
targetPort: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getDefaultWidgetConfig(): GatewayWidgetConfig {
|
|
42
|
+
return {
|
|
43
|
+
type: GatewayTunnelInfoType.HTTPS,
|
|
44
|
+
target: "localhost",
|
|
45
|
+
targetPort: 443,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@customElement("gateway-widget")
|
|
50
|
+
export class GatewayWidget extends OrWidget {
|
|
51
|
+
|
|
52
|
+
protected widgetConfig!: GatewayWidgetConfig;
|
|
53
|
+
|
|
54
|
+
@state()
|
|
55
|
+
protected _loading = true;
|
|
56
|
+
|
|
57
|
+
@state()
|
|
58
|
+
protected _activeTunnel?: GatewayTunnelInfo;
|
|
59
|
+
|
|
60
|
+
protected _startedByUser = false;
|
|
61
|
+
|
|
62
|
+
static getManifest(): WidgetManifest {
|
|
63
|
+
return {
|
|
64
|
+
displayName: "Gateway",
|
|
65
|
+
displayIcon: "lan-connect",
|
|
66
|
+
minColumnWidth: 1,
|
|
67
|
+
minColumnHeight: 1,
|
|
68
|
+
getContentHtml(config: GatewayWidgetConfig): OrWidget {
|
|
69
|
+
return new GatewayWidget(config);
|
|
70
|
+
},
|
|
71
|
+
getSettingsHtml(config: GatewayWidgetConfig): WidgetSettings {
|
|
72
|
+
return new GatewaySettings(config);
|
|
73
|
+
},
|
|
74
|
+
getDefaultConfig(): GatewayWidgetConfig {
|
|
75
|
+
return getDefaultWidgetConfig();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
refreshContent(force: boolean): void {
|
|
81
|
+
this.widgetConfig = JSON.parse(JSON.stringify(this.widgetConfig)) as GatewayWidgetConfig;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static get styles() {
|
|
85
|
+
return [...super.styles, styling];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
disconnectedCallback() {
|
|
89
|
+
if(this._activeTunnel) {
|
|
90
|
+
if(this._startedByUser) {
|
|
91
|
+
this._stopTunnel(this._activeTunnel).then(() => {
|
|
92
|
+
console.warn("Stopped the active tunnel, as it was created through the widget.")
|
|
93
|
+
});
|
|
94
|
+
} else {
|
|
95
|
+
console.warn("Keeping the active tunnel open, as it is not started through the widget.")
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
super.disconnectedCallback();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
protected firstUpdated(_changedProps: PropertyValues) {
|
|
102
|
+
if(this.widgetConfig) {
|
|
103
|
+
|
|
104
|
+
// Apply a timeout of 500 millis, so the tunnel has time to close upon disconnectedCallback() of a different widget.
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
|
|
107
|
+
// Check if the tunnel is already active upon widget initialization
|
|
108
|
+
const tunnelInfo = this._getTunnelInfoByConfig(this.widgetConfig);
|
|
109
|
+
this._getActiveTunnel(tunnelInfo).then(info => {
|
|
110
|
+
if(info) {
|
|
111
|
+
console.log("Existing tunnel found!", info);
|
|
112
|
+
this._setActiveTunnel(info, true)
|
|
113
|
+
}
|
|
114
|
+
}).catch(e => {
|
|
115
|
+
console.error(e);
|
|
116
|
+
}).finally(() => {
|
|
117
|
+
this._loading = false;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
}, 500)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
protected render(): TemplateResult {
|
|
125
|
+
const disabled = this.getEditMode?.() || !this._isConfigComplete(this.widgetConfig);
|
|
126
|
+
return html`
|
|
127
|
+
<div id="gateway-widget-wrapper">
|
|
128
|
+
<div id="gateway-widget-container">
|
|
129
|
+
${when(this._loading, () => html`
|
|
130
|
+
<or-loading-indicator></or-loading-indicator>
|
|
131
|
+
`, () => {
|
|
132
|
+
if (this._activeTunnel) {
|
|
133
|
+
return html`
|
|
134
|
+
|
|
135
|
+
<or-mwc-input .type="${InputType.BUTTON}" icon="stop" label="${i18next.t('gatewayTunnels.stop')}" .disabled="${disabled}"
|
|
136
|
+
@or-mwc-input-changed="${(ev: OrInputChangedEvent) => this._onStopTunnelClick(ev)}"
|
|
137
|
+
></or-mwc-input>
|
|
138
|
+
|
|
139
|
+
${when(this.widgetConfig.type === GatewayTunnelInfoType.TCP, () => html`
|
|
140
|
+
<or-mwc-input .type="${InputType.BUTTON}" icon="content-copy" label="${i18next.t('gatewayTunnels.copyAddress')}" outlined .disabled="${disabled}"
|
|
141
|
+
@or-mwc-input-changed="${(ev: OrInputChangedEvent) => this._onCopyTunnelAddressClick(ev)}"
|
|
142
|
+
></or-mwc-input>
|
|
143
|
+
`, () => html`
|
|
144
|
+
<or-mwc-input .type="${InputType.BUTTON}" icon="open-in-new" label="${i18next.t('gatewayTunnels.open')}" outlined .disabled="${disabled}"
|
|
145
|
+
@or-mwc-input-changed="${(ev: OrInputChangedEvent) => this._onTunnelNavigateClick(ev)}"
|
|
146
|
+
></or-mwc-input>
|
|
147
|
+
`)}
|
|
148
|
+
|
|
149
|
+
`;
|
|
150
|
+
} else {
|
|
151
|
+
return html`
|
|
152
|
+
<or-mwc-input .type="${InputType.BUTTON}" label="${i18next.t('gatewayTunnels.start')}" outlined .disabled="${disabled}"
|
|
153
|
+
@or-mwc-input-changed="${(ev: OrInputChangedEvent) => this._onStartTunnelClick(ev)}"
|
|
154
|
+
></or-mwc-input>
|
|
155
|
+
`;
|
|
156
|
+
}
|
|
157
|
+
})}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* HTML callback function when 'copy address' button is pressed for a TCP tunnel.
|
|
165
|
+
*/
|
|
166
|
+
protected _onCopyTunnelAddressClick(ev: OrInputChangedEvent) {
|
|
167
|
+
if (this._isConfigComplete(this.widgetConfig)) {
|
|
168
|
+
const tunnelInfo = this._getTunnelInfoByConfig(this.widgetConfig);
|
|
169
|
+
const address = this._getTunnelAddress(tunnelInfo);
|
|
170
|
+
if(address) {
|
|
171
|
+
navigator.clipboard.writeText(address).finally(() => {
|
|
172
|
+
showSnackbar(undefined, i18next.t('gatewayTunnels.copySuccess'));
|
|
173
|
+
});
|
|
174
|
+
} else {
|
|
175
|
+
console.warn("Could not copy tunnel address as it could not be found.");
|
|
176
|
+
showSnackbar(undefined, i18next.t('errorOccurred'));
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
console.warn("Could not copy tunnel address as configuration is not complete.");
|
|
180
|
+
showSnackbar(undefined, i18next.t('errorOccurred'));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* HTML callback function when 'start' button is pressed, meant to create / start a new tunnel.
|
|
186
|
+
*/
|
|
187
|
+
protected _onStartTunnelClick(ev: OrInputChangedEvent) {
|
|
188
|
+
this._tryStartTunnel(this.widgetConfig);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* HTML callback function when 'stop' button is pressed, meant to destroy the active tunnel.
|
|
193
|
+
*/
|
|
194
|
+
protected _onStopTunnelClick(ev: OrInputChangedEvent) {
|
|
195
|
+
this._tryStopTunnel(this.widgetConfig);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* HTML callback function when 'open' button is pressed, meant to start using the tunnel.
|
|
200
|
+
*/
|
|
201
|
+
protected _onTunnelNavigateClick(ev: OrInputChangedEvent) {
|
|
202
|
+
if (this._isConfigComplete(this.widgetConfig)) {
|
|
203
|
+
this._navigateToTunnel(this._getTunnelInfoByConfig(this.widgetConfig));
|
|
204
|
+
} else {
|
|
205
|
+
console.warn("Could not navigate to tunnel as configuration is not complete.")
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Internal function to set the active tunnel.
|
|
211
|
+
* Navigates the user to the tunnel after updating. This can be disabled using the {@link silent} parameter.
|
|
212
|
+
*/
|
|
213
|
+
protected _setActiveTunnel(tunnelInfo?: GatewayTunnelInfo, silent = false) {
|
|
214
|
+
this._activeTunnel = tunnelInfo;
|
|
215
|
+
if(tunnelInfo && !silent) {
|
|
216
|
+
this._navigateToTunnel(tunnelInfo);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Function that tries to start the tunnel. It checks the configuration beforehand,
|
|
222
|
+
* and acts as a controller to call the correct functions throughout the starting process.
|
|
223
|
+
*/
|
|
224
|
+
protected _tryStartTunnel(widgetConfig: GatewayWidgetConfig): void {
|
|
225
|
+
if (this._isConfigComplete(widgetConfig)) {
|
|
226
|
+
|
|
227
|
+
const tunnelInfo = this._getTunnelInfoByConfig(widgetConfig);
|
|
228
|
+
this._loading = true;
|
|
229
|
+
|
|
230
|
+
this._getActiveTunnel(tunnelInfo).then(activeTunnel => {
|
|
231
|
+
|
|
232
|
+
if (activeTunnel) {
|
|
233
|
+
console.log("Found an active tunnel!", activeTunnel);
|
|
234
|
+
this._setActiveTunnel(activeTunnel);
|
|
235
|
+
this._loading = false;
|
|
236
|
+
|
|
237
|
+
} else {
|
|
238
|
+
this._startTunnel(tunnelInfo).then(newTunnel => {
|
|
239
|
+
|
|
240
|
+
if (newTunnel) {
|
|
241
|
+
console.log("Started a new tunnel!", newTunnel);
|
|
242
|
+
this._setActiveTunnel(newTunnel);
|
|
243
|
+
this._startedByUser = true;
|
|
244
|
+
|
|
245
|
+
} else {
|
|
246
|
+
console.warn("No new tunnel!");
|
|
247
|
+
}
|
|
248
|
+
}).catch(e => {
|
|
249
|
+
console.error(e);
|
|
250
|
+
showSnackbar(undefined, i18next.t("errorOccurred"));
|
|
251
|
+
}).finally(() => {
|
|
252
|
+
this._loading = false;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
}).catch(e => {
|
|
257
|
+
console.error(e);
|
|
258
|
+
showSnackbar(undefined, i18next.t("errorOccurred"));
|
|
259
|
+
this._loading = false;
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Internal function that starts the tunnel by communicating with the Manager API.
|
|
266
|
+
*/
|
|
267
|
+
protected async _startTunnel(info: GatewayTunnelInfo): Promise<GatewayTunnelInfo | undefined> {
|
|
268
|
+
if (!info.realm || !info.gatewayId || !info.target || !info.targetPort) {
|
|
269
|
+
throw new Error("Could not start tunnel, as some provided information was not set.");
|
|
270
|
+
}
|
|
271
|
+
return (await manager.rest.api.GatewayServiceResource.startTunnel(info)).data;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Internal function that requests the Manager API for the active tunnel based on the {@link GatewayTunnelInfo} parameter.
|
|
276
|
+
* Returns `undefined` if no tunnel could be found on the Manager instance.
|
|
277
|
+
*/
|
|
278
|
+
protected async _getActiveTunnel(info: GatewayTunnelInfo): Promise<GatewayTunnelInfo | undefined> {
|
|
279
|
+
if (!info.realm || !info.gatewayId || !info.target || !info.targetPort) {
|
|
280
|
+
throw new Error("Could not get active tunnel, as some provided information was not set.")
|
|
281
|
+
}
|
|
282
|
+
const response = await manager.rest.api.GatewayServiceResource.getActiveTunnelInfo(info.realm, info.gatewayId, info.target, info.targetPort);
|
|
283
|
+
if(response.status === 204) {
|
|
284
|
+
return undefined;
|
|
285
|
+
} else {
|
|
286
|
+
return response.data;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Function that tries to destroy the currently active tunnel.
|
|
292
|
+
*/
|
|
293
|
+
protected _tryStopTunnel(config: GatewayWidgetConfig): void {
|
|
294
|
+
const tunnelInfo = this._getTunnelInfoByConfig(config);
|
|
295
|
+
this._loading = true;
|
|
296
|
+
this._stopTunnel(tunnelInfo)
|
|
297
|
+
.then(() => {
|
|
298
|
+
this._setActiveTunnel(undefined);
|
|
299
|
+
this._startedByUser = false;
|
|
300
|
+
})
|
|
301
|
+
.catch(e => {
|
|
302
|
+
console.warn(e);
|
|
303
|
+
})
|
|
304
|
+
.finally(() => {
|
|
305
|
+
this._loading = false;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Internal function that requests the Manager API to destroy a tunnel that is in line with the {@link GatewayTunnelInfo} parameter.
|
|
311
|
+
*/
|
|
312
|
+
protected async _stopTunnel(info: GatewayTunnelInfo): Promise<void> {
|
|
313
|
+
return (await manager.rest.api.GatewayServiceResource.stopTunnel(info)).data;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Function that navigates the user to an HTTP web page that interacts with a service through the tunnel.
|
|
318
|
+
* It will open in a new browser tab automatically.
|
|
319
|
+
*/
|
|
320
|
+
protected _navigateToTunnel(info: GatewayTunnelInfo): void {
|
|
321
|
+
if (!info.realm || !info.gatewayId || !info.target || !info.targetPort) {
|
|
322
|
+
console.warn("Could not navigate to tunnel, as some provided information was not set.");
|
|
323
|
+
}
|
|
324
|
+
const address = this._getTunnelAddress(info);
|
|
325
|
+
switch (info.type) {
|
|
326
|
+
case GatewayTunnelInfoType.HTTPS:
|
|
327
|
+
case GatewayTunnelInfoType.HTTP:
|
|
328
|
+
window.open(address)?.focus();
|
|
329
|
+
break;
|
|
330
|
+
default:
|
|
331
|
+
console.error("Unknown error when navigating to tunnel.");
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Internal function to get the tunnel address based on {@link GatewayTunnelInfo}
|
|
338
|
+
*/
|
|
339
|
+
protected _getTunnelAddress(info: GatewayTunnelInfo): string | undefined {
|
|
340
|
+
switch (info.type) {
|
|
341
|
+
case GatewayTunnelInfoType.HTTPS:
|
|
342
|
+
case GatewayTunnelInfoType.HTTP:
|
|
343
|
+
return "//" + info.id + "." + window.location.host;
|
|
344
|
+
case GatewayTunnelInfoType.TCP:
|
|
345
|
+
return info.id + "." + window.location.hostname + ":" + info.assignedPort;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Internal function to check whether the {@link GatewayWidgetConfig} includes the necessary information to control the tunnel.
|
|
351
|
+
* Uses several `undefined` or empty checks. Useful for checking the object before interacting with Manager APIs.
|
|
352
|
+
*/
|
|
353
|
+
protected _isConfigComplete(widgetConfig: GatewayWidgetConfig): boolean {
|
|
354
|
+
if (!widgetConfig.gatewayId) {
|
|
355
|
+
console.error("Could not start tunnel, since no gateway asset was configured.");
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
if (!widgetConfig.type) {
|
|
359
|
+
console.error("Could not start tunnel, since no gateway type / protocol was configured.");
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
if (!widgetConfig.target) {
|
|
363
|
+
console.error("Could not start tunnel, since no gateway target was configured.");
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
if (!widgetConfig.targetPort) {
|
|
367
|
+
console.error("Could not start tunnel, since no gateway target port was configured.");
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Internal function that parses a {@link GatewayWidgetConfig} into a new {@link GatewayTunnelInfo}.
|
|
375
|
+
*/
|
|
376
|
+
protected _getTunnelInfoByConfig(config: GatewayWidgetConfig): GatewayTunnelInfo {
|
|
377
|
+
return {
|
|
378
|
+
realm: manager.displayRealm,
|
|
379
|
+
gatewayId: config.gatewayId,
|
|
380
|
+
type: config.type,
|
|
381
|
+
target: config.target,
|
|
382
|
+
targetPort: config.targetPort
|
|
383
|
+
} as GatewayTunnelInfo;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
}
|