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 Group List and Details pages to React #3411

Merged
merged 28 commits into from
Feb 22, 2019
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c7f6855
Migrate Groups list, Group members and Group datasources pages to React
kravets-levko Feb 18, 2019
2c005d2
Add members/data sources dialog
kravets-levko Feb 18, 2019
826c618
Better rendering / refine components; load and filter users/data sour…
kravets-levko Feb 19, 2019
c4c95ff
Mark selected items and items already in group
kravets-levko Feb 19, 2019
24f1ba7
Save selected members/data sources
kravets-levko Feb 20, 2019
211fb53
Cleanup
kravets-levko Feb 20, 2019
e6672f2
Merge branch 'master' into feature/react-group-details-page
kravets-levko Feb 20, 2019
832c829
Cleanup
kravets-levko Feb 20, 2019
726807f
Fix tests
kravets-levko Feb 20, 2019
6d95191
Refine Add members/data sources dialog
kravets-levko Feb 20, 2019
d245122
Split Group details page into Group Members, Group Datasources and fe…
kravets-levko Feb 20, 2019
2303469
CR1 part 1
kravets-levko Feb 20, 2019
a6e0260
Fix permissions
kravets-levko Feb 20, 2019
d824d01
CR1 part 2
kravets-levko Feb 20, 2019
8374962
CR1 part 3: Refine Add members/data sources dialog
kravets-levko Feb 21, 2019
013c815
CR1 part 4 Empty states
kravets-levko Feb 21, 2019
f37572d
Merge branch 'master' into feature/react-group-details-page
kravets-levko Feb 21, 2019
e5b67e5
Go to first page when deleting group
kravets-levko Feb 21, 2019
f49b9a8
Disabled state for items already in group
kravets-levko Feb 21, 2019
99992d6
CR2
kravets-levko Feb 21, 2019
42defdc
Merge branch 'master' into feature/react-group-details-page
kravets-levko Feb 21, 2019
82bc4c9
Fix: header messed up on small screens
kravets-levko Feb 21, 2019
3b1b88a
Merge branch 'feature/react-group-details-page' of github.com:kravets…
kravets-levko Feb 21, 2019
92c57e9
Include only group ids in data sources
kravets-levko Feb 21, 2019
18c2d2b
Don't change backend
kravets-levko Feb 21, 2019
55df45f
Merge branch 'master' into feature/react-group-details-page
kravets-levko Feb 21, 2019
e7a458f
Replace <form> with onPressEnter
kravets-levko Feb 22, 2019
9aff0c5
Fix DeleteGroupButton tooltip (not hidden when button disabled)
kravets-levko Feb 22, 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
13 changes: 11 additions & 2 deletions client/app/assets/less/ant.less
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
@import '~antd/lib/grid/style/index';
@import '~antd/lib/switch/style/index';
@import '~antd/lib/drawer/style/index';
@import '~antd/lib/divider/style/index';
@import '~antd/lib/dropdown/style/index';
@import '~antd/lib/menu/style/index';
@import '~antd/lib/list/style/index';
@import 'inc/ant-variables';

// Remove bold in labels for Ant checkboxes and radio buttons
Expand Down Expand Up @@ -51,7 +55,6 @@
}

// Button overrides

.@{btn-prefix-cls} {
transition-duration: 150ms;
}
Expand Down Expand Up @@ -180,6 +183,12 @@
}
}
}

// Custom styles

&-headerless &-tbody > tr:first-child > td {
border-top: @border-width-base @border-style-base @border-color-split;
}
}

// styling for short modals (no lines)
Expand Down Expand Up @@ -210,4 +219,4 @@

.ant-popover {
z-index: 1000; // make sure it doesn't cover drawer
}
}
4 changes: 4 additions & 0 deletions client/app/assets/less/inc/base.less
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ strong {

}

.clickable {
cursor: pointer;
}

