From a72ae578bf4a23e10dfc2036b9ff6a6ac5e7b557 Mon Sep 17 00:00:00 2001 From: ashit-rath Date: Fri, 26 Apr 2024 15:38:25 +0530 Subject: [PATCH] chore: Refactor DataSidePane to accept selector as prop to show usage messages in EE (#32870) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Adds a couple of props to alter the data displayed in the datasources left pane to show the right count of entities using datasource. This props are going to be used in Packages and Workflow editors datasources page. PR for https://github.com/appsmithorg/appsmith-ee/pull/4026 ## Automation /ok-to-test tags="@tag.IDE, @tag.Datasource, @tag.Sanity" ### :mag: Cypress test results > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: > Commit: 66a5282be88c27fab11316bd136d88bc74de5d5b > Cypress dashboard url: Click here! ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [x] No ## Summary by CodeRabbit - **New Features** - Enhanced the `DataSidePane` to display the name and count of actions, improving the informational context for users. - Added a new selector function to calculate and display the count of queries for each datasource in the app. - Introduced a `DatasourceFactory` for generating mock datasource objects for testing purposes. - Expanded the list of plugin package names in the `MockPluginsState` to include additional plugins like `MY_SQL`, `S3`, `SNOWFLAKE`, `FIRESTORE`, `GRAPHQL`, `APPSMITH_AI`, `MS_SQL`, `ORACLE`, and `WORKFLOW`. --- .../src/ce/selectors/entitiesSelector.ts | 21 +++- .../Editor/IDE/LeftPane/DataSidePane.test.tsx | 102 ++++++++++++++++++ .../Editor/IDE/LeftPane/DataSidePane.tsx | 18 ++-- .../test/factories/AppIDEFactoryUtils.ts | 7 ++ .../test/factories/DatasourceFactory.ts | 52 +++++++++ app/client/test/factories/MockPluginsState.ts | 11 +- 6 files changed, 201 insertions(+), 10 deletions(-) create mode 100644 app/client/src/pages/Editor/IDE/LeftPane/DataSidePane.test.tsx create mode 100644 app/client/test/factories/DatasourceFactory.ts diff --git a/app/client/src/ce/selectors/entitiesSelector.ts b/app/client/src/ce/selectors/entitiesSelector.ts index 316a452700..a6c39e992b 100644 --- a/app/client/src/ce/selectors/entitiesSelector.ts +++ b/app/client/src/ce/selectors/entitiesSelector.ts @@ -20,7 +20,7 @@ import { PluginPackageName, PluginType, } from "entities/Action"; -import { find, get, groupBy, keyBy, sortBy } from "lodash"; +import { countBy, find, get, groupBy, keyBy, sortBy } from "lodash"; import ImageAlt from "assets/images/placeholder-image.svg"; import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants"; @@ -1540,3 +1540,22 @@ export const getJSSegmentItems = createSelector( export const getSelectedTableName = (state: AppState) => state.ui.datasourcePane.selectedTableName; + +export const getDatasourceUsageCountForApp = createSelector( + getActions, + getDatasources, + (state: AppState, editorType: string) => editorType, + (actions, datasources, editorType) => { + const actionCount = countBy(actions, "config.datasource.id"); + const actionDsMap: Record = {}; + + datasources.forEach((ds) => { + actionDsMap[ds.id] = `No queries in this ${editorType}`; + }); + Object.keys(actionCount).forEach((dsId) => { + actionDsMap[dsId] = `${actionCount[dsId]} queries in this ${editorType}`; + }); + + return actionDsMap; + }, +); diff --git a/app/client/src/pages/Editor/IDE/LeftPane/DataSidePane.test.tsx b/app/client/src/pages/Editor/IDE/LeftPane/DataSidePane.test.tsx new file mode 100644 index 0000000000..9b687fa2bd --- /dev/null +++ b/app/client/src/pages/Editor/IDE/LeftPane/DataSidePane.test.tsx @@ -0,0 +1,102 @@ +import React from "react"; +import "@testing-library/jest-dom"; +import { screen } from "@testing-library/react"; +import DataSidePane from "./DataSidePane"; +import { datasourceFactory } from "test/factories/DatasourceFactory"; +import { getIDETestState } from "test/factories/AppIDEFactoryUtils"; +import { PostgresFactory } from "test/factories/Actions/Postgres"; +import type { AppState } from "@appsmith/reducers"; +import { render } from "test/testUtils"; + +const productsDS = datasourceFactory().build({ + name: "Products", + id: "products-ds-id", +}); +const usersDS = datasourceFactory().build({ name: "Users", id: "users-ds-id" }); +const ordersDS = datasourceFactory().build({ + name: "Orders", + id: "orders-ds-id", +}); +const usersAction1 = PostgresFactory.build({ + datasource: { + id: usersDS.id, + }, +}); +const usersAction2 = PostgresFactory.build({ + datasource: { + id: usersDS.id, + }, +}); +const ordersAction1 = PostgresFactory.build({ + datasource: { + id: ordersDS.id, + }, +}); + +describe("DataSidePane", () => { + it("renders the ds and count by using the default selector if dsUsageSelector not passed as a props", () => { + const state = getIDETestState({ + actions: [usersAction1, usersAction2, ordersAction1], + datasources: [productsDS, usersDS, ordersDS], + }) as AppState; + render(, { + url: "app/untitled-application-1/page1/edit/datasource/users-ds-id", + initialState: state, + }); + + expect(screen.getByText("Databases")).toBeInTheDocument(); + expect(screen.getByText("Products")).toBeInTheDocument(); + expect(screen.getByText("Users")).toBeInTheDocument(); + + const usersDSParentElement = + screen.getByText("Users").parentElement?.parentElement; + + expect(usersDSParentElement).toHaveTextContent("2 queries in this app"); + + const productsDSParentElement = + screen.getByText("Products").parentElement?.parentElement; + + expect(productsDSParentElement).toHaveTextContent("No queries in this app"); + + const ortdersDSParentElement = + screen.getByText("Orders").parentElement?.parentElement; + + expect(ortdersDSParentElement).toHaveTextContent("1 queries in this app"); + }); + + it("it uses the selector dsUsageSelector passed as prop", () => { + const state = getIDETestState({ + datasources: [productsDS, usersDS, ordersDS], + }) as AppState; + + const usageSelector = () => { + return { + [usersDS.id]: "Rendering description for users", + [productsDS.id]: "Rendering description for products", + }; + }; + + render(, { + url: "app/untitled-application-1/page1/edit/datasource/users-ds-id", + initialState: state, + }); + + expect(screen.getByText("Databases")).toBeInTheDocument(); + expect(screen.getByText("Products")).toBeInTheDocument(); + expect(screen.getByText("Users")).toBeInTheDocument(); + + const usersDSParentElement = + screen.getByText("Users").parentElement?.parentElement; + + expect(usersDSParentElement).toHaveTextContent( + "Rendering description for users", + ); + + const productsDSParentElement = + screen.getByText("Products").parentElement?.parentElement; + + expect(productsDSParentElement).toHaveTextContent( + "Rendering description for products", + ); + }); +}); diff --git a/app/client/src/pages/Editor/IDE/LeftPane/DataSidePane.tsx b/app/client/src/pages/Editor/IDE/LeftPane/DataSidePane.tsx index 1700f2220b..3248fdbfd5 100644 --- a/app/client/src/pages/Editor/IDE/LeftPane/DataSidePane.tsx +++ b/app/client/src/pages/Editor/IDE/LeftPane/DataSidePane.tsx @@ -3,8 +3,8 @@ import styled from "styled-components"; import { Flex, List, Text } from "design-system"; import { useSelector } from "react-redux"; import { - getActions, getCurrentPageId, + getDatasourceUsageCountForApp, getDatasources, getDatasourcesGroupedByPluginCategory, getPlugins, @@ -15,7 +15,7 @@ import { integrationEditorURL, } from "@appsmith/RouteBuilder"; import { getSelectedDatasourceId } from "@appsmith/navigation/FocusSelectors"; -import { countBy, keyBy } from "lodash"; +import { get, keyBy } from "lodash"; import CreateDatasourcePopover from "./CreateDatasourcePopover"; import { useLocation } from "react-router"; import { @@ -54,7 +54,12 @@ const StyledList = styled(List)` gap: 0; `; -const DataSidePane = () => { +interface DataSidePaneProps { + dsUsageSelector?: (...args: any[]) => Record; +} + +const DataSidePane = (props: DataSidePaneProps) => { + const { dsUsageSelector = getDatasourceUsageCountForApp } = props; const editorType = useEditorType(history.location.pathname); const pageId = useSelector(getCurrentPageId) as string; const [currentSelectedDatasource, setCurrentSelectedDatasource] = useState< @@ -64,8 +69,7 @@ const DataSidePane = () => { const groupedDatasources = useSelector(getDatasourcesGroupedByPluginCategory); const plugins = useSelector(getPlugins); const groupedPlugins = keyBy(plugins, "id"); - const actions = useSelector(getActions); - const actionCount = countBy(actions, "config.datasource.id"); + const dsUsageMap = useSelector((state) => dsUsageSelector(state, editorType)); const goToDatasource = useCallback((id: string) => { history.push(datasourcesEditorIdURL({ datasourceId: id })); }, []); @@ -135,9 +139,7 @@ const DataSidePane = () => { className: "t--datasource", title: data.name, onClick: () => goToDatasource(data.id), - description: `${ - actionCount[data.id] || "No" - } queries in this ${editorType}`, + description: get(dsUsageMap, data.id, ""), descriptionType: "block", isSelected: currentSelectedDatasource === data.id, startIcon: ( diff --git a/app/client/test/factories/AppIDEFactoryUtils.ts b/app/client/test/factories/AppIDEFactoryUtils.ts index ccbe49bb66..37a12888bc 100644 --- a/app/client/test/factories/AppIDEFactoryUtils.ts +++ b/app/client/test/factories/AppIDEFactoryUtils.ts @@ -8,6 +8,7 @@ import type { IDETabs } from "reducers/uiReducers/ideReducer"; import { IDETabsDefaultValue } from "reducers/uiReducers/ideReducer"; import type { JSCollection } from "entities/JSCollection"; import type { FocusHistory } from "reducers/uiReducers/focusHistoryReducer"; +import type { Datasource } from "entities/Datasource"; interface IDEStateArgs { ideView?: EditorViewMode; @@ -17,11 +18,13 @@ interface IDEStateArgs { tabs?: IDETabs; branch?: string; focusHistory?: FocusHistory; + datasources?: Datasource[]; } export const getIDETestState = ({ actions = [], branch, + datasources = [], focusHistory = {}, ideView = EditorViewMode.FullScreen, js = [], @@ -47,6 +50,10 @@ export const getIDETestState = ({ ...initialState, entities: { ...initialState.entities, + datasources: { + ...initialState.entities.datasources, + list: datasources, + }, plugins: MockPluginsState, pageList: pageList, actions: actionData, diff --git a/app/client/test/factories/DatasourceFactory.ts b/app/client/test/factories/DatasourceFactory.ts new file mode 100644 index 0000000000..1e8936eaca --- /dev/null +++ b/app/client/test/factories/DatasourceFactory.ts @@ -0,0 +1,52 @@ +import * as Factory from "factory.ts"; +import { PluginPackageName } from "entities/Action"; +import { PluginIDs } from "test/factories/MockPluginsState"; +import { DatasourceConnectionMode, type Datasource } from "entities/Datasource"; +import { SSLType } from "entities/Datasource/RestAPIForm"; + +interface DatasourceFactory extends Datasource { + pluginPackageName?: PluginPackageName; +} + +export const datasourceFactory = (pluginPackageName?: PluginPackageName) => + Factory.Sync.makeFactory({ + id: "ds-id", + userPermissions: [ + "create:datasourceActions", + "execute:datasources", + "delete:datasources", + "manage:datasources", + "read:datasources", + ], + name: "Mock_DB", + pluginId: PluginIDs[pluginPackageName || PluginPackageName.POSTGRES], + workspaceId: "workspace-id", + datasourceStorages: { + "65fc11feb48e3e52a6d91d34": { + datasourceId: "65fc124fb48e3e52a6d91d44", + environmentId: "65fc11feb48e3e52a6d91d34", + datasourceConfiguration: { + url: "mockdb.internal.appsmith.com", + connection: { + mode: DatasourceConnectionMode.READ_ONLY, + ssl: { + authType: SSLType.DEFAULT, + authTypeControl: false, + certificateFile: { + name: "", + base64Content: null, + }, + }, + }, + authentication: { + authenticationType: "dbAuth", + username: "mockdb", + }, + }, + isConfigured: true, + isValid: true, + }, + }, + invalids: [], + messages: [], + }); diff --git a/app/client/test/factories/MockPluginsState.ts b/app/client/test/factories/MockPluginsState.ts index 33fadf1625..b69edc1fef 100644 --- a/app/client/test/factories/MockPluginsState.ts +++ b/app/client/test/factories/MockPluginsState.ts @@ -1,12 +1,21 @@ import type { PluginDataState } from "reducers/entityReducers/pluginsReducer"; import { PluginPackageName } from "entities/Action"; -export const PluginIDs = { +export const PluginIDs: Record = { [PluginPackageName.POSTGRES]: "65e58df196506a506bd7069c", [PluginPackageName.REST_API]: "65e58df196506a506bd7069d", [PluginPackageName.MONGO]: "65e58df196506a506bd7069e", [PluginPackageName.GOOGLE_SHEETS]: "65e58df296506a506bd706a9", [PluginPackageName.JS]: "65e58df296506a506bd706ad", + [PluginPackageName.MY_SQL]: "65e58df296506a506bd7069f", + [PluginPackageName.S3]: "65e58df296506a506bd706a8", + [PluginPackageName.SNOWFLAKE]: "65e58df296506a506bd706ab", + [PluginPackageName.FIRESTORE]: "65e58df296506a506bd706a6", + [PluginPackageName.GRAPHQL]: "65e58df396506a506bd706be", + [PluginPackageName.APPSMITH_AI]: "65e58fe2225bee69e71c536a", + [PluginPackageName.MS_SQL]: "65e58df296506a506bd706a5", + [PluginPackageName.ORACLE]: "65e58df396506a506bd706bf", + [PluginPackageName.WORKFLOW]: "", // this is added for the typing of PluginIDs to pass }; export default {