mirror of
https://github.com/Kong/insomnia
synced 2024-11-08 23:00:30 +00:00
515 lines
16 KiB
JavaScript
515 lines
16 KiB
JavaScript
// @flow
|
|
import * as React from 'react';
|
|
import autobind from 'autobind-decorator';
|
|
import classnames from 'classnames';
|
|
import { SortableContainer, SortableElement, arrayMove } from 'react-sortable-hoc/dist/es6';
|
|
import { Dropdown, DropdownButton, DropdownItem } from '../base/dropdown';
|
|
import PromptButton from '../base/prompt-button';
|
|
import Button from '../base/button';
|
|
import Link from '../base/link';
|
|
import EnvironmentEditor from '../editors/environment-editor';
|
|
import Editable from '../base/editable';
|
|
import Modal from '../base/modal';
|
|
import ModalBody from '../base/modal-body';
|
|
import ModalHeader from '../base/modal-header';
|
|
import ModalFooter from '../base/modal-footer';
|
|
import * as models from '../../../models';
|
|
import { DEBOUNCE_MILLIS } from '../../../common/constants';
|
|
import type { Workspace } from '../../../models/workspace';
|
|
import type { Environment } from '../../../models/environment';
|
|
import * as db from '../../../common/database';
|
|
import HelpTooltip from '../help-tooltip';
|
|
import Tooltip from '../tooltip';
|
|
|
|
const ROOT_ENVIRONMENT_NAME = 'Base Environment';
|
|
|
|
type Props = {
|
|
activeEnvironmentId: string | null,
|
|
editorFontSize: number,
|
|
editorIndentSize: number,
|
|
editorKeyMap: string,
|
|
lineWrapping: boolean,
|
|
render: Function,
|
|
getRenderContext: Function,
|
|
nunjucksPowerUserMode: boolean,
|
|
isVariableUncovered: boolean,
|
|
};
|
|
|
|
type State = {
|
|
workspace: Workspace | null,
|
|
isValid: boolean,
|
|
subEnvironments: Array<Environment>,
|
|
rootEnvironment: Environment | null,
|
|
selectedEnvironmentId: string | null,
|
|
};
|
|
|
|
const SidebarListItem = SortableElement(
|
|
({ environment, activeEnvironment, showEnvironment, changeEnvironmentName }) => {
|
|
const classes = classnames({
|
|
'env-modal__sidebar-item': true,
|
|
'env-modal__sidebar-item--active': activeEnvironment === environment,
|
|
});
|
|
|
|
return (
|
|
<li key={environment._id} className={classes}>
|
|
<Button onClick={showEnvironment} value={environment}>
|
|
<i className="fa fa-drag-handle drag-handle" />
|
|
{environment.color ? (
|
|
<i className="space-right fa fa-circle" style={{ color: environment.color }} />
|
|
) : (
|
|
<i className="space-right fa fa-empty" />
|
|
)}
|
|
|
|
{environment.isPrivate && (
|
|
<Tooltip position="top" message="Environment will not be exported or synced">
|
|
<i className="fa fa-eye-slash faint space-right" />
|
|
</Tooltip>
|
|
)}
|
|
|
|
<Editable
|
|
className="inline-block"
|
|
onSubmit={name => changeEnvironmentName(environment, name)}
|
|
value={environment.name}
|
|
/>
|
|
</Button>
|
|
</li>
|
|
);
|
|
},
|
|
);
|
|
|
|
const SidebarList = SortableContainer(
|
|
({ environments, activeEnvironment, showEnvironment, changeEnvironmentName }) => (
|
|
<ul>
|
|
{environments.map((e, i) => (
|
|
<SidebarListItem
|
|
key={e._id}
|
|
environment={e}
|
|
index={i}
|
|
activeEnvironment={activeEnvironment}
|
|
showEnvironment={showEnvironment}
|
|
changeEnvironmentName={changeEnvironmentName}
|
|
/>
|
|
))}
|
|
</ul>
|
|
),
|
|
);
|
|
|
|
@autobind
|
|
class WorkspaceEnvironmentsEditModal extends React.PureComponent<Props, State> {
|
|
environmentEditorRef: ?EnvironmentEditor;
|
|
colorChangeTimeout: any;
|
|
saveTimeout: any;
|
|
modal: Modal;
|
|
editorKey: number;
|
|
|
|
constructor(props: Props) {
|
|
super(props);
|
|
this.state = {
|
|
workspace: null,
|
|
isValid: true,
|
|
subEnvironments: [],
|
|
rootEnvironment: null,
|
|
selectedEnvironmentId: null,
|
|
};
|
|
|
|
this.colorChangeTimeout = null;
|
|
this.editorKey = 0;
|
|
}
|
|
|
|
hide() {
|
|
this.modal && this.modal.hide();
|
|
}
|
|
|
|
_setEditorRef(n: ?EnvironmentEditor) {
|
|
this.environmentEditorRef = n;
|
|
}
|
|
|
|
_setModalRef(n: Modal | null) {
|
|
this.modal = n;
|
|
}
|
|
|
|
async show(workspace: Workspace) {
|
|
const { activeEnvironmentId } = this.props;
|
|
|
|
// Default to showing the currently active environment
|
|
if (activeEnvironmentId) {
|
|
this.setState({ selectedEnvironmentId: activeEnvironmentId });
|
|
}
|
|
|
|
await this._load(workspace);
|
|
|
|
this.modal && this.modal.show();
|
|
}
|
|
|
|
async _load(workspace: Workspace | null, environmentToSelect: Environment | null = null) {
|
|
if (!workspace) {
|
|
console.warn('Failed to reload environment editor without Workspace');
|
|
return;
|
|
}
|
|
|
|
const rootEnvironment = await models.environment.getOrCreateForWorkspace(workspace);
|
|
const subEnvironments = await models.environment.findByParentId(rootEnvironment._id);
|
|
|
|
let selectedEnvironmentId;
|
|
|
|
if (environmentToSelect) {
|
|
selectedEnvironmentId = environmentToSelect._id;
|
|
} else if (this.state.workspace && workspace._id !== this.state.workspace._id) {
|
|
// We've changed workspaces, so load the root one
|
|
selectedEnvironmentId = rootEnvironment._id;
|
|
} else {
|
|
// We haven't changed workspaces, so try loading the last environment, and fall back
|
|
// to the root one
|
|
selectedEnvironmentId = this.state.selectedEnvironmentId || rootEnvironment._id;
|
|
}
|
|
|
|
this.setState({
|
|
workspace,
|
|
rootEnvironment,
|
|
subEnvironments,
|
|
selectedEnvironmentId,
|
|
});
|
|
}
|
|
|
|
async _handleAddEnvironment(isPrivate: boolean = false) {
|
|
const { rootEnvironment, workspace } = this.state;
|
|
|
|
if (!rootEnvironment) {
|
|
console.warn('Failed to add environment. Unknown root environment');
|
|
return;
|
|
}
|
|
|
|
const parentId = rootEnvironment._id;
|
|
const environment = await models.environment.create({
|
|
parentId,
|
|
isPrivate,
|
|
});
|
|
await this._load(workspace, environment);
|
|
}
|
|
|
|
async _handleShowEnvironment(environment: Environment) {
|
|
// Don't allow switching if the current one has errors
|
|
if (this.environmentEditorRef && !this.environmentEditorRef.isValid()) {
|
|
return;
|
|
}
|
|
|
|
if (environment === this._getActiveEnvironment()) {
|
|
return;
|
|
}
|
|
|
|
const { workspace } = this.state;
|
|
|
|
await this._load(workspace, environment);
|
|
}
|
|
|
|
async _handleDeleteEnvironment(environment: Environment) {
|
|
const { rootEnvironment, workspace } = this.state;
|
|
|
|
// Don't delete the root environment
|
|
if (environment === rootEnvironment) {
|
|
return;
|
|
}
|
|
|
|
// Delete the current one, then activate the root environment
|
|
await models.environment.remove(environment);
|
|
await this._load(workspace, rootEnvironment);
|
|
}
|
|
|
|
async _handleChangeEnvironmentName(environment: Environment, name: string) {
|
|
const { workspace } = this.state;
|
|
|
|
// NOTE: Fetch the environment first because it might not be up to date.
|
|
// For example, editing the body updates silently.
|
|
const realEnvironment = await models.environment.getById(environment._id);
|
|
|
|
if (!realEnvironment) {
|
|
return;
|
|
}
|
|
|
|
await models.environment.update(realEnvironment, { name });
|
|
await this._load(workspace);
|
|
}
|
|
|
|
_handleChangeEnvironmentColor(environment: Environment, color: string | null) {
|
|
clearTimeout(this.colorChangeTimeout);
|
|
this.colorChangeTimeout = setTimeout(async () => {
|
|
const { workspace } = this.state;
|
|
await models.environment.update(environment, { color });
|
|
await this._load(workspace);
|
|
}, DEBOUNCE_MILLIS);
|
|
}
|
|
|
|
_didChange() {
|
|
this._saveChanges();
|
|
|
|
// Call this last in case component unmounted
|
|
const isValid = this.environmentEditorRef ? this.environmentEditorRef.isValid() : false;
|
|
if (this.state.isValid !== isValid) {
|
|
this.setState({ isValid });
|
|
}
|
|
}
|
|
|
|
_getActiveEnvironment(): Environment | null {
|
|
const { selectedEnvironmentId, subEnvironments, rootEnvironment } = this.state;
|
|
if (rootEnvironment && rootEnvironment._id === selectedEnvironmentId) {
|
|
return rootEnvironment;
|
|
} else {
|
|
return subEnvironments.find(e => e._id === selectedEnvironmentId) || null;
|
|
}
|
|
}
|
|
|
|
_handleUnsetColor(environment: Environment) {
|
|
this._handleChangeEnvironmentColor(environment, null);
|
|
}
|
|
|
|
componentDidMount() {
|
|
db.onChange(async changes => {
|
|
const { selectedEnvironmentId } = this.state;
|
|
|
|
for (const change of changes) {
|
|
const [
|
|
_, // eslint-disable-line no-unused-vars
|
|
doc,
|
|
fromSync,
|
|
] = change;
|
|
|
|
// Force an editor refresh if any changes from sync come in
|
|
if (doc._id === selectedEnvironmentId && fromSync) {
|
|
this.editorKey = doc.modified;
|
|
await this._load(this.state.workspace);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
async _handleSortEnd(results: {
|
|
oldIndex: number,
|
|
newIndex: number,
|
|
collection: Array<Environment>,
|
|
}) {
|
|
const { oldIndex, newIndex } = results;
|
|
if (newIndex === oldIndex) {
|
|
return;
|
|
}
|
|
|
|
const { subEnvironments } = this.state;
|
|
|
|
const newSubEnvironments = arrayMove(subEnvironments, oldIndex, newIndex);
|
|
this.setState({ subEnvironments: newSubEnvironments });
|
|
|
|
// Do this last so we don't block the sorting
|
|
db.bufferChanges();
|
|
for (let i = 0; i < newSubEnvironments.length; i++) {
|
|
const environment = newSubEnvironments[i];
|
|
await models.environment.update(environment, { metaSortKey: i });
|
|
}
|
|
db.flushChanges();
|
|
}
|
|
|
|
async _handleClickColorChange(environment: Environment) {
|
|
let el = document.querySelector('#env-color-picker');
|
|
|
|
// Remove existing child so we reset the event handlers. This
|
|
// was easier than trying to clean them up later.
|
|
if (el && el.parentNode) {
|
|
el.parentNode.removeChild(el);
|
|
}
|
|
|
|
el = document.createElement('input');
|
|
el.id = 'env-color-picker';
|
|
el.type = 'color';
|
|
document.body && document.body.appendChild(el);
|
|
|
|
let color = environment.color || '#7d69cb';
|
|
|
|
if (!environment.color) {
|
|
await this._handleChangeEnvironmentColor(environment, color);
|
|
}
|
|
|
|
el.setAttribute('value', color);
|
|
el.addEventListener('input', (e: Event) => {
|
|
if (e.target instanceof HTMLInputElement) {
|
|
this._handleChangeEnvironmentColor(environment, e.target && e.target.value);
|
|
}
|
|
});
|
|
|
|
el.click();
|
|
}
|
|
|
|
_saveChanges() {
|
|
// Only save if it's valid
|
|
if (!this.environmentEditorRef || !this.environmentEditorRef.isValid()) {
|
|
return;
|
|
}
|
|
|
|
let data;
|
|
try {
|
|
data = this.environmentEditorRef.getValue();
|
|
} catch (err) {
|
|
// Invalid JSON probably
|
|
return;
|
|
}
|
|
|
|
const activeEnvironment = this._getActiveEnvironment();
|
|
|
|
if (activeEnvironment) {
|
|
clearTimeout(this.saveTimeout);
|
|
this.saveTimeout = setTimeout(() => {
|
|
models.environment.update(activeEnvironment, { data });
|
|
}, DEBOUNCE_MILLIS * 4);
|
|
}
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
editorFontSize,
|
|
editorIndentSize,
|
|
editorKeyMap,
|
|
lineWrapping,
|
|
render,
|
|
getRenderContext,
|
|
nunjucksPowerUserMode,
|
|
isVariableUncovered,
|
|
} = this.props;
|
|
|
|
const { subEnvironments, rootEnvironment, isValid } = this.state;
|
|
|
|
const activeEnvironment = this._getActiveEnvironment();
|
|
|
|
return (
|
|
<Modal ref={this._setModalRef} wide tall {...this.props}>
|
|
<ModalHeader>Manage Environments</ModalHeader>
|
|
<ModalBody noScroll className="env-modal">
|
|
<div className="env-modal__sidebar">
|
|
<li
|
|
className={classnames('env-modal__sidebar-root-item', {
|
|
'env-modal__sidebar-item--active': activeEnvironment === rootEnvironment,
|
|
})}>
|
|
<Button onClick={this._handleShowEnvironment} value={rootEnvironment}>
|
|
{ROOT_ENVIRONMENT_NAME}
|
|
<HelpTooltip className="space-left">
|
|
The variables in this environment are always available, regardless of which
|
|
sub-environment is active. Useful for storing default or fallback values.
|
|
</HelpTooltip>
|
|
</Button>
|
|
</li>
|
|
<div className="pad env-modal__sidebar-heading">
|
|
<h3 className="no-margin">Sub Environments</h3>
|
|
<Dropdown right>
|
|
<DropdownButton>
|
|
<i className="fa fa-plus-circle" />
|
|
<i className="fa fa-caret-down" />
|
|
</DropdownButton>
|
|
<DropdownItem onClick={this._handleAddEnvironment} value={false}>
|
|
<i className="fa fa-eye" /> Environment
|
|
</DropdownItem>
|
|
<DropdownItem
|
|
onClick={this._handleAddEnvironment}
|
|
value={true}
|
|
title="Environment will not be exported or synced">
|
|
<i className="fa fa-eye-slash" /> Private Environment
|
|
</DropdownItem>
|
|
</Dropdown>
|
|
</div>
|
|
<SidebarList
|
|
environments={subEnvironments}
|
|
activeEnvironment={activeEnvironment}
|
|
showEnvironment={this._handleShowEnvironment}
|
|
changeEnvironmentName={this._handleChangeEnvironmentName}
|
|
onSortEnd={this._handleSortEnd}
|
|
helperClass="env-modal__sidebar-item--dragging"
|
|
transitionDuration={0}
|
|
useWindowAsScrollContainer={false}
|
|
/>
|
|
</div>
|
|
<div className="env-modal__main">
|
|
<div className="env-modal__main__header">
|
|
<h1>
|
|
{rootEnvironment === activeEnvironment ? (
|
|
ROOT_ENVIRONMENT_NAME
|
|
) : (
|
|
<Editable
|
|
singleClick
|
|
className="wide"
|
|
onSubmit={name =>
|
|
activeEnvironment &&
|
|
this._handleChangeEnvironmentName(activeEnvironment, name)
|
|
}
|
|
value={activeEnvironment ? activeEnvironment.name : ''}
|
|
/>
|
|
)}
|
|
</h1>
|
|
|
|
{activeEnvironment && rootEnvironment !== activeEnvironment ? (
|
|
<Dropdown className="space-right" right>
|
|
<DropdownButton className="btn btn--clicky">
|
|
{activeEnvironment.color && (
|
|
<i
|
|
className="fa fa-circle space-right"
|
|
style={{ color: activeEnvironment.color }}
|
|
/>
|
|
)}
|
|
Color <i className="fa fa-caret-down" />
|
|
</DropdownButton>
|
|
|
|
<DropdownItem value={activeEnvironment} onClick={this._handleClickColorChange}>
|
|
<i className="fa fa-circle" style={{ color: activeEnvironment.color }} />
|
|
{activeEnvironment.color ? 'Change Color' : 'Assign Color'}
|
|
</DropdownItem>
|
|
|
|
<DropdownItem
|
|
value={activeEnvironment}
|
|
onClick={this._handleUnsetColor}
|
|
disabled={!activeEnvironment.color}>
|
|
<i className="fa fa-minus-circle" />
|
|
Unset Color
|
|
</DropdownItem>
|
|
</Dropdown>
|
|
) : null}
|
|
|
|
{activeEnvironment && rootEnvironment !== activeEnvironment ? (
|
|
<PromptButton
|
|
value={activeEnvironment}
|
|
onClick={this._handleDeleteEnvironment}
|
|
className="btn btn--clicky">
|
|
<i className="fa fa-trash-o" />
|
|
</PromptButton>
|
|
) : null}
|
|
</div>
|
|
<div className="env-modal__editor">
|
|
<EnvironmentEditor
|
|
editorFontSize={editorFontSize}
|
|
editorIndentSize={editorIndentSize}
|
|
editorKeyMap={editorKeyMap}
|
|
lineWrapping={lineWrapping}
|
|
ref={this._setEditorRef}
|
|
key={`${this.editorKey}::${activeEnvironment ? activeEnvironment._id : 'n/a'}`}
|
|
environment={activeEnvironment ? activeEnvironment.data : {}}
|
|
didChange={this._didChange}
|
|
render={render}
|
|
getRenderContext={getRenderContext}
|
|
nunjucksPowerUserMode={nunjucksPowerUserMode}
|
|
isVariableUncovered={isVariableUncovered}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</ModalBody>
|
|
<ModalFooter>
|
|
<div className="margin-left italic txt-sm tall">
|
|
* Environment data can be used for
|
|
<Link href="https://support.insomnia.rest/article/40-template-tags">
|
|
Nunjucks Templating
|
|
</Link>{' '}
|
|
in your requests
|
|
</div>
|
|
<button className="btn" disabled={!isValid} onClick={this.hide}>
|
|
Done
|
|
</button>
|
|
</ModalFooter>
|
|
</Modal>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default WorkspaceEnvironmentsEditModal;
|