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 page to React: Query View #4455

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
45e958b
Query View updates
gabrieldutra Dec 17, 2019
caf6cd8
Merge branch 'migrate-query-source-view-to-react' into migrate-query-…
gabrieldutra Dec 17, 2019
5be69ab
Update document title
gabrieldutra Dec 17, 2019
b318b16
Fix New Visualization
gabrieldutra Dec 17, 2019
a59f1ce
Merge branch 'migrate-query-source-view-to-react' into migrate-query-…
gabrieldutra Dec 18, 2019
0c468b8
Update QueryView with useQueryResults status
gabrieldutra Dec 18, 2019
c45095b
Merge branch 'migrate-query-source-view-to-react' into migrate-query-…
gabrieldutra Dec 20, 2019
40310f1
Add QueryView actions and separate query properties
gabrieldutra Dec 20, 2019
e8aa293
Fix showEmbedDialog
gabrieldutra Dec 22, 2019
9618a2f
Merge branch 'migrate-query-source-view-to-react' into migrate-query-…
gabrieldutra Dec 24, 2019
e154a00
Prettier QueryView
gabrieldutra Dec 24, 2019
7914602
Add Query status and check queryResults
gabrieldutra Dec 24, 2019
2430947
Merge branch 'migrate-query-source-view-to-react' into migrate-query-…
gabrieldutra Dec 26, 2019
f208b0e
Updates to Schedule
gabrieldutra Dec 26, 2019
acb0772
Use QueryMetadata in the View page
gabrieldutra Dec 26, 2019
9de7d65
Merge branch 'migrate-query-source-view-to-react' into migrate-query-…
gabrieldutra Dec 26, 2019
08fef44
Add editorProps
gabrieldutra Dec 26, 2019
8ba53dc
Merge branch 'migrate-query-source-view-to-react' into migrate-query-…
gabrieldutra Dec 26, 2019
7bbba78
Add cancelExecution
gabrieldutra Dec 26, 2019
efbd7cb
Merge branch 'migrate-query-source-view-to-react' into migrate-query-…
gabrieldutra Dec 28, 2019
f22d8e2
Add shortcut for Query Execution
gabrieldutra Dec 28, 2019
f39cd7d
Remove query-view.less
gabrieldutra Dec 29, 2019
2b1c672
Merge branch 'migrate-query-source-view-to-react' into migrate-query-…
gabrieldutra Dec 29, 2019
3f6d887
Move query execution status block to card
kravets-levko Dec 29, 2019
c52bd2c
Extract QueryView Execute button to a component and fix tooltip issues
kravets-levko Dec 29, 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
206 changes: 143 additions & 63 deletions client/app/pages/queries/QueryView.jsx
Original file line number Diff line number Diff line change
@@ -1,97 +1,177 @@
import React, { useMemo, useState, useEffect } from "react";
import React, { useMemo, useState, useEffect, useCallback } 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 { QueryControlDropdown } from "@/components/EditVisualizationButton/QueryControlDropdown";
import { EditVisualizationButton } from "@/components/EditVisualizationButton";

import { DataSource } from "@/services/data-source";
import { pluralize, durationHumanize } from "@/filters";

import { IMG_ROOT, DataSource } from "@/services/data-source";
import QueryPageHeader from "./components/QueryPageHeader";
import QueryVisualizationTabs from "./components/QueryVisualizationTabs";
import { EditVisualizationButton } from "@/components/EditVisualizationButton";
import useQueryResult from "@/lib/hooks/useQueryResult";
import { pluralize } from "@/filters";
import QueryExecutionStatus from "./components/QueryExecutionStatus";
import QueryMetadata from "./components/QueryMetadata";
import QueryViewExecuteButton from "./components/QueryViewExecuteButton";

import useVisualizationTabHandler from "./hooks/useVisualizationTabHandler";
import useQueryExecute from "./hooks/useQueryExecute";
import useUpdateQueryDescription from "./hooks/useUpdateQueryDescription";
import useQueryFlags from "./hooks/useQueryFlags";
import useQueryParameters from "./hooks/useQueryParameters";
import useAddToDashboardDialog from "./hooks/useAddToDashboardDialog";
import useEmbedDialog from "./hooks/useEmbedDialog";
import useEditScheduleDialog from "./hooks/useEditScheduleDialog";
import useEditVisualizationDialog from "./hooks/useEditVisualizationDialog";
import useDeleteVisualization from "./hooks/useDeleteVisualization";

