Skip to content
Snippets Groups Projects
Commit 9ab39a90 authored by Tom Robinson's avatar Tom Robinson
Browse files

Merge branch 'master' of github.com:metabase/metabase into merge-release-0.34.x

parents 9e21946a 91ecd8e5
No related branches found
No related tags found
No related merge requests found
......@@ -4,6 +4,7 @@ import React, { Component } from "react";
import { connect } from "react-redux";
import { push } from "react-router-redux";
import title from "metabase/hoc/Title";
import titleWithLoadingTime from "metabase/hoc/TitleWithLoadingTime";
import Dashboard from "metabase/dashboard/components/Dashboard";
......@@ -21,6 +22,7 @@ import {
getEditingParameter,
getParameters,
getParameterValues,
getLoadingStartTime,
} from "../selectors";
import { getDatabases, getMetadata } from "metabase/selectors/metadata";
import { getUserIsAdmin } from "metabase/selectors/user";
......@@ -47,6 +49,7 @@ const mapStateToProps = (state, props) => {
parameters: getParameters(state, props),
parameterValues: getParameterValues(state, props),
metadata: getMetadata(state),
loadingStartTime: getLoadingStartTime(state),
};
};
......@@ -67,6 +70,7 @@ type DashboardAppState = {
mapDispatchToProps,
)
@title(({ dashboard }) => dashboard && dashboard.name)
@titleWithLoadingTime("loadingStartTime")
// NOTE: should use DashboardControls and DashboardData HoCs here?
export default class DashboardApp extends Component {
state: DashboardAppState = {
......
......@@ -1168,6 +1168,52 @@ const parameterValues = handleActions(
{},
);
const loadingDashCards = handleActions(
{
[FETCH_DASHBOARD]: {
next: (state, { payload }) => ({
...state,
dashcardIds: Object.values(payload.entities.dashcard || {}).map(
dc => dc.id,
),
}),
},
[FETCH_DASHBOARD_CARD_DATA]: {
next: state => ({
...state,
loadingIds: state.dashcardIds,
startTime:
state.dashcardIds.length > 0 &&
// check that performance is defined just in case
typeof performance === "object"
? performance.now()
: null,
}),
},
[FETCH_CARD_DATA]: {
next: (state, { payload: { dashcard_id } }) => {
const loadingIds = state.loadingIds.filter(id => id !== dashcard_id);
return {
...state,
loadingIds,
...(loadingIds.length === 0 ? { startTime: null } : {}),
};
},
},
[CANCEL_FETCH_CARD_DATA]: {
next: (state, { payload: { dashcard_id } }) => {
const loadingIds = state.loadingIds.filter(id => id !== dashcard_id);
return {
...state,
loadingIds,
...(loadingIds.length === 0 ? { startTime: null } : {}),
};
},
},
},
{ dashcardIds: [], loadingIds: [], startTime: null },
);
export default combineReducers({
dashboardId,
isEditing,
......@@ -1179,4 +1225,5 @@ export default combineReducers({
dashcardData,
slowCards,
parameterValues,
loadingDashCards,
});
......@@ -44,6 +44,8 @@ export const getCardData = state => state.dashboard.dashcardData;
export const getSlowCards = state => state.dashboard.slowCards;
export const getCardIdList = state => state.dashboard.cardList;
export const getParameterValues = state => state.dashboard.parameterValues;
export const getLoadingStartTime = state =>
state.dashboard.loadingDashCards.startTime;
export const getDashboard = createSelector(
[getDashboardId, getDashboards],
......
......@@ -4,36 +4,14 @@ import _ from "underscore";
const componentStack = [];
let SEPARATOR = " · ";
let HIERARCHICAL = true;
let BASE_NAME = null;
export const setSeparator = separator => (SEPARATOR = separator);
export const setHierarchical = hierarchical => (HIERARCHICAL = hierarchical);
export const setBaseName = baseName => (BASE_NAME = baseName);
const SEPARATOR = " · ";
const updateDocumentTitle = _.debounce(() => {
if (HIERARCHICAL) {
document.title = componentStack
.map(component => component._documentTitle)
.filter(title => title)
.reverse()
.join(SEPARATOR);
} else {
// update with the top-most title
for (let i = componentStack.length - 1; i >= 0; i--) {
let title = componentStack[i]._documentTitle;
if (title) {
if (BASE_NAME) {
title += SEPARATOR + BASE_NAME;
}
if (document.title !== title) {
document.title = title;
}
break;
}
}
}
document.title = componentStack
.map(component => component._documentTitle)
.filter(title => title)
.reverse()
.join(SEPARATOR);
});
const title = documentTitleOrGetter => ComposedComponent =>
......@@ -64,7 +42,19 @@ const title = documentTitleOrGetter => ComposedComponent =>
if (typeof documentTitleOrGetter === "string") {
this._documentTitle = documentTitleOrGetter;
} else if (typeof documentTitleOrGetter === "function") {
this._documentTitle = documentTitleOrGetter(this.props);
const result = documentTitleOrGetter(this.props);
if (result == null) {
// title functions might return null before data is loaded
this._documentTitle = "";
} else if (typeof result === "string") {
this._documentTitle = result;
} else if (typeof result === "object") {
// The getter can return an object with a `refresh` promise along with
// the title. When that promise resolves, we call
// `documentTitleOrGetter` again.
this._documentTitle = result.title;
result.refresh.then(() => this._updateDocumentTitle());
}
}
updateDocumentTitle();
}
......
import React from "react";
import { delay } from "metabase/lib/promise";
import title from "metabase/hoc/Title";
const SECONDS_UNTIL_DISPLAY = 10;
export default startTimePropName => ComposedComponent =>
title(({ [startTimePropName]: startTime }) => {
if (startTime == null) {
return "";
}
const totalSeconds = (performance.now() - startTime) / 1000;
const title =
totalSeconds < SECONDS_UNTIL_DISPLAY
? "" // don't display the title until SECONDS_UNTIL_DISPLAY have elapsed
: [totalSeconds / 60, totalSeconds % 60] // minutes, seconds
.map(Math.floor) // round both down
.map(x => (x < 10 ? `0${x}` : `${x}`)) // pad with "0" to two digits
.join(":"); // separate with ":"
return { title, refresh: delay(100) };
})(
// remove the start time prop to prevent affecting child components
({ [startTimePropName]: _removed, ...props }) => (
<ComposedComponent {...props} />
),
);
......@@ -14,6 +14,7 @@ import View from "../components/view/View";
// import Notebook from "../components/notebook/Notebook";
import title from "metabase/hoc/Title";
import titleWithLoadingTime from "metabase/hoc/TitleWithLoadingTime";
import {
getCard,
......@@ -42,6 +43,7 @@ import {
getQuestion,
getOriginalQuestion,
getSettings,
getQueryStartTime,
getRawSeries,
getQuestionAlerts,
getVisualizationSettings,
......@@ -140,6 +142,7 @@ const mapStateToProps = (state, props) => {
state,
props,
),
queryStartTime: getQueryStartTime(state),
};
};
......@@ -153,6 +156,7 @@ const mapDispatchToProps = {
mapDispatchToProps,
)
@title(({ card }) => (card && card.name) || t`Question`)
@titleWithLoadingTime("queryStartTime")
@fitViewport
export default class QueryBuilder extends Component {
timeout: any;
......
......@@ -303,6 +303,16 @@ export const cancelQueryDeferred = handleActions(
null,
);
export const queryStartTime = handleActions(
{
[RUN_QUERY]: { next: (state, { payload }) => performance.now() },
[CANCEL_QUERY]: { next: (state, { payload }) => null },
[QUERY_COMPLETED]: { next: (state, { payload }) => null },
[QUERY_ERRORED]: { next: (state, { payload }) => null },
},
null,
);
export const parameterValues = handleActions(
{
[SET_PARAMETER_VALUE]: {
......
......@@ -50,6 +50,8 @@ export const getSettings = state => state.settings.values;
export const getIsNew = state => state.qb.card && !state.qb.card.id;
export const getQueryStartTime = state => state.qb.queryStartTime;
export const getDatabaseId = createSelector(
[getCard],
card => card && card.dataset_query && card.dataset_query.database,
......
......@@ -32,7 +32,7 @@ describe("pulse", () => {
cy.visit("/pulse/create");
cy.get('[placeholder="Important metrics"]')
.wait(10)
.wait(100)
.type("pulse title");
cy.contains("Select a question").click();
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment