Example #1
0
 return async (dispatch, getState) => {
     if (cardId == null) {
         // bulk archive
         let selected = getSelectedEntities(getState()).filter(item => item.archived !== archived);
         selected.map(item => dispatch(setArchived(item.id, archived)));
         // TODO: errors
         if (undoable) {
             dispatch(addUndo(createUndo(
                 archived ? "archived" : "unarchived",
                 selected.map(item => setArchived(item.id, !archived))
             )));
             MetabaseAnalytics.trackEvent("Questions", archived ? "Bulk Archive" : "Bulk Unarchive", selected.length);
         }
     } else {
         let card = {
             ...getState().questions.entities.cards[cardId],
             archived: archived
         };
         let response = await CardApi.update(card);
         if (undoable) {
             dispatch(addUndo(createUndo(
                 archived ? "archived" : "unarchived",
                 [setArchived(cardId, !archived)],
                 !archived && card.collection
             )));
             MetabaseAnalytics.trackEvent("Questions", archived ? "Archive" : "Unarchive");
         }
         return response;
     }
 }
Example #2
0
            cellClickedFn: function(rowIndex, columnIndex, filter) {
                if (!queryResult) return false;

                // lookup the coldef and cell value of the cell we are taking action on
                var coldef = queryResult.data.cols[columnIndex],
                    value = queryResult.data.rows[rowIndex][columnIndex];

                if (coldef.special_type === "id") {
                    // action is on a PK column
                    let newCard = startNewCard("query", card.dataset_query.database);

                    newCard.dataset_query.query.source_table = coldef.table_id;
                    newCard.dataset_query.query.aggregation = ["rows"];
                    newCard.dataset_query.query.filter = ["AND", ["=", coldef.id, value]];

                    // run it
                    setCard(newCard);

                    MetabaseAnalytics.trackEvent("QueryBuilder", "Table Cell Click", "PK");
                } else if (coldef.special_type === "fk") {
                    // action is on an FK column
                    let newCard = startNewCard("query", card.dataset_query.database);

                    newCard.dataset_query.query.source_table = coldef.target.table_id;
                    newCard.dataset_query.query.aggregation = ["rows"];
                    newCard.dataset_query.query.filter = ["AND", ["=", coldef.target.id, value]];

                    // run it
                    setCard(newCard);

                    MetabaseAnalytics.trackEvent("QueryBuilder", "Table Cell Click", "FK");
                } else {
                    // this is applying a filter by clicking on a cell value
                    let dataset_query = angular.copy(card.dataset_query);
                    Query.addFilter(dataset_query.query);

                    if (coldef.unit) {
                        // this is someone using quick filters on a datetime value
                        let start = moment(value).format("YYYY-MM-DD");
                        let end = start;
                        switch(coldef.unit) {
                            case "week": end = moment(value).add(1, "weeks").subtract(1, "days").format("YYYY-MM-DD"); break;
                            case "month": end = moment(value).add(1, "months").subtract(1, "days").format("YYYY-MM-DD"); break;
                            case "quarter": end = moment(value).add(1, "quarters").subtract(1, "days").format("YYYY-MM-DD"); break;
                            case "year": start = moment(value, "YYYY").format("YYYY-MM-DD");
                                         end = moment(value, "YYYY").add(1, "years").subtract(1, "days").format("YYYY-MM-DD"); break;
                        }
                        Query.updateFilter(dataset_query.query, dataset_query.query.filter.length - 1, ["BETWEEN", coldef.id, start, end]);

                    } else {
                        // quick filtering on a normal value (string/number)
                        Query.updateFilter(dataset_query.query, dataset_query.query.filter.length - 1, [filter, coldef.id, value]);
                    }

                    onQueryChanged(dataset_query);
                    runQuery();

                    MetabaseAnalytics.trackEvent("QueryBuilder", "Table Cell Click", "Quick Filter");
                 }
            },
Example #3
0
    return async function(dispatch, getState) {
        const { onChangeLocation } = getState();

        let savedDatabase, formState;

        try {
            //$scope.$broadcast("form:reset");
            database.details = details;
            if (database.id) {
                //$scope.$broadcast("form:api-success", "Successfully saved!");
                savedDatabase = await MetabaseApi.db_update(database);
                MetabaseAnalytics.trackEvent("Databases", "Update", database.engine);
            } else {
                //$scope.$broadcast("form:api-success", "Successfully created!");
                //$scope.$emit("database:created", new_database);
                savedDatabase = await MetabaseApi.db_create(database);
                MetabaseAnalytics.trackEvent("Databases", "Create", database.engine);
                onChangeLocation('/admin/databases?created');
            }

            // this object format is what FormMessage expects:
            formState = { formSuccess: { data: { message: "Successfully saved!" }}};

        } catch (error) {
            //$scope.$broadcast("form:api-error", error);
            console.log("error saving database", error);
            MetabaseAnalytics.trackEvent("Databases", database.id ? "Update Failed" : "Create Failed", database.engine);
            formState = { formError: error };
        }

        return {
            database: savedDatabase,
            formState
        }
    };
