packages/browser/src/components/ConnectApp/ConnectApp.js
// @flow
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Form, Button, Alert, AutoComplete, Input, Modal } from 'antd';
import {
CloseOutlined,
PauseCircleOutlined,
PlayCircleOutlined,
PlusOutlined,
} from '@ant-design/icons';
import { object } from 'prop-types';
import { mediaMin } from '@divyanshu013/media';
import styled from 'react-emotion';
import { withRouter } from 'react-router-dom';
import {
getAppname,
getUrl,
getIsLoading,
getIsConnected,
getHeaders,
} from '../../reducers/app';
import {
connectApp,
disconnectApp,
setMode,
setError,
setHeaders,
} from '../../actions';
import {
getUrlParams,
getLocalStorageItem,
setLocalStorageData,
getCustomHeaders,
isMultiIndexApp,
saveAppToLocalStorage,
normalizeSearchQuery,
} from '../../utils';
import { getMode } from '../../reducers/mode';
import { LOCAL_CONNECTIONS, MODES } from '../../constants';
import Flex from '../Flex';
type Props = {
appname?: string,
url?: string,
connectApp: (string, string, any) => void,
disconnectApp: () => void,
isConnected: boolean,
isLoading: boolean,
error?: object,
history: object,
mode: string,
setMode: string => void,
onErrorClose: () => void,
location: any,
isHidden?: boolean,
setError: any => void,
headers: any[],
setHeaders: any => void,
URLParams: boolean,
showHeaders: boolean,
forceReconnect: boolean,
};
type State = {
appname: string,
url: string,
pastApps: any[],
isShowingAppSwitcher: boolean,
isUrlHidden: boolean,
isShowingHeadersModal: boolean,
customHeaders: any[],
};
const { Item } = Form;
const { Group } = Input;
const formItemProps = {
wrapperCol: {
xs: {
span: 24,
},
},
};
const ConfigurationContainer = styled.div`
display: flex;
flex-wrap: wrap;
align-items: center;
width: 100%;
`;
const ROUTES_WITHOUT_MULTIPLE_INDEX = ['/mappings', '/preview', '/query'];
const shouldConnect = (pathname, appname) => {
let isConnecting = false;
if (ROUTES_WITHOUT_MULTIPLE_INDEX.indexOf(pathname) === -1) {
isConnecting = true;
} else if (!isMultiIndexApp(appname)) {
isConnecting = true;
} else {
isConnecting = false;
}
return isConnecting;
};
class ConnectApp extends Component<Props, State> {
state = {
appname: this.props.appname || '',
url: this.props.url || '',
pastApps: [],
isShowingAppSwitcher: true,
isUrlHidden: false,
isShowingHeadersModal: false,
customHeaders: this.props.headers.length
? this.props.headers
: [{ key: '', value: '' }],
};
componentDidMount() {
// sync state from url
let appname = '';
let url = '';
const { mode, isConnected, isHidden, forceReconnect } = this.props;
const {
appname: queryApp,
url: queryUrl,
mode: queryMode,
sidebar,
footer,
appswitcher,
route,
} = getUrlParams(window.location.search);
const URLParams =
this.props.URLParams !== undefined ? this.props.URLParams : true;
if (queryApp && queryUrl) {
appname = queryApp;
url = queryUrl;
} else {
const { appname: propApp, url: propUrl } = this.props;
appname = propApp || '';
url = propUrl || '';
}
this.setState({
appname,
url,
});
// when you want to explicitly trigger reconnect even when app is connect pass `forceReconnect=true`
if (appname && url && (forceReconnect || !isConnected)) {
const { pathname } = this.props.location;
if (shouldConnect(pathname, appname)) {
this.props.connectApp(appname, url);
saveAppToLocalStorage(appname, url);
if (isHidden) {
this.setAppSwitcher(false);
}
} else {
this.props.setError({
message:
'Sorry can not connect to the app with multiple indexes',
description: 'Please try using single index',
});
}
}
if (isConnected && isHidden) {
this.setAppSwitcher(false);
}
if (!queryApp && !queryUrl && URLParams) {
let searchQuery = `?appname=${appname}&url=${url}`;
const currentMode = queryMode || mode;
searchQuery += `&mode=${currentMode}`;
if (sidebar) {
searchQuery += `&sidebar=${sidebar}`;
}
if (footer) {
searchQuery += `&footer=${footer}`;
}
if (appswitcher) {
searchQuery += `&appswitcher=${appswitcher}`;
}
if (route) {
searchQuery += `&route=${route}`;
}
this.props.setMode(currentMode);
this.props.history.push({
search: normalizeSearchQuery(searchQuery),
});
}
if (queryMode) {
this.props.setMode(queryMode);
}
if (appswitcher && appswitcher === 'false') {
this.setAppSwitcher(false);
}
const customHeaders = getCustomHeaders(appname);
this.props.setHeaders(customHeaders);
this.setState({
customHeaders: customHeaders.length
? customHeaders
: [{ key: '', value: '' }],
});
this.setPastConnections();
}
setAppSwitcher = isShowingAppSwitcher => {
this.setState({
isShowingAppSwitcher,
});
};
setPastConnections = () => {
const pastConnections = JSON.parse(
// $FlowFixMe
getLocalStorageItem(LOCAL_CONNECTIONS) || {},
);
this.setState({
pastApps: (pastConnections || {}).pastApps || [],
});
};
handleChange = e => {
const { value, name } = e.target;
this.setState({
[name]: value,
});
};
handleAppNameChange = appname => {
const { pastApps } = this.state;
const pastApp = pastApps.find(app => app.appname === appname);
if (pastApp) {
this.setState({
url: pastApp.url,
customHeaders: pastApp.headers || [],
});
}
this.setState({
appname,
});
};
handleSubmit = () => {
const { appname, url, customHeaders } = this.state;
const { sidebar, appswitcher, footer, route } = getUrlParams(
window.location.search,
);
const { pathname } = this.props.location;
let searchQuery = '?';
if (sidebar) {
searchQuery += `&sidebar=${sidebar}`;
}
if (footer) {
searchQuery += `&sidebar=${footer}`;
}
if (appswitcher) {
searchQuery += `&appswitcher=${appswitcher}`;
}
if (route) {
searchQuery += `&route=${route}`;
}
if (this.props.isConnected) {
this.props.disconnectApp();
this.props.setMode(MODES.VIEW);
this.props.setHeaders([]);
// this.setState({
// customHeaders: [{ key: '', value: '' }],
// appname: '',
// url: '',
// });
this.props.history.push({
search: normalizeSearchQuery(searchQuery),
});
// window.location.reload(true);
} else if (appname && url) {
if (shouldConnect(pathname, appname)) {
this.props.connectApp(appname, url, customHeaders);
this.props.setHeaders(customHeaders);
// update history with correct appname and url
searchQuery += `&appname=${appname}&url=${url}&mode=${this.props.mode}`;
const { pastApps } = this.state;
const newApps = [...pastApps];
const pastApp = pastApps.find(app => app.appname === appname);
if (!pastApp) {
newApps.push({
appname,
url,
headers: customHeaders.filter(
item => item.key.trim() && item.value.trim(),
),
});
} else {
const appIndex = newApps.findIndex(
item => item.appname === appname,
);
newApps[appIndex] = {
appname,
url,
headers: customHeaders.filter(
item => item.key.trim() && item.value.trim(),
),
};
}
this.setState({
pastApps: newApps,
});
setLocalStorageData(
LOCAL_CONNECTIONS,
JSON.stringify({
pastApps: newApps,
}),
);
this.props.history.push({
search: normalizeSearchQuery(searchQuery),
});
if (this.props.isHidden) {
this.setAppSwitcher(false);
}
} else {
this.props.setError({
message:
'Sorry can not connect to the app with multiple indexes',
description: 'Please try using single index',
});
}
}
};
handleUrlToggle = () => {
this.setState(({ isUrlHidden }) => ({
isUrlHidden: !isUrlHidden,
}));
};
toggleHeadersModal = () => {
this.setState(({ isShowingHeadersModal }) => ({
isShowingHeadersModal: !isShowingHeadersModal,
}));
};
handleHeaderItemChange = (e, index, field) => {
const {
target: { value },
} = e;
const { customHeaders } = this.state;
this.setState({
customHeaders: [
...customHeaders.slice(0, index),
{
...customHeaders[index],
[field]: value,
},
...customHeaders.slice(index + 1),
],
});
};
handleHeadersSubmit = e => {
e.preventDefault();
const { customHeaders } = this.state;
const filteredHeaders = customHeaders.filter(
item => item.key.trim() && item.value.trim(),
);
const { isConnected } = this.props;
if (isConnected) {
const { pastApps } = JSON.parse(
getLocalStorageItem(LOCAL_CONNECTIONS),
);
const currentApp = pastApps.findIndex(
item => item.appname === this.props.appname,
);
pastApps[currentApp].headers = filteredHeaders;
setLocalStorageData(
LOCAL_CONNECTIONS,
JSON.stringify({ pastApps }),
);
}
this.props.setHeaders(filteredHeaders);
this.toggleHeadersModal();
};
handleHeaderAfterClose = () => {
this.setState({
customHeaders: this.props.headers.length
? this.props.headers
: [{ key: '', value: '' }],
});
};
addMoreHeader = () => {
const { customHeaders } = this.state;
this.setState({
customHeaders: [...customHeaders, { key: '', value: '' }],
});
};
handleRemoveHeader = index => {
const { customHeaders } = this.state;
this.setState({
customHeaders: [
...customHeaders.slice(0, index),
...customHeaders.slice(index + 1),
],
});
};
render() {
const {
appname,
url,
pastApps,
isShowingAppSwitcher,
isUrlHidden,
isShowingHeadersModal,
customHeaders,
} = this.state;
const { isLoading, isConnected } = this.props;
const showHeaders =
this.props.showHeaders !== undefined
? this.props.showHeaders
: true;
return (
<div>
{isShowingAppSwitcher && showHeaders && (
<Form
style={{ marginBottom: 10 }}
layout="inline"
onFinish={this.handleSubmit}
>
<ConfigurationContainer>
<Item {...formItemProps} style={{ flex: 1 }}>
<Group
compact
css={{ display: 'flex !important' }}
>
<Input.Password
name="url"
value={url}
placeholder="URL for cluster goes here. e.g. https://username:password@my-search-cluster.com"
onChange={this.handleChange}
disabled={isConnected}
required
visibilityToggle={{
visible: !isUrlHidden,
onVisibleChange: this
.handleUrlToggle,
}}
css={{
color:
isUrlHidden &&
isConnected &&
'transparent !important',
width: '100%',
}}
/>
<Button
type="button"
css={{
'&:hover': {
borderColor:
'#d9d9d9 !important',
},
}}
onClick={this.toggleHeadersModal}
>
Headers
</Button>
</Group>
</Item>
<Item>
<AutoComplete
options={pastApps.map(app => ({
value: app.appname,
}))}
value={appname}
filterOption={(inputValue, option) =>
option.value.includes(inputValue)
}
style={{ width: 200 }}
onChange={this.handleAppNameChange}
onSelect={this.handleAppNameChange}
disabled={isConnected}
placeholder="Enter index"
spellcheck="false"
autocorrect="off"
autocapitalize="off"
/>
</Item>
<Item css={{ marginRight: '0px !important' }}>
<Button
type={isConnected ? undefined : 'primary'}
danger={isConnected}
ghost={isConnected}
htmlType="submit"
icon={
isConnected ? (
<PauseCircleOutlined />
) : (
<PlayCircleOutlined />
)
}
disabled={!(appname && url)}
loading={isLoading}
>
{isConnected ? 'Disconnect' : 'Connect'}
</Button>
</Item>
<Modal
open={isShowingHeadersModal}
onCancel={this.toggleHeadersModal}
onOk={this.handleHeadersSubmit}
maskClosable={false}
destroyOnClose
title="Add Custom Headers"
css={{ top: 10 }}
closable={false}
afterClose={this.handleHeaderAfterClose}
>
<div
css={{
maxHeight: '500px',
overflow: 'auto',
paddingRight: 10,
}}
>
<Flex css={{ marginBottom: 10 }}>
<div
css={{
flex: 1,
marginLeft: 5,
}}
>
Key
</div>
<div
css={{
marginLeft: 10,
flex: 1,
}}
>
Value
</div>
</Flex>
{customHeaders.map((item, i) => (
<Flex
key={`header-${i}`} // eslint-disable-line
css={{ marginBottom: 10 }}
alignItems="center"
>
<div
css={{
flex: 1,
marginLeft: 5,
}}
>
<Input
value={item.key}
onChange={e =>
this.handleHeaderItemChange(
e,
i,
'key',
)
}
/>
</div>
<div
css={{
flex: 1,
marginLeft: 10,
}}
>
<Input
value={item.value}
onChange={e =>
this.handleHeaderItemChange(
e,
i,
'value',
)
}
/>
</div>
<div
css={{
marginLeft: 10,
minWidth: 15,
}}
>
{customHeaders.length > 0 && (
<CloseOutlined
onClick={() =>
this.handleRemoveHeader(
i,
)
}
css={{
cursor: 'pointer',
}}
/>
)}
</div>
</Flex>
))}
</div>
<Button
icon={<PlusOutlined />}
type="primary"
css={{ marginTop: 10, marginLeft: 5 }}
onClick={this.addMoreHeader}
/>
</Modal>
</ConfigurationContainer>
</Form>
)}
{!isLoading && !isConnected && (
<Alert
type="info"
description={
<React.Fragment>
<div>
<h3 style={{ marginTop: '1rem' }}>
Connection Tips
</h3>
<ul>
<li>
You can connect to all indices by
passing an <code>*</code> in the
index name input field.
</li>
<li>
You can also connect to a single
index or multiple indices by passing
them as comma separated values: e.g.
index1,index2,index3.
</li>
<li>
Avoid using a trailing slash{' '}
<code>/</code> after the cluster
address.
</li>
<li>
Your cluster needs to have CORS
enabled for the origin where Dejavu
is running. See below for more on
that.
</li>
</ul>
<h3>CORS Settings</h3>
<p>
To make sure you have enabled CORS
settings for your Elasticsearch
instance, add the following lines in the
ES configuration file:
</p>
<pre>
{`http.port: 9200
http.cors.allow-origin: http://localhost:1358,http://127.0.0.1:1358
http.cors.enabled: true
http.cors.allow-headers : X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization
http.cors.allow-credentials: true`}
</pre>
</div>
<div style={{ marginTop: '2rem' }}>
<p>
If you are running Elasticsearch via
Docker, use the following command:
</p>
<div
style={{
background: '#fefefe',
padding: '8px',
}}
>
<code>
docker run -d --rm --name
elasticsearch -p 127.0.0.1:9200:9200
-e http.port=9200 -e
discovery.type=single-node -e
http.max_content_length=10MB -e
http.cors.enabled=true -e
http.cors.allow-origin=\* -e
http.cors.allow-headers=X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization
-e http.cors.allow-credentials=true
-e network.publish_host=localhost -e
xpack.security.enabled=false
docker.elastic.co/elasticsearch/elasticsearch:8.15.1
</code>
</div>
<p style={{ marginTop: '14px' }}>
Or the following if you are using
OpenSearch:
</p>
<div
style={{
background: '#fefefe',
padding: '8px',
}}
>
<code>
docker run --name opensearch --rm -d
-p 9200:9200 -e http.port=9200 -e
discovery.type=single-node -e
http.max_content_length=10MB -e
http.cors.enabled=true -e
http.cors.allow-origin=\* -e
http.cors.allow-headers=X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization
-e http.cors.allow-credentials=true
-e DISABLE_SECURITY_PLUGIN=true
opensearchproject/opensearch:2.17.0
</code>
</div>
</div>
</React.Fragment>
}
/>
)}
</div>
);
}
}
const mapStateToProps = (state, props) => {
const getURL = () => {
if (props.url && props.url.trim() !== '') return props.url;
if (props.credentials)
return `https://${props.credentials}@scalr.api.appbase.io`;
return getUrl(state);
};
return {
appname: props.app || getAppname(state),
url: getURL(),
isConnected: getIsConnected(state),
isLoading: getIsLoading(state),
mode: getMode(state),
headers: getHeaders(state),
};
};
const mapDispatchToProps = {
connectApp,
disconnectApp,
setMode,
setError,
setHeaders,
};
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(ConnectApp),
);