function QueryView({ query }) {
const canEdit = useMemo(() => currentUser.canEdit(query) || query.can_edit, [query]);
const [selectedTab, setSelectedTab] = useVisualizationTabHandler(query.visualizations);
const parameters = useMemo(() => query.getParametersDefs(), [query]);
function QueryView(props) {
const [query, setQuery] = useState(props.query);
const [dataSource, setDataSource] = useState();
const queryResult = useMemo(() => query.getQueryResult(), [query]);
const queryResultData = useQueryResult(queryResult);
const queryFlags = useQueryFlags(query, dataSource);
const [parameters, areParametersDirty, updateParametersDirtyFlag] = useQueryParameters(query);
const [selectedVisualization, setSelectedVisualization] = useVisualizationTabHandler(query.visualizations);

const {
queryResult,
queryResultData,
isQueryExecuting,
isExecutionCancelling,
executeQuery,
cancelExecution,
} = useQueryExecute(query);

const updateQueryDescription = useUpdateQueryDescription(query, setQuery);
const openAddToDashboardDialog = useAddToDashboardDialog(query);
const openEmbedDialog = useEmbedDialog(query);
const editSchedule = useEditScheduleDialog(query, setQuery);
const addVisualization = useEditVisualizationDialog(query, queryResult, (newQuery, visualization) => {
setQuery(newQuery);
setSelectedVisualization(visualization.id);
});
const editVisualization = useEditVisualizationDialog(query, queryResult, newQuery => setQuery(newQuery));
const deleteVisualization = useDeleteVisualization(query, setQuery);

const canExecuteQuery = useMemo(() => queryFlags.canExecute && !isQueryExecuting && !areParametersDirty, [
isQueryExecuting,
areParametersDirty,
queryFlags.canExecute,
]);

const doExecuteQuery = useCallback(() => {
if (!canExecuteQuery) {
return;
}
executeQuery();
}, [canExecuteQuery, executeQuery]);

useEffect(() => {
document.title = query.name;
}, [query.name]);

useEffect(() => {
DataSource.get({ id: query.data_source_id }).$promise.then(setDataSource);
}, [query]);
}, [query.data_source_id]);

return (
<div className="query-page-wrapper">
<div className="container">
<QueryPageHeader query={query} />
<QueryPageHeader query={query} onChange={setQuery} selectedVisualization={selectedVisualization} />
<div className="query-metadata tiled bg-white p-15">
<EditInPlace
className="w-100"
value={query.description}
isEditable={canEdit}
isEditable={queryFlags.canEdit}
onDone={updateQueryDescription}
placeholder="Add description"
ignoreBlanks
ignoreBlanks={false}
editorProps={{ autosize: { minRows: 2, maxRows: 4 } }}
Copy link
Collaborator

Choose a reason for hiding this comment

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

That's exactly what I had in mind 👍

multiline
/>
<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>
<QueryMetadata layout="horizontal" query={query} dataSource={dataSource} onEditSchedule={editSchedule} />
</div>
<div className="query-content tiled bg-white p-15 m-t-15">
{query.hasParameters() && <Parameters parameters={parameters} />}
<QueryVisualizationTabs
queryResult={queryResult}
visualizations={query.visualizations}
showNewVisualizationButton={query.can_edit}
canDeleteVisualizations={query.can_edit}
selectedTab={selectedTab}
onChangeTab={setSelectedTab}
/>
<Divider />
{query.hasParameters() && (
<Parameters
parameters={parameters}
onValuesChange={() => {
updateParametersDirtyFlag(false);
executeQuery();
}}
onPendingValuesChange={() => updateParametersDirtyFlag()}
/>
)}
{queryResult && queryResultData.status !== "done" && (
<div className="query-alerts m-t-15 m-b-15">
<QueryExecutionStatus
status={queryResultData.status}
updatedAt={queryResultData.updatedAt}
error={queryResultData.error}
isCancelling={isExecutionCancelling}
onCancel={cancelExecution}
/>
</div>
)}
{queryResultData.status === "done" && (
<>
<QueryVisualizationTabs
queryResult={queryResult}
visualizations={query.visualizations}
showNewVisualizationButton={queryFlags.canEdit}
canDeleteVisualizations={queryFlags.canEdit}
selectedTab={selectedVisualization}
onChangeTab={setSelectedVisualization}
onAddVisualization={addVisualization}
onDeleteVisualization={deleteVisualization}
/>
<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>
{queryResultData.status === "done" && (
<>
{queryFlags.canEdit && (
<EditVisualizationButton
openVisualizationEditor={editVisualization}
selectedTab={selectedVisualization}
/>
)}
<QueryControlDropdown
query={query}
queryResult={queryResult}
queryExecuting={isQueryExecuting}
showEmbedDialog={openEmbedDialog}
embed={false}
apiKey={query.api_key}
selectedTab={selectedVisualization}
openAddToDashboardForm={openAddToDashboardDialog}
/>
<span className="m-l-10">
<strong>{queryResultData.rows.length}</strong> {pluralize("row", queryResultData.rows.length)}
</span>
)}
</div>
<Button type="primary">Execute</Button>
<span className="m-l-10">
<strong>{durationHumanize(queryResult.getRuntime())}</strong>
<span className="hidden-xs"> runtime</span>
</span>
</>
)}
<span className="flex-fill" />
{queryResultData.status === "done" && (
<span className="m-r-10 hidden-xs">
Updated <TimeAgo date={queryResult.query_result.retrieved_at} />
</span>
)}
<QueryViewExecuteButton
shortcut="mod+enter, alt+enter"
disabled={!canExecuteQuery || isQueryExecuting}
onClick={doExecuteQuery}>
Execute
</QueryViewExecuteButton>
</div>
</div>
</div>
Expand Down
10 changes: 10 additions & 0 deletions client/app/pages/queries/components/QueryMetadata.less
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@
}
}

