Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate Query Snippets to React #3627

Merged
merged 21 commits into from
Jul 9, 2019
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
cc4a29c
Migrate QuerySnippetsList to React
gabrieldutra Mar 23, 2019
9e6dd33
Updates to QuerySnippetsList
gabrieldutra Mar 24, 2019
f580d4b
Merge branch 'master' into react-query-snippets
gabrieldutra Mar 27, 2019
fb811ae
Add pre-wrap property
gabrieldutra Mar 28, 2019
3e6ee5b
Merge branch 'master' into react-query-snippets
gabrieldutra Mar 28, 2019
a84a3a2
Add Query Snippets Dialog with textarea
gabrieldutra Apr 3, 2019
413002c
Merge branch 'master' into react-query-snippets
gabrieldutra Apr 3, 2019
4e0b929
Change textarea size and treat new/edit path
gabrieldutra Apr 5, 2019
3576f23
Reviewing myself
gabrieldutra Apr 7, 2019
776235e
Merge branch 'master' into react-query-snippets
gabrieldutra Apr 7, 2019
8e82021
Merge branch 'master' into react-query-snippets
gabrieldutra Apr 29, 2019
5d49731
Merge branch 'master' into react-query-snippets
gabrieldutra May 14, 2019
e7ff839
Update Snippets list to use href instead of onClick
gabrieldutra May 14, 2019
ee17902
Revert "Update Snippets list to use href instead of onClick"
gabrieldutra May 16, 2019
8696466
Merge branch 'master' into react-query-snippets
gabrieldutra May 16, 2019
d67b66d
Add option to disable reload on navigateTo
gabrieldutra May 23, 2019
ddd62d8
Merge branch 'master' into react-query-snippets
gabrieldutra May 23, 2019
edbd321
Merge branch 'master' into react-query-snippets
gabrieldutra Jun 12, 2019
c9346a1
Add comment to reload logic on navigateTo
gabrieldutra Jun 12, 2019
161633b
Merge branch 'master' into react-query-snippets
gabrieldutra Jul 8, 2019
8daf941
Fix duplicated formatDate after merge
gabrieldutra Jul 8, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
94 changes: 94 additions & 0 deletions client/app/components/query-snippets/QuerySnippetDialog.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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,
initialValue: get(querySnippet, 'trigger', ''),
autoFocus: !isEditing },
{ name: 'description', title: 'Description', type: 'text', initialValue: get(querySnippet, 'description', '') },
{ name: 'snippet',
title: 'Snippet',
type: 'textarea',
required: true,
props: { autosize: true },
initialValue: get(querySnippet, 'snippet', '') },
].map(field => ({ ...field, readOnly }));

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);
13 changes: 13 additions & 0 deletions client/app/filters/datetime.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import moment from 'moment';
import { clientConfig } from '@/services/auth';

export function formatDate(value) {
if (!value) {
return '';
}

const parsed = moment(value);
if (!parsed.isValid()) {
return '-';
}

return parsed.format(clientConfig.dateFormat);
}

export function formatDateTime(value) {
if (!value) {
return '';
Expand Down
196 changes: 196 additions & 0 deletions client/app/pages/query-snippets/QuerySnippetsList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
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 { 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 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';

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) => (currentUser.isAdmin || currentUser.id === get(querySnippet, 'user.id')) && (
<Button type="danger" className="w-100" onClick={e => this.deleteQuerySnippet(e, querySnippet)}>
Delete
</Button>
), {
width: '1%',
}),
];

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 || currentUser.isAdmin || currentUser.id === get(querySnippet, 'user.id');

QuerySnippetDialog.showModal({
querySnippet,
onSubmit: this.saveQuerySnippet,
readOnly: !canSave,
}).result
.then(() => this.props.controller.update());
};

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',
},
], {
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;
}
gabrieldutra marked this conversation as resolved.
Show resolved Hide resolved
Loading