Example #4
0
 return async (dispatch, getState) => {
     const state = getState();
     if (cardId == null) {
         // bulk move
         let selected = getSelectedEntities(getState());
         if (undoable) {
             dispatch(addUndo(createUndo(
                 "moved",
                 selected.map(item => setCollection(item.id, getCardCollectionId(state, item.id)))
             )));
             MetabaseAnalytics.trackEvent("Questions", "Bulk Move to Collection");
         }
         selected.map(item => dispatch(setCollection(item.id, collectionId)));
     } else {
         const collection = _.findWhere(state.collections.collections, { id: collectionId });
         if (undoable) {
             dispatch(addUndo(createUndo(
                 "moved",
                 [setCollection(cardId, getCardCollectionId(state, cardId))]
             )));
             MetabaseAnalytics.trackEvent("Questions", "Move to Collection");
         }
         const card = await CardApi.update({ id: cardId, collection_id: collectionId });
         return {
             ...card,
             _changedSectionSlug: collection && collection.slug
         }
     }
 }
Example #5
0
 return async (dispatch, getState) => {
     if (cardId == null) {
         // bulk label
         let selected = getSelectedEntities(getState());
         selected.map(item => dispatch(setLabeled(item.id, labelId, labeled)));
         // TODO: errors
         if (undoable) {
             dispatch(addUndo(createUndo(
                 labeled ? "labeled" : "unlabeled",
                 selected.map(item => setLabeled(item.id, labelId, !labeled))
             )));
             MetabaseAnalytics.trackEvent("Questions", labeled ? "Bulk Apply Label" : "Bulk Remove Label", selected.length);
         }
     } else {
         const state = getState();
         const labelSlug = getIn(state.questions, ["entities", "labels", labelId, "slug"]);
         const labels = getIn(state.questions, ["entities", "cards", cardId, "labels"]);
         const newLabels = labels.filter(id => id !== labelId);
         if (labeled) {
             newLabels.push(labelId);
         }
         if (labels.length !== newLabels.length) {
             await CardApi.updateLabels({ cardId, label_ids: newLabels });
             if (undoable) {
                 dispatch(addUndo(createUndo(
                     labeled ? "labeled" : "unlabeled",
                     [setLabeled(cardId, labelId, !labeled)]
                 )));
                 MetabaseAnalytics.trackEvent("Questions", labeled ? "Apply Label" : "Remove Label");
             }
             return { id: cardId, labels: newLabels, _changedLabelSlug: labelSlug, _changedLabeled: labeled };
         }
     }
 }
Example #6
0
 return async (dispatch, getState) => {
     try {
         if (!collection.description) {
             // description must be nil or non empty string
             collection = { ...collection, description: null }
         }
         let response;
         if (collection.id == null) {
             MetabaseAnalytics.trackEvent("Collections", "Create");
             response = await CollectionsApi.create(collection);
         } else {
             MetabaseAnalytics.trackEvent("Collections", "Update");
             response = await CollectionsApi.update(collection);
         }
         if (response.id != null) {
             dispatch(reset("collection"));
         }
         // use `replace` so form url doesn't appear in history
         dispatch(replace(Urls.collection(response)));
         return response;
     } catch (e) {
         // redux-form expects an object with either { field: error } or { _error: error }
         if (e.data && e.data.errors) {
             throw e.data.errors;
         } else if (e.data && e.data.message) {
             throw { _error: e.data.message };
         } else {
             throw { _error: "An unknown error occured" };
         }
     }
 }
Example #7
0
    return async function(dispatch, getState) {
        try {
            dispatch.action(UPDATE_DATABASE_STARTED, { database })
            const savedDatabase = await MetabaseApi.db_update(database);
            MetabaseAnalytics.trackEvent("Databases", "Update", database.engine);

            dispatch.action(UPDATE_DATABASE, { database: savedDatabase })
            setTimeout(() => dispatch.action(CLEAR_FORM_STATE), 3000);
        } catch (error) {
            MetabaseAnalytics.trackEvent("Databases", "Update Failed", database.engine);
            dispatch.action(UPDATE_DATABASE_FAILED, { error });
        }
    };
