forked from getredash/redash
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Migrate Query Snippets to React (getredash#3627)
- Loading branch information
1 parent
167cd1c
commit b3721a8
Showing
12 changed files
with
356 additions
and
183 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
88 changes: 88 additions & 0 deletions
88
client/app/components/query-snippets/QuerySnippetDialog.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { get } from 'lodash'; | ||
import Button from 'antd/lib/button'; | ||
import Modal from 'antd/lib/modal'; | ||
import DynamicForm from '@/components/dynamic-form/DynamicForm'; | ||
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper'; | ||
|
||
class QuerySnippetDialog extends React.Component { | ||
static propTypes = { | ||
dialog: DialogPropType.isRequired, | ||
querySnippet: PropTypes.object, // eslint-disable-line react/forbid-prop-types | ||
readOnly: PropTypes.bool, | ||
onSubmit: PropTypes.func.isRequired, | ||
}; | ||
|
||
static defaultProps = { | ||
querySnippet: null, | ||
readOnly: false, | ||
} | ||
|
||
constructor(props) { | ||
super(props); | ||
this.state = { saving: false }; | ||
} | ||
|
||
handleSubmit = (values, successCallback, errorCallback) => { | ||
const { querySnippet, dialog, onSubmit } = this.props; | ||
const querySnippetId = get(querySnippet, 'id'); | ||
|
||
this.setState({ saving: true }); | ||
onSubmit(querySnippetId ? { id: querySnippetId, ...values } : values).then(() => { | ||
dialog.close(); | ||
successCallback('Saved.'); | ||
}).catch(() => { | ||
this.setState({ saving: false }); | ||
errorCallback('Failed saving snippet.'); | ||
}); | ||
}; | ||
|
||
render() { | ||
const { saving } = this.state; | ||
const { querySnippet, dialog, readOnly } = this.props; | ||
const isEditing = !!get(querySnippet, 'id'); | ||
|
||
const formFields = [ | ||
{ name: 'trigger', title: 'Trigger', type: 'text', required: true, autoFocus: !isEditing }, | ||
{ name: 'description', title: 'Description', type: 'text' }, | ||
{ name: 'snippet', | ||
title: 'Snippet', | ||
type: 'textarea', | ||
required: true, | ||
props: { autosize: { minRows: 3, maxRows: 6 } } }, | ||
].map(field => ({ ...field, readOnly, initialValue: get(querySnippet, field.name, '') })); | ||
|
||
return ( | ||
<Modal | ||
{...dialog.props} | ||
title={(isEditing ? querySnippet.trigger : 'Create Query Snippet')} | ||
footer={[( | ||
<Button key="cancel" onClick={dialog.dismiss}>{readOnly ? 'Close' : 'Cancel'}</Button> | ||
), ( | ||
!readOnly && ( | ||
<Button | ||
key="submit" | ||
htmlType="submit" | ||
loading={saving} | ||
disabled={readOnly} | ||
type="primary" | ||
form="querySnippetForm" | ||
> | ||
{isEditing ? 'Save' : 'Create'} | ||
</Button> | ||
) | ||
)]} | ||
> | ||
<DynamicForm | ||
id="querySnippetForm" | ||
fields={formFields} | ||
onSubmit={this.handleSubmit} | ||
hideSubmitButton | ||
/> | ||
</Modal> | ||
); | ||
} | ||
} | ||
|
||
export default wrapDialog(QuerySnippetDialog); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
import { get } from 'lodash'; | ||
import React from 'react'; | ||
import { react2angular } from 'react2angular'; | ||
|
||
import Button from 'antd/lib/button'; | ||
import Modal from 'antd/lib/modal'; | ||
import PromiseRejectionError from '@/lib/promise-rejection-error'; | ||
import { Paginator } from '@/components/Paginator'; | ||
import QuerySnippetDialog from '@/components/query-snippets/QuerySnippetDialog'; | ||
|
||
import { wrap as liveItemsList, ControllerType } from '@/components/items-list/ItemsList'; | ||
import { ResourceItemsSource } from '@/components/items-list/classes/ItemsSource'; | ||
import { StateStorage } from '@/components/items-list/classes/StateStorage'; | ||
|
||
import LoadingState from '@/components/items-list/components/LoadingState'; | ||
import ItemsTable, { Columns } from '@/components/items-list/components/ItemsTable'; | ||
|
||
import { QuerySnippet } from '@/services/query-snippet'; | ||
import navigateTo from '@/services/navigateTo'; | ||
import settingsMenu from '@/services/settingsMenu'; | ||
import { currentUser } from '@/services/auth'; | ||
import { policy } from '@/services/policy'; | ||
import notification from '@/services/notification'; | ||
import { routesToAngularRoutes } from '@/lib/utils'; | ||
import './QuerySnippetsList.less'; | ||
|
||
const canEditQuerySnippet = querySnippet => (currentUser.isAdmin || currentUser.id === get(querySnippet, 'user.id')); | ||
|
||
class QuerySnippetsList extends React.Component { | ||
static propTypes = { | ||
controller: ControllerType.isRequired, | ||
}; | ||
|
||
listColumns = [ | ||
Columns.custom.sortable((text, querySnippet) => ( | ||
<div> | ||
<a className="table-main-title clickable" onClick={() => this.showSnippetDialog(querySnippet)}> | ||
{querySnippet.trigger} | ||
</a> | ||
</div> | ||
), { | ||
title: 'Trigger', | ||
field: 'trigger', | ||
className: 'text-nowrap', | ||
}), | ||
Columns.custom.sortable(text => text, { | ||
title: 'Description', | ||
field: 'description', | ||
className: 'text-nowrap', | ||
}), | ||
Columns.custom(snippet => ( | ||
<code className="snippet-content"> | ||
{snippet} | ||
</code> | ||
), { | ||
title: 'Snippet', | ||
field: 'snippet', | ||
}), | ||
Columns.avatar({ field: 'user', className: 'p-l-0 p-r-0' }, name => `Created by ${name}`), | ||
Columns.date.sortable({ | ||
title: 'Created At', | ||
field: 'created_at', | ||
className: 'text-nowrap', | ||
width: '1%', | ||
}), | ||
Columns.custom((text, querySnippet) => canEditQuerySnippet(querySnippet) && ( | ||
<Button type="danger" className="w-100" onClick={e => this.deleteQuerySnippet(e, querySnippet)}> | ||
Delete | ||
</Button> | ||
), { | ||
width: '1%', | ||
}), | ||
]; | ||
|
||
componentDidMount() { | ||
const { isNewOrEditPage, querySnippetId } = this.props.controller.params; | ||
|
||
if (isNewOrEditPage) { | ||
if (querySnippetId === 'new') { | ||
if (policy.isCreateQuerySnippetEnabled()) { | ||
this.showSnippetDialog(); | ||
} else { | ||
navigateTo('/query_snippets'); | ||
} | ||
} else { | ||
QuerySnippet.get({ id: querySnippetId }).$promise | ||
.then(this.showSnippetDialog) | ||
.catch((error = {}) => { | ||
// ANGULAR_REMOVE_ME This code is related to Angular's HTTP services | ||
if (error.status && error.data) { | ||
error = new PromiseRejectionError(error); | ||
} | ||
this.props.controller.handleError(error); | ||
}); | ||
} | ||
} | ||
} | ||
|
||
saveQuerySnippet = querySnippet => QuerySnippet.save(querySnippet).$promise; | ||
|
||
deleteQuerySnippet = (event, querySnippet) => { | ||
Modal.confirm({ | ||
title: 'Delete Query Snippet', | ||
content: 'Are you sure you want to delete this query snippet?', | ||
okText: 'Yes', | ||
okType: 'danger', | ||
cancelText: 'No', | ||
onOk: () => { | ||
querySnippet.$delete(() => { | ||
notification.success('Query snippet deleted successfully.'); | ||
this.props.controller.update(); | ||
}, () => { | ||
notification.error('Failed deleting query snippet.'); | ||
}); | ||
}, | ||
}); | ||
} | ||
|
||
showSnippetDialog = (querySnippet = null) => { | ||
const canSave = !querySnippet || canEditQuerySnippet(querySnippet); | ||
navigateTo('/query_snippets/' + get(querySnippet, 'id', 'new'), true, false); | ||
QuerySnippetDialog.showModal({ | ||
querySnippet, | ||
onSubmit: this.saveQuerySnippet, | ||
readOnly: !canSave, | ||
}).result | ||
.then(() => this.props.controller.update()) | ||
.finally(() => { | ||
navigateTo('/query_snippets', true, false); | ||
}); | ||
}; | ||
|
||
render() { | ||
const { controller } = this.props; | ||
|
||
return ( | ||
<div> | ||
<div className="m-b-15"> | ||
<Button | ||
type="primary" | ||
onClick={() => this.showSnippetDialog()} | ||
disabled={!policy.isCreateQuerySnippetEnabled()} | ||
> | ||
<i className="fa fa-plus m-r-5" /> | ||
New Query Snippet | ||
</Button> | ||
</div> | ||
|
||
{!controller.isLoaded && <LoadingState className="" />} | ||
{controller.isLoaded && controller.isEmpty && ( | ||
<div className="text-center"> | ||
There are no query snippets yet. | ||
{policy.isCreateQuerySnippetEnabled() && ( | ||
<div className="m-t-5"> | ||
<a href="/query_snippets/new">Click here</a> to add one. | ||
</div> | ||
)} | ||
</div> | ||
)} | ||
{ | ||
controller.isLoaded && !controller.isEmpty && ( | ||
<div className="table-responsive"> | ||
<ItemsTable | ||
items={controller.pageItems} | ||
columns={this.listColumns} | ||
context={this.actions} | ||
orderByField={controller.orderByField} | ||
orderByReverse={controller.orderByReverse} | ||
toggleSorting={controller.toggleSorting} | ||
/> | ||
<Paginator | ||
totalCount={controller.totalItemsCount} | ||
itemsPerPage={controller.itemsPerPage} | ||
page={controller.page} | ||
onChange={page => controller.updatePagination({ page })} | ||
/> | ||
</div> | ||
) | ||
} | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
export default function init(ngModule) { | ||
settingsMenu.add({ | ||
permission: 'create_query', | ||
title: 'Query Snippets', | ||
path: 'query_snippets', | ||
order: 5, | ||
}); | ||
|
||
ngModule.component('pageQuerySnippetsList', react2angular(liveItemsList( | ||
QuerySnippetsList, | ||
new ResourceItemsSource({ | ||
isPlainList: true, | ||
getRequest() { | ||
return {}; | ||
}, | ||
getResource() { | ||
return QuerySnippet.query.bind(QuerySnippet); | ||
}, | ||
getItemProcessor() { | ||
return (item => new QuerySnippet(item)); | ||
}, | ||
}), | ||
new StateStorage({ orderByField: 'trigger', itemsPerPage: 10 }), | ||
))); | ||
|
||
return routesToAngularRoutes([ | ||
{ | ||
path: '/query_snippets', | ||
title: 'Query Snippets', | ||
key: 'query_snippets', | ||
}, | ||
{ | ||
path: '/query_snippets/:querySnippetId', | ||
title: 'Query Snippets', | ||
key: 'query_snippets', | ||
isNewOrEditPage: true, | ||
}, | ||
], { | ||
reloadOnSearch: false, | ||
template: '<settings-screen><page-query-snippets-list on-error="handleError"></page-query-snippets-list></settings-screen>', | ||
controller($scope, $exceptionHandler) { | ||
'ngInject'; | ||
|
||
$scope.handleError = $exceptionHandler; | ||
}, | ||
}); | ||
} | ||
|
||
init.init = true; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
.snippet-content { | ||
max-width: 500px; | ||
max-height: 56px; | ||
overflow: hidden; | ||
white-space: pre-wrap; | ||
/* autoprefixer: off */ | ||
display: -webkit-box; | ||
-webkit-line-clamp: 3; | ||
-webkit-box-orient: vertical; | ||
} |
Oops, something went wrong.