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; } }
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"); } },
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 } };
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 } } }
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 }; } } }
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" }; } } }
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 }); } };
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; }
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; } };
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, }; } };
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()); };
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); } };
return (dispatch, getState) => { dispatch(updateUrl(card, { dirty: false })); MetabaseAnalytics.trackEvent("QueryBuilder", "Update Card", card.dataset_query.type); return card; }
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 }); };
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; };
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; } };
return async function(dispatch, getState) { await UserApi.delete({ userId: user.id }); MetabaseAnalytics.trackEvent("People Admin", "User Removed"); return user; };
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)); } };
return async function(dispatch, getState) { const { datamodel: { editingDatabase } } = getState(); await MetricApi.delete(metric); MetabaseAnalytics.trackEvent("Data Model", "Retire Metric"); return await loadDatabaseMetadata(editingDatabase.id); };
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) {
async (dispatch, getState) => { MetabaseAnalytics.trackEvent("Permissions", "save"); const { permissions, revision, save } = getState().admin.permissions; let result = await save({ revision: revision, groups: permissions }); return result; }
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); } };
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 }); } };
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 }; }
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); } };
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; };
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 }; }