.resize-vertical {
resize: vertical !important;
transition: height 0s !important;
Expand Down
79 changes: 79 additions & 0 deletions client/app/components/PreviewCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from 'react';
import PropTypes from 'prop-types';

// PreviewCard

export function PreviewCard({ imageUrl, title, body, children, className, ...props }) {
return (
<div {...props} className={className + ' w-100 d-flex align-items-center'}>
<img src={imageUrl} height="32" className="profile__image--settings m-r-5" alt="Logo/Avatar" />
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
<div className="flex-fill">
<div>{title}</div>
{body && <div className="text-muted">{body}</div>}
</div>
{children}
</div>
);
}

PreviewCard.propTypes = {
imageUrl: PropTypes.string.isRequired,
title: PropTypes.node.isRequired,
body: PropTypes.node,
className: PropTypes.string,
children: PropTypes.node,
};

PreviewCard.defaultProps = {
body: null,
className: '',
children: null,
};

// UserPreviewCard

export function UserPreviewCard({ user, withLink, children, ...props }) {
const title = withLink ? <a href={'users/' + user.id}>{user.name}</a> : user.name;
return (
<PreviewCard {...props} imageUrl={user.profile_image_url} title={title} body={user.email}>
{children}
</PreviewCard>
);
}

UserPreviewCard.propTypes = {
user: PropTypes.shape({
profile_image_url: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
}).isRequired,
withLink: PropTypes.bool,
children: PropTypes.node,
};

UserPreviewCard.defaultProps = {
withLink: false,
children: null,
};

// DataSourcePreviewCard

export function DataSourcePreviewCard({ dataSource, withLink, children, ...props }) {
const imageUrl = `/static/images/db-logos/${dataSource.type}.png`;
const title = withLink ? <a href={'data_sources/' + dataSource.id}>{dataSource.name}</a> : dataSource.name;
return <PreviewCard {...props} imageUrl={imageUrl} title={title}>{children}</PreviewCard>;
}

DataSourcePreviewCard.propTypes = {
dataSource: PropTypes.shape({
name: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
}).isRequired,
withLink: PropTypes.bool,
children: PropTypes.node,
};

DataSourcePreviewCard.defaultProps = {
withLink: false,
children: null,
};
181 changes: 181 additions & 0 deletions client/app/components/SelectItemsDialog.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { filter, debounce } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import Modal from 'antd/lib/modal';
import Input from 'antd/lib/input';
import List from 'antd/lib/list';
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';

import LoadingState from '@/components/items-list/components/LoadingState';

class SelectItemsDialog extends React.Component {
static propTypes = {
dialog: DialogPropType.isRequired,
dialogTitle: PropTypes.string,
inputPlaceholder: PropTypes.string,
selectedItemsTitle: PropTypes.string,
searchItems: PropTypes.func.isRequired, // (searchTerm: string): Promise<Items[]> if `searchTerm === ''` load all
itemKey: PropTypes.func, // (item) => string|number - return key of item (by default `id`)
renderItem: PropTypes.func, // (item) => node
save: PropTypes.func, // (selectedItems[]) => Promise<any>
};

static defaultProps = {
dialogTitle: 'Add Items',
inputPlaceholder: 'Search...',
selectedItemsTitle: 'Selected items',
itemKey: item => item.id,
renderItem: () => '',
save: items => items,
};

selectedIds = new Set();
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved

state = {
searchTerm: '',
loading: false,
items: [],
selected: [],
saveInProgress: false,
};

// eslint-disable-next-line react/sort-comp
loadItems = (searchTerm = '') => {
this.setState({ searchTerm, loading: true }, () => {
this.props.searchItems(searchTerm)
.then((items) => {
// If another search appeared while loading data - just reject this set
if (this.state.searchTerm === searchTerm) {
this.setState({ items, loading: false });
}
})
.catch(() => {
if (this.state.searchTerm === searchTerm) {
this.setState({ items: [], loading: false });
}
});
});
};

search = debounce(this.loadItems, 200);

componentDidMount() {
this.loadItems();
}

toggleItem(item) {
const key = this.props.itemKey(item);
if (this.selectedIds.has(key)) {
this.selectedIds.delete(key);
this.setState(({ selected }) => ({
selected: filter(selected, i => this.props.itemKey(i) !== key),
}));
} else {
this.selectedIds.add(key);
this.setState(({ selected }) => ({
selected: [...selected, item],
}));
}
}

save() {
this.setState({ saveInProgress: true }, () => {
const selectedItems = this.state.selected;
Promise.resolve(this.props.save(selectedItems))
.then(() => {
this.props.dialog.close(selectedItems);
})
.catch(() => {
this.setState({ saveInProgress: false });
});
});
}

renderSlot(item, isInSelectedList) {
const { itemKey, renderItem } = this.props;
const key = itemKey(item);
const isSelected = this.selectedIds.has(key);

const searchListAddon = isSelected ? (
<i className="fa fa-check m-r-10" />
) : (
<i className="fa fa-angle-double-right m-r-10" />
);

const selectedListAddon = <i className="fa fa-remove m-r-10" />;

const addons = isInSelectedList ? selectedListAddon : searchListAddon;

const onClick = () => this.toggleItem(item);
return (
<List.Item className="p-0">{renderItem(item, isSelected, addons, onClick)}</List.Item>
);
}

render() {
const { dialog, dialogTitle, inputPlaceholder, selectedItemsTitle } = this.props;
const { loading, saveInProgress, items, selected } = this.state;
const hasResults = items.length > 0;
return (
<Modal
{...dialog.props}
width="80%"
title={dialogTitle}
okText="Save"
okButtonProps={{
loading: saveInProgress,
}}
onOk={() => this.save()}
>
<div className="row m-b-10">
<div className="col-xs-6">
<Input.Search
defaultValue={this.state.searchTerm}
onChange={event => this.search(event.target.value)}
placeholder={inputPlaceholder}
autoFocus
/>
</div>
<div className="col-xs-6">
<h5 className="m-t-10">{selectedItemsTitle}</h5>
</div>
</div>

<div className="row">
<div className="col-xs-6">
{loading && <LoadingState />}
{!loading && !hasResults && (
<div className="d-flex justify-content-center align-items-center text-muted" style={{ height: '150px' }}>
Search results will appear here
</div>
)}
{!loading && hasResults && (
<div className="scrollbox" style={{ maxHeight: '50vh' }}>
{(items.length > 0) && (
<List
size="small"
dataSource={items}
renderItem={item => this.renderSlot(item, false)}
/>
)}
</div>
)}
</div>
<div className="col-xs-6">
<div className="scrollbox" style={{ maxHeight: '50vh' }}>
{(selected.length > 0) && (
<List
size="small"
dataSource={selected}
renderItem={item => this.renderSlot(item, true)}
/>
)}
</div>
</div>
</div>
</Modal>
);
}
}

export default wrapDialog(SelectItemsDialog);
44 changes: 44 additions & 0 deletions client/app/components/groups/CreateGroupDialog.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import Modal from 'antd/lib/modal';
import Input from 'antd/lib/input';
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';

class CreateGroupDialog extends React.Component {
static propTypes = {
dialog: DialogPropType.isRequired,
group: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
};

state = {
name: this.props.group.name,
};

save() {
const { dialog, group } = this.props;
const { name } = this.state;

group.name = name;
dialog.close(group);
}

render() {
const { dialog, group } = this.props;
const isNewGroup = !group.id;
const title = isNewGroup ? 'Create a New Group' : 'Edit Group';
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
const buttonTitle = isNewGroup ? 'Create' : 'Save';
return (
<Modal {...dialog.props} title={title} okText={buttonTitle} onOk={() => this.save()}>
<Input
className="form-control"
defaultValue={this.state.name}
onChange={event => this.setState({ name: event.target.value })}
placeholder="Group Name"
autoFocus
/>
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
</Modal>
);
}
}

export default wrapDialog(CreateGroupDialog);
12 changes: 0 additions & 12 deletions client/app/components/groups/edit-group-dialog.html

This file was deleted.

Loading