From eb6708b593dd97bc7fda8abc549fdd582eca81c3 Mon Sep 17 00:00:00 2001
From: Gustavo Saiani <gustavo@poe.ma>
Date: Sat, 28 Aug 2021 06:04:20 -0300
Subject: [PATCH] Extract sidebar component from DatabaseEditingApp (#17597)

---
 .../DatabaseEditApp/Sidebar/Sidebar.jsx       |  97 ++++++++++++++
 .../Sidebar/Sidebar.unit.spec.js              |  99 +++++++++++++++
 .../databases/containers/DatabaseEditApp.jsx  | 119 ++++--------------
 3 files changed, 217 insertions(+), 98 deletions(-)
 create mode 100644 frontend/src/metabase/admin/databases/components/DatabaseEditApp/Sidebar/Sidebar.jsx
 create mode 100644 frontend/src/metabase/admin/databases/components/DatabaseEditApp/Sidebar/Sidebar.unit.spec.js

diff --git a/frontend/src/metabase/admin/databases/components/DatabaseEditApp/Sidebar/Sidebar.jsx b/frontend/src/metabase/admin/databases/components/DatabaseEditApp/Sidebar/Sidebar.jsx
new file mode 100644
index 00000000000..b242b5e240b
--- /dev/null
+++ b/frontend/src/metabase/admin/databases/components/DatabaseEditApp/Sidebar/Sidebar.jsx
@@ -0,0 +1,97 @@
+import React, { useRef } from "react";
+import PropTypes from "prop-types";
+import { Box } from "grid-styled";
+import { t } from "ttag";
+
+import DeleteDatabaseModal from "metabase/admin/databases/components/DeleteDatabaseModal.jsx";
+import ActionButton from "metabase/components/ActionButton";
+import ModalWithTrigger from "metabase/components/ModalWithTrigger";
+import ConfirmContent from "metabase/components/ConfirmContent";
+
+const propTypes = {
+  database: PropTypes.object.isRequired,
+  deleteDatabase: PropTypes.func.isRequired,
+  syncDatabaseSchema: PropTypes.func.isRequired,
+  rescanDatabaseFields: PropTypes.func.isRequired,
+  discardSavedFieldValues: PropTypes.func.isRequired,
+};
+
+const DatabaseEditAppSidebar = ({
+  database,
+  deleteDatabase,
+  syncDatabaseSchema,
+  rescanDatabaseFields,
+  discardSavedFieldValues,
+}) => {
+  const discardSavedFieldValuesModal = useRef();
+  const deleteDatabaseModal = useRef();
+
+  return (
+    <Box ml={[2, 3]} width={420}>
+      <div className="Actions bg-light rounded p3">
+        <div className="Actions-group">
+          <label className="Actions-groupLabel block text-bold">{t`Actions`}</label>
+          <ol>
+            <li>
+              <ActionButton
+                actionFn={() => syncDatabaseSchema(database.id)}
+                className="Button Button--syncDbSchema"
+                normalText={t`Sync database schema now`}
+                activeText={t`Starting…`}
+                failedText={t`Failed to sync`}
+                successText={t`Sync triggered!`}
+              />
+            </li>
+            <li className="mt2">
+              <ActionButton
+                actionFn={() => rescanDatabaseFields(database.id)}
+                className="Button Button--rescanFieldValues"
+                normalText={t`Re-scan field values now`}
+                activeText={t`Starting…`}
+                failedText={t`Failed to start scan`}
+                successText={t`Scan triggered!`}
+              />
+            </li>
+          </ol>
+        </div>
+
+        <div className="Actions-group">
+          <label className="Actions-groupLabel block text-bold">{t`Danger Zone`}</label>
+          <ol>
+            <li>
+              <ModalWithTrigger
+                ref={discardSavedFieldValuesModal}
+                triggerClasses="Button Button--danger Button--discardSavedFieldValues"
+                triggerElement={t`Discard saved field values`}
+              >
+                <ConfirmContent
+                  title={t`Discard saved field values`}
+                  onClose={() => discardSavedFieldValuesModal.current.toggle()}
+                  onAction={() => discardSavedFieldValues(database.id)}
+                />
+              </ModalWithTrigger>
+            </li>
+
+            <li className="mt2">
+              <ModalWithTrigger
+                ref={deleteDatabaseModal}
+                triggerClasses="Button Button--deleteDatabase Button--danger"
+                triggerElement={t`Remove this database`}
+              >
+                <DeleteDatabaseModal
+                  database={database}
+                  onClose={() => deleteDatabaseModal.current.toggle()}
+                  onDelete={() => deleteDatabase(database.id, true)}
+                />
+              </ModalWithTrigger>
+            </li>
+          </ol>
+        </div>
+      </div>
+    </Box>
+  );
+};
+
+DatabaseEditAppSidebar.propTypes = propTypes;
+
+export default DatabaseEditAppSidebar;
diff --git a/frontend/src/metabase/admin/databases/components/DatabaseEditApp/Sidebar/Sidebar.unit.spec.js b/frontend/src/metabase/admin/databases/components/DatabaseEditApp/Sidebar/Sidebar.unit.spec.js
new file mode 100644
index 00000000000..3b1c82c3841
--- /dev/null
+++ b/frontend/src/metabase/admin/databases/components/DatabaseEditApp/Sidebar/Sidebar.unit.spec.js
@@ -0,0 +1,99 @@
+import React from "react";
+import { fireEvent, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+
+import Sidebar from "./Sidebar";
+
+it("syncs database schema", () => {
+  const databaseId = 1;
+  const database = { id: databaseId };
+  const syncDatabaseSchema = jest.fn();
+
+  render(
+    <Sidebar database={database} syncDatabaseSchema={syncDatabaseSchema} />,
+  );
+
+  const syncButton = screen.getByText("Sync database schema now");
+
+  fireEvent.click(syncButton);
+
+  expect(syncDatabaseSchema).toHaveBeenCalledWith(databaseId);
+});
+
+it("rescans database field values", () => {
+  const databaseId = 1;
+  const database = { id: databaseId };
+  const rescanDatabaseFields = jest.fn();
+
+  render(
+    <Sidebar database={database} rescanDatabaseFields={rescanDatabaseFields} />,
+  );
+
+  const rescanButton = screen.getByText("Re-scan field values now");
+
+  fireEvent.click(rescanButton);
+
+  expect(rescanDatabaseFields).toHaveBeenCalledWith(databaseId);
+});
+
+it("discards saved field values", () => {
+  const databaseId = 1;
+  const database = { id: databaseId };
+  const discardSavedFieldValues = jest.fn();
+
+  render(
+    <Sidebar
+      database={database}
+      discardSavedFieldValues={discardSavedFieldValues}
+    />,
+  );
+
+  const discardButton = screen.getByText("Discard saved field values");
+
+  fireEvent.click(discardButton);
+
+  expect(screen.getAllByText("Discard saved field values").length).toBe(2);
+
+  const cancelButton = screen.getByText("Cancel");
+
+  fireEvent.click(cancelButton);
+
+  fireEvent.click(discardButton);
+
+  const yesButton = screen.getByText("Yes");
+
+  fireEvent.click(yesButton);
+
+  expect(discardSavedFieldValues).toHaveBeenCalledWith(databaseId);
+});
+
+it("removes database", () => {
+  const databaseId = 1;
+  const name = "DB Name";
+  const database = { id: databaseId, name };
+  const deleteDatabase = jest.fn();
+
+  render(<Sidebar database={database} deleteDatabase={deleteDatabase} />);
+
+  const removeDBButton = screen.getByText("Remove this database");
+
+  fireEvent.click(removeDBButton);
+
+  screen.getByText(`Delete the ${name} database?`);
+
+  const cancelButton = screen.getByText("Cancel");
+
+  fireEvent.click(cancelButton);
+
+  fireEvent.click(removeDBButton);
+
+  const input = screen.getByRole("textbox");
+
+  userEvent.type(input, name);
+
+  const deleteButton = screen.getByText("Delete");
+
+  fireEvent.click(deleteButton);
+
+  expect(deleteDatabase).toHaveBeenCalled();
+});
diff --git a/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.jsx b/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.jsx
index 474ea945ccb..25584c48008 100644
--- a/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.jsx
+++ b/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.jsx
@@ -10,14 +10,12 @@ import { Box, Flex } from "grid-styled";
 
 import title from "metabase/hoc/Title";
 
-import DeleteDatabaseModal from "../components/DeleteDatabaseModal";
-import ActionButton from "metabase/components/ActionButton";
 import AddDatabaseHelpCard from "metabase/components/AddDatabaseHelpCard";
 import Button from "metabase/components/Button";
 import Breadcrumbs from "metabase/components/Breadcrumbs";
 import DriverWarning from "metabase/components/DriverWarning";
 import Radio from "metabase/components/Radio";
-import ModalWithTrigger from "metabase/components/ModalWithTrigger";
+import Sidebar from "metabase/admin/databases/components/DatabaseEditApp/Sidebar/Sidebar";
 
 import Databases from "metabase/entities/databases";
 
@@ -38,7 +36,6 @@ import {
   deleteDatabase,
   selectEngine,
 } from "../database";
-import ConfirmContent from "metabase/components/ConfirmContent";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
 import { getIn } from "icepick";
 
@@ -72,10 +69,7 @@ const mapDispatchToProps = {
   selectEngine,
 };
 
-type TabName = "connection" | "scheduling";
-type TabOption = { name: string, value: TabName };
-
-const TABS: TabOption[] = [
+const TABS = [
   {
     name: t`Connection`,
     value: "connection",
@@ -92,19 +86,12 @@ const TABS: TabOption[] = [
 )
 @title(({ database }) => database && database.name)
 export default class DatabaseEditApp extends Component {
-  state: {
-    currentTab: TabName,
-  };
-
   constructor(props, context) {
     super(props, context);
 
     this.state = {
       currentTab: TABS[0].value,
     };
-
-    this.discardSavedFieldValuesModal = React.createRef();
-    this.deleteDatabaseModal = React.createRef();
   }
 
   static propTypes = {
@@ -139,26 +126,30 @@ export default class DatabaseEditApp extends Component {
   render() {
     const {
       database,
+      deleteDatabase,
+      discardSavedFieldValues,
       selectedEngine,
       letUserControlSchedulingSaved,
       letUserControlSchedulingForm,
       initializeError,
+      rescanDatabaseFields,
+      syncDatabaseSchema,
     } = this.props;
     const { currentTab } = this.state;
-    const editingExistingDatabase = database && database.id != null;
+    const editingExistingDatabase = database?.id != null;
     const addingNewDatabase = !editingExistingDatabase;
 
     const showTabs = editingExistingDatabase && letUserControlSchedulingSaved;
 
+    const crumbs = [
+      [t`Databases`, "/admin/databases"],
+      [addingNewDatabase ? t`Add Database` : database.name],
+    ];
+
     return (
       <Box px={[3, 4, 5]} mt={[1, 2, 3]}>
-        <Breadcrumbs
-          className="py4"
-          crumbs={[
-            [t`Databases`, "/admin/databases"],
-            [addingNewDatabase ? t`Add Database` : database.name],
-          ]}
-        />
+        <Breadcrumbs className="py4" crumbs={crumbs} />
+
         <Flex pb={2}>
           <Box>
             <div className="pt0">
@@ -258,82 +249,14 @@ export default class DatabaseEditApp extends Component {
             </div>
           </Box>
 
-          {/* Sidebar Actions */}
           {editingExistingDatabase && (
-            <Box ml={[2, 3]} width={420}>
-              <div className="Actions bg-light rounded p3">
-                <div className="Actions-group">
-                  <label className="Actions-groupLabel block text-bold">{t`Actions`}</label>
-                  <ol>
-                    <li>
-                      <ActionButton
-                        actionFn={() =>
-                          this.props.syncDatabaseSchema(database.id)
-                        }
-                        className="Button Button--syncDbSchema"
-                        normalText={t`Sync database schema now`}
-                        activeText={t`Starting…`}
-                        failedText={t`Failed to sync`}
-                        successText={t`Sync triggered!`}
-                      />
-                    </li>
-                    <li className="mt2">
-                      <ActionButton
-                        actionFn={() =>
-                          this.props.rescanDatabaseFields(database.id)
-                        }
-                        className="Button Button--rescanFieldValues"
-                        normalText={t`Re-scan field values now`}
-                        activeText={t`Starting…`}
-                        failedText={t`Failed to start scan`}
-                        successText={t`Scan triggered!`}
-                      />
-                    </li>
-                  </ol>
-                </div>
-
-                <div className="Actions-group">
-                  <label className="Actions-groupLabel block text-bold">{t`Danger Zone`}</label>
-                  <ol>
-                    <li>
-                      <ModalWithTrigger
-                        ref={this.discardSavedFieldValuesModal}
-                        triggerClasses="Button Button--danger Button--discardSavedFieldValues"
-                        triggerElement={t`Discard saved field values`}
-                      >
-                        <ConfirmContent
-                          title={t`Discard saved field values`}
-                          onClose={() =>
-                            this.discardSavedFieldValuesModal.current.toggle()
-                          }
-                          onAction={() =>
-                            this.props.discardSavedFieldValues(database.id)
-                          }
-                        />
-                      </ModalWithTrigger>
-                    </li>
-
-                    <li className="mt2">
-                      <ModalWithTrigger
-                        ref={this.deleteDatabaseModal}
-                        triggerClasses="Button Button--deleteDatabase Button--danger"
-                        triggerElement={t`Remove this database`}
-                      >
-                        <DeleteDatabaseModal
-                          database={database}
-                          onClose={() =>
-                            this.deleteDatabaseModal.current.toggle()
-                          }
-                          onDelete={() =>
-                            this.props.deleteDatabase(database.id, true)
-                          }
-                        />
-                      </ModalWithTrigger>
-                    </li>
-                  </ol>
-                </div>
-              </div>
-            </Box>
+            <Sidebar
+              database={database}
+              deleteDatabase={deleteDatabase}
+              discardSavedFieldValues={discardSavedFieldValues}
+              rescanDatabaseFields={rescanDatabaseFields}
+              syncDatabaseSchema={syncDatabaseSchema}
+            />
           )}
         </Flex>
       </Box>
-- 
GitLab