blob: aa5576e6823c32296f5261d0a6b34e2bff54421c [file] [log] [blame]
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
import PropTypes from 'prop-types';
import React from "react";
import ReactDOM from "react-dom";
import app from "../../../app";
import FauxtonAPI from "../../../core/api";
import Stores from "./stores";
import Actions from "./actions";
import Components from "../../components/react-components";
import ComponentsStore from "../../components/stores";
import ComponentsActions from "../../components/actions";
import IndexEditorActions from "../index-editor/actions";
import IndexEditorComponents from "../index-editor/components";
import GeneralComponents from "../../fauxton/components";
import DocumentHelper from "../../documents/helpers";
import { Collapse, OverlayTrigger, Popover, Modal } from "react-bootstrap";
import "../../../../assets/js/plugins/prettify";
const store = Stores.sidebarStore;
const { DeleteDatabaseModal, LoadLines, MenuDropDown } = Components;
const { DesignDocSelector } = IndexEditorComponents;
const { ConfirmationModal } = GeneralComponents;
const { deleteDbModalStore } = ComponentsStore;
class MainSidebar extends React.Component {
static propTypes = {
selectedNavItem: PropTypes.string.isRequired
};
getNewButtonLinks = () => { // these are links for the sidebar '+' on All Docs and All Design Docs
return DocumentHelper.getNewButtonLinks(this.props.databaseName);
};
buildDocLinks = () => {
const base = FauxtonAPI.urls('base', 'app', this.props.databaseName);
return FauxtonAPI.getExtensions('docLinks').map(function (link) {
return (
<li key={link.url} className={this.getNavItemClass(link.url)}>
<a id={link.url} href={base + link.url}>{link.title}</a>
</li>
);
}, this);
};
getNavItemClass = (navItem) => {
return (navItem === this.props.selectedNavItem) ? 'active' : '';
};
render() {
const docLinks = this.buildDocLinks();
const dbEncoded = FauxtonAPI.url.encode(this.props.databaseName);
const changesUrl = '#' + FauxtonAPI.urls('changes', 'app', dbEncoded, '');
const permissionsUrl = '#' + FauxtonAPI.urls('permissions', 'app', dbEncoded);
const databaseUrl = FauxtonAPI.urls('allDocs', 'app', dbEncoded, '');
const mangoQueryUrl = FauxtonAPI.urls('mango', 'query-app', dbEncoded);
const runQueryWithMangoText = app.i18n.en_US['run-query-with-mango'];
const buttonLinks = this.getNewButtonLinks();
return (
<ul className="nav nav-list">
<li className={this.getNavItemClass('all-docs')}>
<a id="all-docs"
href={"#/" + databaseUrl}
className="toggle-view">
All Documents
</a>
<div id="new-all-docs-button" className="add-dropdown">
<MenuDropDown links={buttonLinks} />
</div>
</li>
<li className={this.getNavItemClass('mango-query')}>
<a
id="mango-query"
href={'#' + mangoQueryUrl}
className="toggle-view">
{runQueryWithMangoText}
</a>
</li>
<li className={this.getNavItemClass('permissions')}>
<a id="permissions" href={permissionsUrl}>Permissions</a>
</li>
<li className={this.getNavItemClass('changes')}>
<a id="changes" href={changesUrl}>Changes</a>
</li>
{docLinks}
<li className={this.getNavItemClass('design-docs')}>
<a
id="design-docs"
href={"#/" + databaseUrl + '?startkey="_design"&endkey="_design0"'}
className="toggle-view">
Design Documents
</a>
<div id="new-design-docs-button" className="add-dropdown">
<MenuDropDown links={buttonLinks} />
</div>
</li>
</ul>
);
}
}
class IndexSection extends React.Component {
static propTypes = {
urlNamespace: PropTypes.string.isRequired,
indexLabel: PropTypes.string.isRequired,
database: PropTypes.object.isRequired,
designDocName: PropTypes.string.isRequired,
items: PropTypes.array.isRequired,
isExpanded: PropTypes.bool.isRequired,
selectedIndex: PropTypes.string.isRequired,
onDelete: PropTypes.func.isRequired,
onClone: PropTypes.func.isRequired
};
state = {
placement: 'bottom'
};
// this dynamically changes the placement of the menu (top/bottom) to prevent it going offscreen and causing some
// unsightly shifting
setPlacement = (rowId) => {
const rowTop = document.getElementById(rowId).getBoundingClientRect().top;
const toggleHeight = 150; // the height of the menu overlay, arrow, view row
const placement = (rowTop + toggleHeight > window.innerHeight) ? 'top' : 'bottom';
this.setState({ placement: placement });
};
createItems = () => {
// sort the indexes alphabetically
const sortedItems = this.props.items.sort();
return _.map(sortedItems, function (indexName, index) {
const href = FauxtonAPI.urls(this.props.urlNamespace, 'app', encodeURIComponent(this.props.database.id), encodeURIComponent(this.props.designDocName));
const className = (this.props.selectedIndex === indexName) ? 'active' : '';
return (
<li className={className} key={index}>
<a
id={this.props.designDocName + '_' + indexName}
href={"#/" + href + encodeURIComponent(indexName)}
className="toggle-view">
{indexName}
</a>
<OverlayTrigger
trigger="click"
onEnter={this.setPlacement.bind(this, this.props.designDocName + '_' + indexName)}
placement={this.state.placement}
rootClose={true}
ref={overlay => this.itemOverlay = overlay}
overlay={
<Popover id="index-menu-component-popover">
<ul>
<li onClick={this.indexAction.bind(this, 'edit', { indexName: indexName, onEdit: this.props.onEdit })}>
<span className="fonticon fonticon-file-code-o"></span>
Edit
</li>
<li onClick={this.indexAction.bind(this, 'clone', { indexName: indexName, onClone: this.props.onClone })}>
<span className="fonticon fonticon-files-o"></span>
Clone
</li>
<li onClick={this.indexAction.bind(this, 'delete', { indexName: indexName, onDelete: this.props.onDelete })}>
<span className="fonticon fonticon-trash"></span>
Delete
</li>
</ul>
</Popover>
}>
<span className="index-menu-toggle fonticon fonticon-wrench2"></span>
</OverlayTrigger>
</li>
);
}, this);
};
indexAction = (action, params, e) => {
e.preventDefault();
this.itemOverlay.hide();
switch (action) {
case 'delete':
Actions.showDeleteIndexModal(params.indexName, this.props.designDocName, this.props.indexLabel, params.onDelete);
break;
case 'clone':
Actions.showCloneIndexModal(params.indexName, this.props.designDocName, this.props.indexLabel, params.onClone);
break;
case 'edit':
params.onEdit(this.props.database.id, this.props.designDocName, params.indexName);
break;
}
};
toggle = (e) => {
e.preventDefault();
this.props.toggle(this.props.designDocName, this.props.title);
};
render() {
// if this section has no content, omit it to prevent clutter. Otherwise it would show a toggle option that
// would hide/show nothing
if (this.props.items.length === 0) {
return null;
}
let toggleClassNames = 'accordion-header index-group-header';
let toggleBodyClassNames = 'index-list accordion-body collapse';
if (this.props.isExpanded) {
toggleClassNames += ' down';
toggleBodyClassNames += ' in';
}
const title = this.props.title;
const designDocName = this.props.designDocName;
const linkId = "nav-design-function-" + designDocName + this.props.selector;
return (
<li id={linkId}>
<a className={toggleClassNames} data-toggle="collapse" onClick={this.toggle}>
<div className="fonticon-play"></div>
{title}
</a>
<Collapse in={this.props.isExpanded}>
<ul className={toggleBodyClassNames}>
{this.createItems()}
</ul>
</Collapse>
</li>
);
}
}
class DesignDoc extends React.Component {
static propTypes = {
database: PropTypes.object.isRequired,
sidebarListTypes: PropTypes.array.isRequired,
isExpanded: PropTypes.bool.isRequired,
selectedNavInfo: PropTypes.object.isRequired,
toggledSections: PropTypes.object.isRequired,
designDocName: PropTypes.string.isRequired
};
state = {
updatedSidebarListTypes: this.props.sidebarListTypes
};
UNSAFE_componentWillMount() {
if (_.isEmpty(this.state.updatedSidebarListTypes) ||
(_.has(this.state.updatedSidebarListTypes[0], 'selector') && this.state.updatedSidebarListTypes[0].selector !== 'views')) {
const newList = this.state.updatedSidebarListTypes;
newList.unshift({
selector: 'views',
name: 'Views',
urlNamespace: 'view',
indexLabel: 'view',
onDelete: IndexEditorActions.deleteView,
onClone: IndexEditorActions.cloneView,
onEdit: IndexEditorActions.gotoEditViewPage
});
this.setState({ updatedSidebarListTypes: newList });
}
}
indexList = () => {
return _.map(this.state.updatedSidebarListTypes, function (index, key) {
const expanded = _.has(this.props.toggledSections, index.name) && this.props.toggledSections[index.name];
// if an index in this list is selected, pass that down
let selectedIndex = '';
if (this.props.selectedNavInfo.designDocSection === index.name) {
selectedIndex = this.props.selectedNavInfo.indexName;
}
return (
<IndexSection
icon={index.icon}
isExpanded={expanded}
urlNamespace={index.urlNamespace}
indexLabel={index.indexLabel}
onEdit={index.onEdit}
onDelete={index.onDelete}
onClone={index.onClone}
selectedIndex={selectedIndex}
toggle={this.props.toggle}
database={this.props.database}
designDocName={this.props.designDocName}
key={key}
title={index.name}
selector={index.selector}
items={_.keys(this.props.designDoc[index.selector])} />
);
}.bind(this));
};
toggle = (e) => {
e.preventDefault();
this.props.toggle(this.props.designDocName);
};
getNewButtonLinks = () => {
const newUrlPrefix = FauxtonAPI.urls('databaseBaseURL', 'app', encodeURIComponent(this.props.database.id));
const designDocName = this.props.designDocName;
const addNewLinks = _.reduce(FauxtonAPI.getExtensions('sidebar:links'), function (menuLinks, link) {
menuLinks.push({
title: link.title,
url: '#' + newUrlPrefix + '/' + link.url + '/' + encodeURIComponent(designDocName),
icon: 'fonticon-plus-circled'
});
return menuLinks;
}, [{
title: 'New View',
url: '#' + FauxtonAPI.urls('new', 'addView', encodeURIComponent(this.props.database.id), encodeURIComponent(designDocName)),
icon: 'fonticon-plus-circled'
}]);
return [{
title: 'Add New',
links: addNewLinks
}];
};
render () {
const buttonLinks = this.getNewButtonLinks();
let toggleClassNames = 'design-doc-section accordion-header';
let toggleBodyClassNames = 'design-doc-body accordion-body collapse';
if (this.props.isExpanded) {
toggleClassNames += ' down';
toggleBodyClassNames += ' in';
}
const designDocName = this.props.designDocName;
const designDocMetaUrl = FauxtonAPI.urls('designDocs', 'app', this.props.database.id, designDocName);
const metadataRowClass = (this.props.selectedNavInfo.designDocSection === 'metadata') ? 'active' : '';
return (
<li className="nav-header">
<div id={"sidebar-tab-" + designDocName} className={toggleClassNames}>
<div id={"nav-header-" + designDocName} onClick={this.toggle} className='accordion-list-item'>
<div className="fonticon-play"></div>
<p className='design-doc-name'>
<span title={'_design/' + designDocName}>{designDocName}</span>
</p>
</div>
<div className='new-button add-dropdown'>
<MenuDropDown links={buttonLinks} />
</div>
</div>
<Collapse in={this.props.isExpanded}>
<ul className={toggleBodyClassNames} id={this.props.designDocName}>
<li className={metadataRowClass}>
<a href={"#/" + designDocMetaUrl} className="toggle-view accordion-header">
Metadata
</a>
</li>
{this.indexList()}
</ul>
</Collapse>
</li>
);
}
}
class DesignDocList extends React.Component {
UNSAFE_componentWillMount() {
const list = FauxtonAPI.getExtensions('sidebar:list');
this.sidebarListTypes = _.isUndefined(list) ? [] : list;
}
designDocList = () => {
return _.map(this.props.designDocs, function (designDoc, key) {
const ddName = decodeURIComponent(designDoc.safeId);
// only pass down the selected nav info and toggle info if they're relevant for this particular design doc
let expanded = false,
toggledSections = {};
if (_.has(this.props.toggledSections, ddName)) {
expanded = this.props.toggledSections[ddName].visible;
toggledSections = this.props.toggledSections[ddName].indexGroups;
}
let selectedNavInfo = {};
if (this.props.selectedNav.navItem === 'designDoc' && this.props.selectedNav.designDocName === ddName) {
selectedNavInfo = this.props.selectedNav;
}
return (
<DesignDoc
toggle={this.props.toggle}
sidebarListTypes={this.sidebarListTypes}
isExpanded={expanded}
toggledSections={toggledSections}
selectedNavInfo={selectedNavInfo}
key={key}
designDoc={designDoc}
designDocName={ddName}
database={this.props.database} />
);
}.bind(this));
};
render() {
return (
<ul className="nav nav-list">
{this.designDocList()}
</ul>
);
}
}
class SidebarController extends React.Component {
getStoreState = () => {
return {
database: store.getDatabase(),
selectedNav: store.getSelected(),
designDocs: store.getDesignDocs(),
designDocList: store.getDesignDocList(),
availableDesignDocIds: store.getAvailableDesignDocs(),
toggledSections: store.getToggledSections(),
isLoading: store.isLoading(),
deleteDbModalProperties: deleteDbModalStore.getShowDeleteDatabaseModal(),
deleteIndexModalVisible: store.isDeleteIndexModalVisible(),
deleteIndexModalText: store.getDeleteIndexModalText(),
deleteIndexModalOnSubmit: store.getDeleteIndexModalOnSubmit(),
deleteIndexModalIndexName: store.getDeleteIndexModalIndexName(),
deleteIndexModalDesignDoc: store.getDeleteIndexDesignDoc(),
cloneIndexModalVisible: store.isCloneIndexModalVisible(),
cloneIndexModalTitle: store.getCloneIndexModalTitle(),
cloneIndexModalSelectedDesignDoc: store.getCloneIndexModalSelectedDesignDoc(),
cloneIndexModalNewDesignDocName: store.getCloneIndexModalNewDesignDocName(),
cloneIndexModalOnSubmit: store.getCloneIndexModalOnSubmit(),
cloneIndexDesignDocProp: store.getCloneIndexDesignDocProp(),
cloneIndexModalNewIndexName: store.getCloneIndexModalNewIndexName(),
cloneIndexSourceIndexName: store.getCloneIndexModalSourceIndexName(),
cloneIndexSourceDesignDocName: store.getCloneIndexModalSourceDesignDocName(),
cloneIndexModalIndexLabel: store.getCloneIndexModalIndexLabel()
};
};
componentDidMount() {
store.on('change', this.onChange, this);
deleteDbModalStore.on('change', this.onChange, this);
}
componentWillUnmount() {
store.off('change', this.onChange);
deleteDbModalStore.off('change', this.onChange, this);
}
onChange = () => {
const newState = this.getStoreState();
// Workaround to signal Redux store that the design doc list was updated
// which is currently required by QueryOptionsContainer
// It should be removed once Sidebar components are refactored to use Redux
if (this.props.reduxUpdatedDesignDocList) {
this.props.reduxUpdatedDesignDocList(newState.designDocList);
}
this.setState(newState);
};
showDeleteDatabaseModal = (payload) => {
ComponentsActions.showDeleteDatabaseModal(payload);
};
// handles deleting of any index regardless of type. The delete handler and all relevant info is set when the user
// clicks the delete action for a particular index
deleteIndex = () => {
// if the user is currently on the index that's being deleted, pass that info along to the delete handler. That can
// be used to redirect the user to somewhere appropriate
const isOnIndex = this.state.selectedNav.navItem === 'designDoc' &&
('_design/' + this.state.selectedNav.designDocName) === this.state.deleteIndexModalDesignDoc.id &&
this.state.selectedNav.indexName === this.state.deleteIndexModalIndexName;
this.state.deleteIndexModalOnSubmit({
isOnIndex: isOnIndex,
indexName: this.state.deleteIndexModalIndexName,
designDoc: this.state.deleteIndexModalDesignDoc,
designDocs: this.state.designDocs,
database: this.state.database
});
};
cloneIndex = () => {
this.state.cloneIndexModalOnSubmit({
sourceIndexName: this.state.cloneIndexSourceIndexName,
sourceDesignDocName: this.state.cloneIndexSourceDesignDocName,
targetDesignDocName: this.state.cloneIndexModalSelectedDesignDoc,
newDesignDocName: this.state.cloneIndexModalNewDesignDocName,
newIndexName: this.state.cloneIndexModalNewIndexName,
designDocs: this.state.designDocs,
database: this.state.database,
onComplete: Actions.hideCloneIndexModal
});
};
state = this.getStoreState();
render() {
if (this.state.isLoading) {
return <LoadLines />;
}
return (
<nav className="sidenav">
<MainSidebar
selectedNavItem={this.state.selectedNav.navItem}
databaseName={this.state.database.id} />
<DesignDocList
selectedNav={this.state.selectedNav}
toggle={Actions.toggleContent}
toggledSections={this.state.toggledSections}
designDocs={this.state.designDocList}
database={this.state.database} />
<DeleteDatabaseModal
showHide={this.showDeleteDatabaseModal}
modalProps={this.state.deleteDbModalProperties} />
{/* the delete and clone index modals handle all index types, hence the props all being pulled from the store */}
<ConfirmationModal
title="Confirm Deletion"
visible={this.state.deleteIndexModalVisible}
text={this.state.deleteIndexModalText}
onClose={Actions.hideDeleteIndexModal}
onSubmit={this.deleteIndex} />
<CloneIndexModal
visible={this.state.cloneIndexModalVisible}
title={this.state.cloneIndexModalTitle}
close={Actions.hideCloneIndexModal}
submit={this.cloneIndex}
designDocArray={this.state.availableDesignDocIds}
selectedDesignDoc={this.state.cloneIndexModalSelectedDesignDoc}
newDesignDocName={this.state.cloneIndexModalNewDesignDocName}
newIndexName={this.state.cloneIndexModalNewIndexName}
indexLabel={this.state.cloneIndexModalIndexLabel} />
</nav>
);
}
}
class CloneIndexModal extends React.Component {
static propTypes = {
visible: PropTypes.bool.isRequired,
title: PropTypes.string,
close: PropTypes.func.isRequired,
submit: PropTypes.func.isRequired,
designDocArray: PropTypes.array.isRequired,
selectedDesignDoc: PropTypes.string.isRequired,
newDesignDocName: PropTypes.string.isRequired,
newIndexName: PropTypes.string.isRequired,
indexLabel: PropTypes.string.isRequired
};
static defaultProps = {
title: 'Clone Index',
visible: false
};
submit = () => {
if (!this.designDocSelector.validate()) {
return;
}
if (this.props.newIndexName === '') {
FauxtonAPI.addNotification({
msg: 'Please enter the new index name.',
type: 'error',
clear: true
});
return;
}
this.props.submit();
};
close = (e) => {
if (e) {
e.preventDefault();
}
this.props.close();
};
setNewIndexName = (e) => {
Actions.setNewCloneIndexName(e.target.value);
};
render() {
return (
<Modal dialogClassName="clone-index-modal" show={this.props.visible} onHide={this.close}>
<Modal.Header closeButton={true}>
<Modal.Title>{this.props.title}</Modal.Title>
</Modal.Header>
<Modal.Body>
<form className="form" method="post" onSubmit={this.submit}>
<p>
Select the design document where the cloned {this.props.indexLabel} will be created, and then enter
a name for the cloned {this.props.indexLabel}.
</p>
<div className="row">
<DesignDocSelector
ref={node => this.designDocSelector = node}
designDocList={this.props.designDocArray}
selectedDesignDocName={this.props.selectedDesignDoc}
newDesignDocName={this.props.newDesignDocName}
onSelectDesignDoc={Actions.selectDesignDoc}
onChangeNewDesignDocName={Actions.updateNewDesignDocName} />
</div>
<div className="clone-index-name-row">
<label className="new-index-title-label" htmlFor="new-index-name">{this.props.indexLabel} Name</label>
<input type="text" id="new-index-name" value={this.props.newIndexName} onChange={this.setNewIndexName}
placeholder="New view name" />
</div>
</form>
</Modal.Body>
<Modal.Footer>
<a href="#" className="cancel-link" onClick={this.close} data-bypass="true">Cancel</a>
<button onClick={this.submit} data-bypass="true" className="btn btn-primary save">
<i className="icon fonticon-ok-circled" /> Clone {this.props.indexLabel}</button>
</Modal.Footer>
</Modal>
);
}
}
export default {
SidebarController: SidebarController,
DesignDoc: DesignDoc,
CloneIndexModal: CloneIndexModal
};