Skip to content

Commit

Permalink
Migrate Query Snippets to React (getredash#3627)
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrieldutra authored and harveyrendell committed Nov 14, 2019
1 parent 167cd1c commit b3721a8
Show file tree
Hide file tree
Showing 12 changed files with 356 additions and 183 deletions.
2 changes: 2 additions & 0 deletions client/app/components/dynamic-form/DynamicForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ class DynamicForm extends React.Component {
return field.content;
} else if (type === 'number') {
return getFieldDecorator(name, options)(<InputNumber {...props} />);
} else if (type === 'textarea') {
return getFieldDecorator(name, options)(<Input.TextArea {...props} />);
}
return getFieldDecorator(name, options)(<Input {...props} />);
}
Expand Down
8 changes: 7 additions & 1 deletion client/app/components/items-list/components/ItemsTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Table from 'antd/lib/table';
import { FavoritesControl } from '@/components/FavoritesControl';
import { TimeAgo } from '@/components/TimeAgo';
import { durationHumanize } from '@/filters';
import { formatDateTime } from '@/filters/datetime';
import { formatDate, formatDateTime } from '@/filters/datetime';

// `this` refers to previous function in the chain (`Columns.***`).
// Adds `sorter: true` field to column definition
Expand Down Expand Up @@ -35,6 +35,11 @@ export const Columns = {
),
}, overrides);
},
date(overrides) {
return extend({
render: text => formatDate(text),
}, overrides);
},
dateTime(overrides) {
return extend({
render: text => formatDateTime(text),
Expand All @@ -59,6 +64,7 @@ export const Columns = {
},
};

Columns.date.sortable = sortable;
Columns.dateTime.sortable = sortable;
Columns.duration.sortable = sortable;
Columns.timeAgo.sortable = sortable;
Expand Down
1 change: 1 addition & 0 deletions client/app/components/proptypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const Field = PropTypes.shape({
title: PropTypes.string,
type: PropTypes.oneOf([
'text',
'textarea',
'email',
'password',
'number',
Expand Down
88 changes: 88 additions & 0 deletions client/app/components/query-snippets/QuerySnippetDialog.jsx
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);
233 changes: 233 additions & 0 deletions client/app/pages/query-snippets/QuerySnippetsList.jsx
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;
10 changes: 10 additions & 0 deletions client/app/pages/query-snippets/QuerySnippetsList.less
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;
}
Loading

0 comments on commit b3721a8

Please sign in to comment.