Skip to content

Commit 93f4217

Browse files
author
Paul Korzhyk
committed
New Connection UI
1 parent ead294f commit 93f4217

19 files changed

+527
-297
lines changed

client/src/actions/connection.js

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import * as helpers from "lib/helpers";
1616
import { Unknown, FetchError, OK } from "../reducers/connection";
1717
import { clickSidebarUrl } from "../actions/ui";
1818

19+
import { sanitizeUrl } from "../lib/helpers";
20+
1921
export const LOGIN_ERROR = "connection/LOGIN_ERROR";
2022
export const LOGIN_PENDING = "connection/LOGIN_PENDING";
2123
export const LOGIN_SUCCESS = "connection/LOGIN_SUCCESS";
@@ -24,22 +26,25 @@ export const DO_LOGOUT = "connection/DO_LOGOUT";
2426
export const SET_QUERY_TIMEOUT = "connection/SET_QUERY_TIMEOUT";
2527
export const UPDATE_URL = "connection/UPDATE_URL";
2628
export const UPDATE_SERVER_HEALTH = "connection/UPDATE_SERVER_HEALTH";
29+
export const UPDATE_SERVER_VERSION = "connection/UPDATE_SERVER_VERSION";
2730

2831
export const DISMISS_LICENSE_WARNING = "connection/DISMISS_LICENSE_WARNING";
2932

