@lyra/vision
Version:
React-based data management tool for Lyra projects
494 lines (416 loc) • 16 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _propTypes = require('prop-types');
var _propTypes2 = _interopRequireDefault(_propTypes);
var _queryString = require('query-string');
var _queryString2 = _interopRequireDefault(_queryString);
var _localState = require('../util/localState');
var _parseApiQueryString = require('../util/parseApiQueryString');
var _parseApiQueryString2 = _interopRequireDefault(_parseApiQueryString);
var _tryParseParams = require('../util/tryParseParams');
var _tryParseParams2 = _interopRequireDefault(_tryParseParams);
var _DelayedSpinner = require('./DelayedSpinner');
var _DelayedSpinner2 = _interopRequireDefault(_DelayedSpinner);
var _QueryEditor = require('./QueryEditor');
var _QueryEditor2 = _interopRequireDefault(_QueryEditor);
var _ParamsEditor = require('./ParamsEditor');
var _ParamsEditor2 = _interopRequireDefault(_ParamsEditor);
var _ResultView = require('./ResultView');
var _ResultView2 = _interopRequireDefault(_ResultView);
var _NoResultsDialog = require('./NoResultsDialog');
var _NoResultsDialog2 = _interopRequireDefault(_NoResultsDialog);
var _QueryErrorDialog = require('./QueryErrorDialog');
var _QueryErrorDialog2 = _interopRequireDefault(_QueryErrorDialog);
var _reactSplitPane = require('react-split-pane');
var _reactSplitPane2 = _interopRequireDefault(_reactSplitPane);
var _encodeQueryString = require('../util/encodeQueryString');
var _encodeQueryString2 = _interopRequireDefault(_encodeQueryString);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const lyraUrl = /\.api\.lyra\.io.*?(?:query|listen)\/(.*?)\?(.*)/;
const handleCopyUrl = () => {
const emailLink = document.querySelector('#vision-query-url');
emailLink.select();
try {
document.execCommand('copy');
} catch (err) {
// eslint-disable-next-line no-console
console.error('Unable to copy to clipboard :(');
}
};
class VisionGui extends _react2.default.PureComponent {
constructor(props) {
super(props);
const lastQuery = (0, _localState.getState)('lastQuery');
const lastParams = (0, _localState.getState)('lastParams');
const firstDataset = this.props.datasets[0] && this.props.datasets[0].name;
let dataset = (0, _localState.getState)('dataset', firstDataset);
if (!this.props.datasets.includes(dataset)) {
dataset = firstDataset;
}
this.subscribers = {};
this.state = {
query: lastQuery,
params: lastParams && (0, _tryParseParams2.default)(lastParams),
rawParams: lastParams,
queryInProgress: false,
editorHeight: 100,
dataset
};
this.handleChangeDataset = this.handleChangeDataset.bind(this);
this.handleListenExecution = this.handleListenExecution.bind(this);
this.handleListenerMutation = this.handleListenerMutation.bind(this);
this.handleQueryExecution = this.handleQueryExecution.bind(this);
this.handleQueryChange = this.handleQueryChange.bind(this);
this.handleParamsChange = this.handleParamsChange.bind(this);
this.handleHeightChange = this.handleHeightChange.bind(this);
this.handlePaste = this.handlePaste.bind(this);
}
componentDidMount() {
this.context.client.config({ dataset: this.state.dataset });
window.document.addEventListener('paste', this.handlePaste);
}
componentWillUnmount() {
this.cancelQuery();
this.cancelListener();
}
handlePaste(evt) {
const data = evt.clipboardData.getData('text/plain');
const match = data.match(lyraUrl);
if (!match) {
return;
}
var _match = _slicedToArray(match, 3);
const dataset = _match[1],
urlQuery = _match[2];
const qs = _queryString2.default.parse(urlQuery);
let parts;
try {
parts = (0, _parseApiQueryString2.default)(qs);
} catch (err) {
console.warn('Error while trying to parse API URL: ', err.message); // eslint-disable-line no-console
return; // Give up on error
}
if (this.context.client.config().dataset !== dataset) {
this.handleChangeDataset({ target: { value: dataset } });
}
evt.preventDefault();
this.setState({
query: parts.query,
params: parts.params,
rawParams: JSON.stringify(parts.params, null, 2)
});
}
cancelQuery() {
if (!this.subscribers.query) {
return;
}
this.subscribers.query.unsubscribe();
this.subscribers.query = null;
}
cancelListener() {
if (!this.subscribers.listen) {
return;
}
this.subscribers.listen.unsubscribe();
this.subscribers.listen = null;
}
handleChangeDataset(evt) {
const dataset = evt.target.value;
(0, _localState.storeState)('dataset', dataset);
this.setState({ dataset });
this.context.client.config({ dataset });
this.handleQueryExecution();
}
handleListenerMutation(mut) {
const listenMutations = [mut].concat(this.state.listenMutations);
if (listenMutations.length > 50) {
listenMutations.pop();
}
this.setState({ listenMutations });
}
handleListenExecution() {
var _state = this.state;
const query = _state.query,
params = _state.params,
rawParams = _state.rawParams,
listenInProgress = _state.listenInProgress;
if (listenInProgress) {
this.cancelListener();
this.setState({ listenInProgress: false });
return;
}
const client = this.context.client;
const paramsError = params instanceof Error && params;
const url = client.getUrl(client.getDataUrl('listen', (0, _encodeQueryString2.default)(query, params)));
(0, _localState.storeState)('lastQuery', query);
(0, _localState.storeState)('lastParams', rawParams);
this.cancelQuery();
this.setState({
url,
listenMutations: [],
queryInProgress: false,
listenInProgress: !paramsError && Boolean(query),
error: paramsError || undefined,
result: undefined,
queryTime: null,
e2eTime: null
});
if (!query || paramsError) {
return;
}
this.subscribers.listen = client.listen(query, params, {}).subscribe({
next: this.handleListenerMutation,
error: _error => this.setState({
error: _error,
query,
listenInProgress: false
})
});
}
handleQueryExecution() {
var _state2 = this.state;
const query = _state2.query,
params = _state2.params,
rawParams = _state2.rawParams;
const client = this.context.client.observable;
const paramsError = params instanceof Error && params;
(0, _localState.storeState)('lastQuery', query);
(0, _localState.storeState)('lastParams', rawParams);
this.cancelListener();
this.setState({
queryInProgress: !paramsError && Boolean(query),
listenInProgress: false,
listenMutations: [],
error: paramsError || undefined,
result: undefined,
queryTime: null,
e2eTime: null
});
if (!query || paramsError) {
return;
}
const url = client.getUrl(client.getDataUrl('query', (0, _encodeQueryString2.default)(query, params)));
const queryStart = Date.now();
this.subscribers.query = client.fetch(query, params, { filterResponse: false }).subscribe({
next: res => this.setState({
query,
url,
queryTime: res.ms,
e2eTime: Date.now() - queryStart,
result: res.result,
queryInProgress: false,
error: null
}),
error: _error2 => this.setState({
error: _error2,
query,
queryInProgress: false
})
});
}
handleQueryChange(data) {
this.setState({ query: data.query });
}
handleParamsChange(data) {
this.setState({ rawParams: data.raw, params: data.parsed });
}
handleHeightChange(newHeight) {
if (this.state.editorHeight !== newHeight) {
this.setState({ editorHeight: Math.max(newHeight, 75) });
}
}
render() {
var _context = this.context;
const client = _context.client,
components = _context.components;
var _state3 = this.state;
const error = _state3.error,
result = _state3.result,
url = _state3.url,
query = _state3.query,
queryInProgress = _state3.queryInProgress,
listenInProgress = _state3.listenInProgress,
queryTime = _state3.queryTime,
e2eTime = _state3.e2eTime,
listenMutations = _state3.listenMutations;
const Button = components.Button,
Select = components.Select;
const styles = this.context.styles.visionGui;
const dataset = client.config().dataset;
const datasets = this.props.datasets.map(set => set.name);
const hasResult = !error && !queryInProgress && typeof result !== 'undefined';
// Note that because of react-json-inspector, we need at least one
// addressable, non-generated class name. Therefore;
// leave `lyra-vision` untouched!
const visionClass = ['lyra-vision', this.context.styles.visionGui.root].filter(Boolean).join(' ');
const headerClass = ['lyra-vision', this.context.styles.visionGui.header].filter(Boolean).join(' ');
return _react2.default.createElement(
'div',
{ className: visionClass },
_react2.default.createElement(
'div',
{ className: headerClass },
_react2.default.createElement(
'div',
{ className: styles.headerLeft },
_react2.default.createElement(
'label',
{ className: styles.datasetSelectorContainer },
_react2.default.createElement(
'span',
{ className: styles.datasetLabel },
'Dataset'
),
_react2.default.createElement(Select, {
value: this.state.dataset || client.config().dataset,
values: datasets,
onChange: this.handleChangeDataset
})
)
),
_react2.default.createElement(
'div',
{ className: styles.queryUrlContainer },
typeof url === 'string' && _react2.default.createElement(
'span',
null,
'Query URL:',
_react2.default.createElement('input', {
className: styles.queryUrl,
readOnly: true,
id: 'vision-query-url',
value: url
}),
_react2.default.createElement(
'button',
{ onClick: handleCopyUrl },
'Copy'
)
)
),
_react2.default.createElement(
'div',
{ className: styles.queryTimingContainer },
typeof queryTime === 'number' && _react2.default.createElement(
'p',
{
className: queryTime > 0.5 ? styles.queryTiming || 'queryTiming' : styles.queryTimingLong || 'queryTiming'
},
'Query time: ',
queryTime,
'ms (end-to-end: ',
e2eTime,
'ms)'
)
),
_react2.default.createElement(
'div',
{ className: styles.headerFunctions },
_react2.default.createElement(
Button,
{
onClick: this.handleListenExecution,
loading: listenInProgress
},
'Listen'
),
_react2.default.createElement(
Button,
{
onClick: this.handleQueryExecution,
loading: queryInProgress,
color: 'primary'
},
'Run query'
)
)
),
_react2.default.createElement(
'div',
{ className: styles.splitContainer },
_react2.default.createElement(
_reactSplitPane2.default,
{ split: 'vertical', minSize: 150, defaultSize: 400 },
_react2.default.createElement(
'div',
{ className: styles.edit },
_react2.default.createElement(
_reactSplitPane2.default,
{ split: 'horizontal', defaultSize: '80%' },
_react2.default.createElement(
'div',
{ className: styles.inputContainer },
_react2.default.createElement(
'h3',
{ className: styles.inputLabelQuery || 'query' },
'Query'
),
_react2.default.createElement(_QueryEditor2.default, {
className: styles.queryEditor,
value: this.state.query,
onExecute: this.handleQueryExecution,
onChange: this.handleQueryChange,
onHeightChange: this.handleHeightChange,
style: { minHeight: this.state.editorHeight },
schema: this.props.schema
})
),
_react2.default.createElement(
'div',
{ className: styles.inputContainer },
_react2.default.createElement(
'h3',
{ className: styles.inputLabelQuery || 'query' },
'Params'
),
_react2.default.createElement(_ParamsEditor2.default, {
className: styles.paramsEditor,
classNameInvalid: styles.paramsEditorInvalid,
value: this.state.rawParams,
onExecute: this.handleQueryExecution,
onChange: this.handleParamsChange,
onHeightChange: this.handleHeightChange,
style: { minHeight: this.state.editorHeight }
})
)
)
),
_react2.default.createElement(
'div',
{ className: styles.resultContainer },
_react2.default.createElement(
'h3',
{ className: styles.inputLabelQuery || 'resultLabel' },
'Result'
),
_react2.default.createElement(
'div',
{ className: styles.result },
queryInProgress && _react2.default.createElement(_DelayedSpinner2.default, null),
error && _react2.default.createElement(_QueryErrorDialog2.default, { error: error }),
hasResult && _react2.default.createElement(_ResultView2.default, { data: result, query: query }),
Array.isArray(result) && result.length === 0 && _react2.default.createElement(_NoResultsDialog2.default, { query: query, dataset: dataset }),
listenMutations && listenMutations.length > 0 && _react2.default.createElement(_ResultView2.default, { data: listenMutations })
)
)
)
)
);
}
}
VisionGui.propTypes = {
datasets: _propTypes2.default.arrayOf(_propTypes2.default.shape({
name: _propTypes2.default.string
}))
};
VisionGui.contextTypes = {
client: _propTypes2.default.shape({ fetch: _propTypes2.default.func }).isRequired,
styles: _propTypes2.default.object,
components: _propTypes2.default.object
};
exports.default = VisionGui;