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 {