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

Sync QueryView and QuerySource #4430

Merged
merged 11 commits into from
Dec 13, 2019
6 changes: 0 additions & 6 deletions client/app/assets/less/inc/base.less
Original file line number Diff line number Diff line change
Expand Up @@ -261,12 +261,6 @@ text.slicetext {
favorites-control {
float: left;
}

h3 {
width: 100%;
margin-bottom: 5px !important;
display: block !important;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed some difference between old and new header version: when query does not have tags, tags control should stay on the same line with query name (see screenshots). Seems this style is related to that

image
image

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems it's better to keep markup from base branch, unless you have some better UX ideas for this component. Also, if you think that button group does not fit here - feel free to remove it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wraps in the older version too, just take more for it to happen 🙂. That's due to the existing conflict with .page-title { display: block }, fixed in 37c9b46

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now layout is good, but still there some differences:

existing
image

react
image

Notice: favorites control should be on the same line with query name, too much space between query name and tags, and tags should go under buttons on right side.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, in the original there are two QueryTagsControl components, one for the desktop position and another for the mobile one 😪

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I usually feel css is more reliable than js when it can be used, do you have any advantages in mind?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case the only advantage is that tags component will be rendered only once (+markup will simplified a bit). CSS Grid is the perfect solution, but it still is not supported good enough. For JS, there is a matchMedia API which is 100% supported in all our target browsers.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW you meant to use a hook, right? (like this one)

It's an interesting hook to have anyway, I tried to use it once I wanted to check in JS if the browser was in Percy,

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, something like that 🙂

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That hook is not hard to be applied in this context:

const isMobile = useMedia(['(max-width: 880px)'], [true], false);

But I prefer keeping it in css for now as I don't want to break the angular page yet (we can reconsider when cleaning angular stuff)

}
}

