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 pages to React #4429

Merged
merged 35 commits into from
Jan 6, 2020
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
032e04e
Migrate Query Source View page to React: skeleton
kravets-levko Dec 3, 2019
0cf9d98
Merge branch 'master' into migrate-query-source-view-to-react
kravets-levko Dec 11, 2019
d67c12f
Merge branch 'master' into migrate-query-source-view-to-react
gabrieldutra Dec 12, 2019
e61c628
Sync QueryView and QuerySource (#4430)
gabrieldutra Dec 13, 2019
f9bb4ad
Migrate schema browser to react (#4432)
kravets-levko Dec 16, 2019
aab7cd5
Merge branch 'master' into migrate-query-source-view-to-react
gabrieldutra Dec 16, 2019
bf9413d
Restyle code with Prettier
gabrieldutra Dec 16, 2019
f165631
Migrate Query page to React: Save changes (#4452)
kravets-levko Dec 16, 2019
192ad22
Migrate query source to React: Set of updates (#4457)
kravets-levko Dec 17, 2019
c1d49ce
Migrate Query page to React: Visualization Tabs (#4453)
gabrieldutra Dec 17, 2019
b8226b0
Migrate Query Source page to React: Visualizations area (#4463)
kravets-levko Dec 17, 2019
236b4c5
Migrate Query page to React: Delete visualization button (#4461)
gabrieldutra Dec 18, 2019
af5b224
Merge branch 'master' into migrate-query-source-view-to-react
kravets-levko Dec 20, 2019
c0ea3be
Migrate Query Source page to React: Visualization actions (#4467)
gabrieldutra Dec 20, 2019
e6e213a
Migrate Query pages to React: Execute query hook (#4470)
gabrieldutra Dec 20, 2019
cad0c90
Migrate Query Source page to React: Editor area (#4468)
kravets-levko Dec 24, 2019
a1e22d7
Migrate Query Source page to React: metadata, schedule and descriptio…
kravets-levko Dec 25, 2019
b943cf1
Merge branch 'master' into migrate-query-source-view-to-react
gabrieldutra Dec 26, 2019
bea9007
Migrate Query page to React: Cancel query execution (#4496)
kravets-levko Dec 26, 2019
81b712e
Migrate Query Source page to React: refine code (#4499)
kravets-levko Dec 27, 2019
3d5f5b9
Migrate Query Source page to React: alerts (#4504)
kravets-levko Dec 29, 2019
2a06152
Migrate Query Source page to React: unsaved changes alert (#4505)
kravets-levko Dec 29, 2019
6d98d84
Migrate Query Source to React: resizable areas (v2) (#4503)
kravets-levko Dec 29, 2019
bc5ad65
Migrate Query page to React: Query View (#4455)
gabrieldutra Dec 29, 2019
2484bc0
Switch React and Angular versions of pages (until Angular version rem…
kravets-levko Dec 29, 2019
db82b84
Migrate Query pages to React: fix permissions (#4506)
kravets-levko Dec 29, 2019
cd18058
Migrate Query Source page to React: don't reload when saving new quer…
kravets-levko Dec 29, 2019
d13714d
Migrate Query pages to React: fix tests (#4509)
kravets-levko Dec 30, 2019
1c11972
Merge branch 'master' into migrate-query-source-view-to-react
gabrieldutra Dec 31, 2019
014de20
Use skipParametersDirtyFlag in executeQuery
gabrieldutra Dec 31, 2019
0c1b45f
Fix: cannot fork query from Query View page
kravets-levko Jan 2, 2020
2934e53
Optimize query editor: handle query text changes faster
kravets-levko Jan 2, 2020
e2322ab
Revert "Optimize query editor: handle query text changes faster"
gabrieldutra Jan 2, 2020
569a80f
Reduce debounced time to 100
gabrieldutra Jan 2, 2020
9729b72
Migrate Query pages to React: cleanup (#4512)
kravets-levko Jan 6, 2020
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
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;
}
}
}

Expand Down
8 changes: 8 additions & 0 deletions client/app/assets/less/inc/schema-browser.less
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ div.table-name {
overflow-x: hidden;
border: none;
margin-top: 10px;
position: relative;
height: 100%;

.collapse.in {
background: transparent;
Expand Down Expand Up @@ -72,8 +74,14 @@ div.table-name {

.schema-control {
display: flex;
flex-wrap: nowrap;
padding: 0;

.ant-btn {
height: auto;
}

// ANGULAR_REMOVE_ME
.form-control {
margin-right: 5px;
}
Expand Down
16 changes: 11 additions & 5 deletions client/app/assets/less/redash/query.less
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,6 @@ a.label-tag {
min-width: 10px;
overflow-x: hidden;

.schema-container {
}

.editor__left__data-source,
.schema-control,
.query-metadata--history,
Expand All @@ -309,6 +306,16 @@ a.label-tag {
padding: 15px;
}

.editor__left__data-source {
.ant-select {
.ant-select-selection-selected-value {
img, span {
vertical-align: middle;
}
}
}
}

.editor__left__schema {
flex-grow: 1;
display: flex;
Expand All @@ -317,7 +324,7 @@ a.label-tag {
padding-top: 0 !important;
position: relative;

schema-browser {
.schema-container {
position: absolute;
left: 15px;
top: 0;
Expand Down Expand Up @@ -546,7 +553,6 @@ nav .rg-bottom {

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

Expand Down
5 changes: 4 additions & 1 deletion client/app/components/FavoritesControl.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ 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
222 changes: 222 additions & 0 deletions client/app/pages/queries/QuerySource.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import { filter, find, map, clone, includes } from "lodash";
import React, { useState, useRef, useEffect, useMemo, useCallback } from "react";
import PropTypes from "prop-types";
import { react2angular } from "react2angular";
import Select from "antd/lib/select";
import { routesToAngularRoutes } from "@/lib/utils";
import { Query } from "@/services/query";
import { DataSource, SCHEMA_NOT_SUPPORTED } from "@/services/data-source";
import notification from "@/services/notification";
import recordEvent from "@/services/recordEvent";

import QueryPageHeader from "./components/QueryPageHeader";
import SchemaBrowser from "./components/SchemaBrowser";

import "./query-source.less";

function getSchema(dataSource, refresh = undefined) {
if (!dataSource) {
return Promise.resolve([]);
}

return dataSource
.getSchema(refresh)
.then(data => {
if (data.schema) {
return data.schema;
} else if (data.error.code === SCHEMA_NOT_SUPPORTED) {
return [];
}
return Promise.reject(new Error("Schema refresh failed."));
})
.catch(() => {
notification.error("Schema refresh failed.", "Please try again later.");
return Promise.resolve([]);
});
}

function chooseDataSourceId(dataSourceIds, availableDataSources) {
dataSourceIds = map(dataSourceIds, v => parseInt(v, 10));
availableDataSources = map(availableDataSources, ds => ds.id);
return find(dataSourceIds, id => includes(availableDataSources, id)) || null;
}

function QuerySource(props) {
const [query, setQuery] = useState(props.query);
const [allDataSources, setAllDataSources] = useState([]);
const [dataSourcesLoaded, setDataSourcesLoaded] = useState(false);
const dataSources = useMemo(() => filter(allDataSources, ds => !ds.view_only || ds.id === query.data_source_id), [
allDataSources,
query.data_source_id,
]);
const dataSource = useMemo(() => find(dataSources, { id: query.data_source_id }) || null, [query, dataSources]);
const [schema, setSchema] = useState([]);
const refreshSchemaTokenRef = useRef(null);

useEffect(() => {
let cancelDataSourceLoading = false;
DataSource.query().$promise.then(data => {
if (!cancelDataSourceLoading) {
setDataSourcesLoaded(true);
setAllDataSources(data);
}
});

return () => {
// cancel pending operations
cancelDataSourceLoading = true;
refreshSchemaTokenRef.current = null;
};
}, []);

const reloadSchema = useCallback(
(refresh = undefined) => {
const refreshToken = Math.random()
.toString(36)
.substr(2);
refreshSchemaTokenRef.current = refreshToken;
getSchema(dataSource, refresh).then(data => {
if (refreshSchemaTokenRef.current === refreshToken) {
setSchema(data);
}
});
},
[dataSource]
);

useEffect(() => {
reloadSchema();
}, [reloadSchema]);

useEffect(() => {
recordEvent("view_source", "query", query.id);
}, [query.id]);

const handleDataSourceChange = useCallback(
dataSourceId => {
if (dataSourceId) {
try {
localStorage.setItem("lastSelectedDataSourceId", dataSourceId);
} catch (e) {
// `localStorage.setItem` may throw exception if there are no enough space - in this case it could be ignored
}
}
if (query.data_source_id !== dataSourceId) {
recordEvent("update_data_source", "query", query.id, { dataSourceId });
const newQuery = clone(query);
newQuery.data_source_id = dataSourceId;
newQuery.latest_query_data = null;
newQuery.latest_query_data_id = null;
setQuery(newQuery);
// TODO: Save if not new
}
},
[query]
);

useEffect(() => {
// choose data source id for new queries
if (dataSourcesLoaded && query.isNew()) {
const firstDataSourceId = dataSources.length > 0 ? dataSources[0].id : null;
handleDataSourceChange(
chooseDataSourceId(
[query.data_source_id, localStorage.getItem("lastSelectedDataSourceId"), firstDataSourceId],
dataSources
)
);
}
}, [query, dataSourcesLoaded, dataSources, handleDataSourceChange]);

return (
<div className="query-page-wrapper">
<div className="container">
<QueryPageHeader query={query} sourceMode onChange={setQuery} />
</div>
<main className="query-fullscreen">
<nav>
<div className="editor__left__data-source">
<Select
className="w-100"
placeholder="Choose data source..."
value={dataSource ? dataSource.id : undefined}
disabled={!query.can_edit || !dataSourcesLoaded || dataSources.length === 0}
loading={!dataSourcesLoaded}
optionFilterProp="data-name"
showSearch
onChange={handleDataSourceChange}>
{map(dataSources, ds => (
<Select.Option key={`ds-${ds.id}`} value={ds.id} data-name={ds.name}>
<img src={`/static/images/db-logos/${ds.type}.png`} width="20" alt={ds.name} />
<span>{ds.name}</span>
</Select.Option>
))}
</Select>
</div>
<div className="editor__left__schema">
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
<SchemaBrowser schema={schema} onRefresh={() => reloadSchema(true)} />
</div>
</nav>

<div className="content">
<div className="flex-fill p-relative">
<div
className="p-absolute d-flex flex-column p-l-15 p-r-15"
style={{ left: 0, top: 0, right: 0, bottom: 0, overflow: "auto" }}>
<div className="row editor" style={{ minHeight: "11px", maxHeight: "70vh" }}>
Editor
</div>
<section className="flex-fill p-relative t-body query-visualizations-wrapper">Visualizations</section>
</div>
</div>
</div>
</main>
</div>
);
}

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

export default function init(ngModule) {
ngModule.component("pageQuerySource", react2angular(QuerySource));

return {
...routesToAngularRoutes(
[
{
path: "/queries/new2",
},
],
{
layout: "fixed",
reloadOnSearch: false,
template: '<page-query-source ng-if="$resolve.query" query="$resolve.query"></page-query-source>',
resolve: {
query: () => Query.newQuery(),
},
}
),
...routesToAngularRoutes(
[
{
path: "/queries/:queryId/source2",
},
],
{
layout: "fixed",
reloadOnSearch: false,
template: '<page-query-source ng-if="$resolve.query" query="$resolve.query"></page-query-source>',
resolve: {
query: $route => {
"ngInject";

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

init.init = true;
Loading