chore: Adding no access state for Datasource tab based on permissions given via GAC (#38268)

## Description

- Today, when a user gives only View/Edit access to a Datasource, the
schema tables are still being seen on the Datasource tabs while it
doesn't show on the Datasource Editor page. This has been fixed with
this PR. It should only be seen when Datasource has create action
permissions, hence we show the "We can’t show the schema for this
datasource" screen in this case.
- When the user has not given View access to a Datasource, the UI is
broken in Datasource tab. This has been fixed with this PR. We now show
the No access state in this case.

**BEFORE**:

When view access is not given:
<img width="1147" alt="Screenshot 2024-12-20 at 5 52 58 PM"
src="https://github.com/user-attachments/assets/c1d1fd39-d6d3-4fd8-99bf-895698f61490"
/>

When create action permission is not given but view access is given:
<img width="1138" alt="Screenshot 2024-12-20 at 5 54 10 PM"
src="https://github.com/user-attachments/assets/abf0aa86-e541-4453-b7e4-071d123f7a60"
/>


**AFTER**:

When view access is not given:
<img width="1136" alt="Screenshot 2024-12-20 at 5 58 22 PM"
src="https://github.com/user-attachments/assets/e160250b-963c-457e-81b1-380aef1332a1"
/>


When create action permission is not given but view access is given:
<img width="1139" alt="Screenshot 2024-12-20 at 5 57 53 PM"
src="https://github.com/user-attachments/assets/1967a657-622c-46f7-b6d4-78451b6106f0"
/>


Fixes [#38093](https://github.com/appsmithorg/appsmith/issues/38093)

## Automation

/ok-to-test tags="@tag.Sanity, @tag.Datasource, @tag.IDE, @tag.JS,
@tag.Git"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/12452839374>
> Commit: ee5bc1774c02b4b29a702c8baefbad35390708c3
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=12452839374&attempt=3"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Sanity, @tag.Datasource, @tag.IDE, @tag.JS, @tag.Git`
> Spec:
> <hr>Mon, 23 Dec 2024 08:29:06 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced new icons and improved conditional rendering for datasource
components.
  - Added a "not found" message for empty datasource options.
  - Enhanced user feedback with clearer messaging for missing resources.

- **Bug Fixes**
- Adjusted rendering logic to ensure edit buttons only appear when both
conditions are met.

- **Enhancements**
- Improved permission checks and logic for managing datasource
visibility.
- Streamlined component logic for better readability and
maintainability.
  - Enhanced error handling practices in saga functions.

- **Tests**
  - Simplified test structure by removing unnecessary context providers.

- **Chores**
  - Updated import statements and component names for consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Ankita Kinger 2024-12-23 20:27:36 +05:30 committed by GitHub
parent e45cbdfa83
commit 5855115abb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 217 additions and 171 deletions

View File

@ -1,32 +1,30 @@
import React from "react"; import React from "react";
import { Flex } from "@appsmith/ads"; import { Flex, Icon } from "@appsmith/ads";
import { getAssetUrl } from "ee/utils/airgapHelpers"; import { getAssetUrl } from "ee/utils/airgapHelpers";
import { EntityIcon } from "pages/Editor/Explorer/ExplorerIcons"; import { EntityIcon } from "pages/Editor/Explorer/ExplorerIcons";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { import { getPluginImages } from "ee/selectors/entitiesSelector";
getPluginIdFromDatasourceId,
getPluginImages,
} from "ee/selectors/entitiesSelector";
interface Props { interface Props {
datasourceId: string;
datasourceName: string; datasourceName: string;
pluginId: string;
} }
const CurrentDataSource = ({ datasourceId, datasourceName }: Props) => { const CurrentDataSource = ({ datasourceName, pluginId }: Props) => {
const { pluginId, pluginImages } = useSelector((state) => ({ const pluginImages = useSelector((state) => getPluginImages(state));
pluginId: getPluginIdFromDatasourceId(state, datasourceId),
pluginImages: getPluginImages(state),
}));
const datasourceIcon = pluginId ? pluginImages?.[pluginId] : undefined; const datasourceIcon = pluginId ? pluginImages?.[pluginId] : undefined;
return ( return (
<Flex alignItems="center" gap="spaces-2"> <Flex alignItems="center" gap="spaces-2">
<EntityIcon height="16px" width="16px"> <EntityIcon height="16px" width="16px">
<img alt="entityIcon" src={getAssetUrl(datasourceIcon)} /> {datasourceIcon ? (
<img alt="entityIcon" src={getAssetUrl(datasourceIcon)} />
) : (
<Icon name="datasource-v3" />
)}
</EntityIcon> </EntityIcon>
{datasourceName} {datasourceName || "NA"}
</Flex> </Flex>
); );
}; };

View File

@ -6,9 +6,11 @@ import { useGoToDatasource } from "PluginActionEditor/components/PluginActionRes
const CurrentDataSourceLink = ({ const CurrentDataSourceLink = ({
datasourceId, datasourceId,
datasourceName, datasourceName,
pluginId,
}: { }: {
datasourceId: string; datasourceId: string;
datasourceName: string; datasourceName: string;
pluginId: string;
}) => { }) => {
const { goToDatasource } = useGoToDatasource(); const { goToDatasource } = useGoToDatasource();
@ -19,10 +21,7 @@ const CurrentDataSourceLink = ({
return ( return (
<Link onClick={handleClick}> <Link onClick={handleClick}>
<CurrentDataSource <CurrentDataSource datasourceName={datasourceName} pluginId={pluginId} />
datasourceId={datasourceId}
datasourceName={datasourceName}
/>
</Link> </Link>
); );
}; };

View File

@ -55,7 +55,7 @@ const DatasourceInfo = ({
datasourceName={datasourceName} datasourceName={datasourceName}
plugin={plugin} plugin={plugin}
/> />
{showEditButton && ( {showEditButton && datasourceName && (
<Tooltip content={createMessage(EDIT_DS_CONFIG)} placement="top"> <Tooltip content={createMessage(EDIT_DS_CONFIG)} placement="top">
<Button <Button
isIconButton isIconButton

View File

@ -1,7 +1,11 @@
import React from "react"; import React from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { Flex } from "@appsmith/ads"; import { Flex } from "@appsmith/ads";
import { CREATE_NEW_DATASOURCE, createMessage } from "ee/constants/messages"; import {
CREATE_NEW_DATASOURCE,
createMessage,
NOT_FOUND,
} from "ee/constants/messages";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag"; import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import { import {
@ -66,7 +70,7 @@ export const PluginDatasourceSelector = ({
const userWorkspacePermissions = useSelector( const userWorkspacePermissions = useSelector(
(state: AppState) => getCurrentAppWorkspace(state).userPermissions ?? [], (state: AppState) => getCurrentAppWorkspace(state).userPermissions ?? [],
); );
const isChangePermitted = getHasManageActionPermission( const isActionChangePermitted = getHasManageActionPermission(
isFeatureEnabled, isFeatureEnabled,
currentActionConfig?.userPermissions, currentActionConfig?.userPermissions,
); );
@ -108,15 +112,23 @@ export const PluginDatasourceSelector = ({
}); });
} }
if (!showDatasourceSelector || !isChangePermitted) { if (!showDatasourceSelector || !isActionChangePermitted) {
return ( return (
<CurrentDataSourceLink <CurrentDataSourceLink
datasourceId={datasourceId} datasourceId={datasourceId}
datasourceName={datasourceName} datasourceName={datasourceName}
pluginId={plugin?.id || ""}
/> />
); );
} }
if (DATASOURCES_OPTIONS.length < 1) {
DATASOURCES_OPTIONS.push({
label: createMessage(NOT_FOUND),
value: "not found",
});
}
return ( return (
<Flex> <Flex>
<MenuField <MenuField
@ -126,8 +138,8 @@ export const PluginDatasourceSelector = ({
options={DATASOURCES_OPTIONS} options={DATASOURCES_OPTIONS}
> >
<CurrentDataSource <CurrentDataSource
datasourceId={datasourceId}
datasourceName={datasourceName} datasourceName={datasourceName}
pluginId={plugin?.id || ""}
/> />
</MenuField> </MenuField>
</Flex> </Flex>

View File

@ -19,13 +19,12 @@ export interface DatasourceProps {
} }
const DatasourceSelector = (props: DatasourceProps) => { const DatasourceSelector = (props: DatasourceProps) => {
return props.plugin ? ( return props.plugin &&
API_FORM_COMPONENTS.includes(props.plugin.uiComponent) ? ( API_FORM_COMPONENTS.includes(props.plugin.uiComponent) ? (
<ApiDatasourceSelector {...props} formName={API_EDITOR_FORM_NAME} /> <ApiDatasourceSelector {...props} formName={API_EDITOR_FORM_NAME} />
) : ( ) : (
<QueryDatasourceSelector {...props} formName={QUERY_EDITOR_FORM_NAME} /> <QueryDatasourceSelector {...props} formName={QUERY_EDITOR_FORM_NAME} />
) );
) : null;
}; };
export default DatasourceSelector; export default DatasourceSelector;

View File

@ -7,6 +7,7 @@ import {
getIsFetchingDatasourceStructure, getIsFetchingDatasourceStructure,
getPluginIdFromDatasourceId, getPluginIdFromDatasourceId,
getPluginDatasourceComponentFromId, getPluginDatasourceComponentFromId,
getDatasource,
} from "ee/selectors/entitiesSelector"; } from "ee/selectors/entitiesSelector";
import type { AppState } from "ee/reducers"; import type { AppState } from "ee/reducers";
import { fetchDatasourceStructure } from "actions/datasourceActions"; import { fetchDatasourceStructure } from "actions/datasourceActions";
@ -26,6 +27,13 @@ import { useEditorType } from "ee/hooks";
import { useParentEntityInfo } from "ee/hooks/datasourceEditorHooks"; import { useParentEntityInfo } from "ee/hooks/datasourceEditorHooks";
import DatasourceInfo from "./DatasourceInfo"; import DatasourceInfo from "./DatasourceInfo";
import { getPlugin } from "ee/selectors/entitiesSelector"; import { getPlugin } from "ee/selectors/entitiesSelector";
import {
getHasCreateDatasourceActionPermission,
getHasManageDatasourcePermission,
getHasReadDatasourcePermission,
} from "ee/utils/BusinessFeatures/permissionPageHelpers";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
interface Props { interface Props {
datasourceId: string; datasourceId: string;
@ -33,17 +41,19 @@ interface Props {
currentActionId: string; currentActionId: string;
} }
const Datasource = (props: Props) => { const DatasourceTab = (props: Props) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { datasourceId, datasourceName } = props;
const datasourceStructure = useSelector((state) => const datasourceStructure = useSelector((state) =>
getDatasourceStructureById(state, props.datasourceId), getDatasourceStructureById(state, datasourceId),
); );
const { responseTabHeight } = useSelector(getPluginActionDebuggerState); const { responseTabHeight } = useSelector(getPluginActionDebuggerState);
const pluginId = useSelector((state) => const pluginId = useSelector((state) =>
getPluginIdFromDatasourceId(state, props.datasourceId), getPluginIdFromDatasourceId(state, datasourceId),
); );
const plugin = useSelector((state) => getPlugin(state, pluginId || "")); const plugin = useSelector((state) => getPlugin(state, pluginId || ""));
@ -54,32 +64,51 @@ const Datasource = (props: Props) => {
const [selectedTable, setSelectedTable] = useState<string>(); const [selectedTable, setSelectedTable] = useState<string>();
const isLoading = useSelector((state: AppState) => const isLoading = useSelector((state: AppState) =>
getIsFetchingDatasourceStructure(state, props.datasourceId), getIsFetchingDatasourceStructure(state, datasourceId),
); );
const pluginDatasourceForm = useSelector((state) => const pluginDatasourceForm = useSelector((state) =>
getPluginDatasourceComponentFromId(state, pluginId || ""), getPluginDatasourceComponentFromId(state, pluginId || ""),
); );
const datasource = useSelector((state) => getDatasource(state, datasourceId));
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
const canCreateDatasourceActions = getHasCreateDatasourceActionPermission(
isFeatureEnabled,
datasource?.userPermissions || [],
);
const canReadDatasource = getHasReadDatasourcePermission(
isFeatureEnabled,
datasource?.userPermissions || [],
);
const canManageDatasource = getHasManageDatasourcePermission(
isFeatureEnabled,
datasource?.userPermissions || [],
);
useEffect( useEffect(
function resetSelectedTable() { function resetSelectedTable() {
setSelectedTable(undefined); setSelectedTable(undefined);
}, },
[props.datasourceId], [datasourceId],
); );
useEffect( useEffect(
function fetchDatasourceStructureEffect() { function fetchDatasourceStructureEffect() {
function fetchStructure() { function fetchStructure() {
if ( if (
props.datasourceId && datasourceId &&
datasourceStructure === undefined && datasourceStructure === undefined &&
pluginDatasourceForm !== pluginDatasourceForm !==
DatasourceComponentTypes.RestAPIDatasourceForm DatasourceComponentTypes.RestAPIDatasourceForm
) { ) {
dispatch( dispatch(
fetchDatasourceStructure( fetchDatasourceStructure(
props.datasourceId, datasourceId,
true, true,
DatasourceStructureContext.QUERY_EDITOR, DatasourceStructureContext.QUERY_EDITOR,
), ),
@ -89,7 +118,7 @@ const Datasource = (props: Props) => {
fetchStructure(); fetchStructure();
}, },
[props.datasourceId, datasourceStructure, dispatch, pluginDatasourceForm], [datasourceId, datasourceStructure, dispatch, pluginDatasourceForm],
); );
useEffect( useEffect(
@ -98,7 +127,7 @@ const Datasource = (props: Props) => {
setSelectedTable(datasourceStructure.tables[0].name); setSelectedTable(datasourceStructure.tables[0].name);
} }
}, },
[selectedTable, props.datasourceId, isLoading, datasourceStructure], [selectedTable, datasourceId, isLoading, datasourceStructure],
); );
// eslint-disable-next-line react-perf/jsx-no-new-function-as-prop // eslint-disable-next-line react-perf/jsx-no-new-function-as-prop
@ -106,14 +135,14 @@ const Datasource = (props: Props) => {
const entryPoint = DatasourceEditEntryPoints.QUERY_EDITOR_DATASOURCE_SCHEMA; const entryPoint = DatasourceEditEntryPoints.QUERY_EDITOR_DATASOURCE_SCHEMA;
AnalyticsUtil.logEvent("EDIT_DATASOURCE_CLICK", { AnalyticsUtil.logEvent("EDIT_DATASOURCE_CLICK", {
datasourceId: props.datasourceId, datasourceId,
pluginName: "", pluginName: "",
entryPoint: entryPoint, entryPoint: entryPoint,
}); });
const url = datasourcesEditorIdURL({ const url = datasourcesEditorIdURL({
baseParentEntityId: parentEntityId, baseParentEntityId: parentEntityId,
datasourceId: props.datasourceId, datasourceId,
params: { ...omit(getQueryParams(), "viewMode"), viewMode: false }, params: { ...omit(getQueryParams(), "viewMode"), viewMode: false },
generateEditorPath: true, generateEditorPath: true,
}); });
@ -124,7 +153,12 @@ const Datasource = (props: Props) => {
const getStatusState = () => { const getStatusState = () => {
if (isLoading) return SchemaDisplayStatus.SCHEMA_LOADING; if (isLoading) return SchemaDisplayStatus.SCHEMA_LOADING;
if (!datasourceStructure) return SchemaDisplayStatus.NOSCHEMA; /* When a user doesn't have view access on a datasource */
if (!canReadDatasource) return SchemaDisplayStatus.NOACCESS;
/* When a user doesn't have create new query access but has view access on the datasource */
if (!datasourceStructure || !canCreateDatasourceActions)
return SchemaDisplayStatus.NOSCHEMA;
if (datasourceStructure && "error" in datasourceStructure) if (datasourceStructure && "error" in datasourceStructure)
return SchemaDisplayStatus.FAILED; return SchemaDisplayStatus.FAILED;
@ -144,10 +178,10 @@ const Datasource = (props: Props) => {
return ( return (
<Flex flexDirection="column" padding="spaces-3"> <Flex flexDirection="column" padding="spaces-3">
<DatasourceInfo <DatasourceInfo
datasourceId={props.datasourceId} datasourceId={datasourceId}
datasourceName={props.datasourceName} datasourceName={datasourceName}
plugin={plugin} plugin={plugin}
showEditButton={!isLoading} showEditButton={!isLoading && canManageDatasource}
/> />
<StatusDisplay <StatusDisplay
editDatasource={editDatasource} editDatasource={editDatasource}
@ -171,8 +205,8 @@ const Datasource = (props: Props) => {
<Flex h="100%"> <Flex h="100%">
<DatasourceTables <DatasourceTables
currentActionId={props.currentActionId} currentActionId={props.currentActionId}
datasourceId={props.datasourceId} datasourceId={datasourceId}
datasourceName={props.datasourceName} datasourceName={datasourceName}
datasourceStructure={datasourceStructure} datasourceStructure={datasourceStructure}
plugin={plugin} plugin={plugin}
selectedTable={selectedTable} selectedTable={selectedTable}
@ -200,4 +234,4 @@ const Datasource = (props: Props) => {
); );
}; };
export { Datasource }; export { DatasourceTab };

View File

@ -1 +1 @@
export * from "./Datasource"; export * from "./DatasourceTab";

View File

@ -24,7 +24,7 @@ import {
} from "PluginActionEditor/store"; } from "PluginActionEditor/store";
import { doesPluginRequireDatasource } from "ee/entities/Engine/actionHelpers"; import { doesPluginRequireDatasource } from "ee/entities/Engine/actionHelpers";
import useShowSchema from "PluginActionEditor/components/PluginActionResponse/hooks/useShowSchema"; import useShowSchema from "PluginActionEditor/components/PluginActionResponse/hooks/useShowSchema";
import { Datasource } from "PluginActionEditor/components/PluginActionResponse/components/DatasourceTab"; import { DatasourceTab } from "PluginActionEditor/components/PluginActionResponse/components/DatasourceTab";
import { import {
useBlockExecution, useBlockExecution,
useHandleRunClick, useHandleRunClick,
@ -64,10 +64,10 @@ function usePluginActionResponseTabs() {
key: DEBUGGER_TAB_KEYS.DATASOURCE_TAB, key: DEBUGGER_TAB_KEYS.DATASOURCE_TAB,
title: "Datasource", title: "Datasource",
panelComponent: ( panelComponent: (
<Datasource <DatasourceTab
currentActionId={action.id} currentActionId={action.id}
datasourceId={datasource?.id || ""} datasourceId={datasource?.id || action.datasource.id || ""}
datasourceName={datasource?.name || ""} datasourceName={datasource?.name || action.datasource.name || ""}
/> />
), ),
}); });
@ -119,10 +119,10 @@ function usePluginActionResponseTabs() {
key: DEBUGGER_TAB_KEYS.DATASOURCE_TAB, key: DEBUGGER_TAB_KEYS.DATASOURCE_TAB,
title: "Datasource", title: "Datasource",
panelComponent: ( panelComponent: (
<Datasource <DatasourceTab
currentActionId={action.id} currentActionId={action.id}
datasourceId={datasource?.id || ""} datasourceId={datasource?.id || action.datasource.id || ""}
datasourceName={datasource?.name || ""} datasourceName={datasource?.name || action.datasource.name || ""}
/> />
), ),
}); });

View File

@ -394,6 +394,7 @@ export const CREATE_NEW_DATASOURCE_MOST_POPULAR_HEADER = () => "Most popular";
export const CREATE_NEW_DATASOURCE_REST_API = () => "REST API"; export const CREATE_NEW_DATASOURCE_REST_API = () => "REST API";
export const SAMPLE_DATASOURCES = () => "Sample datasources"; export const SAMPLE_DATASOURCES = () => "Sample datasources";
export const EDIT_DS_CONFIG = () => "Edit datasource configuration"; export const EDIT_DS_CONFIG = () => "Edit datasource configuration";
export const NOT_FOUND = () => "Not found";
export const ERROR_EVAL_ERROR_GENERIC = () => export const ERROR_EVAL_ERROR_GENERIC = () =>
`Unexpected error occurred while evaluating the application`; `Unexpected error occurred while evaluating the application`;

View File

@ -8,7 +8,7 @@ import {
import { getJSTabs, getQueryTabs } from "selectors/ideSelectors"; import { getJSTabs, getQueryTabs } from "selectors/ideSelectors";
import type { AppState } from "ee/reducers"; import type { AppState } from "ee/reducers";
import { identifyEntityFromPath } from "navigation/FocusEntity"; import { identifyEntityFromPath } from "navigation/FocusEntity";
import { getCurrentPageId } from "selectors/editorSelectors"; import { getCurrentBasePageId } from "selectors/editorSelectors";
import { getQueryEntityItemUrl } from "ee/pages/Editor/IDE/EditorPane/Query/utils"; import { getQueryEntityItemUrl } from "ee/pages/Editor/IDE/EditorPane/Query/utils";
export type EditorSegmentList = Array<{ export type EditorSegmentList = Array<{
@ -74,10 +74,10 @@ export const selectQuerySegmentEditorTabs = (state: AppState) => {
export const getLastQueryTab = createSelector( export const getLastQueryTab = createSelector(
selectQuerySegmentEditorTabs, selectQuerySegmentEditorTabs,
getCurrentPageId, getCurrentBasePageId,
(tabs, pageId) => { (tabs, basePageId) => {
if (tabs.length) { if (tabs.length) {
const url = getQueryEntityItemUrl(tabs[tabs.length - 1], pageId); const url = getQueryEntityItemUrl(tabs[tabs.length - 1], basePageId);
const urlWithoutQueryParams = url.split("?")[0]; const urlWithoutQueryParams = url.split("?")[0];
return identifyEntityFromPath(urlWithoutQueryParams); return identifyEntityFromPath(urlWithoutQueryParams);

View File

@ -1645,7 +1645,9 @@ export const getQuerySegmentItems = createSelector(
? "AI Queries" ? "AI Queries"
: datasourceIdToNameMap[action.config.datasource.id] ?? "AI Queries"; : datasourceIdToNameMap[action.config.datasource.id] ?? "AI Queries";
} else { } else {
group = datasourceIdToNameMap[action.config.datasource.id]; group =
action.config.datasource?.name ??
datasourceIdToNameMap[action.config.datasource?.id];
} }
return { return {

View File

@ -5,6 +5,9 @@ import { hasCreateWorkspacePermission as hasCreateWorkspacePermission_EE } from
import { hasCreateDatasourcePermission as hasCreateDatasourcePermission_CE } from "ce/utils/permissionHelpers"; import { hasCreateDatasourcePermission as hasCreateDatasourcePermission_CE } from "ce/utils/permissionHelpers";
import { hasCreateDatasourcePermission as hasCreateDatasourcePermission_EE } from "ee/utils/permissionHelpers"; import { hasCreateDatasourcePermission as hasCreateDatasourcePermission_EE } from "ee/utils/permissionHelpers";
import { hasReadDatasourcePermission as hasReadDatasourcePermission_CE } from "ce/utils/permissionHelpers";
import { hasReadDatasourcePermission as hasReadDatasourcePermission_EE } from "ee/utils/permissionHelpers";
import { hasManageDatasourcePermission as hasManageDatasourcePermission_CE } from "ce/utils/permissionHelpers"; import { hasManageDatasourcePermission as hasManageDatasourcePermission_CE } from "ce/utils/permissionHelpers";
import { hasManageDatasourcePermission as hasManageDatasourcePermission_EE } from "ee/utils/permissionHelpers"; import { hasManageDatasourcePermission as hasManageDatasourcePermission_EE } from "ee/utils/permissionHelpers";
@ -58,6 +61,14 @@ export const getHasCreateDatasourcePermission = (
else return hasCreateDatasourcePermission_CE(permissions); else return hasCreateDatasourcePermission_CE(permissions);
}; };
export const getHasReadDatasourcePermission = (
isEnabled: boolean,
permissions?: string[],
) => {
if (isEnabled) return hasReadDatasourcePermission_EE(permissions);
else return hasReadDatasourcePermission_CE(permissions);
};
export const getHasManageDatasourcePermission = ( export const getHasManageDatasourcePermission = (
isEnabled: boolean, isEnabled: boolean,
permissions?: string[], permissions?: string[],

View File

@ -19,6 +19,7 @@ export enum PERMISSION_TYPE {
CREATE_APPLICATION = "create:applications", CREATE_APPLICATION = "create:applications",
/* Datasource permissions */ /* Datasource permissions */
CREATE_DATASOURCES = "create:datasources", CREATE_DATASOURCES = "create:datasources",
READ_DATASOURCES = "read:datasources",
EXECUTE_DATASOURCES = "execute:datasources", EXECUTE_DATASOURCES = "execute:datasources",
CREATE_DATASOURCE_ACTIONS = "create:datasourceActions", CREATE_DATASOURCE_ACTIONS = "create:datasourceActions",
DELETE_DATASOURCES = "delete:datasources", DELETE_DATASOURCES = "delete:datasources",
@ -85,6 +86,8 @@ export const hasCreateWorkspacePermission = (_permissions?: string[]) => true;
export const hasCreateDatasourcePermission = (_permissions?: string[]) => true; export const hasCreateDatasourcePermission = (_permissions?: string[]) => true;
export const hasReadDatasourcePermission = (_permissions?: string[]) => true;
export const hasManageDatasourcePermission = (_permissions?: string[]) => true; export const hasManageDatasourcePermission = (_permissions?: string[]) => true;
export const hasManageWorkspaceDatasourcePermission = ( export const hasManageWorkspaceDatasourcePermission = (

View File

@ -14,9 +14,6 @@ import { getIDETestState } from "test/factories/AppIDEFactoryUtils";
import { PageFactory } from "test/factories/PageFactory"; import { PageFactory } from "test/factories/PageFactory";
import { screen, waitFor } from "@testing-library/react"; import { screen, waitFor } from "@testing-library/react";
import { GoogleSheetFactory } from "test/factories/Actions/GoogleSheetFactory"; import { GoogleSheetFactory } from "test/factories/Actions/GoogleSheetFactory";
import { PluginActionContextProvider } from "PluginActionEditor";
import { PluginPackageName, PluginType } from "entities/Action";
import { DatasourceComponentTypes, UIComponentTypes } from "api/PluginApi";
const FeatureFlags = { const FeatureFlags = {
rollout_side_by_side_enabled: true, rollout_side_by_side_enabled: true,
@ -329,17 +326,6 @@ describe("IDE URL rendering of Queries", () => {
}); });
describe("Postgres Routes", () => { describe("Postgres Routes", () => {
const mockPlugin = {
id: "plugin_id",
name: "Postgres",
packageName: PluginPackageName.POSTGRES,
type: PluginType.DB,
uiComponent: UIComponentTypes.UQIDbEditorForm,
datasourceComponent: DatasourceComponentTypes.AutoForm,
templates: {},
requiresDatasource: true,
};
it("Renders Postgres routes in Full Screen", async () => { it("Renders Postgres routes in Full Screen", async () => {
const page = PageFactory.build(); const page = PageFactory.build();
const anQuery = PostgresFactory.build({ const anQuery = PostgresFactory.build({
@ -358,9 +344,7 @@ describe("IDE URL rendering of Queries", () => {
const { getAllByText, getByRole, getByTestId } = render( const { getAllByText, getByRole, getByTestId } = render(
<Route path={BUILDER_PATH}> <Route path={BUILDER_PATH}>
<PluginActionContextProvider action={anQuery} plugin={mockPlugin}> <IDE />
<IDE />
</PluginActionContextProvider>
</Route>, </Route>,
{ {
url: `/app/applicationSlug/pageSlug-${page.basePageId}/edit/queries/${anQuery.baseId}`, url: `/app/applicationSlug/pageSlug-${page.basePageId}/edit/queries/${anQuery.baseId}`,
@ -418,9 +402,7 @@ describe("IDE URL rendering of Queries", () => {
const { getAllByText, getByRole, getByTestId } = render( const { getAllByText, getByRole, getByTestId } = render(
<Route path={BUILDER_PATH}> <Route path={BUILDER_PATH}>
<PluginActionContextProvider action={anQuery} plugin={mockPlugin}> <IDE />
<IDE />
</PluginActionContextProvider>
</Route>, </Route>,
{ {
url: `/app/applicationSlug/pageSlug-${page.basePageId}/edit/queries/${anQuery.baseId}`, url: `/app/applicationSlug/pageSlug-${page.basePageId}/edit/queries/${anQuery.baseId}`,
@ -551,17 +533,6 @@ describe("IDE URL rendering of Queries", () => {
}); });
describe("Google Sheets Routes", () => { describe("Google Sheets Routes", () => {
const mockPlugin = {
id: "plugin_id",
name: "Google Sheets",
packageName: PluginPackageName.GOOGLE_SHEETS,
type: PluginType.DB,
uiComponent: UIComponentTypes.GraphQLEditorForm,
datasourceComponent: DatasourceComponentTypes.RestAPIDatasourceForm,
templates: {},
requiresDatasource: false,
};
it("Renders Google Sheets routes in Full Screen", async () => { it("Renders Google Sheets routes in Full Screen", async () => {
const page = PageFactory.build(); const page = PageFactory.build();
const anQuery = GoogleSheetFactory.build({ const anQuery = GoogleSheetFactory.build({
@ -581,9 +552,7 @@ describe("IDE URL rendering of Queries", () => {
const { getAllByText, getByRole, getByTestId } = render( const { getAllByText, getByRole, getByTestId } = render(
<Route path={BUILDER_PATH}> <Route path={BUILDER_PATH}>
<PluginActionContextProvider action={anQuery} plugin={mockPlugin}> <IDE />
<IDE />
</PluginActionContextProvider>
</Route>, </Route>,
{ {
url: `/app/applicationSlug/pageSlug-${page.basePageId}/edit/saas/google-sheets-plugin/api/${anQuery.baseId}`, url: `/app/applicationSlug/pageSlug-${page.basePageId}/edit/saas/google-sheets-plugin/api/${anQuery.baseId}`,
@ -634,9 +603,7 @@ describe("IDE URL rendering of Queries", () => {
const { container, getAllByText, getByRole, getByTestId } = render( const { container, getAllByText, getByRole, getByTestId } = render(
<Route path={BUILDER_PATH}> <Route path={BUILDER_PATH}>
<PluginActionContextProvider action={anQuery} plugin={mockPlugin}> <IDE />
<IDE />
</PluginActionContextProvider>
</Route>, </Route>,
{ {
url: `/app/applicationSlug/pageSlug-${page.basePageId}/edit/saas/google-sheets-plugin/api/${anQuery.baseId}`, url: `/app/applicationSlug/pageSlug-${page.basePageId}/edit/saas/google-sheets-plugin/api/${anQuery.baseId}`,

View File

@ -27,6 +27,8 @@ import { closeJSActionTab } from "actions/jsActionActions";
import { closeQueryActionTab } from "actions/pluginActionActions"; import { closeQueryActionTab } from "actions/pluginActionActions";
import { getCurrentBasePageId } from "selectors/editorSelectors"; import { getCurrentBasePageId } from "selectors/editorSelectors";
import { getCurrentEntityInfo } from "../utils"; import { getCurrentEntityInfo } from "../utils";
import { useEditorType } from "ee/hooks";
import { useParentEntityInfo } from "ee/hooks/datasourceEditorHooks";
export const useCurrentEditorState = () => { export const useCurrentEditorState = () => {
const [selectedSegment, setSelectedSegment] = useState<EditorEntityTab>( const [selectedSegment, setSelectedSegment] = useState<EditorEntityTab>(
@ -58,7 +60,9 @@ export const useCurrentEditorState = () => {
export const useSegmentNavigation = (): { export const useSegmentNavigation = (): {
onSegmentChange: (value: string) => void; onSegmentChange: (value: string) => void;
} => { } => {
const basePageId = useSelector(getCurrentBasePageId); const editorType = useEditorType(location.pathname);
const { parentEntityId: baseParentEntityId } =
useParentEntityInfo(editorType);
/** /**
* Callback to handle the segment change * Callback to handle the segment change
@ -70,17 +74,17 @@ export const useSegmentNavigation = (): {
const onSegmentChange = (value: string) => { const onSegmentChange = (value: string) => {
switch (value) { switch (value) {
case EditorEntityTab.QUERIES: case EditorEntityTab.QUERIES:
history.push(queryListURL({ basePageId }), { history.push(queryListURL({ baseParentEntityId }), {
invokedBy: NavigationMethod.SegmentControl, invokedBy: NavigationMethod.SegmentControl,
}); });
break; break;
case EditorEntityTab.JS: case EditorEntityTab.JS:
history.push(jsCollectionListURL({ basePageId }), { history.push(jsCollectionListURL({ baseParentEntityId }), {
invokedBy: NavigationMethod.SegmentControl, invokedBy: NavigationMethod.SegmentControl,
}); });
break; break;
case EditorEntityTab.UI: case EditorEntityTab.UI:
history.push(widgetListURL({ basePageId }), { history.push(widgetListURL({ baseParentEntityId }), {
invokedBy: NavigationMethod.SegmentControl, invokedBy: NavigationMethod.SegmentControl,
}); });
break; break;

View File

@ -12,7 +12,7 @@ import {
} from "ee/constants/messages"; } from "ee/constants/messages";
import DebuggerLogs from "components/editorComponents/Debugger/DebuggerLogs"; import DebuggerLogs from "components/editorComponents/Debugger/DebuggerLogs";
import ErrorLogs from "components/editorComponents/Debugger/Errors"; import ErrorLogs from "components/editorComponents/Debugger/Errors";
import { Datasource } from "PluginActionEditor/components/PluginActionResponse/components/DatasourceTab"; import { DatasourceTab } from "PluginActionEditor/components/PluginActionResponse/components/DatasourceTab";
import type { ActionResponse } from "api/ActionAPI"; import type { ActionResponse } from "api/ActionAPI";
import type { SourceEntity } from "entities/AppsmithConsole"; import type { SourceEntity } from "entities/AppsmithConsole";
import type { Action } from "entities/Action"; import type { Action } from "entities/Action";
@ -219,10 +219,12 @@ function QueryDebuggerTabs({
key: DEBUGGER_TAB_KEYS.DATASOURCE_TAB, key: DEBUGGER_TAB_KEYS.DATASOURCE_TAB,
title: "Datasource", title: "Datasource",
panelComponent: ( panelComponent: (
<Datasource <DatasourceTab
currentActionId={currentActionConfig.id} currentActionId={currentActionConfig.id}
datasourceId={currentActionConfig.datasource.id || ""} datasourceId={currentActionConfig.datasource.id || ""}
datasourceName={datasource?.name || ""} datasourceName={
datasource?.name || currentActionConfig.datasource.name || ""
}
/> />
), ),
}); });

View File

@ -1561,66 +1561,101 @@ function* fetchDatasourceStructureSaga(
yield take(ReduxActionTypes.FETCH_ENVIRONMENT_SUCCESS); yield take(ReduxActionTypes.FETCH_ENVIRONMENT_SUCCESS);
} }
const datasource = shouldBeDefined<Datasource>(
yield select(getDatasource, action.payload.id),
`Datasource not found for id - ${action.payload.id}`,
);
const plugin: Plugin = yield select(getPlugin, datasource?.pluginId);
let errorMessage = ""; let errorMessage = "";
let isSuccess = false; let isSuccess = false;
try { try {
const response: ApiResponse = yield DatasourcesApi.fetchDatasourceStructure( const datasource = shouldBeDefined<Datasource>(
action.payload.id, yield select(getDatasource, action.payload.id),
action.payload.ignoreCache, `Datasource not found for id - ${action.payload.id}`,
); );
const isValidResponse: boolean = yield validateResponse(response, false); const plugin: Plugin = yield select(getPlugin, datasource?.pluginId);
if (isValidResponse) { try {
const response: ApiResponse =
yield DatasourcesApi.fetchDatasourceStructure(
action.payload.id,
action.payload.ignoreCache,
);
const isValidResponse: boolean = yield validateResponse(response, false);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.FETCH_DATASOURCE_STRUCTURE_SUCCESS,
payload: {
data: response.data,
datasourceId: action.payload.id,
},
});
if (isEmpty(response.data)) {
errorMessage = createMessage(DATASOURCE_SCHEMA_NOT_AVAILABLE);
AppsmithConsole.warning({
text: "Datasource structure could not be retrieved",
source: {
id: action.payload.id,
name: datasource.name,
type: ENTITY_TYPE.DATASOURCE,
},
});
} else {
isSuccess = true;
AppsmithConsole.info({
text: "Datasource structure retrieved",
source: {
id: action.payload.id,
name: datasource.name,
type: ENTITY_TYPE.DATASOURCE,
},
});
}
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (!!(response.data as any)?.error) {
isSuccess = false;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
errorMessage = (response.data as any).error?.message;
}
}
} catch (error) {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
errorMessage = (error as any)?.message;
isSuccess = false;
yield put({ yield put({
type: ReduxActionTypes.FETCH_DATASOURCE_STRUCTURE_SUCCESS, type: ReduxActionErrorTypes.FETCH_DATASOURCE_STRUCTURE_ERROR,
payload: { payload: {
data: response.data, error,
show: false,
datasourceId: action.payload.id, datasourceId: action.payload.id,
}, },
}); });
AppsmithConsole.error({
if (isEmpty(response.data)) { text: "Datasource structure could not be retrieved",
errorMessage = createMessage(DATASOURCE_SCHEMA_NOT_AVAILABLE); source: {
AppsmithConsole.warning({ id: action.payload.id,
text: "Datasource structure could not be retrieved", name: datasource.name,
source: { type: ENTITY_TYPE.DATASOURCE,
id: action.payload.id, },
name: datasource.name, });
type: ENTITY_TYPE.DATASOURCE,
},
});
} else {
isSuccess = true;
AppsmithConsole.info({
text: "Datasource structure retrieved",
source: {
id: action.payload.id,
name: datasource.name,
type: ENTITY_TYPE.DATASOURCE,
},
});
}
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (!!(response.data as any)?.error) {
isSuccess = false;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
errorMessage = (response.data as any).error?.message;
}
} }
const currentEnvDetails: { id: string; name: string } = yield select(
getCurrentEnvironmentDetails,
);
AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH", {
datasourceId: datasource?.id,
pluginName: plugin?.name,
environmentId: currentEnvDetails.id,
environmentName: currentEnvDetails.name,
errorMessage: errorMessage,
isSuccess: isSuccess,
source: action.payload.schemaFetchContext,
});
} catch (error) { } catch (error) {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
errorMessage = (error as any)?.message;
isSuccess = false;
yield put({ yield put({
type: ReduxActionErrorTypes.FETCH_DATASOURCE_STRUCTURE_ERROR, type: ReduxActionErrorTypes.FETCH_DATASOURCE_STRUCTURE_ERROR,
payload: { payload: {
@ -1629,28 +1664,7 @@ function* fetchDatasourceStructureSaga(
datasourceId: action.payload.id, datasourceId: action.payload.id,
}, },
}); });
AppsmithConsole.error({
text: "Datasource structure could not be retrieved",
source: {
id: action.payload.id,
name: datasource.name,
type: ENTITY_TYPE.DATASOURCE,
},
});
} }
const currentEnvDetails: { id: string; name: string } = yield select(
getCurrentEnvironmentDetails,
);
AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH", {
datasourceId: datasource?.id,
pluginName: plugin?.name,
environmentId: currentEnvDetails.id,
environmentName: currentEnvDetails.name,
errorMessage: errorMessage,
isSuccess: isSuccess,
source: action.payload.schemaFetchContext,
});
} }
function* addAndFetchDatasourceStructureSaga( function* addAndFetchDatasourceStructureSaga(