sqlpad
Version:
Web app for writing and running SQL queries and visualizing the results. Supports Postgres, MySQL, SQL Server, Crate and Vertica.
473 lines (436 loc) • 13.9 kB
JavaScript
import React from 'react'
import Alert from 'react-s-alert'
import SplitPane from 'react-split-pane'
import { Prompt } from 'react-router-dom'
import fetchJson from '../utilities/fetch-json.js'
import uuid from 'uuid'
import keymaster from 'keymaster'
import QueryResultDataTable from '../common/QueryResultDataTable.js'
import SqlpadTauChart from '../common/SqlpadTauChart.js'
import QueryResultHeader from './QueryResultHeader.js'
import QueryDetailsModal from './QueryDetailsModal'
import EditorNavBar from './EditorNavBar'
import FlexTabPane from './FlexTabPane'
import SchemaSidebar from './SchemaSidebar.js'
import VisSidebar from './VisSidebar'
import SqlEditor from '../common/SqlEditor'
import './QueryEditor.css'
import sqlFormatter from 'sql-formatter'
const NEW_QUERY = {
_id: '',
name: '',
tags: [],
connectionId: '',
queryText: '',
chartConfiguration: {
chartType: '',
fields: {} // key value for chart
}
}
class QueryEditor extends React.Component {
state = {
activeTabKey: 'sql',
availableTags: [],
cacheKey: uuid.v1(),
connections: [],
isRunning: false,
isSaving: false,
unsavedChanges: false,
query: Object.assign({}, NEW_QUERY),
queryResult: undefined,
runQueryStartTime: undefined,
showModal: false,
showValidation: false
}
sqlpadTauChart = undefined
getTagOptions() {
const { availableTags, query } = this.state
const tagOptions = availableTags.map(t => {
return { value: t, label: t }
})
if (query && query.tags) {
query.tags.forEach(t => {
tagOptions.push({ value: t, label: t })
})
}
return tagOptions
}
loadConnectionsFromServer = () => {
const { query } = this.state
fetchJson('GET', '/api/connections/').then(json => {
const { error, connections } = json
if (error) {
Alert.error(error)
}
// if only 1 connection auto-select it
if (connections.length === 1 && query) {
query.connectionId = connections[0]._id
}
this.setState({ connections, query })
})
}
loadQueryFromServer = queryId => {
fetchJson('GET', `/api/queries/${queryId}`).then(json => {
const { error, query } = json
if (error) {
Alert.error(error)
}
this.setState({ query })
})
}
loadTagsFromServer = () => {
fetchJson('GET', '/api/tags').then(json => {
const { error, tags } = json
if (error) {
Alert.error(error)
}
this.setState({ availableTags: tags })
})
}
runQuery = () => {
const { cacheKey, query } = this.state
const selectedText = this.editor.session.getTextRange(
this.editor.getSelectionRange()
)
this.setState({
isRunning: true,
runQueryStartTime: new Date()
})
const postData = {
connectionId: query.connectionId,
cacheKey,
queryName: query.name,
queryText: selectedText || query.queryText
}
fetchJson('POST', '/api/query-result', postData).then(json => {
if (json.error) Alert.error(json.error)
this.setState({
isRunning: false,
queryError: json.error,
queryResult: json.queryResult
})
})
}
handleCloneClick = () => {
const { config } = this.props
const { query } = this.state
delete query._id
query.name = 'Copy of ' + query.name
window.history.replaceState({}, query.name, `${config.baseUrl}/queries/new`)
this.setState({ query, unsavedChanges: true })
}
formatQuery = () => {
const { query } = this.state
query.queryText = sqlFormatter.format(query.queryText)
this.setState({
query,
unsavedChanges: true
})
}
saveQuery = () => {
const { query } = this.state
const { config } = this.props
if (!query.name) {
Alert.error('Query name required')
this.setState({ showValidation: true })
return
}
this.setState({ isSaving: true })
if (query._id) {
fetchJson('PUT', `/api/queries/${query._id}`, query).then(json => {
const { error, query } = json
if (error) {
Alert.error(error)
this.setState({ isSaving: false })
return
}
Alert.success('Query Saved')
this.setState({ isSaving: false, unsavedChanges: false, query })
})
} else {
fetchJson('POST', `/api/queries`, query).then(json => {
const { error, query } = json
if (error) {
Alert.error(error)
this.setState({ isSaving: false })
return
}
window.history.replaceState(
{},
query.name,
`${config.baseUrl}/queries/${query._id}`
)
Alert.success('Query Saved')
this.setState({ isSaving: false, unsavedChanges: false, query })
})
}
}
setQueryState = (field, value) => {
const { query } = this.state
query[field] = value
this.setState({ query, unsavedChanges: true })
}
handleChartConfigurationFieldsChange = (chartFieldId, queryResultField) => {
const { query } = this.state
query.chartConfiguration.fields[chartFieldId] = queryResultField
this.setState({ query, unsavedChanges: true })
}
handleChartTypeChange = e => {
const { query } = this.state
query.chartConfiguration.chartType = e.target.value
this.setState({ query, unsavedChanges: true })
}
handleConnectionChange = connectionId => {
this.setQueryState('connectionId', connectionId)
}
handleModalHide = () => {
this.setState({ showModal: false })
}
handleQueryNameChange = name => this.setQueryState('name', name)
handleMoreClick = () => this.setState({ showModal: true })
handleQueryTagsChange = values =>
this.setQueryState('tags', values.map(v => v.value))
handleQueryTextChange = queryText =>
this.setQueryState('queryText', queryText)
handleSaveImageClick = e => {
if (this.sqlpadTauChart && this.sqlpadTauChart.chart) {
this.sqlpadTauChart.chart.fire('exportTo', 'png')
}
}
handleTabSelect = activeTabKey => this.setState({ activeTabKey })
handleVisualizeClick = () => this.sqlpadTauChart.renderChart(true)
hasRows = () => {
const queryResult = this.state.queryResult
return !!(queryResult && queryResult.rows && queryResult.rows.length)
}
isChartable = () => {
const { isRunning, queryError, activeTabKey } = this.state
const pending = isRunning || queryError
return !pending && activeTabKey === 'vis' && this.hasRows()
}
componentWillReceiveProps(nextProps) {
if (nextProps.queryId === 'new') {
return this.setState({
activeTabKey: 'sql',
queryResult: undefined,
query: Object.assign({}, NEW_QUERY),
unsavedChanges: false
})
}
this.loadQueryFromServer(nextProps.queryId)
}
componentDidMount() {
const { queryId } = this.props
this.loadConnectionsFromServer()
this.loadTagsFromServer()
if (queryId !== 'new') {
this.loadQueryFromServer(queryId)
}
/* Shortcuts
============================================================================== */
// keymaster doesn't fire on input/textarea events by default
// since we are only using command/ctrl shortcuts,
// we want the event to fire all the time for any element
keymaster.filter = () => true
keymaster.unbind('ctrl+s, command+s')
keymaster('ctrl+s, command+s', e => {
this.saveQuery()
e.preventDefault()
return false
})
// there should only ever be 1 QueryEditor on the page,
// but just in case there isn't unbind anything previously bound
// rather something previously not run than something run more than once
keymaster.unbind('ctrl+r, command+r, ctrl+e, command+e')
keymaster('ctrl+r, command+r, ctrl+e, command+e', e => {
Alert.info('Shortcut changed to ctrl+return / command+return')
e.preventDefault()
return false
})
keymaster.unbind('ctrl+return, command+return')
keymaster('ctrl+return, command+return', e => {
this.runQuery()
e.preventDefault()
return false
})
keymaster.unbind('alt+r')
keymaster('alt+r', e => {
Alert.info('Shortcut changed to shift+return')
e.preventDefault()
return false
})
keymaster.unbind('shift+return')
keymaster('shift+return', e => {
this.formatQuery()
e.preventDefault()
return false
})
}
componentWillUnmount() {
keymaster.unbind('ctrl+return, command+return')
keymaster.unbind('ctrl+s, command+s')
keymaster.unbind('ctrl+r, command+r, ctrl+e, command+e')
keymaster.unbind('alt+r')
keymaster.unbind('shift+return')
}
handleFormatClick = () => {
this.formatQuery()
}
handlePaneResize = () => {
if (this.editor) {
this.editor.resize()
}
if (this.dataTable) {
this.dataTable.handleResize()
}
if (this.sqlpadTauChart && this.sqlpadTauChart.chart) {
this.sqlpadTauChart.chart.resize()
}
}
render() {
const { config } = this.props
const {
activeTabKey,
cacheKey,
connections,
isRunning,
isSaving,
query,
queryError,
queryResult,
runQueryStartTime,
runSeconds,
showModal,
showValidation,
unsavedChanges
} = this.state
document.title = query.name || 'New Query'
return (
<div className="flex-100" style={{ flexDirection: 'column' }}>
<EditorNavBar
activeTabKey={activeTabKey}
isRunning={isRunning}
isSaving={isSaving}
onCloneClick={this.handleCloneClick}
onMoreClick={this.handleMoreClick}
onRunClick={this.runQuery}
onSaveClick={this.saveQuery}
onFormatClick={this.handleFormatClick}
onTabSelect={this.handleTabSelect}
query={query}
onQueryNameChange={this.handleQueryNameChange}
showValidation={showValidation}
unsavedChanges={unsavedChanges}
/>
<div style={{ position: 'relative', flexGrow: 1 }}>
<FlexTabPane tabKey="sql" activeTabKey={activeTabKey}>
<SplitPane
split="vertical"
minSize={150}
defaultSize={280}
maxSize={-100}
onChange={this.handlePaneResize}
>
<SchemaSidebar
{...this.props}
connectionId={query.connectionId}
connections={connections}
onConnectionChange={this.handleConnectionChange}
/>
<SplitPane
split="horizontal"
minSize={100}
defaultSize={'60%'}
maxSize={-100}
onChange={this.handlePaneResize}
>
<SqlEditor
config={config}
value={query.queryText}
onChange={this.handleQueryTextChange}
ref={ref => {
this.editor = ref ? ref.editor : null
}}
/>
<div>
<QueryResultHeader
{...this.props}
cacheKey={cacheKey}
isRunning={isRunning}
queryResult={queryResult}
runQueryStartTime={runQueryStartTime}
runSeconds={runSeconds}
/>
<div
style={{
position: 'absolute',
top: 30,
bottom: 0,
left: 0,
right: 0
}}
>
<QueryResultDataTable
{...this.props}
isRunning={isRunning}
queryError={queryError}
queryResult={queryResult}
ref={ref => (this.dataTable = ref)}
/>
</div>
</div>
</SplitPane>
</SplitPane>
</FlexTabPane>
<FlexTabPane tabKey="vis" activeTabKey={activeTabKey}>
<SplitPane
split="vertical"
minSize={150}
defaultSize={280}
maxSize={-100}
onChange={this.handlePaneResize}
>
<VisSidebar
isChartable={this.isChartable()}
onChartConfigurationFieldsChange={
this.handleChartConfigurationFieldsChange
}
onChartTypeChange={this.handleChartTypeChange}
onSaveImageClick={this.handleSaveImageClick}
onVisualizeClick={this.handleVisualizeClick}
query={query}
queryResult={queryResult}
/>
<div className="flex-grow-1" style={{ height: '100%' }}>
<SqlpadTauChart
config={this.props.config}
isRunning={isRunning}
query={query}
queryError={queryError}
queryResult={queryResult}
renderChart={this.isChartable()}
ref={ref => {
this.sqlpadTauChart = ref
}}
/>
</div>
</SplitPane>
</FlexTabPane>
</div>
<QueryDetailsModal
config={config}
onHide={this.handleModalHide}
onQueryTagsChange={this.handleQueryTagsChange}
query={query}
showModal={showModal}
tagOptions={this.getTagOptions()}
/>
<Prompt
when={unsavedChanges}
message={location => `Leave without saving?`}
/>
</div>
)
}
}
export default QueryEditor