From 23082730907e11d5fc54f3e0846175290a424751 Mon Sep 17 00:00:00 2001
From: Anton Kulyk <kuliks.anton@gmail.com>
Date: Wed, 16 Nov 2022 15:56:39 +0000
Subject: [PATCH] Clean up user selectors (#26477)

* Simplify null checks

* Use actual `User` type in plugins file

* Sort plugins file imports

* Add `login_attributes` type to `User`

* Convert user selectors to TypeScript

* Make `currentUser` in `State` nullable

* Simplify personal collection ID selectors

* Handle nullable current user

* Use `checkNotNull` to check nullable user object

* Remove explicit `User` type in `checkNotNull`

* Simplify `getIsSsoUser` selector
---
 frontend/src/metabase-types/api/mocks/user.ts |  1 +
 frontend/src/metabase-types/api/user.ts       |  1 +
 frontend/src/metabase-types/store/state.ts    |  2 +-
 .../UserPasswordApp/UserPasswordApp.tsx       |  9 ++++++--
 .../UserProfileApp/UserProfileApp.tsx         |  9 ++++++--
 .../src/metabase/account/profile/selectors.ts |  9 ++++++--
 .../containers/HomeGreeting/HomeGreeting.tsx  |  9 ++++++--
 frontend/src/metabase/plugins/index.ts        | 20 +++++++++-------
 .../actions/core/initializeQB.ts              |  2 +-
 .../metabase/selectors/{user.js => user.ts}   | 10 ++++----
 .../src/metabase/selectors/user.unit.spec.js  | 23 -------------------
 .../src/metabase/selectors/user.unit.spec.ts  | 21 +++++++++++++++++
 12 files changed, 71 insertions(+), 45 deletions(-)
 rename frontend/src/metabase/selectors/{user.js => user.ts} (73%)
 delete mode 100644 frontend/src/metabase/selectors/user.unit.spec.js
 create mode 100644 frontend/src/metabase/selectors/user.unit.spec.ts

diff --git a/frontend/src/metabase-types/api/mocks/user.ts b/frontend/src/metabase-types/api/mocks/user.ts
index a92fc89907a..674a681f7a9 100644
--- a/frontend/src/metabase-types/api/mocks/user.ts
+++ b/frontend/src/metabase-types/api/mocks/user.ts
@@ -8,6 +8,7 @@ export const createMockUser = (opts?: Partial<User>): User => ({
   email: "user@metabase.test",
   locale: null,
   google_auth: false,
+  login_attributes: null,
   is_active: true,
   is_qbnewb: false,
   is_superuser: false,
diff --git a/frontend/src/metabase-types/api/user.ts b/frontend/src/metabase-types/api/user.ts
index 6f216cd6ca4..5365546e938 100644
--- a/frontend/src/metabase-types/api/user.ts
+++ b/frontend/src/metabase-types/api/user.ts
@@ -19,6 +19,7 @@ export interface BaseUser {
 
 export interface User extends BaseUser {
   google_auth: boolean;
+  login_attributes: string[] | null;
   is_installer: boolean;
   has_invited_second_user: boolean;
   has_question_and_dashboard: boolean;
diff --git a/frontend/src/metabase-types/store/state.ts b/frontend/src/metabase-types/store/state.ts
index 07408871414..c0d0ee77e44 100644
--- a/frontend/src/metabase-types/store/state.ts
+++ b/frontend/src/metabase-types/store/state.ts
@@ -11,7 +11,7 @@ import { SetupState } from "./setup";
 export interface State {
   admin: AdminState;
   app: AppState;
-  currentUser: User;
+  currentUser: User | null;
   embed: EmbedState;
   entities: EntitiesState;
   form: FormState;
diff --git a/frontend/src/metabase/account/password/containers/UserPasswordApp/UserPasswordApp.tsx b/frontend/src/metabase/account/password/containers/UserPasswordApp/UserPasswordApp.tsx
index 4e5f026b815..842a3b5c7e8 100644
--- a/frontend/src/metabase/account/password/containers/UserPasswordApp/UserPasswordApp.tsx
+++ b/frontend/src/metabase/account/password/containers/UserPasswordApp/UserPasswordApp.tsx
@@ -1,11 +1,16 @@
 import { connect } from "react-redux";
+
+import { checkNotNull } from "metabase/core/utils/types";
+
 import { getUser } from "metabase/selectors/user";
-import { State } from "metabase-types/store";
+
+import type { State } from "metabase-types/store";
+
 import { updatePassword, validatePassword } from "../../actions";
 import UserPasswordForm from "../../components/UserPasswordForm";
 
 const mapStateToProps = (state: State) => ({
-  user: getUser(state),
+  user: checkNotNull(getUser(state)),
   onValidatePassword: validatePassword,
   onSubmit: updatePassword,
 });
diff --git a/frontend/src/metabase/account/profile/containers/UserProfileApp/UserProfileApp.tsx b/frontend/src/metabase/account/profile/containers/UserProfileApp/UserProfileApp.tsx
index dbde8a6ed26..b060b1eadf5 100644
--- a/frontend/src/metabase/account/profile/containers/UserProfileApp/UserProfileApp.tsx
+++ b/frontend/src/metabase/account/profile/containers/UserProfileApp/UserProfileApp.tsx
@@ -1,12 +1,17 @@
 import { connect } from "react-redux";
+
+import { checkNotNull } from "metabase/core/utils/types";
+
 import { getUser } from "metabase/selectors/user";
-import { State } from "metabase-types/store";
+
+import type { State } from "metabase-types/store";
+
 import UserProfileForm from "../../components/UserProfileForm";
 import { updateUser } from "../../actions";
 import { getIsSsoUser, getLocales } from "../../selectors";
 
 const mapStateToProps = (state: State) => ({
-  user: getUser(state),
+  user: checkNotNull(getUser(state)),
   locales: getLocales(state),
   isSsoUser: getIsSsoUser(state),
 });
diff --git a/frontend/src/metabase/account/profile/selectors.ts b/frontend/src/metabase/account/profile/selectors.ts
index 46c12d573cc..bff59f25a30 100644
--- a/frontend/src/metabase/account/profile/selectors.ts
+++ b/frontend/src/metabase/account/profile/selectors.ts
@@ -1,9 +1,14 @@
 import { createSelector } from "reselect";
+
 import { getUser } from "metabase/selectors/user";
-import { PLUGIN_IS_PASSWORD_USER } from "metabase/plugins";
 import { getSettings } from "metabase/selectors/settings";
 
-export const getIsSsoUser = createSelector([getUser], user => {
+import { PLUGIN_IS_PASSWORD_USER } from "metabase/plugins";
+
+export const getIsSsoUser = createSelector(getUser, user => {
+  if (!user) {
+    return false;
+  }
   return !PLUGIN_IS_PASSWORD_USER.every(predicate => predicate(user));
 });
 
diff --git a/frontend/src/metabase/home/homepage/containers/HomeGreeting/HomeGreeting.tsx b/frontend/src/metabase/home/homepage/containers/HomeGreeting/HomeGreeting.tsx
index e56cb1f9d2f..ad222fddaea 100644
--- a/frontend/src/metabase/home/homepage/containers/HomeGreeting/HomeGreeting.tsx
+++ b/frontend/src/metabase/home/homepage/containers/HomeGreeting/HomeGreeting.tsx
@@ -1,10 +1,15 @@
 import { connect } from "react-redux";
+
+import { checkNotNull } from "metabase/core/utils/types";
+
 import { getUser } from "metabase/selectors/user";
-import { State } from "metabase-types/store";
+
+import type { State } from "metabase-types/store";
+
 import HomeGreeting from "../../components/HomeGreeting";
 
 const mapStateToProps = (state: State) => ({
-  user: getUser(state),
+  user: checkNotNull(getUser(state)),
   showLogo: state.settings.values["show-metabot"],
 });
 
diff --git a/frontend/src/metabase/plugins/index.ts b/frontend/src/metabase/plugins/index.ts
index 6ae32f1fdd1..8e5ab1964e3 100644
--- a/frontend/src/metabase/plugins/index.ts
+++ b/frontend/src/metabase/plugins/index.ts
@@ -1,20 +1,24 @@
-import { t } from "ttag";
 import React from "react";
+import { t } from "ttag";
+
 import PluginPlaceholder from "metabase/plugins/components/PluginPlaceholder";
-import {
+
+import type {
   DatabaseEntityId,
   PermissionSubject,
 } from "metabase/admin/permissions/types";
-import {
-  Collection,
+
+import type {
   Bookmark,
-  GroupsPermissions,
+  Collection,
   Dataset,
   Group,
+  GroupsPermissions,
+  User,
 } from "metabase-types/api";
-import { AdminPathKey, State } from "metabase-types/store";
-import { User } from "metabase-types/types/User";
-import Question from "metabase-lib/Question";
+import type { AdminPathKey, State } from "metabase-types/store";
+import type Question from "metabase-lib/Question";
+
 import { PluginGroupManagersType } from "./types";
 
 // Plugin integration points. All exports must be objects or arrays so they can be mutated by plugins.
diff --git a/frontend/src/metabase/query_builder/actions/core/initializeQB.ts b/frontend/src/metabase/query_builder/actions/core/initializeQB.ts
index b6e08805b0d..41c7271d2c8 100644
--- a/frontend/src/metabase/query_builder/actions/core/initializeQB.ts
+++ b/frontend/src/metabase/query_builder/actions/core/initializeQB.ts
@@ -308,7 +308,7 @@ async function handleQBInit(
     question = question.lockDisplay();
 
     const currentUser = getUser(getState());
-    if (currentUser.is_qbnewb) {
+    if (currentUser?.is_qbnewb) {
       uiControls.isShowingNewbModal = true;
       MetabaseAnalytics.trackStructEvent("QueryBuilder", "Show Newb Modal");
     }
diff --git a/frontend/src/metabase/selectors/user.js b/frontend/src/metabase/selectors/user.ts
similarity index 73%
rename from frontend/src/metabase/selectors/user.js
rename to frontend/src/metabase/selectors/user.ts
index f981584fbd9..0f46fdf6552 100644
--- a/frontend/src/metabase/selectors/user.js
+++ b/frontend/src/metabase/selectors/user.ts
@@ -1,13 +1,15 @@
 import { createSelector } from "reselect";
 import { PLUGIN_APPLICATION_PERMISSIONS } from "metabase/plugins";
 
-export const getUser = state => state.currentUser;
+import type { State } from "metabase-types/store";
+
+export const getUser = (state: State) => state.currentUser;
 
 export const getUserId = createSelector([getUser], user => user?.id);
 
 export const getUserIsAdmin = createSelector(
   [getUser],
-  user => (user && user.is_superuser) || false,
+  user => user?.is_superuser || false,
 );
 
 export const canManageSubscriptions = createSelector(
@@ -21,10 +23,10 @@ export const canManageSubscriptions = createSelector(
 
 export const getUserAttributes = createSelector(
   [getUser],
-  user => (user && user.login_attributes) || [],
+  user => user?.login_attributes || [],
 );
 
 export const getUserPersonalCollectionId = createSelector(
   [getUser],
-  user => (user && user.personal_collection_id) || null,
+  user => user?.personal_collection_id,
 );
diff --git a/frontend/src/metabase/selectors/user.unit.spec.js b/frontend/src/metabase/selectors/user.unit.spec.js
deleted file mode 100644
index 9bde3e13bdb..00000000000
--- a/frontend/src/metabase/selectors/user.unit.spec.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { getUserIsAdmin } from "./user";
-
-describe("metabase/selectors/user", () => {
-  it("should return true if user is an admin", () => {
-    const state = {
-      currentUser: {
-        is_superuser: true,
-      },
-    };
-
-    expect(getUserIsAdmin(state)).toBe(true);
-  });
-
-  it("should return false if user is not an admin", () => {
-    const state = {
-      currentUser: {
-        is_superuser: false,
-      },
-    };
-
-    expect(getUserIsAdmin(state)).toBe(false);
-  });
-});
diff --git a/frontend/src/metabase/selectors/user.unit.spec.ts b/frontend/src/metabase/selectors/user.unit.spec.ts
new file mode 100644
index 00000000000..07fb28a1a3d
--- /dev/null
+++ b/frontend/src/metabase/selectors/user.unit.spec.ts
@@ -0,0 +1,21 @@
+import { createMockUser } from "metabase-types/api/mocks";
+import { createMockState } from "metabase-types/store/mocks";
+import { getUserIsAdmin } from "./user";
+
+describe("metabase/selectors/user", () => {
+  it("should return true if user is an admin", () => {
+    const state = createMockState({
+      currentUser: createMockUser({ is_superuser: true }),
+    });
+
+    expect(getUserIsAdmin(state)).toBe(true);
+  });
+
+  it("should return false if user is not an admin", () => {
+    const state = createMockState({
+      currentUser: createMockUser({ is_superuser: false }),
+    });
+
+    expect(getUserIsAdmin(state)).toBe(false);
+  });
+});
-- 
GitLab