Example #8
0
    async (dispatch, getState) => {
        const {
            id,
            name,
            description,
            parameters,
            caveats,
            points_of_interest,
            show_in_getting_started
        } = dashboard;

        const cleanDashboard = {
            id,
            name,
            description,
            parameters,
            caveats,
            points_of_interest,
            show_in_getting_started
        };

        const updatedDashboard = await DashboardApi.update(cleanDashboard);

        MetabaseAnalytics.trackEvent("Dashboard", "Update");

        return updatedDashboard;
    }
Example #9
0
  return async function(dispatch, getState) {
    if (
      !MetabaseSettings.ldapEnabled() &&
      !MetabaseUtils.validEmail(credentials.username)
    ) {
      return {
        data: {
          errors: { email: t`Please enter a valid formatted email address.` },
        },
      };
    }

    try {
      let newSession = await SessionApi.create(credentials);

      // since we succeeded, lets set the session cookie
      MetabaseCookies.setSessionCookie(newSession.id);

      MetabaseAnalytics.trackEvent("Auth", "Login");
      // TODO: redirect after login (carry user to intended destination)
      await dispatch(refreshCurrentUser());
      dispatch(push(redirectUrl || "/"));
    } catch (error) {
      return error;
    }
  };
Example #10
0
  return async function(dispatch, getState) {
    if (credentials.password !== credentials.password2) {
      return {
        success: false,
        error: { data: { errors: { password2: t`Passwords do not match` } } },
      };
    }

    try {
      let result = await SessionApi.reset_password({
        token: token,
        password: credentials.password,
      });

      if (result.session_id) {
        // we should have a valid session that we can use immediately!
        MetabaseCookies.setSessionCookie(result.session_id);
      }

      MetabaseAnalytics.trackEvent("Auth", "Password Reset");

      return {
        success: true,
        error: null,
      };
    } catch (error) {
      return {
        success: false,
        error,
      };
    }
  };
Example #11
0
  return async (dispatch, getState) => {
    question = question || getQuestion(getState());

    // Needed for persisting visualization columns for pulses/alerts, see #6749
    const series = getTransformedSeries(getState());
    const questionWithVizSettings = series
      ? getQuestionWithDefaultVisualizationSettings(question, series)
      : question;

    let resultsMetadata = getResultsMetadata(getState());
    const updatedQuestion = await questionWithVizSettings
      .setQuery(question.query().clean())
      .setResultsMetadata(resultsMetadata)
      .apiUpdate();

    // reload the question alerts for the current question
    // (some of the old alerts might be removed during update)
    await dispatch(fetchAlertsForQuestion(updatedQuestion.id()));

    // remove the databases in the store that are used to populate the QB databases list.
    // This is done when saving a Card because the newly saved card will be eligible for use as a source query
    // so we want the databases list to be re-fetched next time we hit "New Question" so it shows up
    dispatch(clearRequestState({ statePath: ["metadata", "databases"] }));

    dispatch(updateUrl(updatedQuestion.card(), { dirty: false }));
    MetabaseAnalytics.trackEvent(
      "QueryBuilder",
      "Update Card",
      updatedQuestion.query().datasetQuery().type,
    );

    dispatch.action(API_UPDATE_QUESTION, updatedQuestion.card());
  };
Example #12
0
    return async function(dispatch, getState) {
        const { datamodel: { editingDatabase } } = getState();

        try {
            // make sure we don't send all the computed metadata
            let slimField = { ...field };
            slimField = _.omit(slimField, "operators_lookup", "valid_operators", "values");

            // update the field and strip out angular junk
            let updatedField = await MetabaseApi.field_update(slimField);
            _.each(updatedField, (value, key) => { if (key.charAt(0) !== "$") { updatedField[key] = value } });

            // refresh idfields
            let table = _.findWhere(editingDatabase.tables, {id: updatedField.table_id});
            dispatch(fetchDatabaseIdfields(table.db_id));

            MetabaseAnalytics.trackEvent("Data Model", "Update Field");

            // TODO: we are not actually using this because the way the react components works actually mutates the original object :(
            return updatedField;

        } catch (error) {
            console.log("error updating field", error);
            //MetabaseAnalytics.trackEvent("Databases", database.id ? "Update Failed" : "Create Failed", database.engine);
        }
    };
Example #13
0
    return (dispatch, getState) => {
        dispatch(updateUrl(card, { dirty: false }));

        MetabaseAnalytics.trackEvent("QueryBuilder", "Update Card", card.dataset_query.type);

        return card;
    }
