insomnia/packages/insomnia-app/app/ui/components/modals/workspace-settings-modal.js
Gregory Schier 0a616fba6b
Version Control (beta) (#1439)
* VCS proof of concept underway!

* Stuff

* Some things

* Replace deprecated Electron makeSingleInstance

* Rename `window` variables so not to be confused with window object

* Don't unnecessarily update request when URL does not change

* Regenerate package-lock

* Fix tests + ESLint

* Publish

 - insomnia-app@1.0.49
 - insomnia-cookies@0.0.12
 - insomnia-httpsnippet@1.16.18
 - insomnia-importers@2.0.13
 - insomnia-libcurl@0.0.23
 - insomnia-prettify@0.1.7
 - insomnia-url@0.1.6
 - insomnia-xpath@1.0.9
 - insomnia-plugin-base64@1.0.6
 - insomnia-plugin-cookie-jar@1.0.8
 - insomnia-plugin-core-themes@1.0.5
 - insomnia-plugin-default-headers@1.1.9
 - insomnia-plugin-file@1.0.7
 - insomnia-plugin-hash@1.0.7
 - insomnia-plugin-jsonpath@1.0.12
 - insomnia-plugin-now@1.0.11
 - insomnia-plugin-os@1.0.13
 - insomnia-plugin-prompt@1.1.9
 - insomnia-plugin-request@1.0.18
 - insomnia-plugin-response@1.0.16
 - insomnia-plugin-uuid@1.0.10

* Broken but w/e

* Some tweaks

* Big refactor. Create local snapshots and push done

* POC merging and a lot of improvements

* Lots of work done on initial UI/UX

* Fix old tests

* Atomic writes and size-based batches

* Update StageEntry definition once again to be better

* Factor out GraphQL query logic

* Merge algorithm, history modal, other minor things

* Fix test

* Merge, checkout, revert w/ user changes now work

* Force UI to refresh when switching branches changes active request

* Rough draft pull() and some cleanup

* E2EE stuff and some refactoring

* Add ability to share project with team and fixed tests

* VCS now created in root component and better remote project handling

* Remove unused definition

* Publish

 - insomnia-account@0.0.2
 - insomnia-app@1.1.1
 - insomnia-cookies@0.0.14
 - insomnia-httpsnippet@1.16.20
 - insomnia-importers@2.0.15
 - insomnia-libcurl@0.0.25
 - insomnia-prettify@0.1.9
 - insomnia-sync@0.0.2
 - insomnia-url@0.1.8
 - insomnia-xpath@1.0.11
 - insomnia-plugin-base64@1.0.8
 - insomnia-plugin-cookie-jar@1.0.10
 - insomnia-plugin-core-themes@1.0.7
 - insomnia-plugin-file@1.0.9
 - insomnia-plugin-hash@1.0.9
 - insomnia-plugin-jsonpath@1.0.14
 - insomnia-plugin-now@1.0.13
 - insomnia-plugin-os@1.0.15
 - insomnia-plugin-prompt@1.1.11
 - insomnia-plugin-request@1.0.20
 - insomnia-plugin-response@1.0.18
 - insomnia-plugin-uuid@1.0.12

* Move some deps around

* Fix Flow errors

* Update package.json

* Fix eslint errors

* Fix tests

* Update deps

* bootstrap insomnia-sync

* TRy fixing appveyor

* Try something else

* Bump lerna

* try powershell

*  Try again

* Fix imports

* Fixed errors

* sync types refactor

* Show remote projects in workspace dropdown

* Improved pulling of non-local workspaces

* Loading indicators and some tweaks

* Clean up sync staging modal

* Some sync improvements:

- No longer store stage
- Upgrade Electron
- Sync UI/UX improvements

* Fix snyc tests

* Upgraded deps and hot loader tweaks (it's broken for some reason)

* Fix tests

* Branches dialog, network refactoring, some tweaks

* Fixed merging when other branch is empty

* A bunch of small fixes from real testing

* Fixed pull merge logic

* Fix tests

* Some bug fixes

* A few small tweaks

* Conflict resolution and other improvements

* Fix tests

* Add revert changes

* Deal with duplicate projects per workspace

* Some tweaks and accessibility improvements

* Tooltip accessibility

* Fix API endpoint

* Fix tests

* Remove jest dep from insomnia-importers
2019-04-17 17:50:03 -07:00

498 lines
16 KiB
JavaScript

// @flow
import * as React from 'react';
import autobind from 'autobind-decorator';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import DebouncedInput from '../base/debounced-input';
import FileInputButton from '../base/file-input-button';
import Modal from '../base/modal';
import ModalBody from '../base/modal-body';
import ModalHeader from '../base/modal-header';
import HelpTooltip from '../help-tooltip';
import PromptButton from '../base/prompt-button';
import * as models from '../../../models/index';
import MarkdownEditor from '../markdown-editor';
import type { Workspace } from '../../../models/workspace';
import type { ClientCertificate } from '../../../models/client-certificate';
type Props = {
clientCertificates: Array<ClientCertificate>,
workspace: Workspace,
editorFontSize: number,
editorIndentSize: number,
editorKeyMap: string,
editorLineWrapping: boolean,
nunjucksPowerUserMode: boolean,
isVariableUncovered: boolean,
handleRender: Function,
handleGetRenderContext: Function,
handleRemoveWorkspace: Function,
handleDuplicateWorkspace: Function,
handleClearAllResponses: Function,
};
type State = {
showAddCertificateForm: boolean,
host: string,
crtPath: string,
keyPath: string,
pfxPath: string,
isPrivate: boolean,
passphrase: string,
showDescription: boolean,
defaultPreviewMode: boolean,
};
@autobind
class WorkspaceSettingsModal extends React.PureComponent<Props, State> {
modal: Modal | null;
constructor(props: Props) {
super(props);
this.state = {
showAddCertificateForm: false,
host: '',
crtPath: '',
keyPath: '',
pfxPath: '',
passphrase: '',
isPrivate: false,
showDescription: false,
defaultPreviewMode: false,
};
}
_workspaceUpdate(patch: Object) {
models.workspace.update(this.props.workspace, patch);
}
_handleAddDescription() {
this.setState({ showDescription: true });
}
_handleSetModalRef(n: ?Modal) {
this.modal = n;
}
_handleRemoveWorkspace() {
this.props.handleRemoveWorkspace();
this.hide();
}
_handleClearAllResponses() {
this.props.handleClearAllResponses();
this.hide();
}
_handleDuplicateWorkspace() {
this.props.handleDuplicateWorkspace(() => {
this.hide();
});
}
_handleToggleCertificateForm() {
this.setState(state => ({
showAddCertificateForm: !state.showAddCertificateForm,
crtPath: '',
keyPath: '',
pfxPath: '',
host: '',
passphrase: '',
isPrivate: false,
}));
}
_handleRename(name: string) {
this._workspaceUpdate({ name });
}
_handleDescriptionChange(description: string) {
this._workspaceUpdate({ description });
if (this.state.defaultPreviewMode !== false) {
this.setState({ defaultPreviewMode: false });
}
}
_handleCreateHostChange(e: SyntheticEvent<HTMLInputElement>) {
this.setState({ host: e.currentTarget.value });
}
_handleCreatePfxChange(pfxPath: string) {
this.setState({ pfxPath });
}
_handleCreateCrtChange(crtPath: string) {
this.setState({ crtPath });
}
_handleCreateKeyChange(keyPath: string) {
this.setState({ keyPath });
}
_handleCreatePassphraseChange(e: SyntheticEvent<HTMLInputElement>) {
this.setState({ passphrase: e.currentTarget.value });
}
_handleCreateIsPrivateChange(e: SyntheticEvent<HTMLInputElement>) {
this.setState({ isPrivate: e.currentTarget.checked });
}
async _handleCreateCertificate(e: SyntheticEvent<HTMLFormElement>) {
e.preventDefault();
const { workspace } = this.props;
const { pfxPath, crtPath, keyPath, host, passphrase, isPrivate } = this.state;
const certificate = {
host,
isPrivate,
parentId: workspace._id,
passphrase: passphrase || null,
disabled: false,
cert: crtPath || null,
key: keyPath || null,
pfx: pfxPath || null,
};
await models.clientCertificate.create(certificate);
this._handleToggleCertificateForm();
}
static async _handleDeleteCertificate(certificate: ClientCertificate) {
await models.clientCertificate.remove(certificate);
}
static async _handleToggleCertificate(certificate: ClientCertificate) {
await models.clientCertificate.update(certificate, {
disabled: !certificate.disabled,
});
}
show() {
const hasDescription = !!this.props.workspace.description;
this.setState({
showDescription: hasDescription,
defaultPreviewMode: hasDescription,
showAddCertificateForm: false,
});
this.modal && this.modal.show();
}
hide() {
this.modal && this.modal.hide();
}
renderModalHeader() {
const { workspace } = this.props;
return (
<ModalHeader key={`header::${workspace._id}`}>
Workspace Settings{' '}
<div className="txt-sm selectable faint monospace">{workspace ? workspace._id : ''}</div>
</ModalHeader>
);
}
renderCertificate(certificate: ClientCertificate) {
return (
<div key={certificate._id}>
<div className="row-spaced">
<div>
<span className="pad-right no-wrap">
<strong>PFX:</strong>{' '}
{certificate.pfx ? <i className="fa fa-check" /> : <i className="fa fa-remove" />}
</span>
<span className="pad-right no-wrap">
<strong>CRT:</strong>{' '}
{certificate.cert ? <i className="fa fa-check" /> : <i className="fa fa-remove" />}
</span>
<span className="pad-right no-wrap">
<strong>Key:</strong>{' '}
{certificate.key ? <i className="fa fa-check" /> : <i className="fa fa-remove" />}
</span>
<span className="pad-right no-wrap" title={certificate.passphrase || null}>
<strong>Passphrase:</strong>{' '}
{certificate.passphrase ? (
<i className="fa fa-check" />
) : (
<i className="fa fa-remove" />
)}
</span>
<span className="pad-right">
<strong>Host:</strong>{' '}
<span className="monospace selectable">{certificate.host}</span>
</span>
</div>
<div className="no-wrap">
<button
className="btn btn--super-compact width-auto"
title="Enable or disable certificate"
onClick={() => WorkspaceSettingsModal._handleToggleCertificate(certificate)}>
{certificate.disabled ? (
<i className="fa fa-square-o" />
) : (
<i className="fa fa-check-square-o" />
)}
</button>
<PromptButton
className="btn btn--super-compact width-auto"
confirmMessage=""
addIcon
onClick={() => WorkspaceSettingsModal._handleDeleteCertificate(certificate)}>
<i className="fa fa-trash-o" />
</PromptButton>
</div>
</div>
</div>
);
}
renderModalBody() {
const {
clientCertificates,
workspace,
editorLineWrapping,
editorFontSize,
editorIndentSize,
editorKeyMap,
handleRender,
handleGetRenderContext,
nunjucksPowerUserMode,
isVariableUncovered,
} = this.props;
const publicCertificates = clientCertificates.filter(c => !c.isPrivate);
const privateCertificates = clientCertificates.filter(c => c.isPrivate);
const {
pfxPath,
crtPath,
keyPath,
isPrivate,
showAddCertificateForm,
showDescription,
defaultPreviewMode,
} = this.state;
return (
<ModalBody key={`body::${workspace._id}`} noScroll>
<Tabs forceRenderTabPanel className="react-tabs">
<TabList>
<Tab tabIndex="-1">
<button>Overview</button>
</Tab>
<Tab tabIndex="-1">
<button>Client Certificates</button>
</Tab>
</TabList>
<TabPanel className="react-tabs__tab-panel pad scrollable pad-top-sm">
<div className="form-control form-control--outlined">
<label>
Name
<DebouncedInput
type="text"
delay={500}
placeholder="Awesome API"
defaultValue={workspace.name}
onChange={this._handleRename}
/>
</label>
</div>
<div>
{showDescription ? (
<MarkdownEditor
className="margin-top"
defaultPreviewMode={defaultPreviewMode}
fontSize={editorFontSize}
indentSize={editorIndentSize}
keyMap={editorKeyMap}
placeholder="Write a description"
lineWrapping={editorLineWrapping}
handleRender={handleRender}
handleGetRenderContext={handleGetRenderContext}
nunjucksPowerUserMode={nunjucksPowerUserMode}
isVariableUncovered={isVariableUncovered}
defaultValue={workspace.description}
onChange={this._handleDescriptionChange}
/>
) : (
<button
onClick={this._handleAddDescription}
className="btn btn--outlined btn--super-duper-compact">
Add Description
</button>
)}
</div>
<h2>Workspace Actions</h2>
<div className="form-control form-control--padded">
<PromptButton
onClick={this._handleRemoveWorkspace}
addIcon
className="width-auto btn btn--clicky inline-block">
<i className="fa fa-trash-o" /> Delete
</PromptButton>
<button
onClick={this._handleDuplicateWorkspace}
className="width-auto btn btn--clicky inline-block space-left">
<i className="fa fa-copy" /> Duplicate
</button>
<PromptButton
onClick={this._handleClearAllResponses}
addIcon
className="width-auto btn btn--clicky inline-block space-left">
<i className="fa fa-trash-o" /> Clear All Responses
</PromptButton>
</div>
</TabPanel>
<TabPanel className="react-tabs__tab-panel pad scrollable">
{!showAddCertificateForm ? (
<div>
{clientCertificates.length === 0 ? (
<p className="notice surprise margin-top-sm">
You have not yet added any certificates
</p>
) : null}
{publicCertificates.length > 0
? publicCertificates.map(this.renderCertificate)
: null}
{privateCertificates.length > 0 ? (
<div>
<h2>
Private Certificates
<HelpTooltip position="right" className="space-left">
Private certificates will not by synced.
</HelpTooltip>
</h2>
{privateCertificates.map(this.renderCertificate)}
</div>
) : null}
<hr className="hr--spaced" />
<div className="text-center">
<button
className="btn btn--clicky auto"
onClick={this._handleToggleCertificateForm}>
New Certificate
</button>
</div>
</div>
) : (
<form onSubmit={this._handleCreateCertificate}>
<div className="form-control form-control--outlined no-pad-top">
<label>
Host
<HelpTooltip position="right" className="space-left">
The host for which this client certificate is valid. Port number is optional
and * can be used as a wildcard.
</HelpTooltip>
<input
type="text"
required="required"
placeholder="my-api.com"
autoFocus="autoFocus"
onChange={this._handleCreateHostChange}
/>
</label>
</div>
<div className="form-row">
<div className="form-control width-auto">
<label>
PFX <span className="faint">(or PKCS12)</span>
<FileInputButton
className="btn btn--clicky"
onChange={this._handleCreatePfxChange}
path={pfxPath}
showFileName
/>
</label>
</div>
<div className="text-center">
<br />
<br />
&nbsp;&nbsp;Or&nbsp;&nbsp;
</div>
<div className="row-fill">
<div className="form-control">
<label>
CRT File
<FileInputButton
className="btn btn--clicky"
name="Cert"
onChange={this._handleCreateCrtChange}
path={crtPath}
showFileName
/>
</label>
</div>
<div className="form-control">
<label>
Key File
<FileInputButton
className="btn btn--clicky"
name="Key"
onChange={this._handleCreateKeyChange}
path={keyPath}
showFileName
/>
</label>
</div>
</div>
</div>
<div className="form-control form-control--outlined">
<label>
Passphrase
<input
type="password"
placeholder="•••••••••••"
onChange={this._handleCreatePassphraseChange}
/>
</label>
</div>
<div className="form-control form-control--slim">
<label>
Private
<HelpTooltip className="space-left">
Private certificates will not be synced
</HelpTooltip>
<input
type="checkbox"
value={isPrivate}
onChange={this._handleCreateIsPrivateChange}
/>
</label>
</div>
<br />
<div className="pad-top text-right">
<button
type="button"
className="btn btn--super-compact space-right"
onClick={this._handleToggleCertificateForm}>
Cancel
</button>
<button className="btn btn--clicky space-right" type="submit">
Create Certificate
</button>
</div>
</form>
)}
</TabPanel>
</Tabs>
</ModalBody>
);
}
render() {
const { workspace } = this.props;
return (
<Modal ref={this._handleSetModalRef} freshState>
{workspace ? this.renderModalHeader() : null}
{workspace ? this.renderModalBody() : null}
</Modal>
);
}
}
export default WorkspaceSettingsModal;