Expand Down
1 change: 0 additions & 1 deletion client/app/assets/less/redash/query.less
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,6 @@ nav .rg-bottom {

.query-tags__mobile {
display: none;
margin: -5px 0 0 0;
padding: 0 0 0 23px;
}

Expand Down
2 changes: 1 addition & 1 deletion client/app/components/FavoritesControl.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class FavoritesControl extends React.Component {
const icon = item.is_favorite ? "fa fa-star" : "fa fa-star-o";
const title = item.is_favorite ? "Remove from favorites" : "Add to favorites";
return (
<a title={title} className="btn-favourite" onClick={event => this.toggleItem(event, item, onChange)}>
<a title={title} className="favorites-control btn-favourite" onClick={event => this.toggleItem(event, item, onChange)}>
<i className={icon} aria-hidden="true" />
</a>
);
Expand Down
110 changes: 110 additions & 0 deletions client/app/pages/queries/QueryView.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React, { useMemo, useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import Divider from 'antd/lib/divider';
import Button from 'antd/lib/button';
import Icon from 'antd/lib/icon';
import { EditInPlace } from '@/components/EditInPlace';
import { Parameters } from '@/components/Parameters';
import { TimeAgo } from '@/components/TimeAgo';
import { currentUser } from '@/services/auth';
import QueryPageHeader from './components/QueryPageHeader';

import { IMG_ROOT, DataSource } from '@/services/data-source';
import QueryVisualizationTabs from './components/QueryVisualizationTabs';
import { EditVisualizationButton } from '@/components/EditVisualizationButton';
import useQueryResult from '@/lib/hooks/useQueryResult';
import { pluralize } from '@/filters';

function QueryView({ query }) {
const canEdit = useMemo(() => (currentUser.canEdit(query) || query.can_edit), [query]);
const parameters = useMemo(() => query.getParametersDefs(), [query]);
const [dataSource, setDataSource] = useState();
const queryResult = useMemo(() => query.getQueryResult(), [query]);
const queryResultData = useQueryResult(queryResult);

useEffect(() => {
DataSource.get({ id: query.data_source_id }).$promise
.then(setDataSource);
}, [query]);
return (
<div className="query-page-wrapper">
<div className="container">
<QueryPageHeader query={query} />
<div className="query-metadata tiled bg-white p-15">
<EditInPlace
className="w-100"
value={query.description}
isEditable={canEdit}
editor="textarea"
placeholder="Add description"
ignoreBlanks
/>
<Divider />
<div className="d-flex flex-wrap">
<div className="m-r-20 m-b-10">
<img src={query.user.profile_image_url} className="profile__image_thumb" alt={query.user.name} />
<strong>{query.user.name}</strong>
{' created '}
<TimeAgo date={query.created_at} />
</div>
<div className="m-r-20 m-b-10">
<img src={query.last_modified_by.profile_image_url} className="profile__image_thumb" alt={query.last_modified_by.name} />
<strong>{query.last_modified_by.name}</strong>
{' updated '}
<TimeAgo date={query.updated_at} />
</div>
{dataSource && (
<div className="m-r-20 m-b-10">
<img src={`${IMG_ROOT}/${dataSource.type}.png`} width="20" alt={dataSource.type} />
{dataSource.name}
</div>
)}
</div>
</div>
<div className="query-content tiled bg-white p-15 m-t-15">
{query.hasParameters() && <Parameters parameters={parameters} />}
<QueryVisualizationTabs queryResult={queryResult} visualizations={query.visualizations} />
<Divider />
<div className="d-flex align-items-center">
<EditVisualizationButton />
<Button className="icon-button hidden-xs">
<Icon type="ellipsis" rotate={90} />
</Button>
<div className="flex-fill m-l-10">
{queryResultData && (
<span>
<strong>{queryResultData.rows.length}</strong>{' '}
{pluralize('row', queryResultData.rows.length)}
</span>
)}
</div>
<Button type="primary">Execute</Button>
</div>
</div>
</div>
</div>
);
}

QueryView.propTypes = { query: PropTypes.object.isRequired }; // eslint-disable-line react/forbid-prop-types

export default function init(ngModule) {
ngModule.component('pageQueryView', react2angular(QueryView));

return {
'/queries-react/:queryId': {
template: '<page-query-view query="$resolve.query"></page-query-view>',
reloadOnSearch: false,
resolve: {
query: (Query, $route) => {
'ngInject';

return Query.get({ id: $route.current.params.queryId }).$promise;
},
},
},
};
}

init.init = true;
69 changes: 34 additions & 35 deletions client/app/pages/queries/components/QueryPageHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ import cx from 'classnames';
import Button from 'antd/lib/button';
import Dropdown from 'antd/lib/dropdown';
import Menu from 'antd/lib/menu';
import Icon from 'antd/lib/icon';
import { EditInPlace } from '@/components/EditInPlace';
import { FavoritesControl } from '@/components/FavoritesControl';
import { QueryTagsControl } from '@/components/tags-control/TagsControl';
import getTags from '@/services/getTags';

function getQueryTags() {
return getTags('api/queries/tags').then(tags => map(tags, t => t.name));
}

function createMenu(menu) {
const handlers = {};

Expand Down Expand Up @@ -39,8 +44,6 @@ export default function QueryPageHeader({ query, sourceMode }) {
console.log('saveName', name);
}

const loadTags = () => getTags('api/queries/tags').then(tags => map(tags, t => t.name));

function saveTags(tags) {
console.log('saveTags', tags);
}
Expand Down Expand Up @@ -89,8 +92,8 @@ export default function QueryPageHeader({ query, sourceMode }) {
];

return (
<div className="p-b-10 m-l-0 m-r-0 page-header--new page-header--query">
<div className="page-title p-0">
<div className="p-b-10 page-header--new page-header--query">
<div className="page-title">
<div className="d-flex flex-nowrap align-items-center">
{!query.isNew() && (
<span className="m-r-5">
Expand All @@ -111,50 +114,46 @@ export default function QueryPageHeader({ query, sourceMode }) {
isDraft={query.is_draft}
isArchived={query.is_archived}
canEdit={query.can_edit}
getAvailableTags={loadTags}
getAvailableTags={getQueryTags}
onEdit={saveTags}
/>
</span>
</h3>
<span className="flex-fill" />
{query.is_draft && !query.isNew() && query.can_edit && (
<Button className="hidden-xs m-r-5" onClick={togglePublished}>
<i className="fa fa-paper-plane m-r-5" /> Publish
</Button>
)}

<span className="flex-fill">&nbsp;</span>

<Button.Group className="p-0 text-right text-nowrap align-self-start d-flex m-t-5">
{query.is_draft && !query.isNew() && query.can_edit && (
<Button onClick={togglePublished}>
<i className="fa fa-paper-plane m-r-5" /> Publish
</Button>
)}

{!query.isNew() && canViewSource && (
<span>
{!sourceMode && (
<Button href={query.getUrl(true, selectedTab)}>
<i className="fa fa-pencil-square-o m-r-5" aria-hidden="true" /> Edit Source
</Button>
)}
{sourceMode && (
<Button href={query.getUrl(false, selectedTab)} data-test="QueryPageShowDataOnly">
<i className="fa fa-table m-r-5" aria-hidden="true" /> Show Data Only
</Button>
)}
</span>
)}
{!query.isNew() && canViewSource && (
<span>
{!sourceMode && (
<Button className="m-r-5" href={query.getUrl(true, selectedTab)}>
<i className="fa fa-pencil-square-o m-r-5" aria-hidden="true" /> Edit Source
</Button>
)}
{sourceMode && (
<Button className="m-r-5" href={query.getUrl(false, selectedTab)} data-test="QueryPageShowDataOnly">
<i className="fa fa-table m-r-5" aria-hidden="true" /> Show Data Only
</Button>
)}
</span>
)}

{!query.isNew() && (
<Dropdown overlay={createMenu(moreActionsMenu)} trigger={['click']}>
<Button><i className="zmdi zmdi-more" /></Button>
</Dropdown>
)}
</Button.Group>
{!query.isNew() && (
<Dropdown overlay={createMenu(moreActionsMenu)} trigger={['click']}>
<Button><Icon type="ellipsis" rotate={90} /></Button>
</Dropdown>
)}
</div>
<span className={cx('query-tags__mobile', { 'query-tags__empty': query.tags.length === 0 })}>
<QueryTagsControl
tags={query.tags}
isDraft={query.is_draft}
isArchived={query.is_archived}
canEdit={query.can_edit}
getAvailableTags={loadTags}
getAvailableTags={getQueryTags}
onEdit={saveTags}
/>
</span>
Expand Down
38 changes: 38 additions & 0 deletions client/app/pages/queries/components/QueryVisualizationTabs.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import PropTypes from 'prop-types';
import { find, orderBy } from 'lodash';
import Tabs from 'antd/lib/tabs';
import { VisualizationRenderer } from '@/visualizations/VisualizationRenderer';
import Button from 'antd/lib/button';

const { TabPane } = Tabs;

export default function QueryVisualizationTabs({ visualizations, queryResult, currentVisualizationId }) {
const tabsProps = {};
if (find(visualizations, { id: currentVisualizationId })) {
tabsProps.activeKey = `${currentVisualizationId}`;
}

const orderedVisualizations = orderBy(visualizations, ['id']);

return (
<Tabs {...tabsProps} tabBarExtraContent={(<Button><i className="fa fa-plus m-r-5" />New Visualization</Button>)}>
{orderedVisualizations.map(visualization => (
<TabPane key={`${visualization.id}`} tab={visualization.name}>
<VisualizationRenderer
visualization={visualization}
queryResult={queryResult}
context="query"
/>
</TabPane>
))}
</Tabs>
);
}

QueryVisualizationTabs.propTypes = {
queryResult: PropTypes.object, // eslint-disable-line react/forbid-prop-types
visualizations: PropTypes.arrayOf(PropTypes.object),
currentVisualizationId: PropTypes.number,
};
QueryVisualizationTabs.defaultProps = { queryResult: null, visualizations: [], currentVisualizationId: null };