Example #14
0
    return async (dispatch, getState) => {
        const questionFromCard = (c: Card): Question => c && new Question(getMetadata(getState()), c);

        const question: Question = overrideWithCard ? questionFromCard(overrideWithCard) : getQuestion(getState());
        const originalQuestion: ?Question = getOriginalQuestion(getState());

        const cardIsDirty = originalQuestion ? question.isDirtyComparedTo(originalQuestion) : true;

        if (shouldUpdateUrl) {
            dispatch(updateUrl(question.card(), { dirty: cardIsDirty }));
        }

        const startTime = new Date();
        const cancelQueryDeferred = defer();

        question.getResults({ cancelDeferred: cancelQueryDeferred, isDirty: cardIsDirty })
            .then((queryResults) => dispatch(queryCompleted(question.card(), queryResults)))
            .catch((error) => dispatch(queryErrored(startTime, error)));

        MetabaseAnalytics.trackEvent("QueryBuilder", "Run Query", question.query().datasetQuery().type);

        // TODO Move this out from Redux action asap
        // HACK: prevent SQL editor from losing focus
        try { ace.edit("id_sql").focus() } catch (e) {}

        dispatch.action(RUN_QUERY, { cancelQueryDeferred });
    };
Example #15
0
    return async (dispatch, getState) => {
      const response = await DashboardApi.update({
        id: dashId,
        archived: archived,
      });

      if (undoable) {
        const type = archived ? "archived" : "unarchived";
        dispatch(
          addUndo(
            createUndo({
              type,
              message: <div>{`Dashboard was ${type}.`}</div>,
              action: setArchived(dashId, !archived),
            }),
          ),
        );
      }

      MetabaseAnalytics.trackEvent(
        "Dashboard",
        archived ? "Archive" : "Unarchive",
      );
      return response;
    };
Example #16
0
    return (dispatch, getState) => {
        const { qb: { card, queryResult, tableMetadata, uiControls } } = getState();

        // if the type didn't actually change then nothing has been modified
        if (type === card.dataset_query.type) {
            return card;
        }

        // if we are going from MBQL -> Native then attempt to carry over the query
        if (type === "native" && queryResult && queryResult.data && queryResult.data.native_form) {
            let updatedCard = Utils.copy(card);
            let datasetQuery = updatedCard.dataset_query;
            let nativeQuery = _.pick(queryResult.data.native_form, "query", "collection");

            // when the driver requires JSON we need to stringify it because it's been parsed already
            if (getEngineNativeType(tableMetadata.db.engine) === "json") {
                nativeQuery.query = formatJsonQuery(queryResult.data.native_form.query, tableMetadata.db.engine);
            } else {
                nativeQuery.query = formatSQL(nativeQuery.query);
            }

            datasetQuery.type = "native";
            datasetQuery.native = nativeQuery;
            delete datasetQuery.query;

            // when the query changes on saved card we change this into a new query w/ a known starting point
            if (!uiControls.isEditing && updatedCard.id) {
                delete updatedCard.id;
                delete updatedCard.name;
                delete updatedCard.description;
            }

            updatedCard.dataset_query = datasetQuery;

            dispatch(loadMetadataForCard(updatedCard));

            MetabaseAnalytics.trackEvent("QueryBuilder", "MBQL->Native");

            return updatedCard;

        // we are translating an empty query
        } else {
            let databaseId = card.dataset_query.database;

            // only carry over the database id if the user can write native queries
            if (type === "native") {
                let nativeDatabases = getNativeDatabases(getState());
                if (!_.findWhere(nativeDatabases, { id: databaseId })) {
                    databaseId = nativeDatabases.length > 0 ? nativeDatabases[0].id : null
                }
            }

            let newCard = startNewCard(type, databaseId);

            dispatch(loadMetadataForCard(newCard));

            return newCard;
        }
    };
Example #17
0
    return async function(dispatch, getState) {
        await UserApi.delete({
            userId: user.id
        });

        MetabaseAnalytics.trackEvent("People Admin", "User Removed");
        return user;
    };
Example #18
0
 return (dispatch, getState) => {
   MetabaseAnalytics.trackEvent("Undo", "Perform Undo");
   let undo = _.findWhere(getState().undo, { id: undoId });
   if (undo) {
     undo.actions.map(action => dispatch(action));
     dispatch(dismissUndo(undoId, false));
   }
 };