30-
export function setQueryTimeout(queryTimeout) {
33+
export function setQueryTimeout(url, queryTimeout) {
34+
queryTimeout = parseInt(queryTimeout) || 20;
3135
return {
3236
type: SET_QUERY_TIMEOUT,
37+
url,
3338
queryTimeout,
3439
};
3540
}
3641

3742
export const updateUrl = url => async (dispatch, getState) => {
38-
dispatch(loginTimeout());
43+
dispatch(loginTimeout(getState().connection.serverHistory[0].url));
3944

4045
dispatch({
4146
type: UPDATE_URL,
42-
url,
47+
url: sanitizeUrl(url),
4348
});
4449

4550
dispatch(checkHealth());
@@ -48,63 +53,76 @@ export const updateUrl = url => async (dispatch, getState) => {
4853
export const checkHealth = ({
4954
openUrlOnError = false,
5055
unknownOnStart = true,
51-
} = {}) => async dispatch => {
52-
unknownOnStart && dispatch(serverHealth(Unknown));
56+
} = {}) => async (dispatch, getState) => {
57+
const url = getState().connection.serverHistory[0].url;
58+
unknownOnStart && dispatch(serverHealth(url, Unknown));
5359
try {
5460
const stub = await helpers.getDgraphClientStub();
55-
await stub.getHealth();
56-
dispatch(serverHealth(OK));
61+
const health = await stub.getHealth();
62+
dispatch(serverHealth(url, OK));
63+
dispatch(serverVersion(url, (health[0] || health).version));
5764
} catch (err) {
5865
console.error(err);
59-
dispatch(serverHealth(FetchError));
66+
dispatch(serverHealth(url, FetchError));
6067
if (openUrlOnError) {
6168
dispatch(clickSidebarUrl("connection"));
6269
}
6370
}
6471
};
6572

66-
export const serverHealth = health => ({
73+
export const serverHealth = (url, health) => ({
6774
type: UPDATE_SERVER_HEALTH,
6875
health,
76+
url,
6977
});
7078

71-
const loginPending = () => ({
79+
export const serverVersion = (url, version) => ({
80+
type: UPDATE_SERVER_VERSION,
81+
url,
82+
version,
83+
});
84+
85+
const loginPending = url => ({
7286
type: LOGIN_PENDING,
87+
url,
7388
});
7489

75-
const loginSuccess = ({ refreshToken }) => ({
90+
const loginSuccess = (url, { refreshToken }) => ({
7691
type: LOGIN_SUCCESS,
92+
url,
7793
refreshToken,
7894
});
7995

80-
const loginTimeout = () => ({
96+
const loginTimeout = url => ({
8197
type: LOGIN_TIMEOUT,
98+
url,
8299
});
83100

84-
const loginError = error => ({
101+
const loginError = (url, error) => ({
85102
type: LOGIN_ERROR,
86103
error,
104+
url,
87105
});
88106

89107
export const loginUser = (userid, password, refreshToken) => async (
90108
dispatch,
91109
getState,
92110
) => {
93-
dispatch(loginPending());
111+
const url = getState().connection.serverHistory[0].url;
112+
dispatch(loginPending(url));
94113

95114
// Issue loginTimeout in case something went wrong with network or server.
96-
setTimeout(() => dispatch(loginTimeout()), 30 * 1000);
115+
setTimeout(() => dispatch(loginTimeout(url)), 30 * 1000);
97116

98117
await new Promise(resolve => setTimeout(resolve, 500));
99118
try {
100119
const stub = await helpers.getDgraphClientStub();
101120
await stub.login(userid, password, refreshToken);
102121
stub.setAutoRefresh(true);
103-
dispatch(loginSuccess(stub.getAuthTokens()));
122+
dispatch(loginSuccess(url, stub.getAuthTokens()));
104123
} catch (err) {
105-
console.error("Login Failed");
106-
console.error(err);
107-
dispatch(loginError(err));
124+
console.error("Login Failed", url, err);
125+
dispatch(loginError(url, err));
108126
}
109127
};
110128

client/src/components/LicenseWarning/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@ export default function() {
4040
dispatch(getClusterState());
4141
}, 60000);
4242

43-
const { licenseWarningDismissedTs } = useSelector(
44-
state => state.connection.currentServer,
43+
const currentServer = useSelector(
44+
state => state.connection.serverHistory[0],
4545
);
46+
const licenseWarningDismissedTs =
47+
currentServer.licenseWarningDismissedTs || -1;
4648
const { clusterState } = useSelector(state => state.cluster);
4749

4850
const license = clusterState?.license;
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// Copyright 2020 Dgraph Labs, Inc. and Contributors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import React, { useEffect, useState } from "react";
16+
import Alert from "react-bootstrap/Alert";
17+
import Button from "react-bootstrap/Button";
18+
import Card from "react-bootstrap/Card";
19+
import Col from "react-bootstrap/Col";
20+
import Container from "react-bootstrap/Container";
21+
import Form from "react-bootstrap/Form";
22+
import ListGroup from "react-bootstrap/ListGroup";
23+
import Modal from "react-bootstrap/Modal";
24+
import Row from "react-bootstrap/Row";
25+
26+
import { useDispatch, useSelector } from "react-redux";
27+
28+
import { sanitizeUrl } from "../lib/helpers";
29+
import * as actions from "../actions/connection";
30+
import { clickSidebarUrl } from "../actions/ui";
31+
32+
import SidebarLoginControl from "./SidebarLoginControl";
33+
34+
import "./ServerConnectionModal.scss";
35+
36+
export default function ServerConnectionModal() {
37+
const dispatch = useDispatch();
38+
const onHide = () => dispatch(clickSidebarUrl(""));
39+
const { serverHistory } = useSelector(state => state.connection);
40+
const [urlInput, setUrlInput] = useState(serverHistory[0].url);
41+
const [showError, setShowError] = useState(false);
42+
43+
const urlInputSanitized = sanitizeUrl(urlInput);
44+
const activeServer = serverHistory.find(s => s.url === urlInputSanitized);
45+
46+
const activeUrl = serverHistory[0].url;
47+
useEffect(() => {
48+
// When connected server URL is changed by any action -- update input
49+
setUrlInput(activeUrl);
50+
}, [activeUrl]);
51+
52+
const connectTo = url => {
53+
if (!url.trim()) {
54+
setShowError(true);
55+
return;
56+
}
57+
dispatch(actions.updateUrl(url));
58+
};
59+
60+
const renderSettings = () => {
61+
if (!activeServer) {
62+
return (
63+
<Alert variant="warning">
64+
New URL entered. Click 'Connect' before customizing{" "}
65+
<strong>{urlInputSanitized}</strong>{" "}
66+
</Alert>
67+
);
68+
}
69+
70+
return (
71+
<>
72+
<Card>
73+
<Card.Body>
74+
<Card.Subtitle>ACL Account</Card.Subtitle>
75+
<SidebarLoginControl />
76+
</Card.Body>
77+
</Card>
78+
79+
<Card>
80+
<Card.Body>
81+
<Card.Subtitle>Connection Settings</Card.Subtitle>
82+
<Form.Group controlId="queryTimeoutInput">
83+
<Form.Label>Query timeout (seconds):</Form.Label>
84+
<Form.Control
85+
type="number"
86+
min="1"
87+
step="1"
88+
placeholder="<timeout in seconds>"
89+
value={activeServer.queryTimeout}
90+
onChange={e => {
91+
const newTimeout = parseInt(e.target.value);
92+
if (newTimeout > 0) {
93+
dispatch(
94+
actions.setQueryTimeout(
95+
activeServer.url,
96+
newTimeout,
97+
),
98+
);
99+
}
100+
}}
101+
/>
102+
</Form.Group>
103+
</Card.Body>
104+
</Card>
105+
</>
106+
);
107+
};
108+
109+
const alreadyConnected = urlInputSanitized === serverHistory[0].url;
110+
const urlInputBlock = (
111+
<div className="url-input-box">
112+
<label htmlFor="serverUrlInput">Dgraph server URL:</label>
113+
114+
<div className="form-group">
115+
<input
116+
id="serverUrlInput"
117+
type="text"
118+
placeholder="https://dgraph.example.com:port"
119+
value={urlInput}
120+
onChange={event => {
121+
const value = event.target.value;
122+
setShowError(value && !value.trim());
123+
setUrlInput(value);
124+
}}
125+
style={{ width: "100%" }}
126+
/>
127+
{showError ? (
128+
<p
129+
style={{
130+
color: "#dc3545",
131+
marginTop: "5px",
132+
}}
133+
>
134+
The URL field cannot be empty
135+
</p>
136+
) : (
137+
<br />
138+
)}
139+
<Button
140+
size="sm"
141+
variant={!alreadyConnected ? "primary" : "default"}
142+
onClick={() => connectTo(urlInput)}
143+
disabled={alreadyConnected || showError}
144+
>
145+
{alreadyConnected ? "Connected" : "Connect"}
146+
</Button>
147+
</div>
148+
</div>
149+
);
150+
151+
const historyDisplay = (
152+
<>
153+
<h6>Recent Servers</h6>
154+
<ListGroup>
155+
{serverHistory.map(s => (
156+
<ListGroup.Item
157+
key={s.url}
158+
title={s.url}
159+
active={s.url === urlInputSanitized}
160+
onClick={() => setUrlInput(sanitizeUrl(s.url))}
161+
>
162+
<p>{s.url}</p>
163+
<p className="minor">{s.version}</p>
164+
<Button
165+
disabled={s.url === serverHistory[0].url}
166+
className="btn-connect"
167+
variant={
168+
s.url === serverHistory[0].url
169+
? "light"
170+
: "primary"
171+
}
172+
size="sm"
173+
onClick={() => connectTo(s.url)}
174+
>
175+
{s.url === serverHistory[0].url ? (
176+
<i className="fas fa-check-circle" />
177+
) : (
178+
<i className="fas fa-chevron-circle-right" />
179+
)}
180+
</Button>
181+
</ListGroup.Item>
182+
))}
183+
</ListGroup>
184+
</>
185+
);
186+
187+
return (
188+
<Modal centered show={true} size="lg" onHide={onHide}>
189+
<Modal.Header closeButton>
190+
<Modal.Title>Server Connection</Modal.Title>
191+
</Modal.Header>
192+
<Modal.Body className="server-connection-modal-body">
193+
<Container>
194+
<Row className="main-row">
195+
<Col xs={6} lg={4} className="col-history">
196+
{historyDisplay}
197+
</Col>
198+
<Col xs={6} lg={8} className="col-props">
199+
{urlInputBlock}
200+
<div className="settings">{renderSettings()}</div>
201+
</Col>
202+
</Row>
203+
</Container>
204+
</Modal.Body>
205+
206+
<Modal.Footer>
207+
<Button
208+
className="pull-left"
209+
variant="secondary"
210+
onClick={onHide}
211+
>
212+
Close
213+
</Button>
214+
</Modal.Footer>
215+
</Modal>
216+
);
217+
}

0 commit comments

Comments
 (0)