feat: activation phase 1 (#25126)

Feature implementations:
- Schema in the Api Right Side Pane; 
- New Bindings UI, which is now a suggested widget; 
- Feature walkthrough for the aforementioned two units only if you are a new user.
Only those users who have the flags `ab_ds_binding_enabled` and `ab_ds_schema_enabled` independently set to true can see the implementation described above.
https://www.notion.so/appsmith/Activation-60c64894f42d4cdcb92220c1dbc73802
This commit is contained in:
Ayangade Adeoluwa 2023-07-12 07:42:16 +01:00 committed by GitHub
parent daece53b66
commit 0dcef48dc8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 2302 additions and 379 deletions

View File

@ -13,8 +13,18 @@ import {
ERROR_ACTION_EXECUTE_FAIL,
createMessage,
} from "../../../../support/Objects/CommonErrorMessages";
import { featureFlagIntercept } from "../../../../support/Objects/FeatureFlags";
describe("API Bugs", function () {
before(() => {
featureFlagIntercept(
{
ab_ds_binding_enabled: false,
},
false,
);
agHelper.RefreshPage();
});
it("1. Bug 14037: User gets an error even when table widget is added from the API page successfully", function () {
apiPage.CreateAndFillApi(tedTestConfig.mockApiUrl, "Api1");
apiPage.RunAPI();

View File

@ -1,3 +1,4 @@
import { featureFlagIntercept } from "../../../../support/Objects/FeatureFlags";
import { ObjectsRegistry } from "../../../../support/Objects/Registry";
const agHelper = ObjectsRegistry.AggregateHelper,
@ -40,4 +41,24 @@ describe("Datasource form related tests", function () {
dataSources.DeleteQuery("Query1");
dataSources.DeleteDatasouceFromWinthinDS(dataSourceName);
});
it("3. Verify if schema (table and column) exist in query editor and searching works", () => {
featureFlagIntercept(
{
ab_ds_schema_enabled: true,
},
false,
);
agHelper.RefreshPage();
dataSources.CreateMockDB("Users");
dataSources.CreateQueryAfterDSSaved();
dataSources.VerifyTableSchemaOnQueryEditor("public.users");
ee.ExpandCollapseEntity("public.users");
dataSources.VerifyColumnSchemaOnQueryEditor("id");
dataSources.FilterAndVerifyDatasourceSchemaBySearch(
"gender",
true,
"column",
);
});
});

View File

@ -2,12 +2,21 @@ import {
autoLayout,
dataSources,
table,
agHelper,
} from "../../../../support/Objects/ObjectsCore";
import { Widgets } from "../../../../support/Pages/DataSources";
import { featureFlagIntercept } from "../../../../support/Objects/FeatureFlags";
describe("Check Suggested Widgets Feature in auto-layout", function () {
before(() => {
autoLayout.ConvertToAutoLayoutAndVerify(false);
featureFlagIntercept(
{
ab_ds_binding_enabled: true,
},
false,
);
agHelper.RefreshPage();
});
it("1. Suggested widget", () => {

View File

@ -38,7 +38,6 @@ describe("Button Widget Functionality", function () {
);
cy.SaveAndRunAPI();
// Going to HomePage where the button widget is located and opening it's property pane.
_.entityExplorer.ExpandCollapseEntity("Widgets");
_.entityExplorer.ExpandCollapseEntity("Container3");
_.entityExplorer.SelectEntityByName("Button1");
@ -69,8 +68,9 @@ describe("Button Widget Functionality", function () {
// Creating a mock query
// cy.CreateMockQuery("Query1");
_.dataSources.CreateDataSource("Postgres");
_.entityExplorer.ActionTemplateMenuByEntityName("public.film", "SELECT");
// Going to HomePage where the button widget is located and opeing it's property pane.
_.dataSources.CreateQueryAfterDSSaved(
`SELECT * FROM public."film" LIMIT 10;`,
);
_.entityExplorer.ExpandCollapseEntity("Container3");
_.entityExplorer.SelectEntityByName("Button1");

View File

@ -222,12 +222,6 @@ describe("Validate CRUD queries for Amazon S3 along with UI flow verifications",
_.entityExplorer.SelectEntityByName("Table1", "Widgets");
_.agHelper.GetNClick(_.propPane._deleteWidget);
_.entityExplorer.SelectEntityByName($queryName, "Queries/JS");
cy.xpath(queryLocators.suggestedWidgetText).click().wait(1000);
cy.get(commonlocators.textWidget).validateWidgetExists();
_.entityExplorer.SelectEntityByName("Text1", "Widgets");
_.agHelper.GetNClick(_.propPane._deleteWidget);
_.entityExplorer.SelectEntityByName($queryName, "Queries/JS");
cy.deleteQueryUsingContext(); //exeute actions & 200 response is verified in this method
});

View File

@ -1,3 +1,4 @@
import { featureFlagIntercept } from "../../../support/Objects/FeatureFlags";
import {
agHelper,
assertHelper,
@ -87,6 +88,13 @@ describe("Validate MsSQL connection & basic querying with UI flows", () => {
dataSources.RunQuery();
});
//agHelper.ActionContextMenuWithInPane("Delete"); Since next case can continue in same template
featureFlagIntercept(
{
ab_ds_binding_enabled: false,
},
false,
);
agHelper.RefreshPage();
});
it("1. Validate simple queries - Show all existing tables, Describe table & verify query responses", () => {

View File

@ -231,6 +231,10 @@ export class DataSources {
_bodyCodeMirror = "//div[contains(@class, 't--actionConfiguration.body')]";
private _reconnectModalDSToolTip = ".t--ds-list .t--ds-list-title";
private _reconnectModalDSToopTipIcon = ".t--ds-list .ads-v2-icon";
private _datasourceTableSchemaInQueryEditor =
".datasourceStructure-query-editor";
private _datasourceColumnSchemaInQueryEditor = ".t--datasource-column";
private _datasourceStructureSearchInput = ".datasourceStructure-search input";
public AssertDSEditViewMode(mode: "Edit" | "View") {
if (mode == "Edit") this.agHelper.AssertElementAbsence(this._editButton);
@ -1198,6 +1202,35 @@ export class DataSources {
});
}
public VerifyTableSchemaOnQueryEditor(schema: string) {
this.agHelper
.GetElement(this._datasourceTableSchemaInQueryEditor)
.contains(schema);
}
public VerifyColumnSchemaOnQueryEditor(schema: string, index = 0) {
this.agHelper
.GetElement(this._datasourceColumnSchemaInQueryEditor)
.eq(index)
.contains(schema);
}
public FilterAndVerifyDatasourceSchemaBySearch(
search: string,
verifySearch = false,
filterBy: "table" | "column" = "column",
) {
this.agHelper.TypeText(this._datasourceStructureSearchInput, search);
if (verifySearch) {
if (filterBy === "column") {
this.VerifyColumnSchemaOnQueryEditor(search);
} else {
this.VerifyTableSchemaOnQueryEditor(search);
}
}
}
public SaveDSFromDialog(save = true) {
this.agHelper.GoBack();
this.agHelper.AssertElementVisible(this._datasourceModalDoNotSave);

View File

@ -14,6 +14,7 @@ import type { PluginType } from "entities/Action";
import type { executeDatasourceQueryRequest } from "api/DatasourcesApi";
import type { ResponseMeta } from "api/ApiResponses";
import { TEMP_DATASOURCE_ID } from "constants/Datasource";
import type { DatasourceStructureContext } from "pages/Editor/Explorer/Datasources/DatasourceStructureContainer";
export const createDatasourceFromForm = (
payload: CreateDatasourceConfig & Datasource,
@ -106,12 +107,17 @@ export const redirectAuthorizationCode = (
};
};
export const fetchDatasourceStructure = (id: string, ignoreCache?: boolean) => {
export const fetchDatasourceStructure = (
id: string,
ignoreCache?: boolean,
schemaFetchContext?: DatasourceStructureContext,
) => {
return {
type: ReduxActionTypes.FETCH_DATASOURCE_STRUCTURE_INIT,
payload: {
id,
ignoreCache,
schemaFetchContext,
},
};
};
@ -160,11 +166,15 @@ export const expandDatasourceEntity = (id: string) => {
};
};
export const refreshDatasourceStructure = (id: string) => {
export const refreshDatasourceStructure = (
id: string,
schemaRefreshContext?: DatasourceStructureContext,
) => {
return {
type: ReduxActionTypes.REFRESH_DATASOURCE_STRUCTURE_INIT,
payload: {
id,
schemaRefreshContext,
},
};
};

View File

@ -62,6 +62,7 @@ import {
import useBrandingTheme from "utils/hooks/useBrandingTheme";
import RouteChangeListener from "RouteChangeListener";
import { initCurrentPage } from "../actions/initActions";
import Walkthrough from "components/featureWalkthrough";
export const SentryRoute = Sentry.withSentryRouting(Route);
@ -175,10 +176,10 @@ function AppRouter(props: {
<ErrorPage code={props.safeCrashCode} />
</>
) : (
<>
<Walkthrough>
<AppHeader />
<Routes />
</>
</Walkthrough>
)}
</Suspense>
</Router>

View File

@ -647,6 +647,9 @@ export const BULK_WIDGET_REMOVED = (widgetName: string) =>
export const BULK_WIDGET_ADDED = (widgetName: string) =>
`${widgetName} widgets are added back`;
export const ACTION_CONFIGURATION_CHANGED = (name: string) =>
`${name}'s configuration has changed`;
// Generate page from DB Messages
export const UNSUPPORTED_PLUGIN_DIALOG_TITLE = () =>
@ -691,10 +694,37 @@ export const ADD_NEW_WIDGET = () => "Add new widget";
export const SUGGESTED_WIDGETS = () => "Suggested widgets";
export const SUGGESTED_WIDGET_TOOLTIP = () => "Add to canvas";
export const WELCOME_TOUR_STICKY_BUTTON_TEXT = () => "Next mission";
export const BINDING_SECTION_LABEL = () => "Bindings";
export const ADD_NEW_WIDGET_SUB_HEADING = () =>
"Select how you want to display data.";
export const CONNECT_EXISTING_WIDGET_LABEL = () => "Select a Widget";
export const CONNECT_EXISTING_WIDGET_SUB_HEADING = () =>
"Replace the data of an existing widget";
export const NO_EXISTING_WIDGETS = () => "Display data in a new widget";
export const BINDING_WALKTHROUGH_TITLE = () => "Display your data";
export const BINDING_WALKTHROUGH_DESC = () =>
"You can replace data of an existing widget of your page or you can select a new widget.";
export const BINDINGS_DISABLED_TOOLTIP = () =>
"You can display data when you have a successful response to your query";
// Data Sources pane
export const EMPTY_ACTIVE_DATA_SOURCES = () => "No active datasources found.";
// Datasource structure
export const SCHEMA_NOT_AVAILABLE = () => "Schema not available";
export const TABLE_OR_COLUMN_NOT_FOUND = () => "Table or column not found.";
export const DATASOURCE_STRUCTURE_INPUT_PLACEHOLDER_TEXT = () =>
"Search for table or attribute";
export const SCHEMA_LABEL = () => "Schema";
export const STRUCTURE_NOT_FETCHED = () =>
"We could not fetch the schema of the database.";
export const TEST_DATASOURCE_AND_FIX_ERRORS = () =>
"Test the datasource and fix the errors.";
export const LOADING_SCHEMA = () => "Loading schema...";
export const SCHEMA_WALKTHROUGH_TITLE = () => "Query data fast";
export const SCHEMA_WALKTHROUGH_DESC = () =>
"Select a template from a database table to quickly create your first query. ";
// Git sync
export const CONNECTED_TO_GIT = () => "Connected to Git";

View File

@ -1,3 +1,4 @@
// Please follow naming convention : https://www.notion.so/appsmith/Using-Feature-Flags-in-Appsmith-d362fe7acc7d4ef0aa12e1f5f9b83b5f?pvs=4#f6d4242e56284e84af25cadef71b7aeb to create feature flags.
export const FEATURE_FLAG = {
TEST_FLAG: "TEST_FLAG",
release_datasource_environments_enabled:
@ -8,6 +9,8 @@ export const FEATURE_FLAG = {
ask_ai_js: "ask_ai_js",
APP_EMBED_VIEW_HIDE_SHARE_SETTINGS_VISIBILITY:
"APP_EMBED_VIEW_HIDE_SHARE_SETTINGS_VISIBILITY",
ab_ds_schema_enabled: "ab_ds_schema_enabled",
ab_ds_binding_enabled: "ab_ds_binding_enabled",
} as const;
export type FeatureFlag = keyof typeof FEATURE_FLAG;
@ -22,4 +25,11 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = {
ask_ai_js: false,
ask_ai_sql: false,
APP_EMBED_VIEW_HIDE_SHARE_SETTINGS_VISIBILITY: false,
ab_ds_schema_enabled: false,
ab_ds_binding_enabled: false,
};
export const AB_TESTING_EVENT_KEYS = {
abTestingFlagLabel: "abTestingFlagLabel",
abTestingFlagValue: "abTestingFlagValue",
};

View File

@ -1,4 +1,4 @@
import React, { memo } from "react";
import React, { memo, useContext, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import styled from "styled-components";
import { generateReactKey } from "utils/generators";
@ -8,9 +8,16 @@ import { addSuggestedWidget } from "actions/widgetActions";
import AnalyticsUtil from "utils/AnalyticsUtil";
import {
ADD_NEW_WIDGET,
ADD_NEW_WIDGET_SUB_HEADING,
BINDING_SECTION_LABEL,
CONNECT_EXISTING_WIDGET_LABEL,
CONNECT_EXISTING_WIDGET_SUB_HEADING,
createMessage,
NO_EXISTING_WIDGETS,
SUGGESTED_WIDGETS,
SUGGESTED_WIDGET_TOOLTIP,
BINDING_WALKTHROUGH_TITLE,
BINDING_WALKTHROUGH_DESC,
} from "@appsmith/constants/messages";
import type { SuggestedWidget } from "api/ActionAPI";
@ -20,8 +27,39 @@ import { getNextWidgetName } from "sagas/WidgetOperationUtils";
import { ASSETS_CDN_URL } from "constants/ThirdPartyConstants";
import { getAssetUrl } from "@appsmith/utils/airgapHelpers";
import { Tooltip } from "design-system";
import type { TextKind } from "design-system";
import { Text } from "design-system";
import {
AB_TESTING_EVENT_KEYS,
FEATURE_FLAG,
} from "@appsmith/entities/FeatureFlag";
import { selectFeatureFlagCheck } from "selectors/featureFlagsSelectors";
import type { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsStructureReducer";
import { useParams } from "react-router";
import { getCurrentApplicationId } from "selectors/editorSelectors";
import { bindDataOnCanvas } from "actions/pluginActionActions";
import { bindDataToWidget } from "actions/propertyPaneActions";
import tableWidgetIconSvg from "../../../widgets/TableWidgetV2/icon.svg";
import selectWidgetIconSvg from "../../../widgets/SelectWidget/icon.svg";
import chartWidgetIconSvg from "../../../widgets/ChartWidget/icon.svg";
import inputWidgetIconSvg from "../../../widgets/InputWidgetV2/icon.svg";
import textWidgetIconSvg from "../../../widgets/TextWidget/icon.svg";
import listWidgetIconSvg from "../../../widgets/ListWidget/icon.svg";
import WalkthroughContext from "components/featureWalkthrough/walkthroughContext";
import {
getFeatureFlagShownStatus,
isUserSignedUpFlagSet,
setFeatureFlagShownStatus,
} from "utils/storage";
import { getCurrentUser } from "selectors/usersSelectors";
const BINDING_GUIDE_GIF = `${ASSETS_CDN_URL}/binding.gif`;
const BINDING_SECTION_ID = "t--api-right-pane-binding";
const WidgetList = styled.div`
height: 100%;
overflow: auto;
${getTypographyByKey("p1")}
margin-left: ${(props) => props.theme.spaces[2] + 1}px;
@ -33,6 +71,8 @@ const WidgetList = styled.div`
.image-wrapper {
position: relative;
margin-top: ${(props) => props.theme.spaces[1]}px;
display: flex;
flex-direction: column;
}
.widget:hover {
@ -42,6 +82,76 @@ const WidgetList = styled.div`
.widget:not(:first-child) {
margin-top: 24px;
}
&.spacing {
.widget:not(:first-child) {
margin-top: 16px;
}
}
`;
const ExistingWidgetList = styled.div`
display: flex;
flex-wrap: wrap;
.image-wrapper {
position: relative;
display: flex;
flex-direction: column;
width: 110px;
margin: 4px;
border: 1px solid var(--ads-v2-color-gray-300);
border-radius: var(--ads-v2-border-radius);
&:hover {
border: 1px solid var(--ads-v2-color-gray-600);
}
}
img {
height: 54px;
}
.widget:hover {
cursor: pointer;
}
`;
const ItemWrapper = styled.div`
display: flex;
align-items: center;
padding: 4px;
.widget-name {
padding-left: 8px;
overflow: hidden;
text-overflow: ellipsis;
}
img {
height: 16px;
width: 16px;
}
`;
const SubSection = styled.div`
margin-bottom: ${(props) => props.theme.spaces[7]}px;
overflow-y: scroll;
height: 100%;
`;
const HeadingWrapper = styled.div`
display: flex;
flex-direction: column;
margin-left: ${(props) => props.theme.spaces[2] + 1}px;
padding-bottom: 12px;
`;
const SuggestedWidgetContainer = styled.div`
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
`;
type WidgetBindingInfo = {
@ -49,6 +159,8 @@ type WidgetBindingInfo = {
propertyName: string;
widgetName: string;
image?: string;
icon?: string;
existingImage?: string;
};
export const WIDGET_DATA_FIELD_MAP: Record<string, WidgetBindingInfo> = {
@ -57,42 +169,56 @@ export const WIDGET_DATA_FIELD_MAP: Record<string, WidgetBindingInfo> = {
propertyName: "listData",
widgetName: "List",
image: `${ASSETS_CDN_URL}/widgetSuggestion/list.svg`,
existingImage: `${ASSETS_CDN_URL}/widgetSuggestion/list.svg`,
icon: listWidgetIconSvg,
},
TABLE_WIDGET: {
label: "tabledata",
propertyName: "tableData",
widgetName: "Table",
image: `${ASSETS_CDN_URL}/widgetSuggestion/table.svg`,
existingImage: `${ASSETS_CDN_URL}/widgetSuggestion/existing_table.svg`,
icon: tableWidgetIconSvg,
},
TABLE_WIDGET_V2: {
label: "tabledata",
propertyName: "tableData",
widgetName: "Table",
image: `${ASSETS_CDN_URL}/widgetSuggestion/table.svg`,
existingImage: `${ASSETS_CDN_URL}/widgetSuggestion/existing_table.svg`,
icon: tableWidgetIconSvg,
},
CHART_WIDGET: {
label: "chart-series-data-control",
propertyName: "chartData",
widgetName: "Chart",
image: `${ASSETS_CDN_URL}/widgetSuggestion/chart.svg`,
existingImage: `${ASSETS_CDN_URL}/widgetSuggestion/chart.svg`,
icon: chartWidgetIconSvg,
},
SELECT_WIDGET: {
label: "options",
propertyName: "options",
widgetName: "Select",
image: `${ASSETS_CDN_URL}/widgetSuggestion/dropdown.svg`,
existingImage: `${ASSETS_CDN_URL}/widgetSuggestion/dropdown.svg`,
icon: selectWidgetIconSvg,
},
TEXT_WIDGET: {
label: "text",
propertyName: "text",
widgetName: "Text",
image: `${ASSETS_CDN_URL}/widgetSuggestion/text.svg`,
existingImage: `${ASSETS_CDN_URL}/widgetSuggestion/text.svg`,
icon: textWidgetIconSvg,
},
INPUT_WIDGET_V2: {
label: "text",
propertyName: "defaultText",
widgetName: "Input",
image: `${ASSETS_CDN_URL}/widgetSuggestion/input.svg`,
existingImage: `${ASSETS_CDN_URL}/widgetSuggestion/input.svg`,
icon: inputWidgetIconSvg,
},
};
@ -179,10 +305,66 @@ type SuggestedWidgetProps = {
hasWidgets: boolean;
};
function renderHeading(heading: string, subHeading: string) {
return (
<HeadingWrapper>
<Text kind="heading-xs">{heading}</Text>
<Text kind="body-s">{subHeading}</Text>
</HeadingWrapper>
);
}
function renderWidgetItem(
icon: string | undefined,
name: string | undefined,
textKind: TextKind,
) {
return (
<ItemWrapper>
{icon && <img alt="widget-icon" src={icon} />}
<Text className="widget-name" kind={textKind}>
{name}
</Text>
</ItemWrapper>
);
}
function renderWidgetImage(image: string | undefined) {
if (!!image) {
return <img alt="widget-info-image" src={getAssetUrl(image)} />;
}
return null;
}
function SuggestedWidgets(props: SuggestedWidgetProps) {
const dispatch = useDispatch();
const dataTree = useSelector(getDataTree);
const canvasWidgets = useSelector(getWidgets);
const applicationId = useSelector(getCurrentApplicationId);
const user = useSelector(getCurrentUser);
const {
isOpened: isWalkthroughOpened,
popFeature,
pushFeature,
} = useContext(WalkthroughContext) || {};
// A/B feature flag for query binding.
const isEnabledForQueryBinding = useSelector((state) =>
selectFeatureFlagCheck(state, FEATURE_FLAG.ab_ds_binding_enabled),
);
const params = useParams<{
pageId: string;
apiId?: string;
queryId?: string;
}>();
const closeWalkthrough = async () => {
if (isWalkthroughOpened) {
popFeature && popFeature();
await setFeatureFlagShownStatus(FEATURE_FLAG.ab_ds_binding_enabled, true);
}
};
const addWidget = (
suggestedWidget: SuggestedWidget,
@ -202,45 +384,216 @@ function SuggestedWidgets(props: SuggestedWidgetProps) {
AnalyticsUtil.logEvent("SUGGESTED_WIDGET_CLICK", {
widget: suggestedWidget.type,
[AB_TESTING_EVENT_KEYS.abTestingFlagLabel]:
FEATURE_FLAG.ab_ds_binding_enabled,
[AB_TESTING_EVENT_KEYS.abTestingFlagValue]: isEnabledForQueryBinding,
isWalkthroughOpened,
});
closeWalkthrough();
dispatch(addSuggestedWidget(payload));
};
const label = props.hasWidgets
const handleBindData = (widgetId: string) => {
dispatch(
bindDataOnCanvas({
queryId: (params.apiId || params.queryId) as string,
applicationId: applicationId as string,
pageId: params.pageId,
}),
);
closeWalkthrough();
dispatch(
bindDataToWidget({
widgetId: widgetId,
}),
);
};
const isTableWidgetPresentOnCanvas = () => {
const canvasWidgetLength = Object.keys(canvasWidgets).length;
return (
// widgetKey == 0 condition represents MainContainer
canvasWidgetLength > 1 &&
Object.keys(canvasWidgets).some((widgetKey: string) => {
return (
canvasWidgets[widgetKey]?.type === "TABLE_WIDGET_V2" &&
parseInt(widgetKey, 0) !== 0
);
})
);
};
const labelOld = props.hasWidgets
? createMessage(ADD_NEW_WIDGET)
: createMessage(SUGGESTED_WIDGETS);
const labelNew = createMessage(BINDING_SECTION_LABEL);
const addNewWidgetLabel = createMessage(ADD_NEW_WIDGET);
const addNewWidgetSubLabel = createMessage(ADD_NEW_WIDGET_SUB_HEADING);
const connectExistingWidgetLabel = createMessage(
CONNECT_EXISTING_WIDGET_LABEL,
);
const connectExistingWidgetSubLabel = createMessage(
CONNECT_EXISTING_WIDGET_SUB_HEADING,
);
const isWidgetsPresentOnCanvas = Object.keys(canvasWidgets).length > 0;
const checkAndShowWalkthrough = async () => {
const isFeatureWalkthroughShown = await getFeatureFlagShownStatus(
FEATURE_FLAG.ab_ds_binding_enabled,
);
const isNewUser = user && (await isUserSignedUpFlagSet(user.email));
// Adding walkthrough tutorial
isNewUser &&
!isFeatureWalkthroughShown &&
pushFeature &&
pushFeature({
targetId: BINDING_SECTION_ID,
onDismiss: async () => {
AnalyticsUtil.logEvent("WALKTHROUGH_DISMISSED", {
[AB_TESTING_EVENT_KEYS.abTestingFlagLabel]:
FEATURE_FLAG.ab_ds_binding_enabled,
[AB_TESTING_EVENT_KEYS.abTestingFlagValue]:
isEnabledForQueryBinding,
});
await setFeatureFlagShownStatus(
FEATURE_FLAG.ab_ds_binding_enabled,
true,
);
},
details: {
title: createMessage(BINDING_WALKTHROUGH_TITLE),
description: createMessage(BINDING_WALKTHROUGH_DESC),
imageURL: BINDING_GUIDE_GIF,
},
offset: {
position: "left",
left: -40,
highlightPad: 5,
indicatorLeft: -3,
},
eventParams: {
[AB_TESTING_EVENT_KEYS.abTestingFlagLabel]:
FEATURE_FLAG.ab_ds_binding_enabled,
[AB_TESTING_EVENT_KEYS.abTestingFlagValue]: isEnabledForQueryBinding,
},
});
};
useEffect(() => {
if (isEnabledForQueryBinding) checkAndShowWalkthrough();
}, [isEnabledForQueryBinding]);
return (
<Collapsible label={label}>
<WidgetList>
{props.suggestedWidgets.map((suggestedWidget) => {
const widgetInfo: WidgetBindingInfo | undefined =
WIDGET_DATA_FIELD_MAP[suggestedWidget.type];
<SuggestedWidgetContainer id={BINDING_SECTION_ID}>
{!!isEnabledForQueryBinding ? (
<Collapsible label={labelNew}>
{isTableWidgetPresentOnCanvas() && (
<SubSection>
{renderHeading(
connectExistingWidgetLabel,
connectExistingWidgetSubLabel,
)}
{!isWidgetsPresentOnCanvas && (
<Text kind="body-s">{createMessage(NO_EXISTING_WIDGETS)}</Text>
)}
if (!widgetInfo) return null;
{/* Table Widget condition is added temporarily as connect to existing
functionality is currently working only for Table Widget,
in future we want to support it for all widgets */}
{
<ExistingWidgetList>
{Object.keys(canvasWidgets).map((widgetKey) => {
const widget: FlattenedWidgetProps | undefined =
canvasWidgets[widgetKey];
const widgetInfo: WidgetBindingInfo | undefined =
WIDGET_DATA_FIELD_MAP[widget.type];
return (
<div
className={`widget t--suggested-widget-${suggestedWidget.type}`}
key={suggestedWidget.type}
onClick={() => addWidget(suggestedWidget, widgetInfo)}
>
<Tooltip content={createMessage(SUGGESTED_WIDGET_TOOLTIP)}>
<div className="image-wrapper">
{widgetInfo.image && (
<img
alt="widget-info-image"
src={getAssetUrl(widgetInfo.image)}
/>
)}
if (!widgetInfo || widget?.type !== "TABLE_WIDGET_V2")
return null;
return (
<div
className={`widget t--suggested-widget-${widget.type}`}
key={widget.type + widget.widgetId}
onClick={() => handleBindData(widgetKey)}
>
<Tooltip
content={createMessage(SUGGESTED_WIDGET_TOOLTIP)}
>
<div className="image-wrapper">
{renderWidgetImage(widgetInfo.existingImage)}
{renderWidgetItem(
widgetInfo.icon,
widget.widgetName,
"body-s",
)}
</div>
</Tooltip>
</div>
);
})}
</ExistingWidgetList>
}
</SubSection>
)}
<SubSection>
{renderHeading(addNewWidgetLabel, addNewWidgetSubLabel)}
<WidgetList className="spacing">
{props.suggestedWidgets.map((suggestedWidget) => {
const widgetInfo: WidgetBindingInfo | undefined =
WIDGET_DATA_FIELD_MAP[suggestedWidget.type];
if (!widgetInfo) return null;
return (
<div
className={`widget t--suggested-widget-${suggestedWidget.type}`}
key={suggestedWidget.type}
onClick={() => addWidget(suggestedWidget, widgetInfo)}
>
<Tooltip content={createMessage(SUGGESTED_WIDGET_TOOLTIP)}>
{renderWidgetItem(
widgetInfo.icon,
widgetInfo.widgetName,
"body-m",
)}
</Tooltip>
</div>
);
})}
</WidgetList>
</SubSection>
</Collapsible>
) : (
<Collapsible label={labelOld}>
<WidgetList>
{props.suggestedWidgets.map((suggestedWidget) => {
const widgetInfo: WidgetBindingInfo | undefined =
WIDGET_DATA_FIELD_MAP[suggestedWidget.type];
if (!widgetInfo) return null;
return (
<div
className={`widget t--suggested-widget-${suggestedWidget.type}`}
key={suggestedWidget.type}
onClick={() => addWidget(suggestedWidget, widgetInfo)}
>
<Tooltip content={createMessage(SUGGESTED_WIDGET_TOOLTIP)}>
<div className="image-wrapper">
{renderWidgetImage(widgetInfo.image)}
</div>
</Tooltip>
</div>
</Tooltip>
</div>
);
})}
</WidgetList>
</Collapsible>
);
})}
</WidgetList>
</Collapsible>
)}
</SuggestedWidgetContainer>
);
}

View File

@ -1,8 +1,8 @@
import React, { useMemo } from "react";
import React, { useContext, useMemo } from "react";
import styled from "styled-components";
import { Collapse, Classes as BPClasses } from "@blueprintjs/core";
import { Classes, getTypographyByKey } from "design-system-old";
import { Button, Icon, Link } from "design-system";
import { Button, Divider, Icon, Link, Text } from "design-system";
import { useState } from "react";
import Connections from "./Connections";
import SuggestedWidgets from "./SuggestedWidgets";
@ -17,8 +17,12 @@ import type { AppState } from "@appsmith/reducers";
import { getDependenciesFromInverseDependencies } from "../Debugger/helpers";
import {
BACK_TO_CANVAS,
BINDINGS_DISABLED_TOOLTIP,
BINDING_SECTION_LABEL,
createMessage,
NO_CONNECTIONS,
SCHEMA_WALKTHROUGH_DESC,
SCHEMA_WALKTHROUGH_TITLE,
} from "@appsmith/constants/messages";
import type {
SuggestedWidget,
@ -31,16 +35,39 @@ import {
} from "selectors/editorSelectors";
import { builderURL } from "RouteBuilder";
import { hasManagePagePermission } from "@appsmith/utils/permissionHelpers";
import DatasourceStructureHeader from "pages/Editor/Explorer/Datasources/DatasourceStructureHeader";
import { DatasourceStructureContainer as DataStructureList } from "pages/Editor/Explorer/Datasources/DatasourceStructureContainer";
import { DatasourceStructureContext } from "pages/Editor/Explorer/Datasources/DatasourceStructureContainer";
import { selectFeatureFlagCheck } from "selectors/featureFlagsSelectors";
import {
AB_TESTING_EVENT_KEYS,
FEATURE_FLAG,
} from "@appsmith/entities/FeatureFlag";
import {
getDatasourceStructureById,
getPluginDatasourceComponentFromId,
getPluginNameFromId,
} from "selectors/entitiesSelector";
import { DatasourceComponentTypes } from "api/PluginApi";
import { fetchDatasourceStructure } from "actions/datasourceActions";
import WalkthroughContext from "components/featureWalkthrough/walkthroughContext";
import {
getFeatureFlagShownStatus,
isUserSignedUpFlagSet,
setFeatureFlagShownStatus,
} from "utils/storage";
import { PluginName } from "entities/Action";
import { getCurrentUser } from "selectors/usersSelectors";
import { Tooltip } from "design-system";
import { ASSETS_CDN_URL } from "constants/ThirdPartyConstants";
const SCHEMA_GUIDE_GIF = `${ASSETS_CDN_URL}/schema.gif`;
const SCHEMA_SECTION_ID = "t--api-right-pane-schema";
const SideBar = styled.div`
height: 100%;
width: 100%;
-webkit-animation: slide-left 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
animation: slide-left 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
& > div {
margin-top: ${(props) => props.theme.spaces[11]}px;
}
& > a {
margin-top: 0;
@ -90,15 +117,30 @@ const SideBar = styled.div`
const BackToCanvasLink = styled(Link)`
margin-left: ${(props) => props.theme.spaces[1] + 1}px;
margin-top: ${(props) => props.theme.spaces[11]}px;
margin-bottom: ${(props) => props.theme.spaces[11]}px;
`;
const Label = styled.span`
cursor: pointer;
`;
const CollapsibleWrapper = styled.div<{ isOpen: boolean }>`
const CollapsibleWrapper = styled.div<{
isOpen: boolean;
isDisabled?: boolean;
}>`
display: flex;
flex-direction: column;
overflow: hidden;
${(props) => !!props.isDisabled && `opacity: 0.6`};
&&&&&& .${BPClasses.COLLAPSE} {
flex-grow: 1;
overflow-y: auto !important;
}
.${BPClasses.COLLAPSE_BODY} {
padding-top: ${(props) => props.theme.spaces[3]}px;
height: 100%;
}
& > .icon-text:first-child {
@ -142,14 +184,36 @@ const Placeholder = styled.div`
text-align: center;
`;
const DataStructureListWrapper = styled.div`
overflow-y: scroll;
height: 100%;
`;
const SchemaSideBarSection = styled.div<{ height: number; marginTop?: number }>`
margin-top: ${(props) => props?.marginTop && `${props.marginTop}px`};
height: auto;
display: flex;
width: 100%;
flex-direction: column;
${(props) => props.height && `max-height: ${props.height}%;`}
`;
type CollapsibleProps = {
expand?: boolean;
children: ReactNode;
label: string;
customLabelComponent?: JSX.Element;
isDisabled?: boolean;
};
type DisabledCollapsibleProps = {
label: string;
tooltipLabel?: string;
};
export function Collapsible({
children,
customLabelComponent,
expand = true,
label,
}: CollapsibleProps) {
@ -162,8 +226,14 @@ export function Collapsible({
return (
<CollapsibleWrapper isOpen={isOpen}>
<Label className="icon-text" onClick={() => setIsOpen(!isOpen)}>
<Icon name="down-arrow" size="lg" />
<span className="label">{label}</span>
<Icon name={isOpen ? "down-arrow" : "arrow-right-s-line"} size="lg" />
{!!customLabelComponent ? (
customLabelComponent
) : (
<Text className="label" kind="heading-xs">
{label}
</Text>
)}
</Label>
<Collapse isOpen={isOpen} keepChildrenMounted>
{children}
@ -172,6 +242,24 @@ export function Collapsible({
);
}
export function DisabledCollapsible({
label,
tooltipLabel = "",
}: DisabledCollapsibleProps) {
return (
<Tooltip content={tooltipLabel}>
<CollapsibleWrapper isDisabled isOpen={false}>
<Label className="icon-text">
<Icon name="arrow-right-s-line" size="lg" />
<Text className="label" kind="heading-xs">
{label}
</Text>
</Label>
</CollapsibleWrapper>
</Tooltip>
);
}
export function useEntityDependencies(actionName: string) {
const deps = useSelector((state: AppState) => state.evaluations.dependencies);
const entityDependencies = useMemo(
@ -194,9 +282,12 @@ export function useEntityDependencies(actionName: string) {
function ActionSidebar({
actionName,
context,
datasourceId,
entityDependencies,
hasConnections,
hasResponse,
pluginId,
suggestedWidgets,
}: {
actionName: string;
@ -207,11 +298,16 @@ function ActionSidebar({
directDependencies: string[];
inverseDependencies: string[];
} | null;
datasourceId: string;
pluginId: string;
context: DatasourceStructureContext;
}) {
const dispatch = useDispatch();
const widgets = useSelector(getWidgets);
const applicationId = useSelector(getCurrentApplicationId);
const pageId = useSelector(getCurrentPageId);
const user = useSelector(getCurrentUser);
const { pushFeature } = useContext(WalkthroughContext) || {};
const params = useParams<{
pageId: string;
apiId?: string;
@ -232,6 +328,100 @@ function ActionSidebar({
);
};
const pluginName = useSelector((state) =>
getPluginNameFromId(state, pluginId || ""),
);
const pluginDatasourceForm = useSelector((state) =>
getPluginDatasourceComponentFromId(state, pluginId || ""),
);
// A/B feature flag for datasource structure.
const isEnabledForDSSchema = useSelector((state) =>
selectFeatureFlagCheck(state, FEATURE_FLAG.ab_ds_schema_enabled),
);
// A/B feature flag for query binding.
const isEnabledForQueryBinding = useSelector((state) =>
selectFeatureFlagCheck(state, FEATURE_FLAG.ab_ds_binding_enabled),
);
const datasourceStructure = useSelector((state) =>
getDatasourceStructureById(state, datasourceId),
);
useEffect(() => {
if (
datasourceId &&
datasourceStructure === undefined &&
pluginDatasourceForm !== DatasourceComponentTypes.RestAPIDatasourceForm
) {
dispatch(
fetchDatasourceStructure(
datasourceId,
true,
DatasourceStructureContext.QUERY_EDITOR,
),
);
}
}, []);
const checkAndShowWalkthrough = async () => {
const isFeatureWalkthroughShown = await getFeatureFlagShownStatus(
FEATURE_FLAG.ab_ds_schema_enabled,
);
const isNewUser = user && (await isUserSignedUpFlagSet(user.email));
// Adding walkthrough tutorial
isNewUser &&
!isFeatureWalkthroughShown &&
pushFeature &&
pushFeature({
targetId: SCHEMA_SECTION_ID,
onDismiss: async () => {
AnalyticsUtil.logEvent("WALKTHROUGH_DISMISSED", {
[AB_TESTING_EVENT_KEYS.abTestingFlagLabel]:
FEATURE_FLAG.ab_ds_schema_enabled,
[AB_TESTING_EVENT_KEYS.abTestingFlagValue]: isEnabledForDSSchema,
});
await setFeatureFlagShownStatus(
FEATURE_FLAG.ab_ds_schema_enabled,
true,
);
},
details: {
title: createMessage(SCHEMA_WALKTHROUGH_TITLE),
description: createMessage(SCHEMA_WALKTHROUGH_DESC),
imageURL: SCHEMA_GUIDE_GIF,
},
offset: {
position: "left",
left: -40,
highlightPad: 5,
indicatorLeft: -3,
style: {
transform: "none",
},
},
eventParams: {
[AB_TESTING_EVENT_KEYS.abTestingFlagLabel]:
FEATURE_FLAG.ab_ds_schema_enabled,
[AB_TESTING_EVENT_KEYS.abTestingFlagValue]: isEnabledForDSSchema,
},
});
};
const showSchema =
isEnabledForDSSchema &&
pluginDatasourceForm !== DatasourceComponentTypes.RestAPIDatasourceForm &&
pluginName !== PluginName.SMTP;
useEffect(() => {
if (showSchema) {
checkAndShowWalkthrough();
}
}, [showSchema]);
const hasWidgets = Object.keys(widgets).length > 1;
const pagePermissions = useSelector(getPagePermissions);
@ -242,7 +432,13 @@ function ActionSidebar({
canEditPage && hasResponse && suggestedWidgets && !!suggestedWidgets.length;
const showSnipingMode = hasResponse && hasWidgets;
if (!hasConnections && !showSuggestedWidgets && !showSnipingMode) {
if (
!hasConnections &&
!showSuggestedWidgets &&
!showSnipingMode &&
// putting this here to make the placeholder only appear for rest APIs.
pluginDatasourceForm === DatasourceComponentTypes.RestAPIDatasourceForm
) {
return <Placeholder>{createMessage(NO_CONNECTIONS)}</Placeholder>;
}
@ -257,33 +453,69 @@ function ActionSidebar({
{createMessage(BACK_TO_CANVAS)}
</BackToCanvasLink>
{hasConnections && (
{showSchema && (
<SchemaSideBarSection height={50} id={SCHEMA_SECTION_ID}>
<Collapsible
customLabelComponent={
<DatasourceStructureHeader datasourceId={datasourceId || ""} />
}
expand={!showSuggestedWidgets}
label="Schema"
>
<DataStructureListWrapper>
<DataStructureList
context={context}
currentActionId={params?.queryId || ""}
datasourceId={datasourceId || ""}
datasourceStructure={datasourceStructure}
pluginName={pluginName}
step={0}
/>
</DataStructureListWrapper>
</Collapsible>
</SchemaSideBarSection>
)}
{showSchema && isEnabledForQueryBinding && <Divider />}
{hasConnections && !isEnabledForQueryBinding && (
<Connections
actionName={actionName}
entityDependencies={entityDependencies}
/>
)}
{canEditPage && hasResponse && Object.keys(widgets).length > 1 && (
<Collapsible label="Connect widget">
{/*<div className="description">Go to canvas and select widgets</div>*/}
<SnipingWrapper>
<Button
className={"t--select-in-canvas"}
kind="secondary"
onClick={handleBindData}
size="md"
>
Select widget
</Button>
</SnipingWrapper>
</Collapsible>
)}
{showSuggestedWidgets && (
<SuggestedWidgets
actionName={actionName}
hasWidgets={hasWidgets}
suggestedWidgets={suggestedWidgets as SuggestedWidget[]}
/>
{!isEnabledForQueryBinding &&
canEditPage &&
hasResponse &&
Object.keys(widgets).length > 1 && (
<Collapsible label="Connect widget">
<SnipingWrapper>
<Button
className={"t--select-in-canvas"}
kind="secondary"
onClick={handleBindData}
size="md"
>
Select widget
</Button>
</SnipingWrapper>
</Collapsible>
)}
{showSuggestedWidgets ? (
<SchemaSideBarSection height={40} marginTop={12}>
<SuggestedWidgets
actionName={actionName}
hasWidgets={hasWidgets}
suggestedWidgets={suggestedWidgets as SuggestedWidget[]}
/>
</SchemaSideBarSection>
) : (
isEnabledForQueryBinding && (
<DisabledCollapsible
label={createMessage(BINDING_SECTION_LABEL)}
tooltipLabel={createMessage(BINDINGS_DISABLED_TOOLTIP)}
/>
)
)}
</SideBar>
);

View File

@ -45,8 +45,8 @@ export function useTableOrSpreadsheet() {
const isFetchingSpreadsheets = useSelector(getIsFetchingGsheetSpreadsheets);
const isFetchingDatasourceStructure = useSelector(
getIsFetchingDatasourceStructure,
const isFetchingDatasourceStructure = useSelector((state: AppState) =>
getIsFetchingDatasourceStructure(state, config.datasource),
);
const selectedDatasourcePluginPackageName = useSelector((state: AppState) =>

View File

@ -0,0 +1,82 @@
import React, { lazy, useEffect, useState, Suspense } from "react";
import type { FeatureParams } from "./walkthroughContext";
import WalkthroughContext from "./walkthroughContext";
import { createPortal } from "react-dom";
import { hideIndicator } from "pages/Editor/GuidedTour/utils";
import { retryPromise } from "utils/AppsmithUtils";
import { useLocation } from "react-router-dom";
const WalkthroughRenderer = lazy(() => {
return retryPromise(
() =>
import(
/* webpackChunkName: "walkthrough-renderer" */ "./walkthroughRenderer"
),
);
});
const LoadingFallback = () => null;
export default function Walkthrough({ children }: any) {
const [activeWalkthrough, setActiveWalkthrough] =
useState<FeatureParams | null>();
const [feature, setFeature] = useState<FeatureParams[]>([]);
const location = useLocation();
const pushFeature = (value: FeatureParams) => {
const alreadyExists = feature.some((f) => f.targetId === value.targetId);
if (!alreadyExists) {
if (Array.isArray(value)) {
setFeature((e) => [...e, ...value]);
} else {
setFeature((e) => [...e, value]);
}
}
updateActiveWalkthrough();
};
const popFeature = () => {
hideIndicator();
setFeature((e) => {
e.shift();
return [...e];
});
};
const updateActiveWalkthrough = () => {
if (feature.length > 0) {
const highlightArea = document.querySelector(`#${feature[0].targetId}`);
if (highlightArea) {
setActiveWalkthrough(feature[0]);
} else {
setActiveWalkthrough(null);
}
} else {
setActiveWalkthrough(null);
}
};
useEffect(() => {
if (feature.length > -1) updateActiveWalkthrough();
}, [feature.length, location]);
return (
<WalkthroughContext.Provider
value={{
pushFeature,
popFeature,
feature,
isOpened: !!activeWalkthrough,
}}
>
{children}
{activeWalkthrough &&
createPortal(
<Suspense fallback={<LoadingFallback />}>
<WalkthroughRenderer {...activeWalkthrough} />
</Suspense>,
document.body,
)}
</WalkthroughContext.Provider>
);
}

View File

@ -0,0 +1,92 @@
import type { OffsetType, PositionType } from "./walkthroughContext";
const DEFAULT_POSITION: PositionType = "top";
export const PADDING_HIGHLIGHT = 10;
type PositionCalculator = {
offset?: OffsetType;
targetId: string;
};
export function getPosition({ offset, targetId }: PositionCalculator) {
const target = document.querySelector(`#${targetId}`);
const bodyCoordinates = document.body.getBoundingClientRect();
if (!target) return null;
let coordinates;
if (target) {
coordinates = target.getBoundingClientRect();
}
if (!coordinates) return null;
const offsetValues = { top: offset?.top || 0, left: offset?.left || 0 };
const extraStyles = offset?.style || {};
/**
* . - - - - - - - - - - - - - - - - - .
* | Body |
* | |
* | . - - - - - - - - - - . |
* | | Offset | |
* | | . - - - - - - - . | |
* | | | / / / / / / / | | |
* | | | / / /Target/ /| | |
* | | | / / / / / / / | | |
* | | . - - - - - - - . | |
* | | | |
* | . _ _ _ _ _ _ _ _ _ _ . |
* | |
* . - - - - - - - - - - - - - - - - - .
*/
switch (offset?.position || DEFAULT_POSITION) {
case "top":
return {
bottom:
bodyCoordinates.height -
coordinates.top -
offsetValues.top +
PADDING_HIGHLIGHT +
"px",
left: coordinates.left + offsetValues.left + PADDING_HIGHLIGHT + "px",
transform: "translateX(-50%)",
...extraStyles,
};
case "bottom":
return {
top:
coordinates.height +
coordinates.top +
offsetValues.top +
PADDING_HIGHLIGHT +
"px",
left: coordinates.left + offsetValues.left - PADDING_HIGHLIGHT + "px",
transform: "translateX(-50%)",
...extraStyles,
};
case "left":
return {
top: coordinates.top + offsetValues.top - PADDING_HIGHLIGHT + "px",
right:
bodyCoordinates.width -
coordinates.left -
offsetValues.left +
PADDING_HIGHLIGHT +
"px",
transform: "translateY(-50%)",
...extraStyles,
};
case "right":
return {
top: coordinates.top + offsetValues.top - PADDING_HIGHLIGHT + "px",
left:
coordinates.left +
coordinates.width +
offsetValues.left +
PADDING_HIGHLIGHT +
"px",
transform: "translateY(-50%)",
...extraStyles,
};
}
}

View File

@ -0,0 +1,54 @@
import React from "react";
export type PositionType = "top" | "bottom" | "left" | "right";
export type OffsetType = {
// Position for the instructions and indicator
position?: PositionType;
// Adds an offset to top or bottom properties (of Instruction div) depending upon the position
top?: number;
// Adds an offset to left or right properties (of Instruction div) depending upon the position
left?: number;
// Style for the Instruction div overrides all other styles
style?: any;
// Indicator top and left offsets
indicatorTop?: number;
indicatorLeft?: number;
// container offset for highlight
highlightPad?: number;
};
export type FeatureDetails = {
// Title to show on the instruction screen
title: string;
// Description to show on the instruction screen
description: string;
// Gif or Image to give a walkthrough
imageURL?: string;
};
export type FeatureParams = {
// To execute a function on dismissing the tutorial walkthrough.
onDismiss?: () => void;
// Target Id without # to highlight the feature
targetId: string;
// Details for the instruction screen
details?: FeatureDetails;
// Offsets for the instruction screen and the indicator
offset?: OffsetType;
// Event params
eventParams?: Record<string, any>;
};
type WalkthroughContextType = {
pushFeature: (feature: FeatureParams) => void;
popFeature: () => void;
feature: FeatureParams[];
isOpened: boolean;
};
const WalkthroughContext = React.createContext<
WalkthroughContextType | undefined
>(undefined);
export default WalkthroughContext;

View File

@ -0,0 +1,258 @@
import { Icon, Text } from "design-system";
import { showIndicator } from "pages/Editor/GuidedTour/utils";
import React, { useContext, useEffect, useState } from "react";
import styled from "styled-components";
import { PADDING_HIGHLIGHT, getPosition } from "./utils";
import type {
FeatureDetails,
FeatureParams,
OffsetType,
} from "./walkthroughContext";
import WalkthroughContext from "./walkthroughContext";
import AnalyticsUtil from "utils/AnalyticsUtil";
const CLIPID = "clip__feature";
const Z_INDEX = 1000;
const WalkthroughWrapper = styled.div`
left: 0px;
top: 0px;
position: fixed;
width: 100%;
height: 100%;
color: rgb(0, 0, 0, 0.7);
z-index: ${Z_INDEX};
// This allows the user to click on the target element rather than the overlay div
pointer-events: none;
`;
const SvgWrapper = styled.svg`
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
`;
const InstructionsWrapper = styled.div`
padding: var(--ads-v2-spaces-4);
position: absolute;
background: white;
display: flex;
flex-direction: column;
width: 296px;
pointer-events: auto;
border-radius: var(--ads-radius-1);
`;
const ImageWrapper = styled.div`
border-radius: var(--ads-radius-1);
background: #f1f5f9;
display: flex;
align-items: center;
justify-content: center;
margin-top: 8px;
padding: var(--ads-v2-spaces-7);
img {
max-height: 220px;
}
`;
const InstructionsHeaderWrapper = styled.div`
display: flex;
p {
flex-grow: 1;
}
span {
align-self: flex-start;
margin-top: 5px;
cursor: pointer;
}
`;
type RefRectParams = {
// body params
bh: number;
bw: number;
// target params
th: number;
tw: number;
tx: number;
ty: number;
};
/*
* Clip Path Polygon :
* 1) 0 0 ----> (body start) (body start)
* 2) 0 ${boundingRect.bh} ----> (body start) (body end)
* 3) ${boundingRect.tx} ${boundingRect.bh} ----> (target start) (body end)
* 4) ${boundingRect.tx} ${boundingRect.ty} ----> (target start) (target start)
* 5) ${boundingRect.tx + boundingRect.tw} ${boundingRect.ty} ----> (target end) (target start)
* 6) ${boundingRect.tx + boundingRect.tw} ${boundingRect.ty + boundingRect.th} ----> (target end) (target end)
* 7) ${boundingRect.tx} ${boundingRect.ty + boundingRect.th} ----> (target start) (target end)
* 8) ${boundingRect.tx} ${boundingRect.bh} ----> (target start) (body end)
* 9) ${boundingRect.bw} ${boundingRect.bh} ----> (body end) (body end)
* 10) ${boundingRect.bw} 0 ----> (body end) (body start)
*
*
* 1 10
*
* Body
*
*
* 4 5
* / / / / / / /
* / / /Target/ /
* / / / / / / /
* 7 6
*
*
* 2 3,8 9
*/
/**
* Creates a Highlighting Clipping mask around a target container
* @param targetId Id for the target container to show highlighting around it
*/
const WalkthroughRenderer = ({
details,
offset,
onDismiss,
targetId,
eventParams = {},
}: FeatureParams) => {
const [boundingRect, setBoundingRect] = useState<RefRectParams | null>(null);
const { popFeature } = useContext(WalkthroughContext) || {};
const updateBoundingRect = () => {
const highlightArea = document.querySelector(`#${targetId}`);
if (highlightArea) {
const boundingRect = highlightArea.getBoundingClientRect();
const bodyRect = document.body.getBoundingClientRect();
const offsetHighlightPad =
typeof offset?.highlightPad === "number"
? offset?.highlightPad
: PADDING_HIGHLIGHT;
setBoundingRect({
bw: bodyRect.width,
bh: bodyRect.height,
tw: boundingRect.width + 2 * offsetHighlightPad,
th: boundingRect.height + 2 * offsetHighlightPad,
tx: boundingRect.x - offsetHighlightPad,
ty: boundingRect.y - offsetHighlightPad,
});
showIndicator(`#${targetId}`, offset?.position, {
top: offset?.indicatorTop || 0,
left: offset?.indicatorLeft || 0,
zIndex: Z_INDEX + 1,
});
}
};
useEffect(() => {
updateBoundingRect();
const highlightArea = document.querySelector(`#${targetId}`);
AnalyticsUtil.logEvent("WALKTHROUGH_SHOWN", eventParams);
window.addEventListener("resize", updateBoundingRect);
const resizeObserver = new ResizeObserver(updateBoundingRect);
if (highlightArea) {
resizeObserver.observe(highlightArea);
}
return () => {
window.removeEventListener("resize", updateBoundingRect);
if (highlightArea) resizeObserver.unobserve(highlightArea);
};
}, [targetId]);
const onDismissWalkthrough = () => {
onDismiss && onDismiss();
popFeature && popFeature();
};
if (!boundingRect) return null;
return (
<WalkthroughWrapper className="t--walkthrough-overlay">
<SvgWrapper
height={boundingRect.bh}
width={boundingRect.bw}
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<clipPath id={CLIPID}>
<polygon
// See the comments above the component declaration to understand the below points assignment.
points={`
0 0,
0 ${boundingRect.bh},
${boundingRect.tx} ${boundingRect.bh},
${boundingRect.tx} ${boundingRect.ty},
${boundingRect.tx + boundingRect.tw} ${boundingRect.ty},
${boundingRect.tx + boundingRect.tw} ${
boundingRect.ty + boundingRect.th
},
${boundingRect.tx} ${boundingRect.ty + boundingRect.th},
${boundingRect.tx} ${boundingRect.bh},
${boundingRect.bw} ${boundingRect.bh},
${boundingRect.bw} 0
`}
/>
</clipPath>
</defs>
<rect
style={{
clipPath: 'url("#' + CLIPID + '")',
fill: "currentcolor",
height: boundingRect.bh,
pointerEvents: "auto",
width: boundingRect.bw,
}}
/>
</SvgWrapper>
<InstructionsComponent
details={details}
offset={offset}
onClose={onDismissWalkthrough}
targetId={targetId}
/>
</WalkthroughWrapper>
);
};
const InstructionsComponent = ({
details,
offset,
onClose,
targetId,
}: {
details?: FeatureDetails;
offset?: OffsetType;
targetId: string;
onClose: () => void;
}) => {
if (!details) return null;
const positionAttr = getPosition({
targetId,
offset,
});
return (
<InstructionsWrapper style={{ ...positionAttr }}>
<InstructionsHeaderWrapper>
<Text kind="heading-s" renderAs="p">
{details.title}
</Text>
<Icon name="close" onClick={onClose} size="md" />
</InstructionsHeaderWrapper>
<Text>{details.description}</Text>
{details.imageURL && (
<ImageWrapper>
<img src={details.imageURL} />
</ImageWrapper>
)}
</InstructionsWrapper>
);
};
export default WalkthroughRenderer;

View File

@ -282,7 +282,7 @@ function ConditionComponent(props: any, index: number) {
props.onDeletePressed(index);
}}
size="md"
startIcon="cross-line"
startIcon="close"
/>
</ConditionBox>
);
@ -397,7 +397,7 @@ function ConditionBlock(props: any) {
onDeletePressed(index);
}}
size="md"
startIcon="cross-line"
startIcon="close"
top={"24px"}
/>
</GroupConditionBox>

View File

@ -24,6 +24,7 @@ export const DatasourceCreateEntryPoints = {
export const DatasourceEditEntryPoints = {
DATASOURCE_CARD_EDIT: "DATASOURCE_CARD_EDIT",
DATASOURCE_FORM_EDIT: "DATASOURCE_FORM_EDIT",
QUERY_EDITOR_DATASOURCE_SCHEMA: "QUERY_EDITOR_DATASOURCE_SCHEMA",
};
export const DB_QUERY_DEFAULT_TABLE_NAME = "<<your_table_name>>";

View File

@ -3015,7 +3015,7 @@ export const theme: Theme = {
},
},
actionSidePane: {
width: 265,
width: 280,
},
onboarding: {
statusBarHeight: 92,

View File

@ -40,6 +40,7 @@ export enum PluginName {
SNOWFLAKE = "Snowflake",
ARANGODB = "ArangoDB",
REDSHIFT = "Redshift",
SMTP = "SMTP",
}
export enum PaginationType {

View File

@ -15,6 +15,7 @@ import { useDispatch, useSelector } from "react-redux";
import { getApiRightPaneSelectedTab } from "selectors/apiPaneSelectors";
import isUndefined from "lodash/isUndefined";
import { Button, Tab, TabPanel, Tabs, TabsList, Tag } from "design-system";
import { DatasourceStructureContext } from "../Explorer/Datasources/DatasourceStructureContainer";
import type { Datasource } from "entities/Datasource";
import { getCurrentEnvironment } from "@appsmith/utils/Environments";
@ -125,7 +126,7 @@ const DataSourceNameContainer = styled.div`
const SomeWrapper = styled.div`
height: 100%;
padding: 0 var(--ads-v2-spaces-6);
padding: 0 var(--ads-v2-spaces-4);
`;
const NoEntityFoundWrapper = styled.div`
@ -311,9 +312,12 @@ function ApiRightPane(props: any) {
<SomeWrapper>
<ActionRightPane
actionName={props.actionName}
context={DatasourceStructureContext.API_EDITOR}
datasourceId={props.datasourceId}
entityDependencies={entityDependencies}
hasConnections={hasDependencies}
hasResponse={props.hasResponse}
pluginId={props.pluginId}
suggestedWidgets={props.suggestedWidgets}
/>
</SomeWrapper>

View File

@ -735,9 +735,11 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
applicationId={props.applicationId}
currentActionDatasourceId={currentActionDatasourceId}
currentPageId={props.currentPageId}
datasourceId={props.currentActionDatasourceId}
datasources={props.datasources}
hasResponse={props.hasResponse}
onClick={updateDatasource}
pluginId={props.pluginId}
suggestedWidgets={props.suggestedWidgets}
/>
</Wrapper>

View File

@ -24,7 +24,7 @@ import { Spinner } from "design-system";
import LogoInput from "@appsmith/pages/Editor/NavigationSettings/LogoInput";
import SwitchSettingForLogoConfiguration from "./SwitchSettingForLogoConfiguration";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
import { useFeatureFlagCheck } from "selectors/featureFlagsSelectors";
import { selectFeatureFlagCheck } from "selectors/featureFlagsSelectors";
/**
* TODO - @Dhruvik - ImprovedAppNav
@ -48,8 +48,8 @@ export type LogoConfigurationSwitches = {
function NavigationSettings() {
const application = useSelector(getCurrentApplication);
const applicationId = useSelector(getCurrentApplicationId);
const isAppLogoEnabled = useFeatureFlagCheck(
FEATURE_FLAG.APP_NAVIGATION_LOGO_UPLOAD,
const isAppLogoEnabled = useSelector((state) =>
selectFeatureFlagCheck(state, FEATURE_FLAG.APP_NAVIGATION_LOGO_UPLOAD),
);
const dispatch = useDispatch();
const [navigationSetting, setNavigationSetting] = useState(

View File

@ -38,12 +38,13 @@ type Props = DatasourceDBEditorProps &
export const Form = styled.form<{
showFilterComponent: boolean;
viewMode: boolean;
}>`
display: flex;
flex-direction: column;
height: ${({ theme }) => `calc(100% - ${theme.backBanner})`};
${(props) =>
!props.viewMode && `height: ${`calc(100% - ${props?.theme.backBanner})`};`}
overflow-y: scroll;
flex: 8 8 80%;
padding-bottom: 20px;
margin-left: ${(props) => (props.showFilterComponent ? "24px" : "0px")};
`;
@ -90,6 +91,7 @@ class DatasourceDBEditor extends JSONtoForm<Props> {
e.preventDefault();
}}
showFilterComponent={showFilterComponent}
viewMode={viewMode}
>
{messages &&
messages.map((msg, i) => {

View File

@ -72,6 +72,7 @@ interface DatasourceRestApiEditorProps {
toggleSaveActionFlag: (flag: boolean) => void;
triggerSave?: boolean;
datasourceDeleteTrigger: () => void;
viewMode: boolean;
}
type Props = DatasourceRestApiEditorProps &
@ -247,6 +248,7 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
e.preventDefault();
}}
showFilterComponent={this.props.showFilterComponent}
viewMode={this.props.viewMode}
>
{this.renderEditor()}
</Form>

View File

@ -32,7 +32,6 @@ import type { RouteComponentProps } from "react-router";
import EntityNotFoundPane from "pages/Editor/EntityNotFoundPane";
import { DatasourceComponentTypes } from "api/PluginApi";
import DatasourceSaasForm from "../SaaSEditor/DatasourceForm";
import {
getCurrentApplicationId,
getPagePermissions,
@ -493,51 +492,49 @@ class DatasourceEditorRouter extends React.Component<Props, State> {
} = this.props;
const shouldViewMode = viewMode && !isInsideReconnectModal;
// Check for specific form types first
if (
pluginDatasourceForm === DatasourceComponentTypes.RestAPIDatasourceForm &&
!shouldViewMode
) {
return (
<>
<RestAPIDatasourceForm
applicationId={this.props.applicationId}
datasource={datasource}
datasourceId={datasourceId}
formData={formData}
formName={formName}
hiddenHeader={isInsideReconnectModal}
isFormDirty={isFormDirty}
isSaving={isSaving}
location={location}
pageId={pageId}
pluginName={pluginName}
pluginPackageName={pluginPackageName}
showFilterComponent={this.state.filterParams.showFilterPane}
/>
{this.renderSaveDisacardModal()}
</>
);
}
// Default to DB Editor Form
return (
<>
<DataSourceEditorForm
applicationId={this.props.applicationId}
currentEnvironment={this.getEnvironmentId()}
datasourceId={datasourceId}
formConfig={formConfig}
formData={formData}
formName={DATASOURCE_DB_FORM}
hiddenHeader={isInsideReconnectModal}
isSaving={isSaving}
pageId={pageId}
pluginType={pluginType}
setupConfig={this.setupConfig}
showFilterComponent={this.state.filterParams.showFilterPane}
viewMode={viewMode && !isInsideReconnectModal}
/>
{
// Check for specific form types first
pluginDatasourceForm ===
DatasourceComponentTypes.RestAPIDatasourceForm &&
!shouldViewMode ? (
<RestAPIDatasourceForm
applicationId={this.props.applicationId}
datasource={datasource}
datasourceId={datasourceId}
formData={formData}
formName={formName}
hiddenHeader={isInsideReconnectModal}
isFormDirty={isFormDirty}
isSaving={isSaving}
location={location}
pageId={pageId}
pluginName={pluginName}
pluginPackageName={pluginPackageName}
showFilterComponent={this.state.filterParams.showFilterPane}
viewMode={shouldViewMode}
/>
) : (
// Default to DB Editor Form
<DataSourceEditorForm
applicationId={this.props.applicationId}
currentEnvironment={this.getEnvironmentId()}
datasourceId={datasourceId}
formConfig={formConfig}
formData={formData}
formName={DATASOURCE_DB_FORM}
hiddenHeader={isInsideReconnectModal}
isSaving={isSaving}
pageId={pageId}
pluginType={pluginType}
setupConfig={this.setupConfig}
showFilterComponent={this.state.filterParams.showFilterPane}
viewMode={viewMode && !isInsideReconnectModal}
/>
)
}
{this.renderSaveDisacardModal()}
</>
);

View File

@ -123,11 +123,11 @@ const Datasources = React.memo(() => {
<Entity
addButtonHelptext={createMessage(CREATE_DATASOURCE_TOOLTIP)}
className={"group datasources"}
entityId="datasources_section"
entityId={pageId + "_datasources"}
icon={null}
isDefaultExpanded={
isDatasourcesOpen === null || isDatasourcesOpen === undefined
? false
? true
: isDatasourcesOpen
}
isSticky

View File

@ -21,6 +21,7 @@ import {
import { getDatasource } from "selectors/entitiesSelector";
import type { TreeDropdownOption } from "pages/Editor/Explorer/ContextMenu";
import ContextMenu from "pages/Editor/Explorer/ContextMenu";
import { DatasourceStructureContext } from "./DatasourceStructureContainer";
export function DataSourceContextMenu(props: {
datasourceId: string;
@ -36,7 +37,12 @@ export function DataSourceContextMenu(props: {
[dispatch, props.entityId],
);
const dispatchRefresh = useCallback(() => {
dispatch(refreshDatasourceStructure(props.datasourceId));
dispatch(
refreshDatasourceStructure(
props.datasourceId,
DatasourceStructureContext.EXPLORER,
),
);
}, [dispatch, props.datasourceId]);
const [confirmDelete, setConfirmDelete] = useState(false);

View File

@ -13,9 +13,16 @@ import {
} from "actions/datasourceActions";
import { useDispatch, useSelector } from "react-redux";
import type { AppState } from "@appsmith/reducers";
import { DatasourceStructureContainer } from "./DatasourceStructureContainer";
import {
DatasourceStructureContainer,
DatasourceStructureContext,
} from "./DatasourceStructureContainer";
import { isStoredDatasource, PluginType } from "entities/Action";
import { getAction } from "selectors/entitiesSelector";
import {
getAction,
getDatasourceStructureById,
getIsFetchingDatasourceStructure,
} from "selectors/entitiesSelector";
import {
datasourcesEditorIdURL,
saasEditorDatasourceIdURL,
@ -81,13 +88,13 @@ const ExplorerDatasourceEntity = React.memo(
const updateDatasourceNameCall = (id: string, name: string) =>
updateDatasourceName({ id: props.datasource.id, name });
const datasourceStructure = useSelector((state: AppState) => {
return state.entities.datasources.structure[props.datasource.id];
});
const datasourceStructure = useSelector((state: AppState) =>
getDatasourceStructureById(state, props.datasource.id),
);
const isFetchingDatasourceStructure = useSelector((state: AppState) => {
return state.entities.datasources.fetchingDatasourceStructure;
});
const isFetchingDatasourceStructure = useSelector((state: AppState) =>
getIsFetchingDatasourceStructure(state, props.datasource.id),
);
const expandDatasourceId = useSelector((state: AppState) => {
return state.ui.datasourcePane.expandDatasourceId;
@ -95,7 +102,13 @@ const ExplorerDatasourceEntity = React.memo(
//Debounce fetchDatasourceStructure request.
const debounceFetchDatasourceRequest = debounce(async () => {
dispatch(fetchDatasourceStructure(props.datasource.id, true));
dispatch(
fetchDatasourceStructure(
props.datasource.id,
true,
DatasourceStructureContext.EXPLORER,
),
);
}, 300);
const getDatasourceStructure = useCallback(
@ -155,6 +168,7 @@ const ExplorerDatasourceEntity = React.memo(
updateEntityName={updateDatasourceNameCall}
>
<DatasourceStructureContainer
context={DatasourceStructureContext.EXPLORER}
datasourceId={props.datasource.id}
datasourceStructure={datasourceStructure}
step={props.step}

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useContext } from "react";
import Entity, { EntityClassNames } from "../Entity";
import { datasourceTableIcon } from "../ExplorerIcons";
import QueryTemplates from "./QueryTemplates";
@ -9,16 +9,26 @@ import { SIDEBAR_ID } from "constants/Explorer";
import { hasCreateDatasourceActionPermission } from "@appsmith/utils/permissionHelpers";
import { useSelector } from "react-redux";
import type { AppState } from "@appsmith/reducers";
import { getDatasource } from "selectors/entitiesSelector";
import { getDatasource, getPlugin } from "selectors/entitiesSelector";
import { getPagePermissions } from "selectors/editorSelectors";
import { Menu, MenuTrigger, Button, Tooltip, MenuContent } from "design-system";
import { SHOW_TEMPLATES, createMessage } from "@appsmith/constants/messages";
import styled from "styled-components";
import { DatasourceStructureContext } from "./DatasourceStructureContainer";
import AnalyticsUtil from "utils/AnalyticsUtil";
import type { Plugin } from "api/PluginApi";
import WalkthroughContext from "components/featureWalkthrough/walkthroughContext";
import { setFeatureFlagShownStatus } from "utils/storage";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
type DatasourceStructureProps = {
dbStructure: DatasourceTable;
step: number;
datasourceId: string;
context: DatasourceStructureContext;
isDefaultOpen?: boolean;
forceExpand?: boolean;
currentActionId: string;
};
const StyledMenuContent = styled(MenuContent)`
@ -32,10 +42,17 @@ export function DatasourceStructure(props: DatasourceStructureProps) {
const [active, setActive] = useState(false);
useCloseMenuOnScroll(SIDEBAR_ID, active, () => setActive(false));
const { isOpened: isWalkthroughOpened, popFeature } =
useContext(WalkthroughContext) || {};
const datasource = useSelector((state: AppState) =>
getDatasource(state, props.datasourceId),
);
const plugin: Plugin | undefined = useSelector((state) =>
getPlugin(state, datasource?.pluginId || ""),
);
const datasourcePermissions = datasource?.userPermissions || [];
const pagePermissions = useSelector(getPagePermissions);
@ -44,62 +61,98 @@ export function DatasourceStructure(props: DatasourceStructureProps) {
...pagePermissions,
]);
const lightningMenu = canCreateDatasourceActions ? (
<Menu open={active}>
<Tooltip
content={createMessage(SHOW_TEMPLATES)}
isDisabled={active}
mouseLeaveDelay={0}
placement="right"
>
<MenuTrigger>
<Button
className={`button-icon t--template-menu-trigger ${EntityClassNames.CONTEXT_MENU}`}
isIconButton
kind="tertiary"
onClick={() => setActive(!active)}
startIcon="increase-control-v2"
const onSelect = () => {
setActive(false);
};
const onEntityClick = () => {
AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_TABLE_SELECT", {
datasourceId: props.datasourceId,
pluginName: plugin?.name,
});
canCreateDatasourceActions && setActive(!active);
dbStructure.templates.length === 0 &&
isWalkthroughOpened &&
closeWalkthrough();
};
const closeWalkthrough = () => {
popFeature && popFeature();
setFeatureFlagShownStatus(FEATURE_FLAG.ab_ds_schema_enabled, true);
};
const lightningMenu =
canCreateDatasourceActions && dbStructure.templates.length > 0 ? (
<Menu open={active}>
<Tooltip
content={createMessage(SHOW_TEMPLATES)}
isDisabled={active}
mouseLeaveDelay={0}
placement="right"
>
<MenuTrigger>
<Button
className={`button-icon t--template-menu-trigger ${EntityClassNames.CONTEXT_MENU}`}
isIconButton
kind="tertiary"
onClick={() => setActive(!active)}
startIcon={
props.context !== DatasourceStructureContext.EXPLORER
? "add-line"
: "increase-control-v2"
}
/>
</MenuTrigger>
</Tooltip>
<StyledMenuContent
align="start"
className="t--structure-template-menu-popover"
onInteractOutside={() => setActive(false)}
side="right"
>
<QueryTemplates
context={props.context}
currentActionId={props.currentActionId}
datasourceId={props.datasourceId}
onSelect={onSelect}
templates={dbStructure.templates}
/>
</MenuTrigger>
</Tooltip>
<StyledMenuContent
align="start"
className="t--structure-template-menu-popover"
onInteractOutside={() => setActive(false)}
side="right"
>
<QueryTemplates
datasourceId={props.datasourceId}
onSelect={() => setActive(false)}
templates={dbStructure.templates}
/>
</StyledMenuContent>
</Menu>
) : null;
</StyledMenuContent>
</Menu>
) : null;
if (dbStructure.templates) templateMenu = lightningMenu;
const columnsAndKeys = dbStructure.columns.concat(dbStructure.keys);
return (
<Entity
action={() => canCreateDatasourceActions && setActive(!active)}
action={onEntityClick}
active={active}
className={`datasourceStructure`}
className={`datasourceStructure${
props.context !== DatasourceStructureContext.EXPLORER &&
`-${props.context}`
}`}
contextMenu={templateMenu}
entityId={"DatasourceStructure"}
entityId={`${props.datasourceId}-${dbStructure.name}-${props.context}`}
forceExpand={props.forceExpand}
icon={datasourceTableIcon}
isDefaultExpanded={props?.isDefaultOpen}
name={dbStructure.name}
step={props.step}
>
{columnsAndKeys.map((field, index) => {
return (
<DatasourceField
field={field}
key={`${field.name}${index}`}
step={props.step + 1}
/>
);
})}
<>
{columnsAndKeys.map((field, index) => {
return (
<DatasourceField
field={field}
key={`${field.name}${index}`}
step={props.step + 1}
/>
);
})}
</>
</Entity>
);
}

View File

@ -1,57 +1,205 @@
import {
createMessage,
DATASOURCE_STRUCTURE_INPUT_PLACEHOLDER_TEXT,
SCHEMA_NOT_AVAILABLE,
TABLE_OR_COLUMN_NOT_FOUND,
} from "@appsmith/constants/messages";
import type {
DatasourceStructure as DatasourceStructureType,
DatasourceTable,
} from "entities/Datasource";
import type { ReactElement } from "react";
import React, { memo } from "react";
import React, { memo, useEffect, useMemo, useState } from "react";
import EntityPlaceholder from "../Entity/Placeholder";
import { useEntityUpdateState } from "../hooks";
import DatasourceStructure from "./DatasourceStructure";
import { Input, Text } from "design-system";
import styled from "styled-components";
import { getIsFetchingDatasourceStructure } from "selectors/entitiesSelector";
import { useSelector } from "react-redux";
import type { AppState } from "@appsmith/reducers";
import DatasourceStructureLoadingContainer from "./DatasourceStructureLoadingContainer";
import DatasourceStructureNotFound from "./DatasourceStructureNotFound";
import AnalyticsUtil from "utils/AnalyticsUtil";
type Props = {
datasourceId: string;
datasourceStructure?: DatasourceStructureType;
step: number;
context: DatasourceStructureContext;
pluginName?: string;
currentActionId?: string;
};
export enum DatasourceStructureContext {
EXPLORER = "entity-explorer",
QUERY_EDITOR = "query-editor",
// this does not exist yet, but in case it does in the future.
API_EDITOR = "api-editor",
}
const DatasourceStructureSearchContainer = styled.div`
margin-bottom: 8px;
position: sticky;
top: 0;
overflow: hidden;
z-index: 10;
background: white;
`;
const Container = (props: Props) => {
const isLoading = useEntityUpdateState(props.datasourceId);
let view: ReactElement<Props> = <div />;
const isLoading = useSelector((state: AppState) =>
getIsFetchingDatasourceStructure(state, props.datasourceId),
);
let view: ReactElement<Props> | JSX.Element = <div />;
const [datasourceStructure, setDatasourceStructure] = useState<
DatasourceStructureType | undefined
>(props.datasourceStructure);
const [hasSearchedOccured, setHasSearchedOccured] = useState(false);
useEffect(() => {
if (datasourceStructure !== props.datasourceStructure) {
setDatasourceStructure(props.datasourceStructure);
}
}, [props.datasourceStructure]);
const flatStructure = useMemo(() => {
if (!props.datasourceStructure?.tables?.length) return [];
const list: string[] = [];
props.datasourceStructure.tables.map((table) => {
table.columns.forEach((column) => {
list.push(`${table.name}~${column.name}`);
});
});
return list;
}, [props.datasourceStructure]);
const handleOnChange = (value: string) => {
if (!props.datasourceStructure?.tables?.length) return;
if (value.length > 0) {
!hasSearchedOccured && setHasSearchedOccured(true);
} else {
hasSearchedOccured && setHasSearchedOccured(false);
}
const tables = new Set();
const columns = new Set();
flatStructure.forEach((structure) => {
const segments = structure.split("~");
// if the value is present in the columns, add the column and its parent table.
if (segments[1].toLowerCase().includes(value)) {
tables.add(segments[0]);
columns.add(segments[1]);
return;
}
// if the value is present in the table but not in the columns, add the table
if (segments[0].toLowerCase().includes(value)) {
tables.add(segments[0]);
return;
}
});
const filteredDastasourceStructure = props.datasourceStructure.tables
.map((structure) => ({
...structure,
columns:
// if the size of the columns set is 0, then simply default to the entire column
columns.size === 0
? structure.columns
: structure.columns.filter((column) => columns.has(column.name)),
keys:
columns.size === 0
? structure.keys
: structure.keys.filter((key) => columns.has(key.name)),
}))
.filter((table) => tables.has(table.name));
setDatasourceStructure({ tables: filteredDastasourceStructure });
AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_SEARCH", {
datasourceId: props.datasourceId,
pluginName: props.pluginName,
});
};
if (!isLoading) {
if (props.datasourceStructure?.tables?.length) {
view = (
<>
{props.datasourceStructure.tables.map(
(structure: DatasourceTable) => {
{props.context !== DatasourceStructureContext.EXPLORER && (
<DatasourceStructureSearchContainer>
<Input
className="datasourceStructure-search"
onChange={(value) => handleOnChange(value)}
placeholder={createMessage(
DATASOURCE_STRUCTURE_INPUT_PLACEHOLDER_TEXT,
)}
size={"md"}
startIcon="search"
type="text"
/>
</DatasourceStructureSearchContainer>
)}
{!!datasourceStructure?.tables?.length &&
datasourceStructure.tables.map((structure: DatasourceTable) => {
return (
<DatasourceStructure
context={props.context}
currentActionId={props.currentActionId || ""}
datasourceId={props.datasourceId}
dbStructure={structure}
key={`${props.datasourceId}${structure.name}`}
forceExpand={hasSearchedOccured}
key={`${props.datasourceId}${structure.name}-${props.context}`}
step={props.step + 1}
/>
);
},
})}
{!datasourceStructure?.tables?.length && (
<Text kind="body-s" renderAs="p">
{createMessage(TABLE_OR_COLUMN_NOT_FOUND)}
</Text>
)}
</>
);
} else {
view = (
<EntityPlaceholder step={props.step + 1}>
{props.datasourceStructure &&
props.datasourceStructure.error &&
props.datasourceStructure.error.message &&
props.datasourceStructure.error.message !== "null"
? props.datasourceStructure.error.message
: createMessage(SCHEMA_NOT_AVAILABLE)}
</EntityPlaceholder>
);
if (props.context !== DatasourceStructureContext.EXPLORER) {
view = (
<DatasourceStructureNotFound
datasourceId={props.datasourceId}
error={
!!props.datasourceStructure &&
"error" in props.datasourceStructure
? props.datasourceStructure.error
: { message: createMessage(SCHEMA_NOT_AVAILABLE) }
}
pluginName={props?.pluginName || ""}
/>
);
} else {
view = (
<EntityPlaceholder step={props.step + 1}>
{props.datasourceStructure &&
props.datasourceStructure.error &&
props.datasourceStructure.error.message &&
props.datasourceStructure.error.message !== "null"
? props.datasourceStructure.error.message
: createMessage(SCHEMA_NOT_AVAILABLE)}
</EntityPlaceholder>
);
}
}
} else if (
// intentionally leaving this here in case we want to show loading states in the explorer or query editor page
props.context !== DatasourceStructureContext.EXPLORER &&
isLoading
) {
view = <DatasourceStructureLoadingContainer />;
}
return view;

View File

@ -0,0 +1,46 @@
import React, { useCallback } from "react";
import { useDispatch } from "react-redux";
import { Icon, Text } from "design-system";
import styled from "styled-components";
import { refreshDatasourceStructure } from "actions/datasourceActions";
import { SCHEMA_LABEL, createMessage } from "@appsmith/constants/messages";
import { DatasourceStructureContext } from "./DatasourceStructureContainer";
type Props = {
datasourceId: string;
};
const HeaderWrapper = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
`;
export default function DatasourceStructureHeader(props: Props) {
const dispatch = useDispatch();
const dispatchRefresh = useCallback(
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
event.stopPropagation();
dispatch(
refreshDatasourceStructure(
props.datasourceId,
DatasourceStructureContext.QUERY_EDITOR,
),
);
},
[dispatch, props.datasourceId],
);
return (
<HeaderWrapper>
<Text kind="heading-xs" renderAs="h3">
{createMessage(SCHEMA_LABEL)}
</Text>
<div onClick={(event) => dispatchRefresh(event)}>
<Icon name="refresh" size={"md"} />
</div>
</HeaderWrapper>
);
}

View File

@ -0,0 +1,36 @@
import React from "react";
import { createMessage, LOADING_SCHEMA } from "@appsmith/constants/messages";
import { Spinner, Text } from "design-system";
import styled from "styled-components";
const LoadingContainer = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
& > p {
margin-left: 0.5rem;
}
`;
const SpinnerWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
`;
const DatasourceStructureLoadingContainer = () => {
return (
<LoadingContainer>
<SpinnerWrapper>
<Spinner size={"sm"} />
</SpinnerWrapper>
<Text kind="body-m" renderAs="p">
{createMessage(LOADING_SCHEMA)}
</Text>
</LoadingContainer>
);
};
export default DatasourceStructureLoadingContainer;

View File

@ -0,0 +1,73 @@
import React from "react";
import { useSelector } from "react-redux";
import styled from "styled-components";
import { Text, Button } from "design-system";
import type { APIResponseError } from "api/ApiResponses";
import { EDIT_DATASOURCE, createMessage } from "@appsmith/constants/messages";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { DatasourceEditEntryPoints } from "constants/Datasource";
import history from "utils/history";
import { getQueryParams } from "utils/URLUtils";
import { datasourcesEditorIdURL } from "RouteBuilder";
import { omit } from "lodash";
import { getCurrentPageId } from "selectors/editorSelectors";
export type Props = {
error: APIResponseError | { message: string } | undefined;
datasourceId: string;
pluginName?: string;
};
const NotFoundContainer = styled.div`
display: flex;
height: 100%;
width: 100%;
flex-direction: column;
`;
const NotFoundText = styled(Text)`
margin-bottom: 1rem;
margin-top: 0.3rem;
`;
const ButtonWrapper = styled.div`
width: fit-content;
`;
const DatasourceStructureNotFound = (props: Props) => {
const { datasourceId, error, pluginName } = props;
const pageId = useSelector(getCurrentPageId);
const editDatasource = () => {
AnalyticsUtil.logEvent("EDIT_DATASOURCE_CLICK", {
datasourceId: datasourceId,
pluginName: pluginName,
entryPoint: DatasourceEditEntryPoints.QUERY_EDITOR_DATASOURCE_SCHEMA,
});
const url = datasourcesEditorIdURL({
pageId,
datasourceId: datasourceId,
params: { ...omit(getQueryParams(), "viewMode"), viewMode: false },
});
history.push(url);
};
return (
<NotFoundContainer>
{error?.message && (
<NotFoundText kind="body-s" renderAs="p">
{error.message}
</NotFoundText>
)}
<ButtonWrapper>
<Button kind="secondary" onClick={editDatasource} size={"md"}>
{createMessage(EDIT_DATASOURCE)}
</Button>
</ButtonWrapper>
</NotFoundContainer>
);
};
export default DatasourceStructureNotFound;

View File

@ -1,4 +1,4 @@
import React, { useCallback } from "react";
import React, { useCallback, useContext } from "react";
import { useDispatch, useSelector } from "react-redux";
import { createActionRequest } from "actions/pluginActionActions";
import type { AppState } from "@appsmith/reducers";
@ -11,25 +11,66 @@ import type { QueryAction } from "entities/Action";
import history from "utils/history";
import type { Datasource, QueryTemplate } from "entities/Datasource";
import { INTEGRATION_TABS } from "constants/routes";
import { getDatasource, getPlugin } from "selectors/entitiesSelector";
import {
getAction,
getDatasource,
getPlugin,
} from "selectors/entitiesSelector";
import { integrationEditorURL } from "RouteBuilder";
import { MenuItem } from "design-system";
import type { Plugin } from "api/PluginApi";
import { DatasourceStructureContext } from "./DatasourceStructureContainer";
import WalkthroughContext from "components/featureWalkthrough/walkthroughContext";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
import { setFeatureFlagShownStatus } from "utils/storage";
import styled from "styled-components";
import { change, getFormValues } from "redux-form";
import { QUERY_EDITOR_FORM_NAME } from "@appsmith/constants/forms";
import { diff } from "deep-diff";
import { UndoRedoToastContext, showUndoRedoToast } from "utils/replayHelpers";
import AnalyticsUtil from "utils/AnalyticsUtil";
type QueryTemplatesProps = {
templates: QueryTemplate[];
datasourceId: string;
onSelect: () => void;
context: DatasourceStructureContext;
currentActionId: string;
};
enum QueryTemplatesEvent {
EXPLORER_TEMPLATE = "explorer-template",
QUERY_EDITOR_TEMPLATE = "query-editor-template",
}
const TemplateMenuItem = styled(MenuItem)`
& > span {
text-transform: lowercase;
}
& > span:first-letter {
text-transform: capitalize;
}
`;
export function QueryTemplates(props: QueryTemplatesProps) {
const dispatch = useDispatch();
const { isOpened: isWalkthroughOpened, popFeature } =
useContext(WalkthroughContext) || {};
const applicationId = useSelector(getCurrentApplicationId);
const actions = useSelector((state: AppState) => state.entities.actions);
const currentPageId = useSelector(getCurrentPageId);
const dataSource: Datasource | undefined = useSelector((state: AppState) =>
getDatasource(state, props.datasourceId),
);
const currentAction = useSelector((state) =>
getAction(state, props.currentActionId),
);
const formName = QUERY_EDITOR_FORM_NAME;
const formValues = useSelector((state) => getFormValues(formName)(state));
const plugin: Plugin | undefined = useSelector((state: AppState) =>
getPlugin(state, !!dataSource?.pluginId ? dataSource.pluginId : ""),
);
@ -55,14 +96,24 @@ export function QueryTemplates(props: QueryTemplatesProps) {
},
eventData: {
actionType: "Query",
from: "explorer-template",
from:
props?.context === DatasourceStructureContext.EXPLORER
? QueryTemplatesEvent.EXPLORER_TEMPLATE
: QueryTemplatesEvent.QUERY_EDITOR_TEMPLATE,
dataSource: dataSource?.name,
datasourceId: props.datasourceId,
pluginName: plugin?.name,
queryType: template.title,
},
...queryactionConfiguration,
}),
);
if (isWalkthroughOpened) {
popFeature && popFeature();
setFeatureFlagShownStatus(FEATURE_FLAG.ab_ds_schema_enabled, true);
}
history.push(
integrationEditorURL({
pageId: currentPageId,
@ -80,19 +131,84 @@ export function QueryTemplates(props: QueryTemplatesProps) {
],
);
const updateQueryAction = useCallback(
(template: QueryTemplate) => {
if (!currentAction) return;
const queryactionConfiguration: Partial<QueryAction> = {
actionConfiguration: {
body: template.body,
pluginSpecifiedTemplates: template.pluginSpecifiedTemplates,
formData: template.configuration,
...template.actionConfiguration,
},
};
const newFormValueState = {
...formValues,
...queryactionConfiguration,
};
const differences = diff(formValues, newFormValueState) || [];
differences.forEach((diff) => {
if (diff.kind === "E" || diff.kind === "N") {
const path = diff?.path?.join(".") || "";
const value = diff?.rhs;
if (path) {
dispatch(change(QUERY_EDITOR_FORM_NAME, path, value));
}
}
});
AnalyticsUtil.logEvent("AUTOMATIC_QUERY_GENERATION", {
datasourceId: props.datasourceId,
pluginName: plugin?.name || "",
templateCommand: template?.title,
isWalkthroughOpened,
});
if (isWalkthroughOpened) {
popFeature && popFeature();
setFeatureFlagShownStatus(FEATURE_FLAG.ab_ds_schema_enabled, true);
}
showUndoRedoToast(
currentAction.name,
false,
false,
true,
UndoRedoToastContext.QUERY_TEMPLATES,
);
},
[
dispatch,
actions,
currentPageId,
applicationId,
props.datasourceId,
dataSource,
],
);
return (
<>
{props.templates.map((template) => {
return (
<MenuItem
<TemplateMenuItem
key={template.title}
onSelect={() => {
createQueryAction(template);
if (props.currentActionId) {
updateQueryAction(template);
} else {
createQueryAction(template);
}
props.onSelect();
}}
>
{template.title}
</MenuItem>
</TemplateMenuItem>
);
})}
</>

View File

@ -255,7 +255,7 @@ export type EntityProps = {
export const Entity = forwardRef(
(props: EntityProps, ref: React.Ref<HTMLDivElement>) => {
const isEntityOpen = useSelector((state: AppState) =>
getEntityCollapsibleState(state, props.name),
getEntityCollapsibleState(state, props.entityId),
);
const isDefaultExpanded = useMemo(() => !!props.isDefaultExpanded, []);
const { canEditEntityName = false, showAddButton = false } = props;
@ -270,7 +270,7 @@ export const Entity = forwardRef(
const open = (shouldOpen: boolean | undefined) => {
if (!!props.children && props.name && isOpen !== shouldOpen) {
dispatch(setEntityCollapsibleState(props.name, !!shouldOpen));
dispatch(setEntityCollapsibleState(props.entityId, !!shouldOpen));
}
};

View File

@ -118,13 +118,13 @@ function Files() {
openMenu={isMenuOpen}
/>
}
entityId={pageId + "_widgets"}
entityId={pageId + "_actions"}
icon={null}
isDefaultExpanded={
isFilesOpen === null || isFilesOpen === undefined ? false : isFilesOpen
isFilesOpen === null || isFilesOpen === undefined ? true : isFilesOpen
}
isSticky
key={pageId + "_widgets"}
key={pageId + "_actions"}
name="Queries/JS"
onCreate={onCreate}
onToggle={onFilesToggle}

View File

@ -22,7 +22,10 @@ import {
} from "actions/JSLibraryActions";
import EntityAddButton from "../Entity/AddButton";
import type { TJSLibrary } from "workers/common/JSLibrary";
import { getPagePermissions } from "selectors/editorSelectors";
import {
getCurrentPageId,
getPagePermissions,
} from "selectors/editorSelectors";
import { hasCreateActionPermission } from "@appsmith/utils/permissionHelpers";
import recommendedLibraries from "./recommendedLibraries";
import { useTransition, animated } from "react-spring";
@ -266,6 +269,7 @@ function LibraryEntity({ lib }: { lib: TJSLibrary }) {
}
function JSDependencies() {
const pageId = useSelector(getCurrentPageId) || "";
const libraries = useSelector(selectLibrariesForExplorer);
const transitions = useTransition(libraries, {
keys: (lib) => lib.name,
@ -311,7 +315,7 @@ function JSDependencies() {
</AddButtonWrapper>
</Tooltip>
}
entityId="library_section"
entityId={pageId + "_library_section"}
icon={null}
isDefaultExpanded={isOpen}
isSticky

View File

@ -345,9 +345,8 @@ export const useFilteredEntities = (
};
export const useEntityUpdateState = (entityId: string) => {
return useSelector(
(state: AppState) =>
get(state, "ui.explorer.entity.updatingEntity") === entityId,
return useSelector((state: AppState) =>
get(state, "ui.explorer.entity.updatingEntity")?.includes(entityId),
);
};

View File

@ -231,10 +231,6 @@ function GeneratePageForm() {
useState<string>("");
const datasourcesStructure = useSelector(getDatasourcesStructure);
const isFetchingDatasourceStructure = useSelector(
getIsFetchingDatasourceStructure,
);
const generateCRUDSupportedPlugin: GenerateCRUDEnabledPluginMap = useSelector(
getGenerateCRUDEnabledPluginMap,
);
@ -249,6 +245,10 @@ function GeneratePageForm() {
DEFAULT_DROPDOWN_OPTION,
);
const isFetchingDatasourceStructure = useSelector((state: AppState) =>
getIsFetchingDatasourceStructure(state, selectedDatasource.id || ""),
);
const [isSelectedTableEmpty, setIsSelectedTableEmpty] =
useState<boolean>(false);

View File

@ -59,7 +59,12 @@ class IndicatorHelper {
this.indicatorWidthOffset +
"px";
} else if (position === "bottom") {
this.indicatorWrapper.style.top = coordinates.height + offset.top + "px";
this.indicatorWrapper.style.top =
coordinates.top +
coordinates.height -
this.indicatorHeightOffset +
offset.top +
"px";
this.indicatorWrapper.style.left =
coordinates.width / 2 +
coordinates.left -
@ -68,7 +73,7 @@ class IndicatorHelper {
"px";
} else if (position === "left") {
this.indicatorWrapper.style.top =
coordinates.top + this.indicatorHeightOffset + offset.top + "px";
coordinates.top - this.indicatorHeightOffset + offset.top + "px";
this.indicatorWrapper.style.left =
coordinates.left - this.indicatorWidthOffset + offset.left + "px";
} else {
@ -90,6 +95,7 @@ class IndicatorHelper {
offset: {
top: number;
left: number;
zIndex?: number;
},
) {
if (this.timerId || this.indicatorWrapper) this.destroy();
@ -111,6 +117,9 @@ class IndicatorHelper {
loop: true,
});
if (offset.zIndex) {
this.indicatorWrapper.style.zIndex = `${offset.zIndex}`;
}
// This is to invoke at the start and then recalculate every 3 seconds
// 3 seconds is an arbitrary value here to avoid calling getBoundingClientRect to many times
this.calculate(primaryReference, position, offset);
@ -237,7 +246,7 @@ export function highlightSection(
export function showIndicator(
selector: string,
position = "right",
offset = { top: 0, left: 0 },
offset: { top: number; left: number; zIndex?: number } = { top: 0, left: 0 },
) {
let primaryReference: Element | null = null;

View File

@ -9,7 +9,12 @@ import {
getPluginNameFromId,
} from "selectors/entitiesSelector";
import FormControl from "../FormControl";
import type { Action, QueryAction, SaaSAction } from "entities/Action";
import {
PluginName,
type Action,
type QueryAction,
type SaaSAction,
} from "entities/Action";
import { useDispatch, useSelector } from "react-redux";
import ActionNameEditor from "components/editorComponents/ActionNameEditor";
import DropdownField from "components/editorComponents/form/fields/DropdownField";
@ -126,6 +131,9 @@ import { ENTITY_TYPE as SOURCE_ENTITY_TYPE } from "entities/AppsmithConsole";
import { DocsLink, openDoc } from "../../../constants/DocumentationLinks";
import ActionExecutionInProgressView from "components/editorComponents/ActionExecutionInProgressView";
import { CloseDebugger } from "components/editorComponents/Debugger/DebuggerTabs";
import { DatasourceStructureContext } from "../Explorer/Datasources/DatasourceStructureContainer";
import { selectFeatureFlagCheck } from "selectors/featureFlagsSelectors";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
const QueryFormContainer = styled.form`
flex: 1;
@ -304,12 +312,12 @@ const DocumentationButton = styled(Button)`
const SidebarWrapper = styled.div<{ show: boolean }>`
border-left: 1px solid var(--ads-v2-color-border);
padding: 0 var(--ads-v2-spaces-7) var(--ads-v2-spaces-7);
overflow: auto;
padding: 0 var(--ads-v2-spaces-4) var(--ads-v2-spaces-4);
overflow: hidden;
border-bottom: 0;
display: ${(props) => (props.show ? "flex" : "none")};
width: ${(props) => props.theme.actionSidePane.width}px;
margin-top: 38px;
margin-top: 10px;
/* margin-left: var(--ads-v2-spaces-7); */
`;
@ -354,6 +362,7 @@ type QueryFormProps = {
id,
value,
}: UpdateActionPropertyActionPayload) => void;
datasourceId: string;
};
type ReduxProps = {
@ -870,6 +879,23 @@ export function EditorJSONtoForm(props: Props) {
//TODO: move this to a common place
const onClose = () => dispatch(showDebugger(false));
// A/B feature flag for datasource structure.
const isEnabledForDSSchema = useSelector((state) =>
selectFeatureFlagCheck(state, FEATURE_FLAG.ab_ds_schema_enabled),
);
// A/B feature flag for query binding.
const isEnabledForQueryBinding = useSelector((state) =>
selectFeatureFlagCheck(state, FEATURE_FLAG.ab_ds_binding_enabled),
);
// here we check for normal conditions for opening action pane
// or if any of the flags are true, We should open the actionpane by default.
const shouldOpenActionPaneByDefault =
((hasDependencies || !!output) && !guidedTourEnabled) ||
((isEnabledForDSSchema || isEnabledForQueryBinding) &&
currentActionPluginName !== PluginName.SMTP);
// when switching between different redux forms, make sure this redux form has been initialized before rendering anything.
// the initialized prop below comes from redux-form.
if (!props.initialized) {
@ -1070,14 +1096,15 @@ export function EditorJSONtoForm(props: Props) {
)}
</SecondaryWrapper>
</div>
<SidebarWrapper
show={(hasDependencies || !!output) && !guidedTourEnabled}
>
<SidebarWrapper show={shouldOpenActionPaneByDefault}>
<ActionRightPane
actionName={actionName}
context={DatasourceStructureContext.QUERY_EDITOR}
datasourceId={props.datasourceId}
entityDependencies={entityDependencies}
hasConnections={hasDependencies}
hasResponse={!!output}
pluginId={props.pluginId}
suggestedWidgets={executedQueryData?.suggestedWidgets}
/>
</SidebarWrapper>

View File

@ -252,6 +252,7 @@ class QueryEditor extends React.Component<Props> {
return (
<QueryEditorForm
dataSources={dataSources}
datasourceId={this.props.datasourceId}
editorConfig={editorConfig}
executedQueryData={responses[actionId]}
formData={this.props.formData}
@ -261,6 +262,7 @@ class QueryEditor extends React.Component<Props> {
onCreateDatasourceClick={this.onCreateDatasourceClick}
onDeleteClick={this.handleDeleteClick}
onRunClick={this.handleRunClick}
pluginId={this.props.pluginId}
runErrorMessage={runErrorMessage[actionId]}
settingConfig={settingConfig}
uiComponent={uiComponent}

View File

@ -437,6 +437,7 @@ class DatasourceSaaSEditor extends JSONtoForm<Props, State> {
e.preventDefault();
}}
showFilterComponent={false}
viewMode={viewMode}
>
{(!viewMode || createFlow || isInsideReconnectModal) && (
<>

View File

@ -1,5 +1,5 @@
import { getDependenciesFromInverseDependencies } from "components/editorComponents/Debugger/helpers";
import _, { debounce } from "lodash";
import _, { debounce, random } from "lodash";
import { useEffect, useMemo, useState } from "react";
import ReactDOM from "react-dom";
import { useLocation } from "react-router";
@ -296,3 +296,12 @@ export function useHref<T extends URLBuilderParams>(
return href;
}
// Ended up not using it, but leaving it here, incase anyone needs a helper function to generate random numbers.
export const generateRandomNumbers = (
lowerBound = 1000,
upperBound = 9000,
allowFloating = false,
) => {
return random(lowerBound, upperBound, allowFloating);
};

View File

@ -14,6 +14,7 @@ import { Center } from "pages/setup/common";
import { Spinner } from "design-system";
import { isValidLicense } from "@appsmith/selectors/tenantSelectors";
import { redirectUserAfterSignup } from "@appsmith/utils/signupHelpers";
import { setUserSignedUpFlag } from "utils/storage";
export function SignupSuccess() {
const dispatch = useDispatch();
@ -23,8 +24,11 @@ export function SignupSuccess() {
"enableFirstTimeUserExperience",
);
const validLicense = useSelector(isValidLicense);
const user = useSelector(getCurrentUser);
useEffect(() => {
PerformanceTracker.stopTracking(PerformanceTransactionName.SIGN_UP);
user?.email && setUserSignedUpFlag(user?.email);
}, []);
const redirectUsingQueryParam = useCallback(
@ -49,7 +53,6 @@ export function SignupSuccess() {
redirectUsingQueryParam();
}, []);
const user = useSelector(getCurrentUser);
const { cloudHosting } = getAppsmithConfigs();
const isCypressEnv = !!(window as any).Cypress;

View File

@ -20,8 +20,7 @@ export interface DatasourceDataState {
loading: boolean;
isTesting: boolean;
isListing: boolean; // fetching unconfigured datasource list
fetchingDatasourceStructure: boolean;
isRefreshingStructure: boolean;
fetchingDatasourceStructure: Record<string, boolean>;
structure: Record<string, DatasourceStructure>;
isFetchingMockDataSource: false;
mockDatasourceList: any[];
@ -48,8 +47,7 @@ const initialState: DatasourceDataState = {
loading: false,
isTesting: false,
isListing: false,
fetchingDatasourceStructure: false,
isRefreshingStructure: false,
fetchingDatasourceStructure: {},
structure: {},
isFetchingMockDataSource: false,
mockDatasourceList: [],
@ -143,8 +141,15 @@ const datasourceReducer = createReducer(initialState, {
},
[ReduxActionTypes.REFRESH_DATASOURCE_STRUCTURE_INIT]: (
state: DatasourceDataState,
action: ReduxAction<{ id: string }>,
) => {
return { ...state, isRefreshingStructure: true };
return {
...state,
fetchingDatasourceStructure: {
...state.fetchingDatasourceStructure,
[action.payload.id]: true,
},
};
},
[ReduxActionTypes.EXECUTE_DATASOURCE_QUERY_INIT]: (
state: DatasourceDataState,
@ -158,8 +163,15 @@ const datasourceReducer = createReducer(initialState, {
},
[ReduxActionTypes.FETCH_DATASOURCE_STRUCTURE_INIT]: (
state: DatasourceDataState,
action: ReduxAction<{ id: string }>,
) => {
return { ...state, fetchingDatasourceStructure: true };
return {
...state,
fetchingDatasourceStructure: {
...state.fetchingDatasourceStructure,
[action.payload.id]: true,
},
};
},
[ReduxActionTypes.FETCH_DATASOURCE_STRUCTURE_SUCCESS]: (
state: DatasourceDataState,
@ -167,7 +179,10 @@ const datasourceReducer = createReducer(initialState, {
) => {
return {
...state,
fetchingDatasourceStructure: false,
fetchingDatasourceStructure: {
...state.fetchingDatasourceStructure,
[action.payload.datasourceId]: false,
},
structure: {
...state.structure,
[action.payload.datasourceId]: action.payload.data,
@ -180,7 +195,10 @@ const datasourceReducer = createReducer(initialState, {
) => {
return {
...state,
isRefreshingStructure: false,
fetchingDatasourceStructure: {
...state.fetchingDatasourceStructure,
[action.payload.datasourceId]: false,
},
structure: {
...state.structure,
[action.payload.datasourceId]: action.payload.data,
@ -189,10 +207,14 @@ const datasourceReducer = createReducer(initialState, {
},
[ReduxActionErrorTypes.FETCH_DATASOURCE_STRUCTURE_ERROR]: (
state: DatasourceDataState,
action: ReduxAction<{ datasourceId: string }>,
) => {
return {
...state,
fetchingDatasourceStructure: false,
fetchingDatasourceStructure: {
...state.fetchingDatasourceStructure,
[action.payload.datasourceId]: false,
},
};
},
[ReduxActionTypes.FETCH_DATASOURCES_SUCCESS]: (
@ -464,10 +486,14 @@ const datasourceReducer = createReducer(initialState, {
},
[ReduxActionErrorTypes.REFRESH_DATASOURCE_STRUCTURE_ERROR]: (
state: DatasourceDataState,
action: ReduxAction<{ datasourceId: string }>,
) => {
return {
...state,
isRefreshingStructure: false,
fetchingDatasourceStructure: {
...state.fetchingDatasourceStructure,
[action.payload.datasourceId]: false,
},
};
},
[ReduxActionErrorTypes.EXECUTE_DATASOURCE_QUERY_ERROR]: (

View File

@ -150,6 +150,7 @@ import {
isGoogleSheetPluginDS,
} from "utils/editorContextUtils";
import { getDefaultEnvId } from "@appsmith/api/ApiUtils";
import type { DatasourceStructureContext } from "pages/Editor/Explorer/Datasources/DatasourceStructureContainer";
function* fetchDatasourcesSaga(
action: ReduxAction<{ workspaceId?: string } | undefined>,
@ -1173,17 +1174,19 @@ function* updateDatasourceSuccessSaga(action: UpdateDatasourceSuccessAction) {
}
function* fetchDatasourceStructureSaga(
action: ReduxAction<{ id: string; ignoreCache: boolean }>,
action: ReduxAction<{
id: string;
ignoreCache: boolean;
schemaFetchContext: DatasourceStructureContext;
}>,
) {
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);
AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH", {
datasourceId: datasource?.id,
pluginName: plugin?.name,
});
let errorMessage = "";
let isSuccess = false;
try {
const response: ApiResponse = yield DatasourcesApi.fetchDatasourceStructure(
@ -1201,11 +1204,7 @@ function* fetchDatasourceStructureSaga(
});
if (isEmpty(response.data)) {
AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH_FAILURE", {
datasourceId: datasource?.id,
pluginName: plugin?.name,
errorMessage: createMessage(DATASOURCE_SCHEMA_NOT_AVAILABLE),
});
errorMessage = createMessage(DATASOURCE_SCHEMA_NOT_AVAILABLE);
AppsmithConsole.warning({
text: "Datasource structure could not be retrieved",
source: {
@ -1215,10 +1214,7 @@ function* fetchDatasourceStructureSaga(
},
});
} else {
AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH_SUCCESS", {
datasourceId: datasource?.id,
pluginName: plugin?.name,
});
isSuccess = true;
AppsmithConsole.info({
text: "Datasource structure retrieved",
source: {
@ -1229,25 +1225,17 @@ function* fetchDatasourceStructureSaga(
});
}
if (!!(response.data as any)?.error) {
AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH_FAILURE", {
datasourceId: datasource?.id,
pluginName: plugin?.name,
errorCode: (response.data as any).error?.code,
errorMessage: (response.data as any).error?.message,
});
errorMessage = (response.data as any).error?.message;
}
}
} catch (error) {
AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH_FAILURE", {
datasourceId: datasource?.id,
pluginName: plugin?.name,
errorMessage: (error as any)?.message,
});
errorMessage = (error as any)?.message;
yield put({
type: ReduxActionErrorTypes.FETCH_DATASOURCE_STRUCTURE_ERROR,
payload: {
error,
show: false,
datasourceId: action.payload.id,
},
});
AppsmithConsole.error({
@ -1259,6 +1247,13 @@ function* fetchDatasourceStructureSaga(
},
});
}
AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH", {
datasourceId: datasource?.id,
pluginName: plugin?.name,
errorMessage: errorMessage,
isSuccess: isSuccess,
source: action.payload.schemaFetchContext,
});
}
function* addAndFetchDatasourceStructureSaga(
@ -1291,16 +1286,19 @@ function* addAndFetchDatasourceStructureSaga(
}
}
function* refreshDatasourceStructure(action: ReduxAction<{ id: string }>) {
function* refreshDatasourceStructure(
action: ReduxAction<{
id: string;
schemaRefreshContext: DatasourceStructureContext;
}>,
) {
const datasource = shouldBeDefined<Datasource>(
yield select(getDatasource, action.payload.id),
`Datasource is not found for it - ${action.payload.id}`,
);
const plugin: Plugin = yield select(getPlugin, datasource?.pluginId);
AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH", {
datasourceId: datasource?.id,
pluginName: plugin?.name,
});
let errorMessage = "";
let isSuccess = false;
try {
const response: ApiResponse = yield DatasourcesApi.fetchDatasourceStructure(
@ -1318,11 +1316,7 @@ function* refreshDatasourceStructure(action: ReduxAction<{ id: string }>) {
});
if (isEmpty(response.data)) {
AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH_FAILURE", {
datasourceId: datasource?.id,
pluginName: plugin?.name,
errorMessage: createMessage(DATASOURCE_SCHEMA_NOT_AVAILABLE),
});
errorMessage = createMessage(DATASOURCE_SCHEMA_NOT_AVAILABLE);
AppsmithConsole.warning({
text: "Datasource structure could not be retrieved",
source: {
@ -1332,10 +1326,7 @@ function* refreshDatasourceStructure(action: ReduxAction<{ id: string }>) {
},
});
} else {
AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH_SUCCESS", {
datasourceId: datasource?.id,
pluginName: plugin?.name,
});
isSuccess = true;
AppsmithConsole.info({
text: "Datasource structure retrieved",
source: {
@ -1346,25 +1337,17 @@ function* refreshDatasourceStructure(action: ReduxAction<{ id: string }>) {
});
}
if (!!(response.data as any)?.error) {
AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH_FAILURE", {
datasourceId: datasource?.id,
pluginName: plugin?.name,
errorCode: (response.data as any).error?.code,
errorMessage: (response.data as any).error?.message,
});
errorMessage = (response.data as any)?.message;
}
}
} catch (error) {
AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH_FAILURE", {
datasourceId: datasource?.id,
pluginName: plugin?.name,
errorMessage: (error as any)?.message,
});
errorMessage = (error as any)?.message;
yield put({
type: ReduxActionErrorTypes.REFRESH_DATASOURCE_STRUCTURE_ERROR,
payload: {
error,
show: false,
datasourceId: action.payload.id,
},
});
AppsmithConsole.error({
@ -1376,6 +1359,14 @@ function* refreshDatasourceStructure(action: ReduxAction<{ id: string }>) {
},
});
}
AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH", {
datasourceId: datasource?.id,
pluginName: plugin?.name,
errorMessage: errorMessage,
isSuccess: isSuccess,
source: action.payload.schemaRefreshContext,
});
}
function* executeDatasourceQuerySaga(

View File

@ -46,8 +46,10 @@ import {
} from "./EvaluationsSaga";
import { createBrowserHistory } from "history";
import {
getDatasource,
getEditorConfig,
getPluginForm,
getPlugins,
getSettingConfig,
} from "selectors/entitiesSelector";
import type { Action } from "entities/Action";
@ -73,6 +75,11 @@ import {
import { AppThemingMode } from "selectors/appThemingSelectors";
import { generateAutoHeightLayoutTreeAction } from "actions/autoHeightActions";
import { SelectionRequestType } from "sagas/WidgetSelectUtils";
import { startFormEvaluations } from "actions/evaluationActions";
import { getCurrentEnvironment } from "@appsmith/utils/Environments";
import { getUIComponent } from "pages/Editor/QueryEditor/helpers";
import type { Plugin } from "api/PluginApi";
import { UIComponentTypes } from "api/PluginApi";
export type UndoRedoPayload = {
operation: ReplayReduxActionTypes;
@ -309,18 +316,49 @@ function* replayActionSaga(
/**
* Update all the diffs in the action object.
* We need this for debugger logs, dynamicBindingPathList and to call relevant APIs */
const currentEnvironment = getCurrentEnvironment();
const plugins: Plugin[] = yield select(getPlugins);
const uiComponent = getUIComponent(replayEntity.pluginId, plugins);
const datasource: Datasource | undefined = yield select(
getDatasource,
replayEntity.datasource?.id || "",
);
yield all(
updates.map((u) =>
put(
setActionProperty({
actionId: replayEntity.id,
propertyName: u.modifiedProperty,
value:
u.kind === "A" ? _.get(replayEntity, u.modifiedProperty) : u.update,
skipSave: true,
}),
),
),
updates.map((u) => {
// handle evaluations after update.
const postEvalActions =
uiComponent === UIComponentTypes.UQIDbEditorForm
? [
startFormEvaluations(
replayEntity.id,
replayEntity.actionConfiguration,
replayEntity.datasource.id || "",
replayEntity.pluginId,
u.modifiedProperty,
true,
datasource?.datasourceStorages[currentEnvironment]
.datasourceConfiguration,
),
]
: [];
return put(
setActionProperty(
{
actionId: replayEntity.id,
propertyName: u.modifiedProperty,
value:
u.kind === "A"
? _.get(replayEntity, u.modifiedProperty)
: u.update,
skipSave: true,
},
postEvalActions,
),
);
}),
);
//Save the updated action object

View File

@ -21,6 +21,11 @@ import { setSnipingMode } from "actions/propertyPaneActions";
import { selectWidgetInitAction } from "actions/widgetSelectionActions";
import { SelectionRequestType } from "sagas/WidgetSelectUtils";
import { toast } from "design-system";
import {
AB_TESTING_EVENT_KEYS,
FEATURE_FLAG,
} from "@appsmith/entities/FeatureFlag";
import { selectFeatureFlagCheck } from "selectors/featureFlagsSelectors";
const WidgetTypes = WidgetFactory.widgetTypes;
@ -36,6 +41,10 @@ export function* bindDataToWidgetSaga(
),
);
const widgetState: CanvasWidgetsReduxState = yield select(getCanvasWidgets);
const isDSBindingEnabled: boolean = yield select(
selectFeatureFlagCheck,
FEATURE_FLAG.ab_ds_binding_enabled,
);
const selectedWidget = widgetState[action.payload.widgetId];
if (!selectedWidget || !selectedWidget.type) {
@ -149,6 +158,9 @@ export function* bindDataToWidgetSaga(
apiId: queryId,
propertyPath,
propertyValue,
[AB_TESTING_EVENT_KEYS.abTestingFlagLabel]:
FEATURE_FLAG.ab_ds_binding_enabled,
[AB_TESTING_EVENT_KEYS.abTestingFlagValue]: isDSBindingEnabled,
});
if (queryId && isValidProperty) {
// set the property path to dynamic, i.e. enable JS mode

View File

@ -117,8 +117,11 @@ export const getDatasourceFirstTableName = (
return "";
};
export const getIsFetchingDatasourceStructure = (state: AppState): boolean => {
return state.entities.datasources.fetchingDatasourceStructure;
export const getIsFetchingDatasourceStructure = (
state: AppState,
datasourceId: string,
): boolean => {
return state.entities.datasources.fetchingDatasourceStructure[datasourceId];
};
export const getMockDatasources = (state: AppState): MockDatasource[] => {
@ -222,6 +225,30 @@ export const getPluginNameFromId = (
return plugin.name;
};
export const getPluginPackageNameFromId = (
state: AppState,
pluginId: string,
): string => {
const plugin = state.entities.plugins.list.find(
(plugin) => plugin.id === pluginId,
);
if (!plugin) return "";
return plugin.packageName;
};
export const getPluginDatasourceComponentFromId = (
state: AppState,
pluginId: string,
): string => {
const plugin = state.entities.plugins.list.find(
(plugin) => plugin.id === pluginId,
);
if (!plugin) return "";
return plugin.datasourceComponent;
};
export const getPluginTypeFromDatasourceId = (
state: AppState,
datasourceId: string,

View File

@ -1,14 +1,17 @@
import type { AppState } from "@appsmith/reducers";
import { useSelector } from "react-redux";
import type { FeatureFlag } from "@appsmith/entities/FeatureFlag";
export const selectFeatureFlags = (state: AppState) =>
state.ui.users.featureFlag.data;
export function useFeatureFlagCheck(flagName: FeatureFlag): boolean {
const flagValues = useSelector(selectFeatureFlags);
// React hooks should not be placed in a selectors file.
export const selectFeatureFlagCheck = (
state: AppState,
flagName: FeatureFlag,
): boolean => {
const flagValues = selectFeatureFlags(state);
if (flagName in flagValues) {
return flagValues[flagName];
}
return false;
}
};

View File

@ -310,8 +310,6 @@ export type EventName =
| "DISCARD_DATASOURCE_CHANGES"
| "TEST_DATA_SOURCE_FAILED"
| "DATASOURCE_SCHEMA_FETCH"
| "DATASOURCE_SCHEMA_FETCH_SUCCESS"
| "DATASOURCE_SCHEMA_FETCH_FAILURE"
| "EDIT_ACTION_CLICK"
| "QUERY_TEMPLATE_SELECTED"
| "RUN_API_FAILURE"
@ -335,7 +333,16 @@ export type EventName =
| "JS_VARIABLE_MUTATED"
| "EXPLORER_WIDGET_CLICK"
| "WIDGET_SEARCH"
| "MAKE_APPLICATION_PUBLIC";
| "MAKE_APPLICATION_PUBLIC"
| WALKTHROUGH_EVENTS
| DATASOURCE_SCHEMA_EVENTS;
export type DATASOURCE_SCHEMA_EVENTS =
| "DATASOURCE_SCHEMA_SEARCH"
| "DATASOURCE_SCHEMA_TABLE_SELECT"
| "AUTOMATIC_QUERY_GENERATION";
type WALKTHROUGH_EVENTS = "WALKTHROUGH_DISMISSED" | "WALKTHROUGH_SHOWN";
export type AI_EVENTS =
| "AI_QUERY_SENT"

View File

@ -13,6 +13,7 @@ import {
BULK_WIDGET_ADDED,
WIDGET_REMOVED,
BULK_WIDGET_REMOVED,
ACTION_CONFIGURATION_CHANGED,
} from "@appsmith/constants/messages";
import { toast } from "design-system";
import { setApiPaneConfigSelectedTabIndex } from "../actions/apiPaneActions";
@ -47,25 +48,51 @@ export const processUndoRedoToasts = (
showUndoRedoToast(widgetName, isMultipleToasts, isCreated, !isUndo);
};
// context can be extended.
export enum UndoRedoToastContext {
WIDGET = "widget",
QUERY_TEMPLATES = "query-templates",
}
/**
* shows a toast for undo/redo
*
* @param widgetName
* @param actionName
* @param isMultiple
* @param isCreated
* @param shouldUndo
* @param toastContext
* @returns
*/
export const showUndoRedoToast = (
widgetName: string | undefined,
actionName: string | undefined,
isMultiple: boolean,
isCreated: boolean,
shouldUndo: boolean,
toastContext = UndoRedoToastContext.WIDGET,
) => {
if (shouldDisallowToast(shouldUndo)) return;
if (
shouldDisallowToast(shouldUndo) &&
toastContext === UndoRedoToastContext.WIDGET
)
return;
let actionDescription;
let actionText = "";
switch (toastContext) {
case UndoRedoToastContext.WIDGET:
actionDescription = getWidgetDescription(isCreated, isMultiple);
actionText = createMessage(actionDescription, actionName);
break;
case UndoRedoToastContext.QUERY_TEMPLATES:
actionDescription = ACTION_CONFIGURATION_CHANGED;
actionText = createMessage(actionDescription, actionName);
break;
default:
actionText = "";
}
const actionDescription = getActionDescription(isCreated, isMultiple);
const widgetText = createMessage(actionDescription, widgetName);
const action = shouldUndo ? "undo" : "redo";
const actionKey = shouldUndo
? `${modText()} Z`
@ -73,10 +100,10 @@ export const showUndoRedoToast = (
? `${modText()} ${shiftText()} Z`
: `${modText()} Y`;
toast.show(`${widgetText}. Press ${actionKey} to ${action}`);
toast.show(`${actionText}. Press ${actionKey} to ${action}`);
};
function getActionDescription(isCreated: boolean, isMultiple: boolean) {
function getWidgetDescription(isCreated: boolean, isMultiple: boolean) {
if (isCreated) return isMultiple ? BULK_WIDGET_ADDED : WIDGET_ADDED;
else return isMultiple ? BULK_WIDGET_REMOVED : WIDGET_REMOVED;
}

View File

@ -23,6 +23,8 @@ export const STORAGE_KEYS: {
FIRST_TIME_USER_ONBOARDING_TELEMETRY_CALLOUT_VISIBILITY:
"FIRST_TIME_USER_ONBOARDING_TELEMETRY_CALLOUT_VISIBILITY",
SIGNPOSTING_APP_STATE: "SIGNPOSTING_APP_STATE",
FEATURE_WALKTHROUGH: "FEATURE_WALKTHROUGH",
USER_SIGN_UP: "USER_SIGN_UP",
};
const store = localforage.createInstance({
@ -418,3 +420,77 @@ export const setFirstTimeUserOnboardingTelemetryCalloutVisibility = async (
log.error(error);
}
};
export const setFeatureFlagShownStatus = async (key: string, value: any) => {
try {
let flagsJSON: Record<string, any> | null = await store.getItem(
STORAGE_KEYS.FEATURE_WALKTHROUGH,
);
if (typeof flagsJSON === "object" && flagsJSON) {
flagsJSON[key] = value;
} else {
flagsJSON = { [key]: value };
}
await store.setItem(STORAGE_KEYS.FEATURE_WALKTHROUGH, flagsJSON);
return true;
} catch (error) {
log.error("An error occurred while updating FEATURE_WALKTHROUGH");
log.error(error);
}
};
export const getFeatureFlagShownStatus = async (key: string) => {
try {
const flagsJSON: Record<string, any> | null = await store.getItem(
STORAGE_KEYS.FEATURE_WALKTHROUGH,
);
if (typeof flagsJSON === "object" && flagsJSON) {
return !!flagsJSON[key];
}
return false;
} catch (error) {
log.error("An error occurred while reading FEATURE_WALKTHROUGH");
log.error(error);
}
};
export const setUserSignedUpFlag = async (email: string) => {
try {
let userSignedUp: Record<string, any> | null = await store.getItem(
STORAGE_KEYS.USER_SIGN_UP,
);
if (typeof userSignedUp === "object" && userSignedUp) {
userSignedUp[email] = Date.now();
} else {
userSignedUp = { [email]: Date.now() };
}
await store.setItem(STORAGE_KEYS.USER_SIGN_UP, userSignedUp);
return true;
} catch (error) {
log.error("An error occurred while updating USER_SIGN_UP");
log.error(error);
}
};
export const isUserSignedUpFlagSet = async (email: string) => {
try {
const userSignedUp: Record<string, any> | null = await store.getItem(
STORAGE_KEYS.USER_SIGN_UP,
);
if (typeof userSignedUp === "object" && userSignedUp) {
return !!userSignedUp[email];
}
return false;
} catch (error) {
log.error("An error occurred while reading USER_SIGN_UP");
log.error(error);
}
};

View File

@ -79,8 +79,6 @@ public enum AnalyticsEvents {
UPDATE_EXISTING_LICENSE("Update_Existing_License"),
DS_SCHEMA_FETCH_EVENT("Datasource_Schema_Fetch"),
DS_SCHEMA_FETCH_EVENT_SUCCESS("Datasource_Schema_Fetch_Success"),
DS_SCHEMA_FETCH_EVENT_FAILED("Datasource_Schema_Fetch_Failed"),
DS_TEST_EVENT("Test_Datasource_Clicked"),
DS_TEST_EVENT_SUCCESS("Test_Datasource_Success"),

View File

@ -43,8 +43,6 @@ public class WidgetSuggestionHelper {
widgetTypeList = handleJsonNode((JsonNode) data);
} else if (data instanceof List && !((List) data).isEmpty()) {
widgetTypeList = handleList((List) data);
} else if (data != null) {
widgetTypeList.add(getWidget(WidgetType.TEXT_WIDGET));
}
return widgetTypeList;
}
@ -93,13 +91,9 @@ public class WidgetSuggestionHelper {
* Get fields from nested object
* use the for table, list, chart and Select
*/
if (dataFields.objectFields.isEmpty()) {
widgetTypeList.add(getWidget(WidgetType.TEXT_WIDGET));
} else {
if (!dataFields.objectFields.isEmpty()) {
String nestedFieldName = dataFields.getObjectFields().get(0);
if (node.get(nestedFieldName).size() == 0) {
widgetTypeList.add(getWidget(WidgetType.TEXT_WIDGET));
} else {
if (node.get(nestedFieldName).size() != 0) {
dataFields = collectFieldsFromData(
node.get(nestedFieldName).get(0).fields());
widgetTypeList = getWidgetsForTypeNestedObject(
@ -134,7 +128,7 @@ public class WidgetSuggestionHelper {
}
return getWidgetsForTypeArray(fields, numericFields);
}
return List.of(getWidget(WidgetType.TABLE_WIDGET_V2), getWidget(WidgetType.TEXT_WIDGET));
return List.of(getWidget(WidgetType.TABLE_WIDGET_V2));
}
/*
@ -170,7 +164,6 @@ public class WidgetSuggestionHelper {
if (length > 1 && !fields.isEmpty()) {
widgetTypeList.add(getWidget(WidgetType.SELECT_WIDGET, fields.get(0), fields.get(0)));
} else {
widgetTypeList.add(getWidget(WidgetType.TEXT_WIDGET));
widgetTypeList.add(getWidget(WidgetType.INPUT_WIDGET));
}
return widgetTypeList;
@ -178,6 +171,7 @@ public class WidgetSuggestionHelper {
private static List<WidgetSuggestionDTO> getWidgetsForTypeArray(List<String> fields, List<String> numericFields) {
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(getWidget(WidgetType.TABLE_WIDGET_V2));
if (!fields.isEmpty()) {
if (fields.size() < 2) {
widgetTypeList.add(getWidget(WidgetType.SELECT_WIDGET, fields.get(0), fields.get(0)));
@ -188,14 +182,11 @@ public class WidgetSuggestionHelper {
widgetTypeList.add(getWidget(WidgetType.CHART_WIDGET, fields.get(0), numericFields.get(0)));
}
}
widgetTypeList.add(getWidget(WidgetType.TABLE_WIDGET_V2));
widgetTypeList.add(getWidget(WidgetType.TEXT_WIDGET));
return widgetTypeList;
}
private static List<WidgetSuggestionDTO> getWidgetsForTypeNumber() {
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(getWidget(WidgetType.TEXT_WIDGET));
widgetTypeList.add(getWidget(WidgetType.INPUT_WIDGET));
return widgetTypeList;
}
@ -213,6 +204,7 @@ public class WidgetSuggestionHelper {
* For the CHART widget we need at least one field of type int and one string type field
* For the DROP_DOWN at least one String type field
* */
widgetTypeList.add(getWidgetNestedData(WidgetType.TABLE_WIDGET_V2, nestedFieldName));
if (!fields.isEmpty()) {
if (fields.size() < 2) {
widgetTypeList.add(
@ -226,8 +218,6 @@ public class WidgetSuggestionHelper {
WidgetType.CHART_WIDGET, nestedFieldName, fields.get(0), numericFields.get(0)));
}
}
widgetTypeList.add(getWidgetNestedData(WidgetType.TABLE_WIDGET_V2, nestedFieldName));
widgetTypeList.add(getWidgetNestedData(WidgetType.TEXT_WIDGET, nestedFieldName));
return widgetTypeList;
}

View File

@ -360,9 +360,7 @@ public class AnalyticsServiceCEImpl implements AnalyticsServiceCE {
AnalyticsEvents.DS_TEST_EVENT,
AnalyticsEvents.DS_TEST_EVENT_SUCCESS,
AnalyticsEvents.DS_TEST_EVENT_FAILED,
AnalyticsEvents.DS_SCHEMA_FETCH_EVENT,
AnalyticsEvents.DS_SCHEMA_FETCH_EVENT_SUCCESS,
AnalyticsEvents.DS_SCHEMA_FETCH_EVENT_FAILED);
AnalyticsEvents.DS_SCHEMA_FETCH_EVENT);
}
public <T extends BaseDomain> Mono<T> sendCreateEvent(T object, Map<String, Object> extraProperties) {

View File

@ -78,7 +78,7 @@ public class DatasourceStructureSolutionCEImpl implements DatasourceStructureSol
if (Boolean.FALSE.equals(datasourceStorage.getIsValid())) {
return analyticsService
.sendObjectEvent(
AnalyticsEvents.DS_SCHEMA_FETCH_EVENT_FAILED,
AnalyticsEvents.DS_SCHEMA_FETCH_EVENT,
datasourceStorage,
getAnalyticsPropertiesForTestEventStatus(datasourceStorage, false))
.then(Mono.just(new DatasourceStructure()));
@ -128,7 +128,7 @@ public class DatasourceStructureSolutionCEImpl implements DatasourceStructureSol
})
.onErrorResume(error -> analyticsService
.sendObjectEvent(
AnalyticsEvents.DS_SCHEMA_FETCH_EVENT_FAILED,
AnalyticsEvents.DS_SCHEMA_FETCH_EVENT,
datasourceStorage,
getAnalyticsPropertiesForTestEventStatus(datasourceStorage, false, error))
.then(Mono.error(error)))
@ -138,7 +138,7 @@ public class DatasourceStructureSolutionCEImpl implements DatasourceStructureSol
return analyticsService
.sendObjectEvent(
AnalyticsEvents.DS_SCHEMA_FETCH_EVENT_SUCCESS,
AnalyticsEvents.DS_SCHEMA_FETCH_EVENT,
datasourceStorage,
getAnalyticsPropertiesForTestEventStatus(datasourceStorage, true, null))
.then(

View File

@ -369,7 +369,6 @@ public class ActionExecutionSolutionCETest {
mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value")));
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();
@ -405,7 +404,6 @@ public class ActionExecutionSolutionCETest {
mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value")));
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();
@ -438,7 +436,6 @@ public class ActionExecutionSolutionCETest {
mockResult.setBody("response-body");
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();
@ -743,7 +740,6 @@ public class ActionExecutionSolutionCETest {
mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value")));
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();
@ -792,7 +788,6 @@ public class ActionExecutionSolutionCETest {
mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value")));
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();
@ -839,7 +834,6 @@ public class ActionExecutionSolutionCETest {
mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW)));
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();
@ -943,10 +937,9 @@ public class ActionExecutionSolutionCETest {
mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW)));
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "x", "y"));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "x", "x"));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "x", "x"));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "x", "y"));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();
@ -1054,10 +1047,9 @@ public class ActionExecutionSolutionCETest {
mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW)));
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "id", "ppu"));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "id", "type"));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "id", "type"));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "id", "ppu"));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();
@ -1157,10 +1149,9 @@ public class ActionExecutionSolutionCETest {
mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW)));
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "url", "width"));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "url", "url"));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "url", "url"));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "url", "width"));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();
@ -1216,9 +1207,8 @@ public class ActionExecutionSolutionCETest {
mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW)));
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "CarType", "carID"));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "CarType", "carID"));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();
@ -1258,7 +1248,6 @@ public class ActionExecutionSolutionCETest {
mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW)));
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.INPUT_WIDGET));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();
@ -1301,7 +1290,6 @@ public class ActionExecutionSolutionCETest {
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();
@ -1381,7 +1369,6 @@ public class ActionExecutionSolutionCETest {
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.INPUT_WIDGET));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();
@ -1448,11 +1435,10 @@ public class ActionExecutionSolutionCETest {
mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW)));
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.TEXT_WIDGET, "users"));
widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.CHART_WIDGET, "users", "name", "id"));
widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.TABLE_WIDGET_V2, "users"));
widgetTypeList.add(
WidgetSuggestionHelper.getWidgetNestedData(WidgetType.SELECT_WIDGET, "users", "name", "status"));
widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.CHART_WIDGET, "users", "name", "id"));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();
@ -1501,7 +1487,6 @@ public class ActionExecutionSolutionCETest {
mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW)));
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();
@ -1561,7 +1546,6 @@ public class ActionExecutionSolutionCETest {
mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW)));
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();
@ -1605,7 +1589,6 @@ public class ActionExecutionSolutionCETest {
mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW)));
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.TEXT_WIDGET, "users"));
widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.TABLE_WIDGET_V2, "users"));
mockResult.setSuggestedWidgets(widgetTypeList);
@ -1650,7 +1633,6 @@ public class ActionExecutionSolutionCETest {
mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW)));
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.TEXT_WIDGET, null));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();
@ -1695,10 +1677,9 @@ public class ActionExecutionSolutionCETest {
mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW)));
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "url", "width"));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "url", "url"));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "url", "url"));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "url", "width"));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();
@ -1745,9 +1726,8 @@ public class ActionExecutionSolutionCETest {
mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW)));
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "width", "url"));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET));
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "width", "url"));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();
@ -1784,7 +1764,6 @@ public class ActionExecutionSolutionCETest {
mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW)));
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();
@ -1830,7 +1809,6 @@ public class ActionExecutionSolutionCETest {
.block();
List<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET));
mockResult.setSuggestedWidgets(widgetTypeList);
ActionDTO action = new ActionDTO();