diff --git a/resources/frontend_client/app/home/components/Activity.react.js b/resources/frontend_client/app/home/components/Activity.react.js index aa254448e3a0e4544644348a54ff78034cc5d0c2..9626cfecd8dc67b4a244dd7d30a49a76d5cbc287 100644 --- a/resources/frontend_client/app/home/components/Activity.react.js +++ b/resources/frontend_client/app/home/components/Activity.react.js @@ -152,7 +152,7 @@ export default class Activity extends Component { } render() { - let { activity } = this.props; + let { activity, user } = this.props; let { error } = this.state; return ( @@ -172,7 +172,7 @@ export default class Activity extends Component { <li key={item.id} className="mt3"> <ActivityItem item={item} - description={this.activityDescription(item, item.user)} + description={this.activityDescription(item, user)} userColors={this.initialsCssClasses(item.user)} /> <ActivityStory story={this.activityDescription(item, item.user)} /> diff --git a/resources/frontend_client/app/home/components/Cards.react.js b/resources/frontend_client/app/home/components/Cards.react.js index 761c5e0d75951c878fb5c8f745f892379b160179..4562963ca277f1e98b02bb86983c08e9ca58773b 100644 --- a/resources/frontend_client/app/home/components/Cards.react.js +++ b/resources/frontend_client/app/home/components/Cards.react.js @@ -24,6 +24,17 @@ export default class Cards extends Component { } } + tableName(table_id) { + const { databaseMetadata } = this.props; + for (var tableIdx in databaseMetadata.tables) { + if (databaseMetadata.tables[tableIdx].id === table_id) { + return databaseMetadata.tables[tableIdx].display_name; + } + } + + return ""; + } + renderCards(cards) { let items = cards.slice().sort((a, b) => b.created_at - a.created_at); @@ -59,7 +70,7 @@ export default class Cards extends Component { } render() { - let { cards } = this.props; + let { cards, cardsFilter, databaseMetadata } = this.props; let { error } = this.state; return ( @@ -69,8 +80,19 @@ export default class Cards extends Component { { cards.length === 0 ? <div className="flex flex-column layout-centered pt4" style={{fontSize: '1.08rem', marginTop: '100px'}}> <span className="QuestionCircle">?</span> - <div className="text-normal mt3 mb1 h2 text-bold">Hmmm, looks like you don't have any saved questions yet.</div> - <div className="text-normal text-grey-2">Save a question and get this baby going!</div> + <div className="text-normal mt3 mb1 h2 text-bold"> + { cardsFilter.database && cardsFilter.table ? + "No questions have been saved against "+this.tableName(cardsFilter.table)+" yet." + : null} + + { cardsFilter.database && !cardsFilter.table ? + "No questions have been saved against "+databaseMetadata.name+" yet." + : null} + + { !cardsFilter.database && !cardsFilter.table ? + "You don't have any saved questions yet." + : null} + </div> </div> : this.renderCards(cards) diff --git a/src/metabase/api/activity.clj b/src/metabase/api/activity.clj index 4a72061d21fb420b842f01ee06b3679cd79cf02e..7ecdb74ff1257faf0a7c7a7bfb79f41e04a3217b 100644 --- a/src/metabase/api/activity.clj +++ b/src/metabase/api/activity.clj @@ -13,7 +13,7 @@ (defendpoint GET "/" "Get recent activity." [] - (-> (db/sel :many Activity (k/order :timestamp :DESC)) + (-> (db/sel :many Activity (k/order :timestamp :DESC) (k/limit 40)) (hydrate :user :table :database :model_exists))) (defendpoint GET "/recent_views" @@ -30,7 +30,7 @@ (k/where (= :user_id *current-user-id*)) (k/group :user_id :model :model_id) (k/order :max_ts :desc) - (k/limit 15)) + (k/limit 10)) (map #(assoc % :model_object (delay (case (:model %) "card" (-> (Card (:model_id %)) (select-keys [:id :name :description :display])) diff --git a/src/metabase/events/view_log.clj b/src/metabase/events/view_log.clj index 693ee8a4f8114d07bc22795f262c9e6a349b227d..0bf3ab1348979206e7c9731362248ca329d2bf41 100644 --- a/src/metabase/events/view_log.clj +++ b/src/metabase/events/view_log.clj @@ -49,7 +49,7 @@ [object] (or (:actor_id object) (:user_id object) (:creator_id object))) -(defn- process-view-count-event +(defn process-view-count-event "Handle processing for a single event notification received on the view-counts-channel" [event] ;; try/catch here to prevent individual topic processing exceptions from bubbling up. better to handle them here. diff --git a/src/metabase/models/user.clj b/src/metabase/models/user.clj index 8cdd71c9b00786aa2a4c5121e2d65ea86e5a8850..ce14738436684a5af2bccc01a2e8c5615c962ad9 100644 --- a/src/metabase/models/user.clj +++ b/src/metabase/models/user.clj @@ -47,7 +47,10 @@ (pre-cascade-delete [_ {:keys [id]}] (cascade-delete 'Session :user_id id) - (cascade-delete 'Activity :user_id id))) + (cascade-delete 'Dashboard :creator_id id) + (cascade-delete 'Card :creator_id id) + (cascade-delete 'Activity :user_id id) + (cascade-delete 'ViewLog :user_id id))) (def ^:const current-user-fields diff --git a/test/metabase/api/activity_test.clj b/test/metabase/api/activity_test.clj new file mode 100644 index 0000000000000000000000000000000000000000..70255c8b4ffc2afa4d7b1eab92094b77000bde25 --- /dev/null +++ b/test/metabase/api/activity_test.clj @@ -0,0 +1,167 @@ +(ns metabase.api.activity-test + "Tests for /api/activity endpoints." + (:require [expectations :refer :all] + [metabase.db :as db] + [metabase.http-client :refer :all] + (metabase.models [activity :refer [Activity]] + [card :refer [Card]] + [dashboard :refer [Dashboard]] + [view-log :refer [ViewLog]]) + [metabase.test.data :refer :all] + [metabase.test.data.users :refer :all] + [metabase.test.util :refer [match-$ expect-eval-actual-first random-name with-temp]] + [metabase.util :as u])) + +;; GET / + +; Things we are testing for: +; 1. ordered by timestamp DESC +; 2. :user and :model_exists are hydrated + +; NOTE: timestamp matching was being a real PITA so I cheated a bit. ideally we'd fix that +(expect-let [_ (korma.core/delete Activity) + activity1 (db/ins Activity + :topic "install" + :details {} + :timestamp (u/parse-iso8601 "2015-09-09T12:13:14.888Z")) + activity2 (db/ins Activity + :topic "dashboard-create" + :user_id (user->id :crowberto) + :model "dashboard" + :model_id 1234 + :details {:description "Because I can!" + :name "Bwahahaha" + :public_perms 2} + :timestamp (u/parse-iso8601 "2015-09-10T18:53:01.632Z")) + activity3 (db/ins Activity + :topic "user-joined" + :user_id (user->id :rasta) + :model "user" + :details {} + :timestamp (u/parse-iso8601 "2015-09-10T05:33:43.641Z"))] + [(match-$ (db/sel :one Activity :id (:id activity2)) + {:id $ + :topic "dashboard-create" + :user_id $ + :user (match-$ (fetch-user :crowberto) + {:id (user->id :crowberto) + :email $ + :date_joined $ + :first_name $ + :last_name $ + :last_login $ + :is_superuser $ + :common_name $}) + :model $ + :model_id $ + :model_exists false + :database_id nil + :database nil + :table_id nil + :table nil + :custom_id nil + :details $}) + (match-$ (db/sel :one Activity :id (:id activity3)) + {:id $ + :topic "user-joined" + :user_id $ + :user (match-$ (fetch-user :rasta) + {:id (user->id :rasta) + :email $ + :date_joined $ + :first_name $ + :last_name $ + :last_login $ + :is_superuser $ + :common_name $}) + :model $ + :model_id $ + :model_exists nil + :database_id nil + :database nil + :table_id nil + :table nil + :custom_id nil + :details $}) + (match-$ (db/sel :one Activity :id (:id activity1)) + {:id $ + :topic "install" + :user_id nil + :user nil + :model $ + :model_id $ + :model_exists nil + :database_id nil + :database nil + :table_id nil + :table nil + :custom_id nil + :details $})] + (->> ((user->client :crowberto) :get 200 "activity") + (map #(dissoc % :timestamp)))) + + +;; GET /recent_views + +; Things we are testing for: +; 1. ordering is sorted by most recent +; 2. results are filtered to current user +; 3. `:model_object` is hydrated in each result +; 4. we filter out entries where `:model_object` is nil (object doesn't exist) + +(defn- create-card [] + (db/ins Card + :name "rand-name" + :creator_id (user->id :crowberto) + :public_perms 2 + :display "table" + :dataset_query {} + :visualization_settings {})) + +(defn- create-dash [] + (db/ins Dashboard + :name "rand-name" + :description "rand-name" + :creator_id (user->id :crowberto) + :public_perms 2)) + +(expect-let [card1 (create-card) + dash1 (create-dash) + card2 (create-card)] + [{:cnt 1 + :user_id (user->id :crowberto) + :model "card" + :model_id (:id card1) + :model_object {:id (:id card1) + :name (:name card1) + :description (:description card1) + :display (name (:display card1))}} + {:cnt 1 + :user_id (user->id :crowberto) + :model "dashboard" + :model_id (:id dash1) + :model_object {:id (:id dash1) + :name (:name dash1) + :description (:description dash1)}} + {:cnt 1 + :user_id (user->id :crowberto) + :model "card" + :model_id (:id card2) + :model_object {:id (:id card2) + :name (:name card2) + :description (:description card2) + :display (name (:display card2))}}] + (let [create-view (fn [user model model-id] + (db/ins ViewLog + :user_id user + :model model + :model_id model-id + :timestamp (u/new-sql-timestamp)))] + (do + (create-view (user->id :crowberto) "card" (:id card2)) + (create-view (user->id :crowberto) "dashboard" (:id dash1)) + (create-view (user->id :crowberto) "card" (:id card1)) + (create-view (user->id :crowberto) "card" 36478) + (create-view (user->id :rasta) "card" (:id card1)) + (->> ((user->client :crowberto) :get 200 "activity/recent_views") + (map #(dissoc % :max_ts)))))) diff --git a/test/metabase/events/activity_feed_test.clj b/test/metabase/events/activity_feed_test.clj new file mode 100644 index 0000000000000000000000000000000000000000..d9d029de78607c13a0ebf1d258c9d98d0b68356a --- /dev/null +++ b/test/metabase/events/activity_feed_test.clj @@ -0,0 +1,246 @@ +(ns metabase.events.activity-feed-test + (:require [expectations :refer :all] + [korma.core :as k] + [metabase.db :as db] + [metabase.events.activity-feed :refer :all] + (metabase.models [activity :refer [Activity]] + [card :refer [Card]] + [dashboard :refer [Dashboard]] + [dashboard-card :refer [DashboardCard]] + [database :refer [Database]] + [session :refer [Session]] + [user :refer [User]]) + [metabase.test.data :refer :all] + [metabase.test.util :refer [expect-eval-actual-first with-temp random-name]] + [metabase.test-setup :refer :all])) + +;; TODO - we can simplify the cleanup work we do by using the :in-context :expectations-options +;; the only downside is that it then runs the annotated function on ALL tests :/ + + +(defn- create-test-objects + "Simple helper function which creates a series of test objects for use in the tests" + [] + (let [rand-name (random-name) + user (db/ins User + :email (str rand-name "@metabase.com") + :first_name rand-name + :last_name rand-name + :password rand-name) + ;; i don't know why, but the below `ins` doesn't return an object :( + session (db/ins Session + :id rand-name + :user_id (:id user)) + dashboard (db/ins Dashboard + :name rand-name + :description rand-name + :creator_id (:id user) + :public_perms 2) + card (db/ins Card + :name rand-name + :creator_id (:id user) + :public_perms 2 + :display "table" + :dataset_query {:database (db-id) + :type :query + :query {:source_table (id :categories)}} + :visualization_settings {}) + dashcard (db/ins DashboardCard + :card_id (:id card) + :dashboard_id (:id dashboard))] + {:card card + :dashboard dashboard + :dashcard dashcard + :session {:id rand-name} + :user user})) + + +;; `:card-create` event +(expect-let [{:keys [card dashboard dashcard user]} (create-test-objects)] + {:topic :card-create + :user_id (:id user) + :model "card" + :model_id (:id card) + :database_id (db-id) + :table_id (id :categories) + :details {:description (:description card) + :name (:name card) + :public_perms (:public_perms card)}} + (do + (k/delete Activity) + (process-activity-event {:topic :card-create + :item card}) + (-> (db/sel :one Activity :topic "card-create") + (select-keys [:topic :user_id :model :model_id :database_id :table_id :details])))) + +;; `:card-update` event +(expect-let [{:keys [card dashboard dashcard user]} (create-test-objects)] + {:topic :card-update + :user_id (:id user) + :model "card" + :model_id (:id card) + :database_id (db-id) + :table_id (id :categories) + :details {:description (:description card) + :name (:name card) + :public_perms (:public_perms card)}} + (do + (k/delete Activity) + (process-activity-event {:topic :card-update + :item card}) + (-> (db/sel :one Activity :topic "card-update") + (select-keys [:topic :user_id :model :model_id :database_id :table_id :details])))) + +;; `:card-delete` event +(expect-let [{:keys [card dashboard dashcard user]} (create-test-objects)] + {:topic :card-delete + :user_id (:id user) + :model "card" + :model_id (:id card) + :database_id (db-id) + :table_id (id :categories) + :details {:description (:description card) + :name (:name card) + :public_perms (:public_perms card)}} + (do + (k/delete Activity) + (process-activity-event {:topic :card-delete + :item card}) + (-> (db/sel :one Activity :topic "card-delete") + (select-keys [:topic :user_id :model :model_id :database_id :table_id :details])))) + +;; `:dashboard-create` event +(expect-let [{:keys [card dashboard dashcard user]} (create-test-objects)] + {:topic :dashboard-create + :user_id (:id user) + :model "dashboard" + :model_id (:id dashboard) + :details {:description (:description dashboard) + :name (:name dashboard) + :public_perms (:public_perms dashboard)}} + (do + (k/delete Activity) + (process-activity-event {:topic :dashboard-create + :item dashboard}) + (-> (db/sel :one Activity :topic "dashboard-create") + (select-keys [:topic :user_id :model :model_id :details])))) + +;; `:dashboard-delete` event +(expect-let [{:keys [card dashboard dashcard user]} (create-test-objects)] + {:topic :dashboard-delete + :user_id (:id user) + :model "dashboard" + :model_id (:id dashboard) + :details {:description (:description dashboard) + :name (:name dashboard) + :public_perms (:public_perms dashboard)}} + (do + (k/delete Activity) + (process-activity-event {:topic :dashboard-delete + :item dashboard}) + (-> (db/sel :one Activity :topic "dashboard-delete") + (select-keys [:topic :user_id :model :model_id :details])))) + +;; `:dashboard-add-cards` event +(expect-let [{:keys [card dashboard dashcard user]} (create-test-objects)] + {:topic :dashboard-add-cards + :user_id (:id user) + :model "dashboard" + :model_id (:id dashboard) + :details {:description (:description dashboard) + :name (:name dashboard) + :public_perms (:public_perms dashboard) + :dashcards [{:description (:description card) + :name (:name card) + :public_perms (:public_perms card) + :id (:id dashcard) + :card_id (:id card)}]}} + (do + (k/delete Activity) + (process-activity-event {:topic :dashboard-add-cards + :item {:id (:id dashboard) :actor_id (:id user) :dashcards [dashcard]}}) + (-> (db/sel :one Activity :topic "dashboard-add-cards") + (select-keys [:topic :user_id :model :model_id :details])))) + +;; `:dashboard-remove-cards` event +(expect-let [{:keys [card dashboard dashcard user]} (create-test-objects)] + {:topic :dashboard-remove-cards + :user_id (:id user) + :model "dashboard" + :model_id (:id dashboard) + :details {:description (:description dashboard) + :name (:name dashboard) + :public_perms (:public_perms dashboard) + :dashcards [{:description (:description card) + :name (:name card) + :public_perms (:public_perms card) + :id (:id dashcard) + :card_id (:id card)}]}} + (do + (k/delete Activity) + (process-activity-event {:topic :dashboard-remove-cards + :item {:id (:id dashboard) :actor_id (:id user) :dashcards [dashcard]}}) + (-> (db/sel :one Activity :topic "dashboard-remove-cards") + (select-keys [:topic :user_id :model :model_id :details])))) + +;; `:database-sync-*` events +(expect + [1 + {:topic :database-sync + :user_id nil + :model "database" + :model_id (db-id) + :database_id (db-id) + :custom_id "abc" + :details {:status "started"}} + {:topic :database-sync + :user_id nil + :model "database" + :model_id (db-id) + :database_id (db-id) + :custom_id "abc" + :details {:status "completed" :running_time 0}}] + (do + (k/delete Activity) + (let [_ (process-activity-event {:topic :database-sync-begin + :item {:database_id (db-id) :custom_id "abc"}}) + activity1 (-> (db/sel :one Activity :topic "database-sync") + (select-keys [:topic :user_id :model :model_id :database_id :custom_id :details])) + _ (process-activity-event {:topic :database-sync-end + :item {:database_id (db-id) :custom_id "abc"}}) + activity2 (-> (db/sel :one Activity :topic "database-sync") + (select-keys [:topic :user_id :model :model_id :database_id :custom_id :details]) + (assoc-in [:details :running_time] 0)) + activity-cnt (:cnt (first (k/select Activity (k/aggregate (count :*) :cnt) (k/where {:topic "database-sync"}))))] + [activity-cnt + activity1 + activity2]))) + +;; `:install` event +(expect + {:topic :install + :user_id nil + :model "install" + :model_id nil + :details {}} + (do + (k/delete Activity) + (process-activity-event {:topic :install + :item {}}) + (-> (db/sel :one Activity :topic "install") + (select-keys [:topic :user_id :model :model_id :details])))) + +;; `:user-login` event +(expect-let [{{user-id :id} :user {session-id :id :as session} :session} (create-test-objects)] + {:topic :user-joined + :user_id user-id + :model "user" + :model_id user-id + :details {}} + (do + (k/delete Activity) + (process-activity-event {:topic :user-login + :item {:user_id user-id + :session_id session-id}}) + (-> (db/sel :one Activity :topic "user-joined") + (select-keys [:topic :user_id :model :model_id :details])))) diff --git a/test/metabase/events/view_log_test.clj b/test/metabase/events/view_log_test.clj new file mode 100644 index 0000000000000000000000000000000000000000..a73564b3a53dac627853694f48333de8da90248e --- /dev/null +++ b/test/metabase/events/view_log_test.clj @@ -0,0 +1,59 @@ +(ns metabase.events.view-log-test + (:require [expectations :refer :all] + [korma.core :as k] + [metabase.db :as db] + [metabase.events.view-log :refer :all] + (metabase.models [user :refer [User]] + [view-log :refer [ViewLog]]) + [metabase.test.data :refer :all] + [metabase.test.util :refer [expect-eval-actual-first with-temp random-name]] + [metabase.test-setup :refer :all])) + + +(defn- create-test-user [] + (let [rand-name (random-name)] + (db/ins User + :email (str rand-name "@metabase.com") + :first_name rand-name + :last_name rand-name + :password rand-name))) + + +;; `:card-create` event +(expect-let [{user-id :id} (create-test-user) + card {:id 1234 + :creator_id user-id}] + {:user_id user-id + :model "card" + :model_id (:id card)} + (do + (process-view-count-event {:topic :card-create + :item card}) + (-> (db/sel :one ViewLog :user_id user-id) + (select-keys [:user_id :model :model_id])))) + +;; `:card-read` event +(expect-let [{user-id :id} (create-test-user) + card {:id 1234 + :actor_id user-id}] + {:user_id user-id + :model "card" + :model_id (:id card)} + (do + (process-view-count-event {:topic :card-read + :item card}) + (-> (db/sel :one ViewLog :user_id user-id) + (select-keys [:user_id :model :model_id])))) + +;; `:dashboard-read` event +(expect-let [{user-id :id} (create-test-user) + dashboard {:id 1234 + :actor_id user-id}] + {:user_id user-id + :model "dashboard" + :model_id (:id dashboard)} + (do + (process-view-count-event {:topic :dashboard-read + :item dashboard}) + (-> (db/sel :one ViewLog :user_id user-id) + (select-keys [:user_id :model :model_id]))))