Skip to content

Commit 4edd6ca

Browse files
authored
Merge pull request #2482 from msupply-foundation/2479-URL-QR
Show server URL & QR
2 parents a474689 + 885ac4a commit 4edd6ca

File tree

8 files changed

+158
-29
lines changed

8 files changed

+158
-29
lines changed

client/packages/android/app/src/main/java/org/openmsupply/client/NativeApi.java

+31-2
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,13 @@
2929
import java.net.HttpURLConnection;
3030
import java.net.InetAddress;
3131
import java.net.MalformedURLException;
32+
import java.net.NetworkInterface;
33+
import java.net.SocketException;
3234
import java.net.URL;
3335
import java.nio.charset.StandardCharsets;
3436
import java.util.ArrayDeque;
3537
import java.util.Deque;
38+
import java.util.Enumeration;
3639

3740
import javax.net.ssl.SSLHandshakeException;
3841

@@ -344,15 +347,34 @@ public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
344347
});
345348
}
346349

350+
// Attempt to get a non-loopback address for the local server
351+
// and fallback to loopback if there is an error
352+
private String getLocalAddress(NsdServiceInfo serviceInfo){
353+
try {
354+
for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {
355+
NetworkInterface intf = en.nextElement();
356+
for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) {
357+
InetAddress inetAddress = enumIpAddr.nextElement();
358+
if (!inetAddress.isLoopbackAddress() && !inetAddress.isLinkLocalAddress() && inetAddress.isSiteLocalAddress()) {
359+
return inetAddress.getHostAddress();
360+
}
361+
}
362+
}
363+
} catch (SocketException ex) {
364+
Log.e(OM_SUPPLY, ex.toString());
365+
}
366+
return serviceInfo.getHost().getHostAddress();
367+
}
347368
private JSObject serviceInfoToObject(NsdServiceInfo serviceInfo) {
348369
String serverHardwareId = parseAttribute(serviceInfo, discoveryConstants.HARDWARE_ID_KEY);
370+
Boolean isLocal = serverHardwareId.equals(discoveryConstants.hardwareId);
349371
return new JSObject()
350372
.put("protocol", parseAttribute(serviceInfo, discoveryConstants.PROTOCOL_KEY))
351373
.put("clientVersion", parseAttribute(serviceInfo, discoveryConstants.CLIENT_VERSION_KEY))
352374
.put("port", serviceInfo.getPort())
353-
.put("ip", serviceInfo.getHost().getHostAddress())
375+
.put("ip", isLocal ? getLocalAddress(serviceInfo) : serviceInfo.getHost().getHostAddress())
354376
.put("hardwareId", serverHardwareId)
355-
.put("isLocal", serverHardwareId.equals(discoveryConstants.hardwareId));
377+
.put("isLocal", isLocal);
356378

357379
}
358380