Example #19
0
    return async function(dispatch, getState) {
        const { datamodel: { editingDatabase } } = getState();

        await MetricApi.delete(metric);
        MetabaseAnalytics.trackEvent("Data Model", "Retire Metric");

        return await loadDatabaseMetadata(editingDatabase.id);
    };
Example #20
0
 setQueryModeFn: function(type) {
     if (!card.dataset_query.type || type !== card.dataset_query.type) {
         // switching to a new query type represents a brand new card & query on the given mode
         let newCard = startNewCard(type, card.dataset_query.database);
         setCard(newCard, {resetDirty: true, runQuery: false});
         MetabaseAnalytics.trackEvent('QueryBuilder', 'Query Started', type);
     }
 },
            }, function(result) {
                $scope.databases = _.filter($scope.databases, function(database) {
                    return database.id != databaseId;
                });
                $scope.hasSampleDataset = hasSampleDataset($scope.databases);

                MetabaseAnalytics.trackEvent("Databases", "Delete", "Using List");
            }, function(error) {
            return Metabase.db_create(database).$promise.then(function(new_database) {
                $scope.$broadcast("form:api-success", "Successfully created!");
                $scope.$emit("database:created", new_database);

                MetabaseAnalytics.trackEvent("Databases", "Create", database.engine);

                $location.url('/admin/databases?created');
            }, function(error) {
Example #23
0
 async (dispatch, getState) => {
     MetabaseAnalytics.trackEvent("Permissions", "save");
     const { permissions, revision, save } = getState().admin.permissions;
     let result = await save({
         revision: revision,
         groups: permissions
     });
     return result;
 }
Example #24
0
 return async function(dispatch, getState) {
   try {
     let call = await MetabaseApi.db_discard_values({ dbId: databaseId });
     MetabaseAnalytics.trackEvent("Databases", "Manual Sync");
     return call;
   } catch (error) {
     console.log("error syncing database", error);
   }
 };
Example #25
0
  return async function(dispatch, getState) {
    try {
      dispatch.action(UPDATE_DATABASE_STARTED, { database });
      const action = await dispatch(Databases.actions.update(database));
      const savedDatabase = Databases.HACK_getObjectFromAction(action);
      MetabaseAnalytics.trackEvent("Databases", "Update", database.engine);

      dispatch.action(UPDATE_DATABASE, { database: savedDatabase });
      setTimeout(() => dispatch.action(CLEAR_FORM_STATE), 3000);
    } catch (error) {
      MetabaseAnalytics.trackEvent(
        "Databases",
        "Update Failed",
        database.engine,
      );
      dispatch.action(UPDATE_DATABASE_FAILED, { error });
    }
  };
Example #26
0
 return async (dispatch, getState) => {
     if (favorited) {
         await CardApi.favorite({ cardId });
     } else {
         await CardApi.unfavorite({ cardId });
     }
     MetabaseAnalytics.trackEvent("Questions", favorited ? "Favorite" : "Unfavorite");
     return { id: cardId, favorite: favorited };
 }
Example #27
0
 return async function(dispatch, getState) {
     try {
         let call = MetabaseApi.db_sync_metadata({"dbId": databaseId});
         MetabaseAnalytics.trackEvent("Databases", "Manual Sync");
         return call;
     } catch(error) {
         console.log('error syncing database', error);
     }
 };
Example #28
0
    return async function (dispatch, getState) {
        try {
            dispatch.action(CREATE_DATABASE_STARTED, {})
            const createdDatabase = await MetabaseApi.db_create(database);
            MetabaseAnalytics.trackEvent("Databases", "Create", database.engine);

            // update the db metadata already here because otherwise there will be a gap between "Adding..." status
            // and seeing the db that was just added
            await dispatch(fetchDatabases())

            dispatch.action(CREATE_DATABASE)
            dispatch(push('/admin/databases?created=' + createdDatabase.id));
        } catch (error) {
            console.error("error creating a database", error);
            MetabaseAnalytics.trackEvent("Databases", "Create Failed", database.engine);
            dispatch.action(CREATE_DATABASE_FAILED, { error })
        }
    };
        $scope.sync = function() {
            var call = Metabase.db_sync_metadata({
                'dbId': $scope.database.id
            });

            MetabaseAnalytics.trackEvent("Databases", "Manual Sync");

            return call.$promise;
        };
Example #30
0
 return async (dispatch, getState) => {
     if (favorited) {
         await DashboardApi.favorite({ dashId });
     } else {
         await DashboardApi.unfavorite({ dashId });
     }
     MetabaseAnalytics.trackEvent("Dashboard", favorited ? "Favorite" : "Unfavorite");
     return { id: dashId, favorite: favorited };
 }