feat: Schema tab UI update (#37420)
## Description Updated schema tab as per new design. https://www.figma.com/design/8L9BXMzNTKboGWlHpdXyYP/Appsmith-IDE?node-id=3071-101845&node-type=text&m=dev Fixes #35289 ## Automation /ok-to-test tags="@tag.Sanity" ### 🔍 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/12022221992> > Commit: c38fc9948554344a45c172fe291f49a0a5cd9b61 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=12022221992&attempt=2" target="_blank">Cypress dashboard</a>. > Tags: `@tag.Sanity` > Spec: > <hr>Tue, 26 Nov 2024 03:45:12 UTC <!-- end of auto-generated comment: Cypress test results --> ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [x] No <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes - **New Features** - Introduced new components: `CurrentDataSource`, `DatasourceSelector`, `SchemaTables`, `TableColumns`, `StatusDisplay`, `CurrentDataSourceLink`, `Schema`, `MenuField`, and custom hooks `useCreateDatasource` and `useGoToDatasource`. - Added constants for improved user feedback in schema-related messages. - **Improvements** - Enhanced layout and styling for various components, including `BottomView` and `Schema`. - Updated test identifiers for better consistency and testability. - **Bug Fixes** - Adjusted test cases to ensure accurate schema validation. - **Refactor** - Updated internal logic for handling datasource interactions and state management across components. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
d0fc6d793b
commit
b778d2cf6a
|
|
@ -79,7 +79,7 @@ describe(
|
|||
"public.users",
|
||||
);
|
||||
dataSources.SelectTableFromPreviewSchemaList("public.users");
|
||||
dataSources.VerifyColumnSchemaOnQueryEditor("id", 1);
|
||||
dataSources.VerifyColumnSchemaOnQueryEditor("id", 0);
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ export class DataSources {
|
|||
"')]/ancestor::div[@class='form-config-top']/following-sibling::div//div[contains(@class, 'rc-select-multiple')]";
|
||||
private _datasourceSchemaRefreshBtn = ".datasourceStructure-refresh";
|
||||
private _datasourceStructureHeader = ".datasourceStructure-header";
|
||||
_datasourceSchemaColumn = ".t--datasource-column";
|
||||
_datasourceSchemaColumn = ".t--datasource-column .t--field-name";
|
||||
_datasourceStructureSearchInput = ".datasourceStructure-search input";
|
||||
_jsModeSortingControl = ".t--actionConfiguration\\.formData\\.sortBy\\.data";
|
||||
public _queryEditorCollapsibleIcon = ".collapsible-icon";
|
||||
|
|
@ -296,7 +296,7 @@ export class DataSources {
|
|||
_imgFireStoreLogo = "//img[contains(@src, 'firestore.svg')]";
|
||||
_dsVirtuosoElement = `div .t--schema-virtuoso-container`;
|
||||
private _dsVirtuosoList = `[data-test-id="virtuoso-item-list"]`;
|
||||
private _dsSchemaContainer = `[data-testid="datasource-schema-container"]`;
|
||||
private _dsSchemaContainer = `[data-testid="t--datasource-schema-container"]`;
|
||||
private _dsVirtuosoElementTable = (targetTableName: string) =>
|
||||
`${this._dsSchemaEntityItem}[data-testid='t--entity-item-${targetTableName}']`;
|
||||
private _dsPageTabListItem = (buttonText: string) =>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ const ViewWrapper = styled.div`
|
|||
& {
|
||||
.ads-v2-tabs__list {
|
||||
padding: var(--ads-v2-spaces-1) var(--ads-v2-spaces-7);
|
||||
padding-left: var(--ads-v2-spaces-3);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,210 +0,0 @@
|
|||
import { Button, Flex, Link } from "@appsmith/ads";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
DatasourceStructureContext,
|
||||
type DatasourceColumns,
|
||||
type DatasourceKeys,
|
||||
} from "entities/Datasource";
|
||||
import { DatasourceStructureContainer as DatasourceStructureList } from "pages/Editor/DatasourceInfo/DatasourceStructureContainer";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
getDatasourceStructureById,
|
||||
getIsFetchingDatasourceStructure,
|
||||
getPluginImages,
|
||||
getPluginIdFromDatasourceId,
|
||||
getPluginDatasourceComponentFromId,
|
||||
} from "ee/selectors/entitiesSelector";
|
||||
import DatasourceField from "pages/Editor/DatasourceInfo/DatasourceField";
|
||||
import { find } from "lodash";
|
||||
import type { AppState } from "ee/reducers";
|
||||
import RenderInterimDataState from "pages/Editor/DatasourceInfo/RenderInterimDataState";
|
||||
import { getPluginActionDebuggerState } from "../../../store";
|
||||
import {
|
||||
fetchDatasourceStructure,
|
||||
refreshDatasourceStructure,
|
||||
} from "actions/datasourceActions";
|
||||
import history from "utils/history";
|
||||
import { datasourcesEditorIdURL } from "ee/RouteBuilder";
|
||||
import { EntityIcon } from "pages/Editor/Explorer/ExplorerIcons";
|
||||
import { getAssetUrl } from "ee/utils/airgapHelpers";
|
||||
import { DatasourceComponentTypes } from "api/PluginApi";
|
||||
|
||||
interface Props {
|
||||
datasourceId: string;
|
||||
datasourceName: string;
|
||||
currentActionId: string;
|
||||
}
|
||||
|
||||
const Schema = (props: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const datasourceStructure = useSelector((state) =>
|
||||
getDatasourceStructureById(state, props.datasourceId),
|
||||
);
|
||||
const { responseTabHeight } = useSelector(getPluginActionDebuggerState);
|
||||
|
||||
const pluginId = useSelector((state) =>
|
||||
getPluginIdFromDatasourceId(state, props.datasourceId),
|
||||
);
|
||||
const pluginImages = useSelector((state) => getPluginImages(state));
|
||||
const datasourceIcon = pluginId ? pluginImages[pluginId] : undefined;
|
||||
|
||||
const [selectedTable, setSelectedTable] = useState<string>();
|
||||
|
||||
const selectedTableItems = find(datasourceStructure?.tables, [
|
||||
"name",
|
||||
selectedTable,
|
||||
]);
|
||||
|
||||
const columnsAndKeys: Array<DatasourceColumns | DatasourceKeys> = [];
|
||||
|
||||
if (selectedTableItems) {
|
||||
columnsAndKeys.push(...selectedTableItems.keys);
|
||||
columnsAndKeys.push(...selectedTableItems.columns);
|
||||
}
|
||||
|
||||
const columns =
|
||||
find(datasourceStructure?.tables, ["name", selectedTable])?.columns || [];
|
||||
|
||||
const isLoading = useSelector((state: AppState) =>
|
||||
getIsFetchingDatasourceStructure(state, props.datasourceId),
|
||||
);
|
||||
|
||||
const pluginDatasourceForm = useSelector((state) =>
|
||||
getPluginDatasourceComponentFromId(state, pluginId || ""),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTable(undefined);
|
||||
}, [props.datasourceId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
props.datasourceId &&
|
||||
datasourceStructure === undefined &&
|
||||
pluginDatasourceForm !== DatasourceComponentTypes.RestAPIDatasourceForm
|
||||
) {
|
||||
dispatch(
|
||||
fetchDatasourceStructure(
|
||||
props.datasourceId,
|
||||
true,
|
||||
DatasourceStructureContext.QUERY_EDITOR,
|
||||
),
|
||||
);
|
||||
}
|
||||
}, [props.datasourceId, datasourceStructure, dispatch, pluginDatasourceForm]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedTable && datasourceStructure?.tables?.length && !isLoading) {
|
||||
setSelectedTable(datasourceStructure.tables[0].name);
|
||||
}
|
||||
}, [selectedTable, props.datasourceId, isLoading, datasourceStructure]);
|
||||
|
||||
const refreshStructure = useCallback(() => {
|
||||
dispatch(
|
||||
refreshDatasourceStructure(
|
||||
props.datasourceId,
|
||||
DatasourceStructureContext.QUERY_EDITOR,
|
||||
),
|
||||
);
|
||||
}, [dispatch, props.datasourceId]);
|
||||
|
||||
const goToDatasource = useCallback(() => {
|
||||
history.push(datasourcesEditorIdURL({ datasourceId: props.datasourceId }));
|
||||
}, [props.datasourceId]);
|
||||
|
||||
if (!datasourceStructure) {
|
||||
return (
|
||||
<Flex alignItems="center" flex="1" height="100%" justifyContent="center">
|
||||
{isLoading ? (
|
||||
<RenderInterimDataState state="LOADING" />
|
||||
) : (
|
||||
<RenderInterimDataState state="NODATA" />
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex
|
||||
flexDirection="row"
|
||||
gap="spaces-3"
|
||||
height={`${responseTabHeight - 45}px`}
|
||||
maxWidth="70rem"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Flex
|
||||
data-testid="datasource-schema-container"
|
||||
flex="1"
|
||||
flexDirection="column"
|
||||
gap="spaces-3"
|
||||
overflow="hidden"
|
||||
padding="spaces-3"
|
||||
paddingRight="spaces-0"
|
||||
>
|
||||
<Flex
|
||||
alignItems={"center"}
|
||||
gap="spaces-2"
|
||||
justifyContent={"space-between"}
|
||||
>
|
||||
<Link onClick={goToDatasource}>
|
||||
<Flex
|
||||
alignItems={"center"}
|
||||
gap="spaces-1"
|
||||
justifyContent={"center"}
|
||||
>
|
||||
<EntityIcon height={`16px`} width={`16px`}>
|
||||
<img alt="entityIcon" src={getAssetUrl(datasourceIcon)} />
|
||||
</EntityIcon>
|
||||
{props.datasourceName}
|
||||
</Flex>
|
||||
</Link>
|
||||
<Button
|
||||
className="datasourceStructure-refresh"
|
||||
isIconButton
|
||||
kind="tertiary"
|
||||
onClick={refreshStructure}
|
||||
size="sm"
|
||||
startIcon="refresh"
|
||||
/>
|
||||
</Flex>
|
||||
<DatasourceStructureList
|
||||
context={DatasourceStructureContext.QUERY_EDITOR}
|
||||
datasourceStructure={datasourceStructure}
|
||||
onEntityTableClick={setSelectedTable}
|
||||
step={0}
|
||||
tableName={selectedTable}
|
||||
{...props}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex
|
||||
borderLeft="1px solid var(--ads-v2-color-border)"
|
||||
flex="1"
|
||||
flexDirection="column"
|
||||
height={`${responseTabHeight - 45}px`}
|
||||
justifyContent={
|
||||
isLoading || columns.length === 0 ? "center" : "flex-start"
|
||||
}
|
||||
overflowY="scroll"
|
||||
padding="spaces-3"
|
||||
>
|
||||
{isLoading ? <RenderInterimDataState state="LOADING" /> : null}
|
||||
{!isLoading && columns.length === 0 ? (
|
||||
<RenderInterimDataState state="NOCOLUMNS" />
|
||||
) : null}
|
||||
{!isLoading &&
|
||||
columnsAndKeys.map((field, index) => {
|
||||
return (
|
||||
<DatasourceField
|
||||
field={field}
|
||||
key={`${field.name}${index}`}
|
||||
step={0}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Schema;
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import React from "react";
|
||||
import { Flex } from "@appsmith/ads";
|
||||
import { getAssetUrl } from "ee/utils/airgapHelpers";
|
||||
import { EntityIcon } from "pages/Editor/Explorer/ExplorerIcons";
|
||||
import { useSelector } from "react-redux";
|
||||
import {
|
||||
getPluginIdFromDatasourceId,
|
||||
getPluginImages,
|
||||
} from "ee/selectors/entitiesSelector";
|
||||
|
||||
interface Props {
|
||||
datasourceId: string;
|
||||
datasourceName: string;
|
||||
}
|
||||
|
||||
const CurrentDataSource = ({ datasourceId, datasourceName }: Props) => {
|
||||
const { pluginId, pluginImages } = useSelector((state) => ({
|
||||
pluginId: getPluginIdFromDatasourceId(state, datasourceId),
|
||||
pluginImages: getPluginImages(state),
|
||||
}));
|
||||
|
||||
const datasourceIcon = pluginId ? pluginImages?.[pluginId] : undefined;
|
||||
|
||||
return (
|
||||
<Flex alignItems="center" gap="spaces-2">
|
||||
<EntityIcon height="16px" width="16px">
|
||||
<img alt="entityIcon" src={getAssetUrl(datasourceIcon)} />
|
||||
</EntityIcon>
|
||||
{datasourceName}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export { CurrentDataSource };
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import React, { useCallback } from "react";
|
||||
import { Link } from "@appsmith/ads";
|
||||
import { CurrentDataSource } from "./CurrentDataSource";
|
||||
import { useGoToDatasource } from "PluginActionEditor/components/PluginActionResponse/hooks/useGoToDatasource";
|
||||
|
||||
const CurrentDataSourceLink = ({
|
||||
datasourceId,
|
||||
datasourceName,
|
||||
}: {
|
||||
datasourceId: string;
|
||||
datasourceName: string;
|
||||
}) => {
|
||||
const { goToDatasource } = useGoToDatasource();
|
||||
|
||||
const handleClick = useCallback(
|
||||
() => goToDatasource(datasourceId),
|
||||
[datasourceId, goToDatasource],
|
||||
);
|
||||
|
||||
return (
|
||||
<Link onClick={handleClick}>
|
||||
<CurrentDataSource
|
||||
datasourceId={datasourceId}
|
||||
datasourceName={datasourceName}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export { CurrentDataSourceLink };
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Flex } from "@appsmith/ads";
|
||||
import { CREATE_NEW_DATASOURCE, createMessage } from "ee/constants/messages";
|
||||
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
|
||||
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
||||
import {
|
||||
getHasCreateDatasourcePermission,
|
||||
getHasManageActionPermission,
|
||||
} from "ee/utils/BusinessFeatures/permissionPageHelpers";
|
||||
import { doesPluginRequireDatasource } from "ee/entities/Engine/actionHelpers";
|
||||
import {
|
||||
getActionByBaseId,
|
||||
getDatasourceByPluginId,
|
||||
getPlugin,
|
||||
getPluginImages,
|
||||
} from "ee/selectors/entitiesSelector";
|
||||
import type { Datasource } from "entities/Datasource";
|
||||
import type { AppState } from "ee/reducers";
|
||||
import { getCurrentAppWorkspace } from "ee/selectors/selectedWorkspaceSelectors";
|
||||
import { useActiveActionBaseId } from "ee/pages/Editor/Explorer/hooks";
|
||||
import { INTEGRATION_TABS } from "constants/routes";
|
||||
import { QUERY_EDITOR_FORM_NAME } from "ee/constants/forms";
|
||||
import MenuField from "components/editorComponents/form/fields/MenuField";
|
||||
import type { InjectedFormProps } from "redux-form";
|
||||
import { reduxForm } from "redux-form";
|
||||
import type { Action } from "entities/Action";
|
||||
import { CurrentDataSourceLink } from "./CurrentDataSourceLink";
|
||||
import { CurrentDataSource } from "./CurrentDataSource";
|
||||
import { useCreateDatasource } from "ee/PluginActionEditor/hooks/useCreateDatasource";
|
||||
|
||||
interface CustomProps {
|
||||
datasourceId: string;
|
||||
datasourceName: string;
|
||||
}
|
||||
|
||||
type Props = InjectedFormProps<Action, CustomProps> & CustomProps;
|
||||
|
||||
interface DATASOURCES_OPTIONS_TYPE {
|
||||
label: string;
|
||||
value: string;
|
||||
image?: string;
|
||||
icon?: string;
|
||||
onSelect?: (value: string) => void;
|
||||
}
|
||||
|
||||
const DatasourceSelector = ({ datasourceId, datasourceName }: Props) => {
|
||||
const activeActionBaseId = useActiveActionBaseId();
|
||||
const currentActionConfig = useSelector((state) =>
|
||||
activeActionBaseId
|
||||
? getActionByBaseId(state, activeActionBaseId)
|
||||
: undefined,
|
||||
);
|
||||
const plugin = useSelector((state: AppState) =>
|
||||
getPlugin(state, currentActionConfig?.pluginId || ""),
|
||||
);
|
||||
|
||||
const dataSources = useSelector((state: AppState) =>
|
||||
getDatasourceByPluginId(state, currentActionConfig?.pluginId || ""),
|
||||
);
|
||||
|
||||
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
|
||||
const userWorkspacePermissions = useSelector(
|
||||
(state: AppState) => getCurrentAppWorkspace(state).userPermissions ?? [],
|
||||
);
|
||||
const isChangePermitted = getHasManageActionPermission(
|
||||
isFeatureEnabled,
|
||||
currentActionConfig?.userPermissions,
|
||||
);
|
||||
const canCreateDatasource = getHasCreateDatasourcePermission(
|
||||
isFeatureEnabled,
|
||||
userWorkspacePermissions,
|
||||
);
|
||||
const showDatasourceSelector = doesPluginRequireDatasource(plugin);
|
||||
const pluginImages = useSelector(getPluginImages);
|
||||
|
||||
const { onCreateDatasourceClick } = useCreateDatasource();
|
||||
|
||||
const DATASOURCES_OPTIONS: Array<DATASOURCES_OPTIONS_TYPE> =
|
||||
dataSources.reduce(
|
||||
(acc: Array<DATASOURCES_OPTIONS_TYPE>, dataSource: Datasource) => {
|
||||
if (dataSource.pluginId === plugin?.id) {
|
||||
acc.push({
|
||||
label: dataSource.name,
|
||||
value: dataSource.id,
|
||||
image: pluginImages[dataSource.pluginId],
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
if (canCreateDatasource) {
|
||||
DATASOURCES_OPTIONS.push({
|
||||
label: createMessage(CREATE_NEW_DATASOURCE),
|
||||
value: "create",
|
||||
icon: "plus",
|
||||
onSelect: () =>
|
||||
onCreateDatasourceClick(
|
||||
INTEGRATION_TABS.NEW,
|
||||
currentActionConfig?.pageId,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (!showDatasourceSelector || !isChangePermitted) {
|
||||
return (
|
||||
<CurrentDataSourceLink
|
||||
datasourceId={datasourceId}
|
||||
datasourceName={datasourceName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex>
|
||||
<MenuField
|
||||
className={"t--switch-datasource"}
|
||||
formName={QUERY_EDITOR_FORM_NAME}
|
||||
name="datasource.id"
|
||||
options={DATASOURCES_OPTIONS}
|
||||
>
|
||||
<CurrentDataSource
|
||||
datasourceId={datasourceId}
|
||||
datasourceName={datasourceName}
|
||||
/>
|
||||
</MenuField>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default reduxForm<Action, CustomProps>({
|
||||
form: QUERY_EDITOR_FORM_NAME,
|
||||
destroyOnUnmount: false,
|
||||
enableReinitialize: true,
|
||||
})(DatasourceSelector);
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
import { Flex } from "@appsmith/ads";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { DatasourceStructureContext } from "entities/Datasource";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
getDatasourceStructureById,
|
||||
getIsFetchingDatasourceStructure,
|
||||
getPluginIdFromDatasourceId,
|
||||
getPluginDatasourceComponentFromId,
|
||||
} from "ee/selectors/entitiesSelector";
|
||||
import type { AppState } from "ee/reducers";
|
||||
import { fetchDatasourceStructure } from "actions/datasourceActions";
|
||||
import history from "utils/history";
|
||||
import { datasourcesEditorIdURL } from "ee/RouteBuilder";
|
||||
import { DatasourceComponentTypes } from "api/PluginApi";
|
||||
import { getPluginActionDebuggerState } from "PluginActionEditor/store";
|
||||
import { SchemaDisplayStatus, StatusDisplay } from "./StatusDisplay";
|
||||
import DatasourceSelector from "./DatasourceSelector";
|
||||
import { SchemaTables } from "./SchemaTables";
|
||||
import { DatasourceEditEntryPoints } from "constants/Datasource";
|
||||
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
||||
import { isEmpty, omit } from "lodash";
|
||||
import { getQueryParams } from "utils/URLUtils";
|
||||
import { getCurrentPageId } from "selectors/editorSelectors";
|
||||
import { TableColumns } from "./TableColumns";
|
||||
import { BOTTOMBAR_HEIGHT } from "./constants";
|
||||
|
||||
interface Props {
|
||||
datasourceId: string;
|
||||
datasourceName: string;
|
||||
currentActionId: string;
|
||||
}
|
||||
|
||||
const Schema = (props: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const datasourceStructure = useSelector((state) =>
|
||||
getDatasourceStructureById(state, props.datasourceId),
|
||||
);
|
||||
|
||||
const { responseTabHeight } = useSelector(getPluginActionDebuggerState);
|
||||
|
||||
const pluginId = useSelector((state) =>
|
||||
getPluginIdFromDatasourceId(state, props.datasourceId),
|
||||
);
|
||||
|
||||
const currentPageId = useSelector(getCurrentPageId);
|
||||
|
||||
const [selectedTable, setSelectedTable] = useState<string>();
|
||||
|
||||
const isLoading = useSelector((state: AppState) =>
|
||||
getIsFetchingDatasourceStructure(state, props.datasourceId),
|
||||
);
|
||||
|
||||
const pluginDatasourceForm = useSelector((state) =>
|
||||
getPluginDatasourceComponentFromId(state, pluginId || ""),
|
||||
);
|
||||
|
||||
useEffect(
|
||||
function resetSelectedTable() {
|
||||
setSelectedTable(undefined);
|
||||
},
|
||||
[props.datasourceId],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
function fetchDatasourceStructureEffect() {
|
||||
function fetchStructure() {
|
||||
if (
|
||||
props.datasourceId &&
|
||||
datasourceStructure === undefined &&
|
||||
pluginDatasourceForm !==
|
||||
DatasourceComponentTypes.RestAPIDatasourceForm
|
||||
) {
|
||||
dispatch(
|
||||
fetchDatasourceStructure(
|
||||
props.datasourceId,
|
||||
true,
|
||||
DatasourceStructureContext.QUERY_EDITOR,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fetchStructure();
|
||||
},
|
||||
[props.datasourceId, datasourceStructure, dispatch, pluginDatasourceForm],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
function selectFirstTable() {
|
||||
if (!selectedTable && datasourceStructure?.tables?.length && !isLoading) {
|
||||
setSelectedTable(datasourceStructure.tables[0].name);
|
||||
}
|
||||
},
|
||||
[selectedTable, props.datasourceId, isLoading, datasourceStructure],
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-perf/jsx-no-new-function-as-prop
|
||||
const editDatasource = () => {
|
||||
const entryPoint = DatasourceEditEntryPoints.QUERY_EDITOR_DATASOURCE_SCHEMA;
|
||||
|
||||
AnalyticsUtil.logEvent("EDIT_DATASOURCE_CLICK", {
|
||||
datasourceId: props.datasourceId,
|
||||
pluginName: "",
|
||||
entryPoint: entryPoint,
|
||||
});
|
||||
|
||||
const url = datasourcesEditorIdURL({
|
||||
basePageId: currentPageId,
|
||||
datasourceId: props.datasourceId,
|
||||
params: { ...omit(getQueryParams(), "viewMode"), viewMode: false },
|
||||
generateEditorPath: true,
|
||||
});
|
||||
|
||||
history.push(url);
|
||||
};
|
||||
|
||||
const getStatusState = () => {
|
||||
if (isLoading) return SchemaDisplayStatus.SCHEMA_LOADING;
|
||||
|
||||
if (!datasourceStructure) return SchemaDisplayStatus.NOSCHEMA;
|
||||
|
||||
if (datasourceStructure && "error" in datasourceStructure)
|
||||
return SchemaDisplayStatus.FAILED;
|
||||
|
||||
if (isEmpty(datasourceStructure)) return SchemaDisplayStatus.CANTSHOW;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const statusState = getStatusState();
|
||||
|
||||
const renderStatus = () => {
|
||||
if (!statusState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex padding="spaces-3">
|
||||
<DatasourceSelector
|
||||
datasourceId={props.datasourceId}
|
||||
datasourceName={props.datasourceName}
|
||||
/>
|
||||
</Flex>
|
||||
<StatusDisplay
|
||||
editDatasource={editDatasource}
|
||||
errorMessage={
|
||||
datasourceStructure?.error && "message" in datasourceStructure.error
|
||||
? datasourceStructure.error.message
|
||||
: ""
|
||||
}
|
||||
state={statusState}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (statusState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex h="100%">
|
||||
<SchemaTables
|
||||
currentActionId={props.currentActionId}
|
||||
datasourceId={props.datasourceId}
|
||||
datasourceName={props.datasourceName}
|
||||
datasourceStructure={datasourceStructure}
|
||||
selectedTable={selectedTable}
|
||||
setSelectedTable={setSelectedTable}
|
||||
/>
|
||||
<TableColumns
|
||||
datasourceStructure={datasourceStructure}
|
||||
isLoading={isLoading}
|
||||
selectedTable={selectedTable}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
gap="spaces-3"
|
||||
height={`${responseTabHeight - BOTTOMBAR_HEIGHT}px`}
|
||||
overflow="hidden"
|
||||
>
|
||||
{renderStatus()}
|
||||
{renderContent()}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export { Schema };
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { Flex, Button } from "@appsmith/ads";
|
||||
import {
|
||||
DatasourceStructureContext,
|
||||
type DatasourceStructure,
|
||||
} from "entities/Datasource";
|
||||
import { DatasourceStructureContainer as DatasourceStructureList } from "pages/Editor/DatasourceInfo/DatasourceStructureContainer";
|
||||
import React, { useCallback } from "react";
|
||||
import DatasourceSelector from "./DatasourceSelector";
|
||||
import { refreshDatasourceStructure } from "actions/datasourceActions";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { SchemaTableContainer } from "./styles";
|
||||
|
||||
interface Props {
|
||||
datasourceId: string;
|
||||
datasourceName: string;
|
||||
currentActionId: string;
|
||||
datasourceStructure: DatasourceStructure;
|
||||
setSelectedTable: (table: string) => void;
|
||||
selectedTable: string | undefined;
|
||||
}
|
||||
|
||||
const SchemaTables = ({
|
||||
currentActionId,
|
||||
datasourceId,
|
||||
datasourceName,
|
||||
datasourceStructure,
|
||||
selectedTable,
|
||||
setSelectedTable,
|
||||
}: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const refreshStructure = useCallback(() => {
|
||||
dispatch(
|
||||
refreshDatasourceStructure(
|
||||
datasourceId,
|
||||
DatasourceStructureContext.QUERY_EDITOR,
|
||||
),
|
||||
);
|
||||
}, [dispatch, datasourceId]);
|
||||
|
||||
return (
|
||||
<SchemaTableContainer
|
||||
data-testid="t--datasource-schema-container"
|
||||
flexDirection="column"
|
||||
gap="spaces-3"
|
||||
overflow="hidden"
|
||||
padding="spaces-3"
|
||||
paddingBottom="spaces-0"
|
||||
w="400px"
|
||||
>
|
||||
<Flex
|
||||
alignItems={"center"}
|
||||
gap="spaces-2"
|
||||
justifyContent={"space-between"}
|
||||
>
|
||||
<DatasourceSelector
|
||||
datasourceId={datasourceId}
|
||||
datasourceName={datasourceName}
|
||||
/>
|
||||
<Button
|
||||
className="datasourceStructure-refresh"
|
||||
isIconButton
|
||||
kind="tertiary"
|
||||
onClick={refreshStructure}
|
||||
size="sm"
|
||||
startIcon="refresh"
|
||||
/>
|
||||
</Flex>
|
||||
<DatasourceStructureList
|
||||
context={DatasourceStructureContext.QUERY_EDITOR}
|
||||
currentActionId={currentActionId}
|
||||
datasourceId={datasourceId}
|
||||
datasourceName={datasourceName}
|
||||
datasourceStructure={datasourceStructure}
|
||||
onEntityTableClick={setSelectedTable}
|
||||
step={0}
|
||||
tableName={selectedTable}
|
||||
/>
|
||||
</SchemaTableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export { SchemaTables };
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
import React, { type ReactNode } from "react";
|
||||
|
||||
import { Button, Flex, Spinner, Text } from "@appsmith/ads";
|
||||
|
||||
import {
|
||||
createMessage,
|
||||
EMPTY_TABLE_MESSAGE_TEXT,
|
||||
EMPTY_TABLE_TITLE_TEXT,
|
||||
FAILED_RECORDS_MESSAGE_TEXT,
|
||||
FAILED_RECORDS_TITLE_TEXT,
|
||||
LOADING_RECORDS_MESSAGE_TEXT,
|
||||
LOADING_SCHEMA_TITLE_TEXT,
|
||||
NO_COLUMNS_MESSAGE_TEXT,
|
||||
EMPTY_SCHEMA_TITLE_TEXT,
|
||||
EMPTY_SCHEMA_MESSAGE_TEXT,
|
||||
EDIT_DATASOURCE,
|
||||
LOADING_RECORDS_TITLE_TEXT,
|
||||
CANT_SHOW_SCHEMA,
|
||||
} from "ee/constants/messages";
|
||||
import { getAssetUrl } from "ee/utils/airgapHelpers";
|
||||
import { ASSETS_CDN_URL } from "constants/ThirdPartyConstants";
|
||||
|
||||
enum SchemaDisplayStatus {
|
||||
SCHEMA_LOADING = "SCHEMA_LOADING",
|
||||
LOADING = "LOADING",
|
||||
NOSCHEMA = "NOSCHEMA",
|
||||
NODATA = "NODATA",
|
||||
FAILED = "FAILED",
|
||||
NOCOLUMNS = "NOCOLUMNS",
|
||||
CANTSHOW = "CANTSHOW",
|
||||
}
|
||||
|
||||
interface Props {
|
||||
state: SchemaDisplayStatus;
|
||||
editDatasource?: () => void;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
const StateData: Record<
|
||||
SchemaDisplayStatus,
|
||||
{ title?: string; message?: string; image: string | ReactNode }
|
||||
> = {
|
||||
SCHEMA_LOADING: {
|
||||
title: createMessage(LOADING_SCHEMA_TITLE_TEXT),
|
||||
message: createMessage(LOADING_RECORDS_MESSAGE_TEXT),
|
||||
image: <Spinner size="md" />,
|
||||
},
|
||||
LOADING: {
|
||||
title: createMessage(LOADING_RECORDS_TITLE_TEXT),
|
||||
message: createMessage(LOADING_RECORDS_MESSAGE_TEXT),
|
||||
image: <Spinner size="md" />,
|
||||
},
|
||||
NOSCHEMA: {
|
||||
title: createMessage(EMPTY_SCHEMA_TITLE_TEXT),
|
||||
message: createMessage(EMPTY_SCHEMA_MESSAGE_TEXT),
|
||||
image: getAssetUrl(`${ASSETS_CDN_URL}/empty-state.svg`),
|
||||
},
|
||||
NODATA: {
|
||||
title: createMessage(EMPTY_TABLE_TITLE_TEXT),
|
||||
message: createMessage(EMPTY_TABLE_MESSAGE_TEXT),
|
||||
image: getAssetUrl(`${ASSETS_CDN_URL}/empty-state.svg`),
|
||||
},
|
||||
FAILED: {
|
||||
title: createMessage(FAILED_RECORDS_TITLE_TEXT),
|
||||
message: createMessage(FAILED_RECORDS_MESSAGE_TEXT),
|
||||
image: getAssetUrl(`${ASSETS_CDN_URL}/failed-state.svg`),
|
||||
},
|
||||
NOCOLUMNS: {
|
||||
title: createMessage(EMPTY_TABLE_TITLE_TEXT),
|
||||
message: createMessage(NO_COLUMNS_MESSAGE_TEXT),
|
||||
image: getAssetUrl(`${ASSETS_CDN_URL}/empty-state.svg`),
|
||||
},
|
||||
CANTSHOW: {
|
||||
message: createMessage(CANT_SHOW_SCHEMA),
|
||||
image: getAssetUrl(`${ASSETS_CDN_URL}/empty-state.svg`),
|
||||
},
|
||||
};
|
||||
|
||||
const StatusDisplay = ({ editDatasource, errorMessage, state }: Props) => {
|
||||
const { image, message, title } = StateData[state];
|
||||
|
||||
return (
|
||||
<Flex
|
||||
alignItems={"center"}
|
||||
flexDirection={"column"}
|
||||
gap="spaces-7"
|
||||
h="100%"
|
||||
justifyContent={"start"}
|
||||
overflowY={"scroll"}
|
||||
p="spaces-3"
|
||||
w="100%"
|
||||
>
|
||||
{typeof image === "string" ? (
|
||||
<Flex alignItems={"center"} h="150px" justifyContent={"center"}>
|
||||
<img alt={title} className="h-full" src={image} />
|
||||
</Flex>
|
||||
) : (
|
||||
image
|
||||
)}
|
||||
<Flex
|
||||
alignItems={"center"}
|
||||
className="text-center"
|
||||
flexDirection="column"
|
||||
justifyContent={"center"}
|
||||
maxWidth="400px"
|
||||
>
|
||||
<Text kind="heading-xs">{title}</Text>
|
||||
<Text kind="body-m">
|
||||
{state === "FAILED" && errorMessage ? errorMessage : message}
|
||||
</Text>
|
||||
{state === "FAILED" && (
|
||||
<Button
|
||||
className="mt-[16px]"
|
||||
kind="secondary"
|
||||
onClick={editDatasource}
|
||||
size={"sm"}
|
||||
>
|
||||
{createMessage(EDIT_DATASOURCE)}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export { StatusDisplay, SchemaDisplayStatus };
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Flex, type FlexProps, SearchInput, Text } from "@appsmith/ads";
|
||||
import { find } from "lodash";
|
||||
|
||||
import type { DatasourceStructure } from "entities/Datasource";
|
||||
import { StatusDisplay, SchemaDisplayStatus } from "./StatusDisplay";
|
||||
import DatasourceField from "pages/Editor/DatasourceInfo/DatasourceField";
|
||||
import {
|
||||
COLUMNS_SEARCH_PLACEHOLDER,
|
||||
COLUMNS_TITLE,
|
||||
createMessage,
|
||||
} from "ee/constants/messages";
|
||||
import Fuse from "fuse.js";
|
||||
import { TableColumn } from "./styles";
|
||||
|
||||
interface Props {
|
||||
isLoading: boolean;
|
||||
datasourceStructure: DatasourceStructure;
|
||||
selectedTable: string | undefined;
|
||||
}
|
||||
|
||||
const Wrapper: React.FC<FlexProps> = (props) => {
|
||||
return (
|
||||
<Flex
|
||||
borderLeft="1px solid var(--ads-v2-color-border)"
|
||||
flex="1"
|
||||
flexDirection="column"
|
||||
height="100%"
|
||||
justifyContent="flex-start"
|
||||
padding="spaces-3"
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const TableColumns: React.FC<Props> = ({
|
||||
datasourceStructure,
|
||||
isLoading,
|
||||
selectedTable,
|
||||
}) => {
|
||||
// Find selected table items
|
||||
const selectedTableItems = useMemo(
|
||||
() =>
|
||||
find(datasourceStructure?.tables, { name: selectedTable }) ?? {
|
||||
columns: [],
|
||||
keys: [],
|
||||
},
|
||||
[datasourceStructure, selectedTable],
|
||||
);
|
||||
|
||||
// Combine columns and keys
|
||||
const columns = useMemo(() => {
|
||||
return selectedTableItems.columns.map((column) => ({
|
||||
name: column.name,
|
||||
type: column.type,
|
||||
keys: selectedTableItems.keys
|
||||
.filter(
|
||||
(key) =>
|
||||
key.columnNames?.includes(column.name) ||
|
||||
key.fromColumns?.includes(column.name),
|
||||
)
|
||||
.map((key) => key.type),
|
||||
}));
|
||||
}, [selectedTableItems]);
|
||||
|
||||
// search
|
||||
const columnsFuzy = useMemo(
|
||||
() =>
|
||||
new Fuse(columns, {
|
||||
keys: ["name"],
|
||||
shouldSort: true,
|
||||
threshold: 0.5,
|
||||
location: 0,
|
||||
}),
|
||||
[columns],
|
||||
);
|
||||
|
||||
const [term, setTerm] = useState("");
|
||||
const filteredColumns = useMemo(
|
||||
() => (term ? columnsFuzy.search(term) : columns),
|
||||
[term, columns, columnsFuzy],
|
||||
);
|
||||
|
||||
const handleSearch = useCallback((value: string) => setTerm(value), []);
|
||||
|
||||
// Reset term whenever selectedTable changes
|
||||
useEffect(
|
||||
function clearTerm() {
|
||||
setTerm("");
|
||||
},
|
||||
[selectedTable],
|
||||
);
|
||||
|
||||
// loading status
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<StatusDisplay state={SchemaDisplayStatus.LOADING} />
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// no columns status
|
||||
if (columns.length === 0) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<StatusDisplay state={SchemaDisplayStatus.NOCOLUMNS} />
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper gap="spaces-3">
|
||||
<Flex alignItems="center" minH="24px">
|
||||
<Text>{createMessage(COLUMNS_TITLE)}</Text>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<SearchInput
|
||||
className="datasourceStructure-search"
|
||||
endIcon="close"
|
||||
onChange={handleSearch}
|
||||
placeholder={createMessage(COLUMNS_SEARCH_PLACEHOLDER, selectedTable)}
|
||||
size={"sm"}
|
||||
startIcon="search"
|
||||
value={term}
|
||||
/>
|
||||
</Flex>
|
||||
<TableColumn flexDirection="column" overflowY="scroll">
|
||||
{filteredColumns.map((field, index) => (
|
||||
<DatasourceField
|
||||
field={field}
|
||||
key={`${field.name}${index}`}
|
||||
step={0}
|
||||
/>
|
||||
))}
|
||||
</TableColumn>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export { TableColumns };
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
// This is bottom bar height + paddings
|
||||
export const BOTTOMBAR_HEIGHT = 42;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from "./Schema";
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import styled from "styled-components";
|
||||
import { Flex } from "@appsmith/ads";
|
||||
|
||||
export const TableColumn = styled(Flex)`
|
||||
& .t--datasource-column {
|
||||
padding: 0;
|
||||
|
||||
& > div {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SchemaTableContainer = styled(Flex)`
|
||||
& .t--entity-item {
|
||||
height: 28px;
|
||||
grid-template-columns: 0 auto 1fr auto auto auto auto auto;
|
||||
|
||||
.entity-icon > .ads-v2-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { useCallback } from "react";
|
||||
import { datasourcesEditorIdURL } from "ee/RouteBuilder";
|
||||
import history from "utils/history";
|
||||
|
||||
function useGoToDatasource() {
|
||||
const goToDatasource = useCallback((datasourceId: string) => {
|
||||
history.push(
|
||||
datasourcesEditorIdURL({ datasourceId, generateEditorPath: true }),
|
||||
);
|
||||
}, []);
|
||||
|
||||
return { goToDatasource };
|
||||
}
|
||||
|
||||
export { useGoToDatasource };
|
||||
|
|
@ -25,7 +25,7 @@ import {
|
|||
} from "PluginActionEditor/store";
|
||||
import { doesPluginRequireDatasource } from "ee/entities/Engine/actionHelpers";
|
||||
import useShowSchema from "PluginActionEditor/components/PluginActionResponse/hooks/useShowSchema";
|
||||
import Schema from "PluginActionEditor/components/PluginActionResponse/components/Schema";
|
||||
import { Schema } from "PluginActionEditor/components/PluginActionResponse/components/Schema";
|
||||
import QueryResponseTab from "PluginActionEditor/components/PluginActionResponse/components/QueryResponseTab";
|
||||
import type { SourceEntity } from "entities/AppsmithConsole";
|
||||
import { ENTITY_TYPE as SOURCE_ENTITY_TYPE } from "ee/entities/AppsmithConsole/utils";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import { useCallback } from "react";
|
||||
import { integrationEditorURL } from "ee/RouteBuilder";
|
||||
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
|
||||
import { DatasourceCreateEntryPoints } from "constants/Datasource";
|
||||
import history from "utils/history";
|
||||
|
||||
function useCreateDatasource() {
|
||||
const onCreateDatasourceClick = useCallback(
|
||||
(selectedTab, pageId?: string) => {
|
||||
history.push(
|
||||
integrationEditorURL({
|
||||
basePageId: pageId,
|
||||
selectedTab,
|
||||
}),
|
||||
);
|
||||
|
||||
AnalyticsUtil.logEvent("NAVIGATE_TO_CREATE_NEW_DATASOURCE_PAGE", {
|
||||
entryPoint: DatasourceCreateEntryPoints.QUERY_EDITOR,
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return { onCreateDatasourceClick };
|
||||
}
|
||||
|
||||
export { useCreateDatasource };
|
||||
|
|
@ -365,7 +365,7 @@ export const DATASOURCE_UPDATE = (dsName: string) =>
|
|||
`${dsName} datasource updated successfully`;
|
||||
export const DATASOURCE_VALID = (dsName: string) =>
|
||||
`${dsName} datasource is valid`;
|
||||
export const EDIT_DATASOURCE = () => "Edit";
|
||||
export const EDIT_DATASOURCE = () => "Edit configuration";
|
||||
export const SAVE_DATASOURCE = () => "Save URL";
|
||||
export const EDIT_DATASOURCE_TOOLTIP = () => "Edit datasource";
|
||||
export const SAVE_DATASOURCE_TOOLTIP = () => "Save URL as a datasource";
|
||||
|
|
@ -804,7 +804,7 @@ export const SCHEMA_NOT_AVAILABLE = () =>
|
|||
"We can't show schema for this datasource";
|
||||
export const TABLE_NOT_FOUND = () => "Table not found.";
|
||||
export const DATASOURCE_STRUCTURE_INPUT_PLACEHOLDER_TEXT = (name: string) =>
|
||||
`Tables in ${name}`;
|
||||
`Search tables in ${name}`;
|
||||
export const SCHEMA_LABEL = () => "Schema";
|
||||
export const STRUCTURE_NOT_FETCHED = () =>
|
||||
"We could not fetch the schema of the database.";
|
||||
|
|
@ -2270,14 +2270,24 @@ export const COMMUNITY_TEMPLATES = {
|
|||
|
||||
// Interim data state info
|
||||
export const EMPTY_TABLE_TITLE_TEXT = () => "Empty table";
|
||||
export const EMPTY_SCHEMA_TITLE_TEXT = () => "Empty schema";
|
||||
export const EMPTY_TABLE_MESSAGE_TEXT = () =>
|
||||
"There are no data records to show";
|
||||
export const EMPTY_SCHEMA_MESSAGE_TEXT = () =>
|
||||
"There are no schema records to show";
|
||||
export const NO_COLUMNS_MESSAGE_TEXT = () => "There are no columns to show";
|
||||
export const LOADING_RECORDS_TITLE_TEXT = () => "Loading records";
|
||||
export const LOADING_SCHEMA_TITLE_TEXT = () => "Loading schema";
|
||||
export const LOADING_RECORDS_MESSAGE_TEXT = () => "This may take a few seconds";
|
||||
export const FAILED_RECORDS_TITLE_TEXT = () => "Failed to load";
|
||||
export const FAILED_RECORDS_MESSAGE_TEXT = () =>
|
||||
"There was an error connecting to the datasource. Please check the datasource configuration and retry. If the issue persists, review the datasource settings.";
|
||||
"There was an error connecting to the datasource. Please check the datasource configuration and retry.";
|
||||
export const DATASOURCE_SWITCHER_MENU_GROUP_NAME = () => "Select a datasource";
|
||||
export const CANT_SHOW_SCHEMA = () =>
|
||||
"We can’t show the schema for this datasource";
|
||||
export const COLUMNS_TITLE = () => "Columns";
|
||||
export const COLUMNS_SEARCH_PLACEHOLDER = (tableName: string) =>
|
||||
`Search columns in ${tableName}`;
|
||||
|
||||
export const DATA_PANE_TITLE = () => "Datasources in your workspace";
|
||||
export const DATASOURCE_LIST_BLANK_DESCRIPTION = () =>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
import {
|
||||
Button,
|
||||
Menu,
|
||||
MenuContent,
|
||||
MenuGroupName,
|
||||
MenuTrigger,
|
||||
Text,
|
||||
MenuGroup,
|
||||
MenuItem,
|
||||
Flex,
|
||||
Icon,
|
||||
} from "@appsmith/ads";
|
||||
import { getAssetUrl } from "ee/utils/airgapHelpers";
|
||||
import React from "react";
|
||||
import { type WrappedFieldProps, type BaseFieldProps, Field } from "redux-form";
|
||||
|
||||
interface iOption {
|
||||
value: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
image?: string;
|
||||
onSelect?: (value: string) => void;
|
||||
}
|
||||
|
||||
interface iMenuFieldProps {
|
||||
options: iOption[];
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
groupName?: string;
|
||||
}
|
||||
|
||||
const MenuFieldRender = (props: iMenuFieldProps & WrappedFieldProps) => {
|
||||
const { children, groupName, input, options } = props;
|
||||
|
||||
const handleMenuSelect = (option: iOption) => {
|
||||
if (option.onSelect) {
|
||||
option.onSelect(option.value); // Trigger custom onSelect
|
||||
} else {
|
||||
input.onChange(option.value); // Default behavior
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuTrigger>
|
||||
<Button endIcon={"arrow-down-s-line"} kind="tertiary" size="sm">
|
||||
{children}
|
||||
</Button>
|
||||
</MenuTrigger>
|
||||
<MenuContent align="start" loop width="235px">
|
||||
{groupName && (
|
||||
<MenuGroupName asChild>
|
||||
<Text kind="body-s">{groupName}</Text>
|
||||
</MenuGroupName>
|
||||
)}
|
||||
<MenuGroup>
|
||||
{options.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
onSelect={() => handleMenuSelect(option)}
|
||||
>
|
||||
<Flex alignItems={"center"} gap="spaces-2">
|
||||
{option.image && (
|
||||
<img
|
||||
alt="Datasource"
|
||||
className="plugin-image h-[12px] w-[12px]"
|
||||
src={getAssetUrl(option.image)}
|
||||
/>
|
||||
)}
|
||||
{option.icon && <Icon name={option.icon} size="md" />}
|
||||
{option.label}
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuGroup>
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuField = (
|
||||
props: BaseFieldProps & iMenuFieldProps & { formName: string },
|
||||
) => (
|
||||
<Field className={props.className} component={MenuFieldRender} {...props} />
|
||||
);
|
||||
|
||||
export default MenuField;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from "ce/PluginActionEditor/hooks/useCreateDatasource";
|
||||
|
|
@ -66,6 +66,7 @@ export interface DatasourceKeys {
|
|||
name: string;
|
||||
type: string;
|
||||
columnNames: string[];
|
||||
fromColumns: string[];
|
||||
}
|
||||
|
||||
export interface DatasourceStructure {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
import React, { useRef } from "react";
|
||||
import {
|
||||
DATASOURCE_FIELD_ICONS_MAP,
|
||||
datasourceColumnIcon,
|
||||
} from "../Explorer/ExplorerIcons";
|
||||
import { DATASOURCE_FIELD_ICONS_MAP } from "../Explorer/ExplorerIcons";
|
||||
import styled from "styled-components";
|
||||
import type { DatasourceColumns, DatasourceKeys } from "entities/Datasource";
|
||||
import { Tooltip } from "@appsmith/ads";
|
||||
import { Tooltip, Tag, Flex } from "@appsmith/ads";
|
||||
import { isEllipsisActive } from "utils/helpers";
|
||||
|
||||
const Wrapper = styled.div<{ step: number }>`
|
||||
|
|
@ -21,33 +17,41 @@ const Wrapper = styled.div<{ step: number }>`
|
|||
|
||||
const FieldName = styled.div`
|
||||
color: var(--ads-v2-color-fg);
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
line-height: 13px;
|
||||
text-overflow: ellipsis;
|
||||
padding-right: 30px;
|
||||
`;
|
||||
|
||||
const FieldValue = styled.div`
|
||||
color: var(--ads-v2-color-fg-subtle);
|
||||
text-align: right;
|
||||
font-size: 10px;
|
||||
line-height: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
margin: 0px 4px;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--ads-v2-spaces-2);
|
||||
`;
|
||||
|
||||
const FieldKeyLabel = styled.span`
|
||||
&:first-letter {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
`;
|
||||
|
||||
interface FieldProps {
|
||||
name: string;
|
||||
type: string;
|
||||
keys?: string[];
|
||||
}
|
||||
|
||||
interface DatabaseFieldProps {
|
||||
field: DatasourceColumns | DatasourceKeys;
|
||||
field: FieldProps;
|
||||
step: number;
|
||||
}
|
||||
|
||||
|
|
@ -55,12 +59,15 @@ export function DatabaseColumns(props: DatabaseFieldProps) {
|
|||
const field = props.field;
|
||||
const fieldName = field.name;
|
||||
const fieldType = field.type;
|
||||
const icon = DATASOURCE_FIELD_ICONS_MAP[fieldType] || datasourceColumnIcon;
|
||||
const fieldKeys = field.keys;
|
||||
const icon =
|
||||
fieldKeys && fieldKeys.length > 0
|
||||
? DATASOURCE_FIELD_ICONS_MAP[fieldKeys[0]]
|
||||
: null;
|
||||
const nameRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
return (
|
||||
<Wrapper className="t--datasource-column" step={props.step}>
|
||||
{icon}
|
||||
<Content>
|
||||
<Tooltip
|
||||
content={fieldName}
|
||||
|
|
@ -68,9 +75,19 @@ export function DatabaseColumns(props: DatabaseFieldProps) {
|
|||
mouseEnterDelay={2}
|
||||
showArrow={false}
|
||||
>
|
||||
<FieldName ref={nameRef}>{fieldName}</FieldName>
|
||||
<FieldName className="t--field-name" ref={nameRef}>
|
||||
{fieldName}
|
||||
</FieldName>
|
||||
</Tooltip>
|
||||
<FieldValue>{fieldType}</FieldValue>
|
||||
{icon && fieldKeys && (
|
||||
<Tag isClosable={false} size="md">
|
||||
<Flex gap="spaces-1">
|
||||
{icon}
|
||||
<FieldKeyLabel>{fieldKeys[0]}</FieldKeyLabel>
|
||||
</Flex>
|
||||
</Tag>
|
||||
)}
|
||||
</Content>
|
||||
</Wrapper>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import styled from "styled-components";
|
||||
import { Text, Button } from "@appsmith/ads";
|
||||
|
|
@ -42,7 +42,7 @@ const DatasourceStructureNotFound = (props: Props) => {
|
|||
|
||||
const basePageId = useSelector(getCurrentBasePageId);
|
||||
|
||||
const editDatasource = () => {
|
||||
const editDatasource = useCallback(() => {
|
||||
let entryPoint = DatasourceEditEntryPoints.QUERY_EDITOR_DATASOURCE_SCHEMA;
|
||||
|
||||
if (props.context === DatasourceStructureContext.DATASOURCE_VIEW_MODE) {
|
||||
|
|
@ -69,7 +69,7 @@ const DatasourceStructureNotFound = (props: Props) => {
|
|||
});
|
||||
|
||||
history.push(url);
|
||||
};
|
||||
}, [basePageId, datasourceId, pluginName, props]);
|
||||
|
||||
return (
|
||||
<NotFoundContainer>
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ const DatasourceViewModeSchema = (props: Props) => {
|
|||
|
||||
return (
|
||||
<ViewModeSchemaContainer>
|
||||
<DataWrapperContainer data-testid="datasource-schema-container">
|
||||
<DataWrapperContainer data-testid="t--datasource-schema-container">
|
||||
<StructureContainer>
|
||||
{props.datasource && (
|
||||
<DatasourceStructureHeader
|
||||
|
|
|
|||
|
|
@ -399,7 +399,7 @@ function GoogleSheetSchema(props: Props) {
|
|||
return (
|
||||
<ViewModeSchemaContainer>
|
||||
<DataWrapperContainer>
|
||||
<StructureContainer data-testid="datasource-schema-container">
|
||||
<StructureContainer data-testid="t--datasource-schema-container">
|
||||
{datasource && (
|
||||
<DatasourceStructureHeader
|
||||
datasource={datasource}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from "ee/constants/messages";
|
||||
import DebuggerLogs from "components/editorComponents/Debugger/DebuggerLogs";
|
||||
import ErrorLogs from "components/editorComponents/Debugger/Errors";
|
||||
import Schema from "PluginActionEditor/components/PluginActionResponse/components/Schema";
|
||||
import { Schema } from "PluginActionEditor/components/PluginActionResponse/components/Schema";
|
||||
import type { ActionResponse } from "api/ActionAPI";
|
||||
import { isString } from "lodash";
|
||||
import type { SourceEntity } from "entities/AppsmithConsole";
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user