@media (min-width: 1000px) {
align-items: flex-start;
justify-content: flex-start;

.query-metadata-item:last-child {
flex: 1 1 auto;
text-align: right;
}
}

.query-metadata-item {
padding: 5px;

Expand Down
60 changes: 60 additions & 0 deletions client/app/pages/queries/components/QueryViewExecuteButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { useState, useMemo, useEffect } from "react";
import PropTypes from "prop-types";
import Button from "antd/lib/button";
import Tooltip from "antd/lib/tooltip";
import { KeyboardShortcuts, humanReadableShortcut } from "@/services/keyboard-shortcuts";

export default function QueryViewExecuteButton({ shortcut, disabled, children, onClick }) {
const [tooltipVisible, setTooltipVisible] = useState(false);

const eventHandlers = useMemo(
() => ({
onMouseEnter: () => setTooltipVisible(true),
onMouseLeave: () => setTooltipVisible(false),
}),
[]
);

useEffect(() => {
if (disabled) {
setTooltipVisible(false);
}
}, [disabled]);

useEffect(() => {
if (shortcut) {
const shortcuts = {
[shortcut]: onClick,
};

KeyboardShortcuts.bind(shortcuts);
return () => {
KeyboardShortcuts.unbind(shortcuts);
};
}
}, [shortcut, onClick]);

return (
<Tooltip placement="top" title={humanReadableShortcut(shortcut, 1)} visible={tooltipVisible}>
<span {...eventHandlers}>
<Button type="primary" disabled={disabled} onClick={onClick} style={disabled ? { pointerEvents: "none" } : {}}>
{children}
</Button>
</span>
</Tooltip>
);
}

QueryViewExecuteButton.propTypes = {
shortcut: PropTypes.string,
disabled: PropTypes.bool,
children: PropTypes.node,
onClick: PropTypes.func,
};

QueryViewExecuteButton.defaultProps = {
shortcut: null,
disabled: false,
children: null,
onClick: () => {},
};
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ export default function QueryVisualizationTabs({
onDelete={() => onDeleteVisualization(visualization.id)}
/>
}>
<VisualizationRenderer visualization={visualization} queryResult={queryResult} context="query" />
{queryResult && (
<VisualizationRenderer visualization={visualization} queryResult={queryResult} context="query" />
)}
</TabPane>
))}
</Tabs>
Expand Down
2 changes: 1 addition & 1 deletion client/app/pages/queries/hooks/useQueryExecute.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default function useQueryExecute(query) {
initializeQueryResultRef.current = noop;

const queryResultData = useQueryResult(queryResult);
const isQueryExecuting = useMemo(() => queryResult && !includes(["done", "failed"], queryResultData.status), [
const isQueryExecuting = useMemo(() => !!queryResult && !includes(["done", "failed"], queryResultData.status), [
queryResult,
queryResultData.status,
]);
Expand Down