@@ -472,6 +494,13 @@ public class FrontEndHost {
472494
JSObject data;
473495

474496
public FrontEndHost(JSObject data) {
497+
String ip = data.getString("ip");
498+
// attempt to translate loopback addresses to an actual IP address
499+
// so that we can display the local server IP for users to connect to the API
500+
if (data.getBool("isLocal") && (ip.equals("127.0.0.1") || ip.equals("localhost"))) {
501+
NsdServiceInfo serviceInfo = createLocalServiceInfo();
502+
data.put("ip", getLocalAddress(serviceInfo));
503+
}
475504
this.data = data;
476505
}
477506

client/packages/common/src/intl/locales/en/common.json

+1
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@
359359
"message.copy-success": "Copied to clipboard successfully",
360360
"messages.ago": "{{time}} ago",
361361
"messages.cant-delete-generic": "You cannot delete one or more of the selected items",
362+
"messages.click-to-expand": "Click to expand",
362363
"messages.confirm-cancel-generic": "You will lose any changes you have made to this form",
363364
"messages.confirm-delete-generic": "This will permanently remove data",
364365
"messages.confirm-delete-shipment": "This will permanently remove Shipment #{{number}}",

client/packages/common/src/ui/discovery/DiscoveredServers.tsx

+1-23
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ import {
1313
FrontEndHost,
1414
useNativeClient,
1515
GqlProvider,
16-
useInitialisationStatus,
17-
InitialisationStatusType,
1816
frontEndHostDiscoveryGraphql,
1917
IconButton,
2018
RefreshIcon,
@@ -160,16 +158,9 @@ const DiscoveredServer: React.FC<DiscoveredServerProps> = ({
160158
server,
161159
connect,
162160
}) => {
163-
const { data: initStatus } = useInitialisationStatus();
164161
const t = useTranslation('app');
165162
const { error } = useNotification();
166163

167-
const getSiteName = () => {
168-
if (initStatus?.status == InitialisationStatusType.Initialised)
169-
return initStatus?.siteName;
170-
return t('messages.not-initialised');
171-
};
172-
173164
const handleConnectionResult = async (result: ConnectionResult) => {
174165
if (result.success) return;
175166

@@ -190,20 +181,7 @@ const DiscoveredServer: React.FC<DiscoveredServerProps> = ({
190181
<CheckboxEmptyIcon fontSize="small" color="primary" />
191182
</Box>
192183
<Box flexShrink={0} flexBasis="200px">
193-
<Typography
194-
sx={{
195-
color:
196-
initStatus?.status == InitialisationStatusType.Initialised
197-
? 'inherit'
198-
: 'gray.light',
199-
fontSize: 20,
200-
fontWeight: 'bold',
201-
lineHeight: 1,
202-
}}
203-
>
204-
{getSiteName()}
205-
</Typography>
206-
<Typography sx={{ fontSize: 11 }}>
184+
<Typography sx={{ fontSize: 14, fontWeight: 'bold' }}>
207185
{frontEndHostDisplay(server)}
208186
</Typography>
209187
</Box>

client/packages/electron/src/electron.ts

+46-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
Menu,
77
MenuItemConstructorOptions,
88
shell,
9+
webContents,
910
} from 'electron';
1011
import dnssd from 'dnssd';
1112
import { IPC_MESSAGES } from './shared';
@@ -183,6 +184,11 @@ const tryToConnectToServer = (window: BrowserWindow, server: FrontEndHost) => {
183184
};
184185

185186
const connectToServer = (window: BrowserWindow, server: FrontEndHost) => {
187+
// translate loopback addresses to allow for clients, such as CCA to connect
188+
// to the API by IP address
189+
if (server.isLocal && isLoopback(server.ip)) {
190+
server.ip = getIpAddress('public');
191+
}
186192
discovery.stop();
187193
connectedServer = server;
188194

@@ -276,15 +282,14 @@ const start = (): void => {
276282
if (!(typeof hardwareId === 'string')) return;
277283

278284
const ip = addresses.find(isV4Format);
279-
280285
if (!ip) return;
281286

282287
discoveredServers.push({
283288
port,
284289
protocol,
285290
ip,
286291
clientVersion: clientVersion || '',
287-
isLocal: ip === getIpAddress() || ip === '127.0.0.1',
292+
isLocal: ip === getIpAddress() || isLoopback(ip),
288293
hardwareId,
289294
});
290295
});
@@ -303,6 +308,11 @@ const start = (): void => {
303308
);
304309
};
305310

311+
const isLoopback = (ip: string) =>
312+
ip === '127.0.0.1' ||
313+
ip.toLowerCase() === 'localhost' ||
314+
ip.toLowerCase() === '::1';
315+
306316
app.on('ready', start);
307317

308318
app.on('window-all-closed', () => {
@@ -446,6 +456,40 @@ const helpMenu: MenuItemConstructorOptions = {
446456
);
447457
},
448458
},
459+
{
460+
label: 'Clear Data',
461+
click: () => {
462+
dialog
463+
.showMessageBox({
464+
type: 'question',
465+
title: 'Confirmation',
466+
message:
467+
'This will clear all local data and close the application. Are you sure?',
468+
buttons: ['Yes', 'No'],
469+
})
470+
.then(result => {
471+
// Bail if the user pressed "No" or escaped (ESC) from the dialog box
472+
if (result.response !== 0) {
473+
return;
474+
}
475+
store.clear();
476+
const contents = webContents.getFocusedWebContents();
477+
if (contents) {
478+
contents.executeJavaScript(`localStorage.clear();`);
479+
}
480+
app.exit();
481+
});
482+
},
483+
},
484+
{
485+
label: 'Developer Tools',
486+
click: () => {
487+
const contents = webContents.getFocusedWebContents();
488+
if (contents) {
489+
contents.openDevTools();
490+
}
491+
},
492+
},
449493
{ role: 'about' },
450494
],
451495
};

client/packages/host/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,19 @@
2727
},
2828
"dependencies": {
2929
"@fontsource-variable/inter": "^5.0.5",
30+
"@openmsupply-client/coldchain": "^0.0.0",
3031
"@openmsupply-client/common": "^0.0.1",
3132
"@openmsupply-client/config": "^0.0.0",
3233
"@openmsupply-client/dashboard": "^0.0.0",
3334
"@openmsupply-client/inventory": "^0.0.0",
3435
"@openmsupply-client/invoices": "^0.0.0",
3536
"@openmsupply-client/requisitions": "^0.0.0",
3637
"@openmsupply-client/system": "^0.0.0",
37-
"@openmsupply-client/coldchain": "^0.0.0",
3838
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
3939
"history": "^5.1.0",
4040
"react": "^18.0.0",
4141
"react-dom": "^18.0.0",
42+
"react-qr-code": "^2.0.12",
4243
"swc-loader": "^0.2.0",
4344
"webpack-bundle-analyzer": "^4.9.0"
4445
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React from 'react';
2+
import {
3+
AppBarButtonsPortal,
4+
Box,
5+
Tooltip,
6+
Typography,
7+
frontEndHostUrl,
8+
useNativeClient,
9+
usePopover,
10+
useTranslation,
11+
} from '@openmsupply-client/common';
12+
import QRCode from 'react-qr-code';
13+
import { ClickAwayListener } from '@mui/base';
14+
15+
const ServerInfoComponent = () => {
16+
const { connectedServer } = useNativeClient();
17+
const t = useTranslation();
18+
const { show, hide, Popover } = usePopover();
19+
const serverUrl = !!connectedServer
20+
? frontEndHostUrl(connectedServer)
21+
: window.location.origin;
22+
23+
return (
24+
<AppBarButtonsPortal>
25+
<Box display="flex" flexDirection="column" padding={1}>
26+
<Tooltip title={t('messages.click-to-expand')}>
27+
<Box
28+
display="flex"
29+
justifyContent="flex-end"
30+
onClick={show}
31+
sx={{ cursor: 'pointer' }}
32+
>
33+
<QRCode value={serverUrl} size={50} />
34+
</Box>
35+
</Tooltip>
36+
<Popover onClick={hide}>
37+
<ClickAwayListener onClickAway={hide}>
38+
<Box
39+
padding={2}
40+
sx={{
41+
backgroundColor: 'background.white',
42+
borderRadius: 1,
43+
boxShadow: theme => theme.shadows[3],
44+
}}
45+
>
46+
<QRCode value={serverUrl} size={256} />
47+
</Box>
48+
</ClickAwayListener>
49+
</Popover>
50+
<Box display="flex">
51+
<Typography color={'gray.dark'} fontWeight={'bold'} paddingRight={1}>
52+
{t('label.server')}:
53+
</Typography>
54+
<Typography color={'gray.main'}>{serverUrl}</Typography>
55+
</Box>
56+
</Box>
57+
</AppBarButtonsPortal>
58+
);
59+
};
60+
61+
export const ServerInfo = React.memo(ServerInfoComponent);

client/packages/host/src/components/Sync/Sync.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from '@openmsupply-client/common';
1717
import { useSync } from '@openmsupply-client/system';
1818
import { SyncProgress } from '../SyncProgress';
19+
import { ServerInfo } from './ServerInfo';
1920

2021
const STATUS_POLLING_INTERVAL = 1000;
2122

@@ -103,7 +104,7 @@ const useUpdateUser = () => {
103104
};
104105
};
105106

106-
export const Sync: React.FC = () => {
107+
export const Sync = () => {
107108
const t = useTranslation('app');
108109
const {
109110
syncStatus,
@@ -122,6 +123,7 @@ export const Sync: React.FC = () => {
122123

123124
return (
124125
<Grid style={{ padding: 15 }} justifyContent="center">
126+
<ServerInfo />
125127
<Grid
126128
container
127129
flexDirection="column"

client/yarn.lock

+13
Original file line numberDiff line numberDiff line change
@@ -14125,6 +14125,11 @@ pvutils@^1.1.3:
1412514125
resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.3.tgz#f35fc1d27e7cd3dfbd39c0826d173e806a03f5a3"
1412614126
integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==
1412714127

14128+
14129+
version "0.0.0"
14130+
resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f"
14131+
integrity sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==
14132+
1412814133
1412914134
version "6.11.0"
1413014135
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
@@ -14314,6 +14319,14 @@ react-lifecycles-compat@^3.0.4:
1431414319
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
1431514320
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
1431614321

14322+
react-qr-code@^2.0.12:
14323+
version "2.0.12"
14324+
resolved "https://registry.yarnpkg.com/react-qr-code/-/react-qr-code-2.0.12.tgz#98f99e9ad5ede46d73ab819e2dd9925c5f5d7a2d"
14325+
integrity sha512-k+pzP5CKLEGBRwZsDPp98/CAJeXlsYRHM2iZn1Sd5Th/HnKhIZCSg27PXO58zk8z02RaEryg+60xa4vyywMJwg==
14326+
dependencies:
14327+
prop-types "^15.8.1"
14328+
qr.js "0.0.0"
14329+
1431714330
react-query@^3.34.12:
1431814331
version "3.39.3"
1431914332
resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.3.tgz#4cea7127c6c26bdea2de5fb63e51044330b03f35"

0 commit comments

Comments
 (0)