Skip to content
Snippets Groups Projects
Commit 6532383a authored by Atte Keinänen's avatar Atte Keinänen
Browse files

WIP

parent 5d5872f1
Branches
Tags
No related merge requests found
......@@ -96,11 +96,17 @@ export default class Query {
// AGGREGATIONS
/* convenience for questions with a single aggregation */
aggregation(): Aggregation {
return aggregations()[0];
}
aggregations(): Aggregation[] {
return Q.getAggregations(this.query());
}
aggregationOptions(): any[] {
return [];
// legacy
return this.tableMetadata().aggregation_options;
}
canAddAggregation(): boolean {
return false;
......
......@@ -11,7 +11,7 @@ import Action, { ActionClick } from "./Action";
import type { ParameterId } from "metabase/meta/types/Parameter";
import type { Metadata as MetadataObject } from "metabase/meta/types/Metadata";
import type { Card as CardObject } from "metabase/meta/types/Card";
import type {Card as CardObject, DatasetQuery} from "metabase/meta/types/Card";
import * as Q from "metabase/lib/query/query";
......@@ -87,6 +87,15 @@ export default class Question {
return this._queries[0];
}
datasetQuery(): DatasetQuery {
return this._card && this._card.dataset_query;
}
display(): string {
return this._card && this._card.display;
}
/**
* Question is valid (as far as we know) and can be executed
*/
......@@ -99,6 +108,10 @@ export default class Question {
return true;
}
canWrite(): boolean {
return this._card && this._card.can_write;
}
metrics(): Query[] {
return this._queries;
}
......@@ -194,7 +207,25 @@ export default class Question {
}
}
// Information
/**
* A user-defined name for the question
*/
displayName(): string {
return this._card && this._card.name;
}
id(): string {
return this._card && this._card.id
}
isSaved(): boolean {
return !!this.id();
}
publicUUID(): string {
return this._card && this._card.public_uuid;
}
getUrl(): string {
return "";
}
......
import React, { Component } from "react";
import React from "react";
import IconBorder from "metabase/components/IconBorder";
import Icon from "metabase/components/Icon";
......
......@@ -83,13 +83,14 @@ export default class CardEditor extends Component {
};
renderMetricSection() {
const { features, query } = this.props;
const { features, question } = this.props;
if (!features.aggregation && !features.breakout) {
return;
}
if (query.isEditable()) {
// TODO Atte Keinänen 5/25/17 How should `isEditable` work for multimetric questions?
if (question.query().isEditable()) {
return <MetricList {...this.props} />
} else {
// TODO: move this into AggregationWidget?
......@@ -103,16 +104,15 @@ export default class CardEditor extends Component {
renderButtons = () => {
// NOTE: Most of stuff is replicated from QueryVisualization header
const { isResultDirty, isAdmin, card, result, setQueryModeFn, tableMetadata, isRunnable, isRunning, runQuery, cancelQuery } = this.props;
const { question, isResultDirty, isAdmin, result, setQueryModeFn, tableMetadata, isRunnable, isRunning, runQuery, cancelQuery } = this.props;
const isSaved = card.id != null;
const isPublicLinksEnabled = MetabaseSettings.get("public_sharing");
const isEmbeddingEnabled = MetabaseSettings.get("embedding");
const getQueryModeButton = () =>
<QueryModeButton
key="queryModeToggle"
mode={card.dataset_query.type}
mode={question.datasetQuery().type}
allowNativeToQuery={false}
allowQueryToNative={false}
/*allowNativeToQuery={isNew && !isDirty}
......@@ -131,12 +131,12 @@ export default class CardEditor extends Component {
<QueryDownloadWidget
key="querydownload"
className="hide sm-show"
card={card}
card={question.card()}
result={result}
/>;
const getQueryEmbedWidget = () =>
<QuestionEmbedWidget key="questionembed" className="hide sm-show" card={card} />;
<QuestionEmbedWidget key="questionembed" className="hide sm-show" card={question.card()} />;
const getRunButton = () => {
......@@ -162,8 +162,8 @@ export default class CardEditor extends Component {
const queryHasCleanResult = !isResultDirty && result && !result.error;
const isEmbeddable = isSaved && (
(isPublicLinksEnabled && (isAdmin || card.public_uuid)) ||
const isEmbeddable = question.isSaved() && (
(isPublicLinksEnabled && (isAdmin || question.card().public_uuid)) ||
(isEmbeddingEnabled && isAdmin)
);
......
......@@ -27,8 +27,31 @@ import * as Urls from "metabase/lib/urls";
import cx from "classnames";
import _ from "underscore";
import Button from "metabase/components/Button";
import type Question from "metabase-lib/lib/Question";
import type {Card} from "metabase/meta/types/Card";
type Props = {
question: Question,
// TODO Atte Keinänen 5/25/17: Replace originalCard with `Question` object
originalCard?: Card,
isEditing: boolean,
// tableMetadata isn't present if the query is a native query or the metadata is still loading
tableMetadata?: TableMetadata,
onSetCardAttribute: (string, any) => void,
reloadCardFn: () => void,
// TODO Atte Keinänen 5/25/17: Define a union type for allowed query modes
setQueryModeFn: (string) => void,
isShowingDataReference: boolean,
toggleDataReferenceFn: () => void,
onChangeLocation: (string) => void,
isNew: boolean,
isEditing: boolean,
isDirty: boolean
}
export default class CardHeader extends Component {
props: Props;
constructor(props, context) {
super(props, context);
......@@ -38,21 +61,7 @@ export default class CardHeader extends Component {
revisions: null
};
}
static propTypes = {
card: PropTypes.object.isRequired,
originalCard: PropTypes.object,
isEditing: PropTypes.bool.isRequired,
tableMetadata: PropTypes.object, // can't be required, sometimes null
onSetCardAttribute: PropTypes.func.isRequired,
reloadCardFn: PropTypes.func.isRequired,
setQueryModeFn: PropTypes.func.isRequired,
isShowingDataReference: PropTypes.bool.isRequired,
toggleDataReferenceFn: PropTypes.func.isRequired,
// isNew: PropTypes.bool.isRequired,
// isDirty: PropTypes.bool.isRequired
};
componentWillUnmount() {
clearTimeout(this.timeout);
if (this.requestPromise) {
......@@ -66,11 +75,11 @@ export default class CardHeader extends Component {
this.timeout = setTimeout(() =>
this.setState({ recentlySaved: null })
, 5000);
}
};
// onCreate = (card, addToDash) => {
// if (card.dataset_query.query) {
// Query.cleanQuery(card.dataset_query.query);
// onCreate = (question, addToDash) => {
// if (question.datasetQuery().query) {
// Query.cleanQuery(question.datasetQuery().query);
// }
//
// // TODO: reduxify
......@@ -88,25 +97,28 @@ export default class CardHeader extends Component {
onSave = (card, addToDash) => {
// MBQL->NATIVE
// if we are a native query with an MBQL query definition, remove the old MBQL stuff (happens when going from mbql -> native)
// if (card.dataset_query.type === "native" && card.dataset_query.query) {
// delete card.dataset_query.query;
// } else if (card.dataset_query.type === "query" && card.dataset_query.native) {
// delete card.dataset_query.native;
// if (question.datasetQuery().type === "native" && question.datasetQuery().query) {
// delete question.datasetQuery().query;
// } else if (question.datasetQuery().type === "query" && question.datasetQuery().native) {
// delete question.datasetQuery().native;
// }
if (card.dataset_query.query) {
Query.cleanQuery(card.dataset_query.query);
const { fromUrl, notifyCardUpdatedFn } = this.props;
const datasetQuery = card.dataset_query;
if (datasetQuery.query) {
Query.cleanQuery(datasetQuery.query);
}
// TODO: reduxify
this.requestPromise = cancelable(CardApi.update(card));
return this.requestPromise.then(updatedCard => {
if (this.props.fromUrl) {
if (fromUrl) {
this.onGoBack();
return;
}
this.props.notifyCardUpdatedFn(updatedCard);
notifyCardUpdatedFn(updatedCard);
this.setState({
recentlySaved: "updated",
......@@ -163,8 +175,26 @@ export default class CardHeader extends Component {
};
getHeaderButtons = () => {
const { card ,isNew, isDirty, isEditing, databases } = this.props;
const database = _.findWhere(databases, { id: card && card.dataset_query && card.dataset_query.database });
const {
question,
originalCard,
isNew,
isDirty,
isEditing,
databases,
uiControls,
toggleTemplateTagsEditor,
isShowingDataReference,
onSetCardAttribute
} = this.props;
const databaseId = question && question.datasetQuery() && question.datasetQuery().database;
const database = _.findWhere(databases, { id: card && databaseId });
const card = question.card();
// TODO Atte Keinänen 5/20/17 Add multi-query support to all components that need metadata
// This only fetches the table metadata of first available query
const tableMetadata = question.query().tableMetadata();
const SaveNewCardButton = () =>
<ModalWithTrigger
......@@ -174,9 +204,9 @@ export default class CardHeader extends Component {
triggerElement="Save"
>
<SaveQuestionModal
card={this.props.card}
originalCard={this.props.originalCard}
tableMetadata={this.props.tableMetadata}
card={card}
originalCard={originalCard}
tableMetadata={tableMetadata}
addToDashboard={false}
saveFn={this.onSave}
createFn={this.onCreate}
......@@ -204,7 +234,7 @@ export default class CardHeader extends Component {
const SaveEditedCardButton = () =>
<ActionButton
key="save"
actionFn={() => this.onSave(this.props.card, false)}
actionFn={() => this.onSave(card, false)}
className="cursor-pointer text-brand-hover bg-white text-grey-4 text-uppercase"
normalText="SAVE CHANGES"
activeText="Saving…"
......@@ -218,7 +248,7 @@ export default class CardHeader extends Component {
</a>;
const DeleteCardButton = () =>
<ArchiveQuestionModal questionId={this.props.card.id}/>;
<ArchiveQuestionModal questionId={card.id}/>;
const MoveQuestionToCollectionButton = () =>
<ModalWithTrigger
......@@ -231,24 +261,24 @@ export default class CardHeader extends Component {
}
>
<MoveToCollection
questionId={this.props.card.id}
initialCollectionId={this.props.card && this.props.card.collection_id}
questionId={card.id}
initialCollectionId={card && card.collection_id}
setCollection={(questionId, collection) => {
this.props.onSetCardAttribute('collection', collection)
this.props.onSetCardAttribute('collection_id', collection.id)
onSetCardAttribute('collection', collection)
onSetCardAttribute('collection_id', collection.id)
}}
/>
</ModalWithTrigger>;
const ToggleTemplateTagsEditorButton = () => {
const parametersButtonClasses = cx('transition-color', {
'text-brand': this.props.uiControls.isShowingTemplateTagsEditor,
'text-brand-hover': !this.props.uiControls.isShowingTemplateTagsEditor
'text-brand': uiControls.isShowingTemplateTagsEditor,
'text-brand-hover': !uiControls.isShowingTemplateTagsEditor
});
return (
<Tooltip key="parameterEdititor" tooltip="Variables">
<a className={parametersButtonClasses}>
<Icon name="variable" size={16} onClick={this.props.toggleTemplateTagsEditor}/>
<Icon name="variable" size={16} onClick={toggleTemplateTagsEditor}/>
</a>
</Tooltip>
);
......@@ -274,9 +304,9 @@ export default class CardHeader extends Component {
}
>
<SaveQuestionModal
card={this.props.card}
originalCard={this.props.originalCard}
tableMetadata={this.props.tableMetadata}
card={card}
originalCard={originalCard}
tableMetadata={tableMetadata}
addToDashboard={true}
saveFn={this.onSave}
createFn={this.onCreate}
......@@ -294,7 +324,7 @@ export default class CardHeader extends Component {
<HistoryModal
revisions={this.state.revisions}
entityType="card"
entityId={this.props.card.id}
entityId={card.id}
onFetchRevisions={this.onFetchRevisions}
onRevertToRevision={this.onRevertToRevision}
onClose={() => this.refs.cardHistory.toggle()}
......@@ -305,7 +335,7 @@ export default class CardHeader extends Component {
const DataReferenceButton = () => {
const dataReferenceButtonClasses = cx('transition-color', {
'text-brand': this.props.isShowingDataReference,
'text-brand': isShowingDataReference,
'text-brand-hover': !this.state.isShowingDataReference
});
......@@ -320,8 +350,8 @@ export default class CardHeader extends Component {
const isNewCardThatCanBeSaved = isNew && isDirty;
const isSaved = !isNew;
const isEditableSavedCard = isSaved && card.can_write;
const isNativeQuery = Query.isNative(card && card.dataset_query);
const isEditableSavedCard = isSaved && question.canWrite();
const isNativeQuery = Query.isNative(question && question.datasetQuery());
const isNativeQueryWithParameters = isNativeQuery && database && _.contains(database.features, "native-parameters");
const getPersistenceButtons = () => {
......@@ -368,27 +398,28 @@ export default class CardHeader extends Component {
};
render() {
const { question, originalCard, isEditing, isNew, onSetCardAttribute, onChangeLocation } = this.props;
const badgeItemStyle = "text-uppercase flex align-center no-decoration text-bold";
const description = this.props.card ? this.props.card.description : null;
const description = question.card().description;
return (
<div className="relative">
<HeaderBar
isEditing={this.props.isEditing}
name={this.props.isNew ? "New question" : this.props.card.name}
description={this.props.card ? this.props.card.description : null}
breadcrumb={(!this.props.card.id && this.props.originalCard) ? (<span className="pl2">started from <a className="link" onClick={this.onFollowBreadcrumb}>{this.props.originalCard.name}</a></span>) : null }
isEditing={isEditing}
name={isNew ? "New question" : question.card().name}
description={description}
breadcrumb={(!question.card().id && originalCard) ? (<span className="pl2">started from <a className="link" onClick={this.onFollowBreadcrumb}>{originalCard.name}</a></span>) : null }
buttons={this.getHeaderButtons()}
setItemAttributeFn={this.props.onSetCardAttribute}
badge={this.props.card.collection &&
setItemAttributeFn={onSetCardAttribute}
badge={question.card().collection &&
<div className="flex">
<Link
to={Urls.collection(this.props.card.collection)}
to={Urls.collection(question.card().collection)}
className={badgeItemStyle}
style={{color: this.props.card.collection.color, fontSize: 12}}
style={{color: question.card().collection.color, fontSize: 12}}
>
<Icon name="collection" size={14} style={{marginRight: "0.5em"}}/>
{this.props.card.collection.name}
{question.card().collection.name}
</Link>
{ description &&
<div
......@@ -412,9 +443,9 @@ export default class CardHeader extends Component {
<Modal isOpen={this.state.modal === "add-to-dashboard"} onClose={this.onCloseModal}>
<AddToDashSelectDashModal
card={this.props.card}
card={question.card()}
onClose={this.onCloseModal}
onChangeLocation={this.props.onChangeLocation}
onChangeLocation={onChangeLocation}
/>
</Modal>
</div>
......
......@@ -10,13 +10,13 @@ import AddButton from "metabase/components/AddButton";
// TODO: Containerize this component in order to reduce the props passing in QB
const MetricList = ({...props}) => {
const { card, query, setDatasetQuery, tableMetadata, hideAddButton, hideClearButton } = props;
const { question, setDatasetQuery, tableMetadata, hideAddButton, hideClearButton } = props;
const aggregations = query.aggregations();
const metricColors = getCardColors(card);
const metrics = question.metrics();
const metricColors = getCardColors(question.card());
const showAddMetricButton = !hideAddButton && !query.isBareRows();
const canAddMetricToVisualization = _.contains(["line", "area", "bar"], props.card.display);
const showAddMetricButton = !hideAddButton && !question.query().isBareRows();
const canAddMetricToVisualization = _.contains(["line", "area", "bar"], question.display());
const addMetricButton =
<ModalWithTrigger
......@@ -36,12 +36,12 @@ const MetricList = ({...props}) => {
return (
<div className="align-center flex flex-full">
{ [...aggregations.entries()].map(([index, aggregation]) =>
{ metrics.map((metric, index) =>
<MetricWidget
key={"agg" + index}
aggregation={aggregation}
index={index}
query={query}
key={"metric" + index}
question={question}
metric={metric}
metricIndex={index}
updateQuery={setDatasetQuery}
clearable={!hideClearButton}
color={metricColors[index]}
......
......@@ -18,9 +18,9 @@ import _ from "underscore";
import type Aggregation from "metabase-lib/lib/query/Aggregation";
type Props = {
aggregationIndex: number,
aggregation: Aggregation[],
query: QueryWrapper,
question: Question
metric: Query,
metricIndex: number,
updateQuery: (datasetQuery: DatasetQuery) => void,
editable: boolean,
clearable: boolean,
......@@ -51,8 +51,8 @@ export default class MetricWidget extends Component {
// };
removeMetric = () => {
const { query, aggregationIndex, updateQuery } = this.props;
query.removeAggregation(aggregationIndex).update(updateQuery)
const { metric, metricIndex, updateQuery } = this.props;
metric.removeMetric(metricIndex).update(updateQuery);
}
open() {
......@@ -66,13 +66,14 @@ export default class MetricWidget extends Component {
}
renderStandardAggregation() {
const { aggregation, query, editable } = this.props;
const tableMetadata = query.tableMetadata();
const { metric, editable } = this.props;
const tableMetadata = metric.tableMetadata();
const aggregation = metric.aggregation();
const fieldId = AggregationClause.getField(aggregation);
let selectedAggregation = getAggregator(AggregationClause.getOperator(aggregation));
// if this table doesn't support the selected aggregation, prompt the user to select a different one
if (selectedAggregation && _.findWhere(tableMetadata.aggregation_options, { short: selectedAggregation.short })) {
if (selectedAggregation && _.findWhere(metric.aggregationOptions(), { short: selectedAggregation.short })) {
return (
<span className="flex align-center">
{ selectedAggregation.name.replace(" of ...", "") }
......@@ -94,20 +95,20 @@ export default class MetricWidget extends Component {
}
renderMetricAggregation() {
const { aggregation, query } = this.props;
const tableMetadata = query.tableMetadata();
const { question, metric } = this.props;
const aggregation = metric.aggregation();
const metricId = AggregationClause.getMetric(aggregation);
let selectedMetric = _.findWhere(tableMetadata.metrics, { id: metricId });
let selectedMetric = _.findWhere(question.availableMetrics(), { id: metricId });
if (selectedMetric) {
return selectedMetric.name.replace(" of ...", "")
}
}
renderCustomAggregation() {
const { aggregation, tableMetadata, customFields } = this.props;
return format(aggregation, { tableMetadata, customFields });
}
// renderCustomAggregation() {
// const { metric, tableMetadata, customFields } = this.props;
// return format(aggregation, { tableMetadata, customFields });
// }
render() {
const { aggregation, query, name, editable, color, clearable } = this.props;
......
......@@ -19,7 +19,6 @@ import TagEditorSidebar from "../components/template_tags/TagEditorSidebar.jsx";
import title from "metabase/hoc/Title";
import {
getCard,
getOriginalCard,
getLastRunCard,
getQueryResult,
......@@ -39,7 +38,7 @@ import {
getIsRunnable,
getIsResultDirty,
getMode,
getQuery
getQuestion
} from "../selectors";
import { getMetadata, getDatabasesList } from "metabase/selectors/metadata";
......@@ -80,10 +79,9 @@ const mapStateToProps = (state, props) => {
isAdmin: getUserIsAdmin(state, props),
fromUrl: props.location.query.from,
query: getQuery(state),
question: getQuestion(state),
mode: getMode(state),
card: getCard(state),
originalCard: getOriginalCard(state),
lastRunCard: getLastRunCard(state),
......@@ -132,7 +130,7 @@ const mapDispatchToProps = {
};
@connect(mapStateToProps, mapDispatchToProps)
@title(({ card }) => (card && card.name) || "Question")
@title(({ question }) => (question && question.displayName()) || "Question")
export default class CardBuilder extends Component {
forceUpdateDebounced: () => void;
......@@ -195,11 +193,12 @@ export default class CardBuilder extends Component {
};
render() {
const { card, databases, uiControls, mode } = this.props;
const { question, databases, uiControls, mode } = this.props;
const datasetQuery = question && question.datasetQuery();
const showDrawer = uiControls.isShowingDataReference || uiControls.isShowingTemplateTagsEditor;
const ModeFooter = mode && mode.ModeFooter;
const isInitializing = !card || !databases;
const isInitializing = !question || !databases;
const showVisualizationSettings = !this.props.isObjectDetail;
return (
......@@ -215,13 +214,18 @@ export default class CardBuilder extends Component {
<div className="wrapper">
<CardEditor
{...this.props}
datasetQuery={card && card.dataset_query}
datasetQuery={datasetQuery}
/>
</div>
</div>
<div ref="viz" id="react_qb_viz" className="flex z1" style={{ "transition": "opacity 0.25s ease-in-out" }}>
<QueryVisualization {...this.props} noHeader className="full wrapper mb2 z1" />
<QueryVisualization
{...this.props}
card={question.card()}
noHeader
className="full wrapper mb2 z1"
/>
</div>
{ ModeFooter ?
......@@ -243,7 +247,11 @@ export default class CardBuilder extends Component {
{ showVisualizationSettings &&
<div className="z2 absolute left bottom mb3 ml4">
<div style={{backgroundColor: "white"}}>
<VisualizationSettings ref="settings" {...this.props} />
<VisualizationSettings
ref="settings"
{...this.props}
card={question.card()}
/>
</div>
</div>
}
......@@ -251,7 +259,7 @@ export default class CardBuilder extends Component {
<div className="z2 absolute right bottom mb3" style={{marginRight: "80px"}}>
<CardFiltersWidget
{...this.props}
datasetQuery={card && card.dataset_query}
datasetQuery={datasetQuery}
/>
</div>
......
......@@ -10,6 +10,8 @@ import { isPK } from "metabase/lib/types";
import Query from "metabase/lib/query";
import Utils from "metabase/lib/utils";
import Question from "metabase-lib/lib/Question";
import { getIn } from "icepick";
import { getMetadata, getDatabasesList } from "metabase/selectors/metadata";
......@@ -167,7 +169,6 @@ export const getIsResultDirty = createSelector(
}
)
import Question from "metabase-lib/lib/Question";
export const getQuestion = createSelector(
[getMetadata, getCard],
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment