diff --git a/app/client/cypress/fixtures/testdata.json b/app/client/cypress/fixtures/testdata.json index 622aac0aae..61d619d292 100644 --- a/app/client/cypress/fixtures/testdata.json +++ b/app/client/cypress/fixtures/testdata.json @@ -121,5 +121,6 @@ "clientSecret": "505dac16a21681f277b5fde97445be18", "accessTokenUrl": "https://oauth.mocklab.io/oauth/token", "oauthResponse": "169444434892406", - "authorizationURL": "https://oauth.mocklab.io/oauth/authorize" + "authorizationURL": "https://oauth.mocklab.io/oauth/authorize", + "basicURl": "https://envyenksqii9nf3.m.pipedream.net" } diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Widget_Error_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Widget_Error_spec.js new file mode 100644 index 0000000000..3daf51823b --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Widget_Error_spec.js @@ -0,0 +1,17 @@ +const dsl = require("../../../../fixtures/buttondsl.json"); + +describe("Widget error state", function() { + before(() => { + cy.addDsl(dsl); + }); + it("Check widget error state", function() { + cy.openPropertyPane("buttonwidget"); + + cy.get(".t--property-control-visible") + .find(".t--js-toggle") + .click(); + cy.testJsontext("visible", "Test"); + + cy.contains(".t--widget-error-count", 1); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/DatasourceBasicProfile_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/DatasourceBasicProfile_spec.js new file mode 100644 index 0000000000..cbfdba5572 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/DatasourceBasicProfile_spec.js @@ -0,0 +1,22 @@ +const testdata = require("../../../../fixtures/testdata.json"); +describe("Create a rest datasource", function() { + beforeEach(() => { + cy.startRoutesForDatasource(); + }); + + it("Create a rest datasource", function() { + cy.NavigateToAPI_Panel(); + cy.CreateAPI("Testapi"); + cy.enterDatasource(testdata.basicURl); + cy.get(".t--store-as-datasource").click(); + cy.addBasicProfileDetails("test", "test@123"); + cy.saveDatasource(); + cy.contains(".datasource-highlight", "envyenksqii9nf3.m.pipedream.net"); + cy.SaveAndRunAPI(); + cy.wait(2000); + var encodedStringBtoA = btoa("test:test@123"); + cy.log(encodedStringBtoA); + cy.ResponseStatusCheck(testdata.successStatusCode); + cy.ResponseTextCheck("Basic ".concat(encodedStringBtoA)); + }); +}); diff --git a/app/client/cypress/locators/DatasourcesEditor.json b/app/client/cypress/locators/DatasourcesEditor.json index 47aa855c89..defed053bb 100644 --- a/app/client/cypress/locators/DatasourcesEditor.json +++ b/app/client/cypress/locators/DatasourcesEditor.json @@ -35,5 +35,8 @@ "grantType": "[data-cy='authentication.grantType']", "authorizationURL":"[data-cy='authentication.authorizationUrl'] input", "authorisecode": "//div[contains(@class,'option') and text()='Authorization Code']", - "saveAndAuthorize": "button:contains('Save and Authorize')" + "saveAndAuthorize": "button:contains('Save and Authorize')", + "basic": "//div[contains(@class,'option') and text()='Basic']", + "basicUsername": "input[name='authentication.username']", + "basicPassword": "input[name='authentication.password']" } diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 617eb1e958..bcf23bfc1a 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -286,6 +286,13 @@ Cypress.Commands.add( }, ); +Cypress.Commands.add("addBasicProfileDetails", (username, password) => { + cy.get(datasource.authType).click(); + cy.xpath(datasource.basic).click(); + cy.get(datasource.basicUsername).type(username); + cy.get(datasource.basicPassword).type(password); +}); + Cypress.Commands.add("firestoreDatasourceForm", () => { cy.get(datasourceEditor.datasourceConfigUrl).type( datasourceFormData["database-url"], @@ -474,6 +481,11 @@ Cypress.Commands.add("ResponseCheck", (textTocheck) => { cy.get(apiwidget.responseText).should("be.visible"); }); +Cypress.Commands.add("ResponseTextCheck", (textTocheck) => { + cy.ResponseCheck(); + cy.get(apiwidget.responseText).contains(textTocheck); +}); + Cypress.Commands.add("NavigateToAPI_Panel", () => { cy.get(pages.addEntityAPI) .should("be.visible") @@ -649,21 +661,20 @@ Cypress.Commands.add("SearchEntityandOpen", (apiname1) => { }); Cypress.Commands.add("enterDatasourceAndPath", (datasource, path) => { - cy.get(apiwidget.resourceUrl) - .first() - .click({ force: true }) - .type(datasource); - /* - cy.xpath(apiwidget.autoSuggest) - .first() - .click({ force: true }); - */ + cy.enterDatasource(datasource); cy.get(apiwidget.editResourceUrl) .first() .click({ force: true }) .type(path, { parseSpecialCharSequences: false }); }); +Cypress.Commands.add("enterDatasource", (datasource) => { + cy.get(apiwidget.resourceUrl) + .first() + .click({ force: true }) + .type(datasource); +}); + Cypress.Commands.add("changeZoomLevel", (zoomValue) => { cy.get(commonlocators.changeZoomlevel) .last() @@ -1132,7 +1143,7 @@ Cypress.Commands.add("widgetText", (text, inputcss, innercss) => { cy.get(inputcss) .first() .trigger("mouseover", { force: true }); - cy.get(innercss).should("have.text", text); + cy.contains(innercss, text); }); Cypress.Commands.add("editColName", (text) => { diff --git a/app/client/src/components/ads/Tabs.tsx b/app/client/src/components/ads/Tabs.tsx index a70206d531..296a31abe4 100644 --- a/app/client/src/components/ads/Tabs.tsx +++ b/app/client/src/components/ads/Tabs.tsx @@ -14,7 +14,6 @@ export type TabProp = { }; const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>` - user-select: none; border-radius: 0px; height: 100%; .react-tabs { diff --git a/app/client/src/components/ads/Toast.tsx b/app/client/src/components/ads/Toast.tsx index 8685e03e1c..34154c8fb8 100644 --- a/app/client/src/components/ads/Toast.tsx +++ b/app/client/src/components/ads/Toast.tsx @@ -17,6 +17,7 @@ type ToastProps = ToastOptions & duration?: number; onUndo?: () => void; dispatchableAction?: { type: ReduxActionType; payload: any }; + showDebugButton?: boolean; hideProgressBar?: boolean; }; @@ -131,7 +132,7 @@ function ToastComponent(props: ToastProps & { undoAction?: () => void }) { ) : null}
{props.text} - {props.variant === Variant.danger ? ( + {props.variant === Variant.danger && props.showDebugButton ? ( ) : null}
diff --git a/app/client/src/components/designSystems/appsmith/ChartComponent.tsx b/app/client/src/components/designSystems/appsmith/ChartComponent.tsx index ca49f9b40b..43b3bf3b7b 100644 --- a/app/client/src/components/designSystems/appsmith/ChartComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/ChartComponent.tsx @@ -185,7 +185,9 @@ class ChartComponent extends React.Component { getSeriesChartData = (data: ChartDataPoint[], categories: string[]) => { const dataMap: { [key: string]: string } = {}; - if (data.length === 0) { + + // if not array or (is array and array length is zero) + if (!Array.isArray(data) || (Array.isArray(data) && data.length === 0)) { return [ { value: "", @@ -218,7 +220,7 @@ class ChartComponent extends React.Component { const seriesChartData: Array> = this.getSeriesChartData(item.data, categories); + >> = this.getSeriesChartData(get(item, "data", []), categories); return { seriesName: item.seriesName, data: seriesChartData, diff --git a/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx b/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx index f5a45770fe..3487aab4b8 100644 --- a/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx +++ b/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx @@ -337,7 +337,7 @@ class DropDownComponent extends React.Component { }; renderTag = (option: DropdownOption) => { - return option.label; + return option?.label; }; isOptionSelected = (selectedOption: DropdownOption) => { diff --git a/app/client/src/components/editorComponents/ApiResponseView.tsx b/app/client/src/components/editorComponents/ApiResponseView.tsx index 09dc38510e..145807ae65 100644 --- a/app/client/src/components/editorComponents/ApiResponseView.tsx +++ b/app/client/src/components/editorComponents/ApiResponseView.tsx @@ -30,6 +30,7 @@ const ResponseContainer = styled.div` ${ResizerCSS} // Initial height of bottom tabs height: 60%; + width: 100%; // Minimum height of bottom tabs as it can be resized min-height: 36px; background-color: ${(props) => props.theme.colors.apiPane.responseBody.bg}; diff --git a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts index 3272c2f646..1054a84b21 100644 --- a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts +++ b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts @@ -422,20 +422,13 @@ export const DynamicAutocompleteInputWrapper = styled.div<{ height: 100%; flex: 1; position: relative; - border-color: ${(props) => - !props.isError && props.isActive && props.skin === Skin.DARK - ? Colors.ALABASTER - : "transparent"}; + border: 1px solid ${(props) => (!props.isError ? "transparent" : "red")}; > span:first-of-type { width: 30px; position: absolute; right: 0px; } &:hover { - border-color: ${(props) => - !props.isError && props.skin === Skin.DARK - ? Colors.ALABASTER - : "transparent"}; .lightning-menu { background: ${(props) => (!props.isNotHover ? "#090707" : "")}; svg { @@ -451,7 +444,6 @@ export const DynamicAutocompleteInputWrapper = styled.div<{ } } } - border: 0px; border-radius: 0px; .lightning-menu { z-index: 1 !important; diff --git a/app/client/src/components/editorComponents/Debugger/index.tsx b/app/client/src/components/editorComponents/Debugger/index.tsx index 2e7ebb0fb7..368d73be84 100644 --- a/app/client/src/components/editorComponents/Debugger/index.tsx +++ b/app/client/src/components/editorComponents/Debugger/index.tsx @@ -18,7 +18,7 @@ const Container = styled.div<{ errorCount: number }>` right: 20px; bottom: 20px; cursor: pointer; - padding: 19px; + padding: ${(props) => props.theme.spaces[6]}px; color: ${(props) => props.theme.colors.debugger.floatingButton.color}; border-radius: 50px; box-shadow: ${(props) => props.theme.colors.debugger.floatingButton.shadow}; @@ -33,11 +33,9 @@ const Container = styled.div<{ errorCount: number }>` .debugger-count { color: ${Colors.WHITE}; - font-size: 14px; - font-weight: 500; ${(props) => getTypographyByKey(props, "h6")} - height: 20px; - padding: 6px; + height: 16px; + padding: ${(props) => props.theme.spaces[1]}px; background-color: ${(props) => !!props.errorCount ? props.theme.colors.debugger.floatingButton.errorCount @@ -75,7 +73,7 @@ function Debugger() { errorCount={errorCount} onClick={onClick} > - +
{errorCount}
); diff --git a/app/client/src/components/editorComponents/WidgetNameComponent/SettingsControl.tsx b/app/client/src/components/editorComponents/WidgetNameComponent/SettingsControl.tsx index b33a28bd0f..6e3dfc2255 100644 --- a/app/client/src/components/editorComponents/WidgetNameComponent/SettingsControl.tsx +++ b/app/client/src/components/editorComponents/WidgetNameComponent/SettingsControl.tsx @@ -1,5 +1,6 @@ import React, { CSSProperties } from "react"; import { ControlIcons } from "icons/ControlIcons"; +import Icon, { IconSize } from "components/ads/Icon"; import { Colors } from "constants/Colors"; import styled from "styled-components"; import { Tooltip, Classes } from "@blueprintjs/core"; @@ -34,18 +35,41 @@ const SettingsWrapper = styled.div` `; const WidgetName = styled.span` - margin-right: 5px; + margin-right: ${(props) => props.theme.spaces[1] + 1}px; + margin-left: ${(props) => props.theme.spaces[3]}px; +`; + +const StyledErrorIcon = styled(Icon)` + &:hover { + svg { + path { + fill: ${Colors.WHITE}; + } + } + } + margin-right: ${(props) => props.theme.spaces[1]}px; `; type SettingsControlProps = { toggleSettings: (e: any) => void; activity: Activities; name: string; + errorCount: number; }; const SettingsIcon = ControlIcons.SETTINGS_CONTROL; -const getStyles = (activity: Activities): CSSProperties | undefined => { +const getStyles = ( + activity: Activities, + errorCount: number, +): CSSProperties | undefined => { + if (errorCount > 0) { + return { + background: "red", + color: Colors.WHITE, + }; + } + switch (activity) { case Activities.ACTIVE: return { @@ -69,7 +93,9 @@ export function SettingsControl(props: SettingsControlProps) { const settingsIcon = ( ); + const errorIcon = ( + + ); return ( + {!!props.errorCount && ( + <> + {errorIcon} + {props.errorCount} + + )} {props.name} {settingsIcon} diff --git a/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx b/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx index 0e5a653bde..98d46c85d1 100644 --- a/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx +++ b/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx @@ -41,6 +41,7 @@ type WidgetNameComponentProps = { parentId?: string; type: WidgetType; showControls?: boolean; + errorCount: number; }; export function WidgetNameComponent(props: WidgetNameComponentProps) { @@ -95,7 +96,8 @@ export function WidgetNameComponent(props: WidgetNameComponentProps) { props.showControls || ((focusedWidget === props.widgetId || selectedWidget === props.widgetId) && !isDragging && - !isResizing); + !isResizing) || + !!props.errorCount; let currentActivity = Activities.NONE; if (focusedWidget === props.widgetId) currentActivity = Activities.HOVERING; @@ -111,6 +113,7 @@ export function WidgetNameComponent(props: WidgetNameComponentProps) { diff --git a/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx b/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx index 94ab53e795..1355fcb2e8 100644 --- a/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx +++ b/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx @@ -173,7 +173,7 @@ class EmbeddedDatasourcePathComponent extends React.Component { hint: () => { const list = datasourceList .filter((datasource) => - datasource.datasourceConfiguration.url.includes( + (datasource.datasourceConfiguration?.url || "").includes( parsed.datasourceUrl, ), ) diff --git a/app/client/src/constants/HelpConstants.ts b/app/client/src/constants/HelpConstants.ts index ecd9087c09..b6004431c8 100644 --- a/app/client/src/constants/HelpConstants.ts +++ b/app/client/src/constants/HelpConstants.ts @@ -41,7 +41,7 @@ export const HelpMap = { }, DROP_DOWN_WIDGET: { path: "/widget-reference/dropdown", - searchKey: "Dropdown", + searchKey: "Select", }, RADIO_GROUP_WIDGET: { path: "/widget-reference/radio", diff --git a/app/client/src/entities/Datasource/RestAPIForm.ts b/app/client/src/entities/Datasource/RestAPIForm.ts index ea4f0239e9..9abf8719a6 100644 --- a/app/client/src/entities/Datasource/RestAPIForm.ts +++ b/app/client/src/entities/Datasource/RestAPIForm.ts @@ -3,6 +3,7 @@ import { Property } from "entities/Action"; export enum AuthType { NONE = "NONE", OAuth2 = "oAuth2", + basic = "basic", } export enum GrantType { @@ -10,7 +11,7 @@ export enum GrantType { AuthorizationCode = "authorization_code", } -export type Authentication = ClientCredentials | AuthorizationCode; +export type Authentication = ClientCredentials | AuthorizationCode | Basic; export interface ApiDatasourceForm { datasourceId: string; pluginId: string; @@ -45,3 +46,9 @@ export interface AuthorizationCode extends Oauth2Common { isAuthorizationHeader: boolean; isAuthorized: boolean; } + +export interface Basic { + authenticationType: AuthType.basic; + username: string; + password: string; +} diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx index 432145bec6..1282219042 100644 --- a/app/client/src/mockResponses/WidgetConfigResponse.tsx +++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx @@ -269,7 +269,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { { label: "Green", value: "GREEN" }, { label: "Red", value: "RED" }, ], - widgetName: "Dropdown", + widgetName: "Select", defaultOptionValue: "GREEN", version: 1, isRequired: false, diff --git a/app/client/src/mockResponses/WidgetSidebarResponse.tsx b/app/client/src/mockResponses/WidgetSidebarResponse.tsx index cec889206f..8a58f1ae9e 100644 --- a/app/client/src/mockResponses/WidgetSidebarResponse.tsx +++ b/app/client/src/mockResponses/WidgetSidebarResponse.tsx @@ -36,7 +36,7 @@ const WidgetSidebarResponse: WidgetCardProps[] = [ }, { type: "DROP_DOWN_WIDGET", - widgetCardName: "Dropdown", + widgetCardName: "Select", key: generateReactKey(), }, { diff --git a/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx b/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx index af1e66bbcb..7edfb2b9e4 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx @@ -206,7 +206,7 @@ class DatasourceRestAPIEditor extends React.Component { if (!this.props.formData) return; const { authentication } = this.props.formData; - if (!authentication || !authentication.grantType) { + if (!authentication || !_.get(authentication, "grantType")) { this.props.change( "authentication.grantType", GrantType.ClientCredentials, @@ -225,7 +225,7 @@ class DatasourceRestAPIEditor extends React.Component { return false; } - if (authentication.grantType === GrantType.AuthorizationCode) { + if (_.get(authentication, "grantType") === GrantType.AuthorizationCode) { if (_.get(authentication, "isAuthorizationHeader") === undefined) { this.props.change("authentication.isAuthorizationHeader", true); return false; @@ -435,6 +435,10 @@ class DatasourceRestAPIEditor extends React.Component { label: "None", value: AuthType.NONE, }, + { + label: "Basic", + value: AuthType.basic, + }, { label: "OAuth 2.0", value: AuthType.OAuth2, @@ -455,6 +459,8 @@ class DatasourceRestAPIEditor extends React.Component { let content; if (authType === AuthType.OAuth2) { content = this.renderOauth2(); + } else if (authType === AuthType.basic) { + content = this.renderBasic(); } if (content) { return ( @@ -465,11 +471,36 @@ class DatasourceRestAPIEditor extends React.Component { } }; + renderBasic = () => { + return ( + <> + + + + + + + + ); + }; + renderOauth2 = () => { const { authentication } = this.props.formData; if (!authentication) return; let content; - switch (authentication?.grantType) { + switch (_.get(authentication, "grantType")) { case GrantType.AuthorizationCode: content = this.renderOauth2AuthorizationCode(); break; @@ -525,7 +556,7 @@ class DatasourceRestAPIEditor extends React.Component { ]} /> - {formData.authentication?.isTokenHeader && ( + {_.get(formData.authentication, "isTokenHeader") && ( ` background-color: white; - width: ${(props) => - !props.isVisible ? "0px" : props.isActionPath ? "100%" : "75%"}; + width: ${(props) => (!props.isVisible ? "0" : "100%")}; height: 100%; `; diff --git a/app/client/src/sagas/ActionExecutionSagas.ts b/app/client/src/sagas/ActionExecutionSagas.ts index 06148a2194..dcd97f86c4 100644 --- a/app/client/src/sagas/ActionExecutionSagas.ts +++ b/app/client/src/sagas/ActionExecutionSagas.ts @@ -553,6 +553,7 @@ export function* executeActionSaga( Toaster.show({ text: createMessage(ERROR_API_EXECUTE, api.name), variant: Variant.danger, + showDebugButton: true, }); } else { PerformanceTracker.stopAsyncTracking( @@ -607,6 +608,7 @@ export function* executeActionSaga( Toaster.show({ text: createMessage(ERROR_API_EXECUTE, api.name), variant: Variant.danger, + showDebugButton: true, }); if (onError) { yield put( @@ -836,7 +838,7 @@ function* runActionSaga( Toaster.show({ text: createMessage(ERROR_ACTION_EXECUTE_FAIL, actionObject.name), - variant: Variant.warning, + variant: Variant.danger, }); } } else { diff --git a/app/client/src/sagas/EvaluationsSaga.ts b/app/client/src/sagas/EvaluationsSaga.ts index ef782826ee..a7e3fddc2d 100644 --- a/app/client/src/sagas/EvaluationsSaga.ts +++ b/app/client/src/sagas/EvaluationsSaga.ts @@ -58,6 +58,9 @@ const evalErrorHandler = (errors: EvalError[]) => { text: `${error.message} Node was: ${node}`, variant: Variant.danger, }); + AppsmithConsole.error({ + text: `${error.message} Node was: ${node}`, + }); // Send the generic error message to sentry for better grouping Sentry.captureException(new Error(error.message), { tags: { @@ -94,6 +97,10 @@ const evalErrorHandler = (errors: EvalError[]) => { Toaster.show({ text: createMessage(ERROR_EVAL_TRIGGER, error.message), variant: Variant.danger, + showDebugButton: true, + }); + AppsmithConsole.error({ + text: createMessage(ERROR_EVAL_TRIGGER, error.message), }); break; } diff --git a/app/client/src/transformers/RestAPIDatasourceFormTransformer.ts b/app/client/src/transformers/RestAPIDatasourceFormTransformer.ts index e6f1dbf14e..a7fe5b1453 100644 --- a/app/client/src/transformers/RestAPIDatasourceFormTransformer.ts +++ b/app/client/src/transformers/RestAPIDatasourceFormTransformer.ts @@ -8,6 +8,7 @@ import { ClientCredentials, GrantType, Oauth2Common, + Basic, } from "entities/Datasource/RestAPIForm"; import _ from "lodash"; @@ -103,6 +104,14 @@ const formToDatasourceAuthentication = ( }; } } + if (authType === AuthType.basic) { + const basic: Basic = { + authenticationType: AuthType.basic, + username: authentication.username, + password: authentication.password, + }; + return basic; + } return null; }; @@ -154,6 +163,14 @@ const datasourceToFormAuthentication = ( }; } } + if (authType === AuthType.basic) { + const basic: Basic = { + authenticationType: AuthType.basic, + username: authentication.username || "", + password: authentication.password || "", + }; + return basic; + } }; const isClientCredentials = ( diff --git a/app/client/src/utils/autocomplete/EntityDefinitions.ts b/app/client/src/utils/autocomplete/EntityDefinitions.ts index 0f0467d9ed..865496c010 100644 --- a/app/client/src/utils/autocomplete/EntityDefinitions.ts +++ b/app/client/src/utils/autocomplete/EntityDefinitions.ts @@ -76,7 +76,7 @@ export const entityDefinitions = { }, DROP_DOWN_WIDGET: { "!doc": - "Dropdown is used to capture user input/s from a specified list of permitted inputs. A Dropdown can capture a single choice as well as multiple choices", + "Select is used to capture user input/s from a specified list of permitted inputs. A Select can capture a single choice as well as multiple choices", "!url": "https://docs.appsmith.com/widget-reference/dropdown", isVisible: isVisible, selectedOptionValue: { diff --git a/app/client/src/utils/helpers.test.ts b/app/client/src/utils/helpers.test.ts new file mode 100644 index 0000000000..b9a613979c --- /dev/null +++ b/app/client/src/utils/helpers.test.ts @@ -0,0 +1,86 @@ +import { flattenObject } from "./helpers"; + +describe("flattenObject test", () => { + it("Check if non nested object is returned correctly", () => { + const testObject = { + isVisible: true, + isDisabled: false, + tableData: false, + }; + + expect(flattenObject(testObject)).toStrictEqual(testObject); + }); + + it("Check if nested objects are returned correctly", () => { + const tests = [ + { + input: { + isVisible: true, + isDisabled: false, + tableData: false, + settings: { + color: [ + { + headers: { + left: true, + }, + }, + ], + }, + }, + output: { + isVisible: true, + isDisabled: false, + tableData: false, + "settings.color[0].headers.left": true, + }, + }, + { + input: { + isVisible: true, + isDisabled: false, + tableData: false, + settings: { + color: true, + }, + }, + output: { + isVisible: true, + isDisabled: false, + tableData: false, + "settings.color": true, + }, + }, + { + input: { + numbers: [1, 2, 3], + color: { header: "red" }, + }, + output: { + "numbers[0]": 1, + "numbers[1]": 2, + "numbers[2]": 3, + "color.header": "red", + }, + }, + { + input: { + name: null, + color: { header: {} }, + users: { + id: undefined, + }, + }, + output: { + "color.header": {}, + name: null, + "users.id": undefined, + }, + }, + ]; + + tests.map((test) => + expect(flattenObject(test.input)).toStrictEqual(test.output), + ); + }); +}); diff --git a/app/client/src/utils/helpers.tsx b/app/client/src/utils/helpers.tsx index 6a9f52bf05..5534d658df 100644 --- a/app/client/src/utils/helpers.tsx +++ b/app/client/src/utils/helpers.tsx @@ -287,3 +287,28 @@ export const scrollbarWidth = () => { document.body.removeChild(scrollDiv); return scrollbarWidth; }; + +// Flatten object +// From { isValid: false, settings: { color: false}} +// To { isValid: false, settings.color: false} +export const flattenObject = (data: Record) => { + const result: Record = {}; + function recurse(cur: any, prop: any) { + if (Object(cur) !== cur) { + result[prop] = cur; + } else if (Array.isArray(cur)) { + for (let i = 0, l = cur.length; i < l; i++) + recurse(cur[i], prop + "[" + i + "]"); + if (cur.length == 0) result[prop] = []; + } else { + let isEmpty = true; + for (const p in cur) { + isEmpty = false; + recurse(cur[p], prop ? prop + "." + p : p); + } + if (isEmpty && prop) result[prop] = {}; + } + } + recurse(data, ""); + return result; +}; diff --git a/app/client/src/widgets/BaseWidget.tsx b/app/client/src/widgets/BaseWidget.tsx index baa8de6de3..0e47cd7579 100644 --- a/app/client/src/widgets/BaseWidget.tsx +++ b/app/client/src/widgets/BaseWidget.tsx @@ -15,6 +15,7 @@ import { CSSUnit, CONTAINER_GRID_PADDING, } from "constants/WidgetConstants"; +import { memoize } from "lodash"; import DraggableComponent from "components/editorComponents/DraggableComponent"; import ResizableComponent from "components/editorComponents/ResizableComponent"; import { WidgetExecuteActionPayload } from "constants/AppsmithActionConstants/ActionConstants"; @@ -35,6 +36,7 @@ import OverlayCommentsWrapper from "comments/inlineComments/OverlayCommentsWrapp import PreventInteractionsOverlay from "components/editorComponents/PreventInteractionsOverlay"; import AppsmithConsole from "utils/AppsmithConsole"; import { ENTITY_TYPE } from "entities/AppsmithConsole"; +import { flattenObject } from "utils/helpers"; /*** * BaseWidget @@ -176,6 +178,10 @@ abstract class BaseWidget< }; } + getErrorCount = memoize((invalidProps) => { + return Object.values(flattenObject(invalidProps)).filter((e) => !!e).length; + }, JSON.stringify); + render() { return this.getWidgetView(); } @@ -209,6 +215,7 @@ abstract class BaseWidget< <> {!this.props.disablePropertyPane && ( { label: "Date Format", controlType: "DROP_DOWN", isJSConvertible: true, - optionWidth: "320px", + optionWidth: "340px", options: [ { label: moment().format("YYYY-MM-DDTHH:mm:ss.sssZ"), @@ -62,9 +62,9 @@ class DatePickerWidget extends BaseWidget { value: "YYYY-MM-DDTHH:mm:ss", }, { - label: moment().format("YYYY-MM-DD hh:mm:ss"), - subText: "YYYY-MM-DD hh:mm:ss", - value: "YYYY-MM-DD hh:mm:ss", + label: moment().format("YYYY-MM-DD hh:mm:ss A"), + subText: "YYYY-MM-DD hh:mm:ss A", + value: "YYYY-MM-DD hh:mm:ss A", }, { label: moment().format("DD/MM/YYYY HH:mm"), diff --git a/app/server/README.md b/app/server/README.md index d59286da54..ec647f1efb 100644 --- a/app/server/README.md +++ b/app/server/README.md @@ -1,6 +1,7 @@ # Appsmith Server This is the server-side repository for the Appsmith framework. +For details on setting up your development machine, please refer to the [Setup Guide](https://github.com/appsmithorg/appsmith/blob/release/contributions/ServerSetup.md) ### How to build ```bash diff --git a/app/server/appsmith-interfaces/pom.xml b/app/server/appsmith-interfaces/pom.xml index 745124bb92..378f1c46d2 100644 --- a/app/server/appsmith-interfaces/pom.xml +++ b/app/server/appsmith-interfaces/pom.xml @@ -107,7 +107,7 @@ commons-io commons-io - 2.6 + 2.7 compile diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/Authentication.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/Authentication.java index ea08952678..5fa624330f 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/Authentication.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/Authentication.java @@ -5,6 +5,7 @@ public class Authentication { // Auth type constants public static final String DB_AUTH = "dbAuth"; public static final String OAUTH2 = "oAuth2"; + public static final String BASIC = "basic"; // Request parameter names public static final String CLIENT_ID = "client_id"; diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/AuthenticationDTO.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/AuthenticationDTO.java index 1c002f2562..2e50af7617 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/AuthenticationDTO.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/AuthenticationDTO.java @@ -22,7 +22,8 @@ import java.util.Set; defaultImpl = DBAuth.class) @JsonSubTypes({ @JsonSubTypes.Type(value = DBAuth.class, name = Authentication.DB_AUTH), - @JsonSubTypes.Type(value = OAuth2.class, name = Authentication.OAUTH2) + @JsonSubTypes.Type(value = OAuth2.class, name = Authentication.OAUTH2), + @JsonSubTypes.Type(value = BasicAuth.class, name = Authentication.BASIC) }) public class AuthenticationDTO implements AppsmithDomain { // In principle, this class should've been abstract. However, when this class is abstract, Spring's deserialization diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/BasicAuth.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/BasicAuth.java new file mode 100644 index 0000000000..243ba06fcb --- /dev/null +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/BasicAuth.java @@ -0,0 +1,26 @@ +package com.appsmith.external.models; + +import com.appsmith.external.annotations.documenttype.DocumentType; +import com.appsmith.external.annotations.encryption.Encrypted; +import com.appsmith.external.constants.Authentication; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@NoArgsConstructor +@AllArgsConstructor +@DocumentType(Authentication.BASIC) +public class BasicAuth extends AuthenticationDTO { + + String username; + + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + @Encrypted + String password; +} diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java index 72b7f0faf1..96ac1c4a82 100644 --- a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java @@ -62,6 +62,8 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeoutException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY; @@ -89,6 +91,40 @@ public class MongoPlugin extends BasePlugin { private static final int SMART_BSON_SUBSTITUTION_INDEX = 0; + /* + * - The regex matches the following two pattern types: + * - mongodb+srv://user:pass@some-url/some-db.... + * - mongodb://user:pass@some-url:port,some-url:port,../some-db.... + * - It has been grouped like this: (mongodb+srv://)((user):(pass))(@some-url/(some-db....)) + */ + private static final String MONGO_URI_REGEX = "^(mongodb(\\+srv)?:\\/\\/)((.+):(.+))(@.+\\/(.+))$"; + + private static final int REGEX_GROUP_HEAD = 1; + + private static final int REGEX_GROUP_USERNAME = 4; + + private static final int REGEX_GROUP_PASSWORD = 5; + + private static final int REGEX_GROUP_TAIL = 6; + + private static final int REGEX_GROUP_DBNAME = 7; + + private static final String KEY_USERNAME = "username"; + + private static final String KEY_PASSWORD = "password"; + + private static final String KEY_URI_HEAD = "uriHead"; + + private static final String KEY_URI_TAIL = "uriTail"; + + private static final String KEY_URI_DBNAME = "dbName"; + + private static final String YES = "Yes"; + + private static final int DATASOURCE_CONFIG_USE_MONGO_URI_PROPERTY_INDEX = 0; + + private static final int DATASOURCE_CONFIG_MONGO_URI_PROPERTY_INDEX = 1; + private static final Integer MONGO_COMMAND_EXCEPTION_UNAUTHORIZED_ERROR_CODE = 13; public MongoPlugin(PluginWrapper wrapper) { @@ -367,9 +403,83 @@ public class MongoPlugin extends BasePlugin { .subscribeOn(scheduler); } - public static String buildClientURI(DatasourceConfiguration datasourceConfiguration) throws AppsmithPluginException { - StringBuilder builder = new StringBuilder(); + private boolean isUsingURI(DatasourceConfiguration datasourceConfiguration) { + List properties = datasourceConfiguration.getProperties(); + if (properties != null && properties.size() > DATASOURCE_CONFIG_USE_MONGO_URI_PROPERTY_INDEX + && properties.get(DATASOURCE_CONFIG_USE_MONGO_URI_PROPERTY_INDEX) != null + && YES.equals(properties.get(DATASOURCE_CONFIG_USE_MONGO_URI_PROPERTY_INDEX).getValue())) { + return true; + } + return false; + } + + private boolean hasNonEmptyURI(DatasourceConfiguration datasourceConfiguration) { + List properties = datasourceConfiguration.getProperties(); + if (properties != null && properties.size() > DATASOURCE_CONFIG_MONGO_URI_PROPERTY_INDEX + && properties.get(DATASOURCE_CONFIG_MONGO_URI_PROPERTY_INDEX) != null + && !StringUtils.isEmpty(properties.get(DATASOURCE_CONFIG_MONGO_URI_PROPERTY_INDEX).getValue())) { + return true; + } + + return false; + } + + private Map extractInfoFromConnectionStringURI(String uri, String regex) { + if (uri.matches(regex)) { + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(uri); + if (matcher.find()) { + Map extractedInfoMap = new HashMap(); + String username = matcher.group(REGEX_GROUP_USERNAME); + extractedInfoMap.put(KEY_USERNAME, username == null ? "" : username); + String password = matcher.group(REGEX_GROUP_PASSWORD); + extractedInfoMap.put(KEY_PASSWORD, password == null ? "" : password); + extractedInfoMap.put(KEY_URI_HEAD, matcher.group(REGEX_GROUP_HEAD)); + extractedInfoMap.put(KEY_URI_TAIL, matcher.group(REGEX_GROUP_TAIL)); + extractedInfoMap.put(KEY_URI_DBNAME, matcher.group(REGEX_GROUP_DBNAME).split("\\?")[0]); + return extractedInfoMap; + } + } + + return null; + } + + private String buildURIfromExtractedInfo(Map extractedInfo, String password) { + return extractedInfo.get(KEY_URI_HEAD) + (extractedInfo.get(KEY_USERNAME) == null ? "" : + extractedInfo.get(KEY_USERNAME) + ":") + (password == null ? "" : password) + + extractedInfo.get(KEY_URI_TAIL); + } + + public String buildClientURI(DatasourceConfiguration datasourceConfiguration) throws AppsmithPluginException { + List properties = datasourceConfiguration.getProperties(); + if (isUsingURI(datasourceConfiguration)) { + if (hasNonEmptyURI(datasourceConfiguration)) { + String uriWithHiddenPassword = + (String)properties.get(DATASOURCE_CONFIG_MONGO_URI_PROPERTY_INDEX).getValue(); + Map extractedInfo = extractInfoFromConnectionStringURI(uriWithHiddenPassword, MONGO_URI_REGEX); + if (extractedInfo != null) { + String password = ((DBAuth)datasourceConfiguration.getAuthentication()).getPassword(); + return buildURIfromExtractedInfo(extractedInfo, password); + } + else { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, + "Appsmith server has failed to parse the Mongo connection string URI. Please check " + + "if the URI has the correct format." + ); + } + } + else { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, + "Could not find any Mongo connection string URI. Please edit the 'Mongo Connection String" + + " URI' field to provide the URI to connect to." + ); + } + } + + StringBuilder builder = new StringBuilder(); final Connection connection = datasourceConfiguration.getConnection(); final List endpoints = datasourceConfiguration.getEndpoints(); @@ -483,52 +593,89 @@ public class MongoPlugin extends BasePlugin { @Override public Set validateDatasource(DatasourceConfiguration datasourceConfiguration) { Set invalids = new HashSet<>(); + List properties = datasourceConfiguration.getProperties(); + if (isUsingURI(datasourceConfiguration)) { + if (!hasNonEmptyURI(datasourceConfiguration)) { + invalids.add("'Mongo Connection String URI' field is empty. Please edit the 'Mongo Connection " + + "URI' field to provide a connection uri to connect with."); + } else { + String mongoUri = (String)properties.get(DATASOURCE_CONFIG_MONGO_URI_PROPERTY_INDEX).getValue(); + if (!mongoUri.matches(MONGO_URI_REGEX)) { + invalids.add("Mongo Connection String URI does not seem to be in the correct format. Please " + + "check the URI once."); + } else { + Map extractedInfo = extractInfoFromConnectionStringURI(mongoUri, MONGO_URI_REGEX); + if (extractedInfo == null) { + invalids.add("Mongo Connection String URI does not seem to be in the correct format. " + + "Please check the URI once."); + } else { + String mongoUriWithHiddenPassword = buildURIfromExtractedInfo(extractedInfo, "****"); + properties.get(DATASOURCE_CONFIG_MONGO_URI_PROPERTY_INDEX).setValue(mongoUriWithHiddenPassword); + DBAuth authentication = datasourceConfiguration.getAuthentication() == null ? + new DBAuth() : (DBAuth) datasourceConfiguration.getAuthentication(); + authentication.setUsername((String) extractedInfo.get(KEY_USERNAME)); + authentication.setPassword((String) extractedInfo.get(KEY_PASSWORD)); + authentication.setDatabaseName((String) extractedInfo.get(KEY_URI_DBNAME)); + datasourceConfiguration.setAuthentication(authentication); - List endpoints = datasourceConfiguration.getEndpoints(); - if (CollectionUtils.isEmpty(endpoints)) { - invalids.add("Missing endpoint(s)."); + // remove any default db set via form auto-fill via browser + if (datasourceConfiguration.getConnection() != null) { + datasourceConfiguration.getConnection().setDefaultDatabaseName(null); + } + } + } + } + } else { + List endpoints = datasourceConfiguration.getEndpoints(); + if (CollectionUtils.isEmpty(endpoints)) { + invalids.add("Missing endpoint(s)."); + + } else if (Connection.Type.REPLICA_SET.equals(datasourceConfiguration.getConnection().getType())) { + if (endpoints.size() == 1 && endpoints.get(0).getPort() != null) { + invalids.add("REPLICA_SET connections should not be given a port." + + " If you are trying to specify all the shards, please add more than one."); + } - } else if (Connection.Type.REPLICA_SET.equals(datasourceConfiguration.getConnection().getType())) { - if (endpoints.size() == 1 && endpoints.get(0).getPort() != null) { - invalids.add("REPLICA_SET connections should not be given a port." + - " If you are trying to specify all the shards, please add more than one."); } - } + if (!CollectionUtils.isEmpty(endpoints)) { + boolean usingUri = endpoints + .stream() + .anyMatch(endPoint -> endPoint.getHost().matches(MONGO_URI_REGEX)); - if (!CollectionUtils.isEmpty(endpoints)) { - boolean usingSrvUrl = endpoints - .stream() - .anyMatch(endPoint -> endPoint.getHost().contains("mongodb+srv")); - - if (usingSrvUrl) { - invalids.add("MongoDb SRV URLs are not yet supported. Please extract the individual fields from " + - "the SRV URL into the datasource configuration form."); - } - } - - DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); - if (authentication != null) { - DBAuth.Type authType = authentication.getAuthType(); - - if (authType == null || !VALID_AUTH_TYPES.contains(authType)) { - invalids.add("Invalid authType. Must be one of " + VALID_AUTH_TYPES_STR); + if (usingUri) { + invalids.add("It seems that you are trying to use a mongo connection string URI. Please " + + "extract relevant fields and fill the form with extracted values. For " + + "details, please check out the Appsmith's documentation for Mongo database. " + + "Alternatively, you may use 'Import from Connection String URI' option from the " + + "dropdown labelled 'Use Mongo Connection String URI' to use the URI connection string" + + " directly."); + } } - if (StringUtils.isEmpty(authentication.getDatabaseName())) { - invalids.add("Missing database name."); + DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); + if (authentication != null) { + DBAuth.Type authType = authentication.getAuthType(); + + if (authType == null || !VALID_AUTH_TYPES.contains(authType)) { + invalids.add("Invalid authType. Must be one of " + VALID_AUTH_TYPES_STR); + } + + if (StringUtils.isEmpty(authentication.getDatabaseName())) { + invalids.add("Missing database name."); + } + } - } - - /* - * - Ideally, it is never expected to be null because the SSL dropdown is set to a initial value. - */ - if (datasourceConfiguration.getConnection() == null - || datasourceConfiguration.getConnection().getSsl() == null - || datasourceConfiguration.getConnection().getSsl().getAuthType() == null) { - invalids.add("Appsmith server has failed to fetch SSL configuration from datasource configuration " + - "form. Please reach out to Appsmith customer support to resolve this."); + /* + * - Ideally, it is never expected to be null because the SSL dropdown is set to a initial value. + */ + if (datasourceConfiguration.getConnection() == null + || datasourceConfiguration.getConnection().getSsl() == null + || datasourceConfiguration.getConnection().getSsl().getAuthType() == null) { + invalids.add("Appsmith server has failed to fetch SSL configuration from datasource configuration " + + "form. Please reach out to Appsmith customer support to resolve this."); + } } return invalids; @@ -581,6 +728,7 @@ public class MongoPlugin extends BasePlugin { final DatasourceStructure structure = new DatasourceStructure(); List tables = new ArrayList<>(); structure.setTables(tables); + final MongoDatabase database = mongoClient.getDatabase(getDatabaseName(datasourceConfiguration)); return Flux.from(database.listCollectionNames()) diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json index 0ee963a8f7..1878ba55df 100644 --- a/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json @@ -3,6 +3,47 @@ { "sectionName": "Connection", "children": [ + { + "label": "Use Mongo Connection String URI Key", + "configProperty": "datasourceConfiguration.properties[0].key", + "controlType": "INPUT_TEXT", + "initialValue": "Use Mongo Connection String URI", + "hidden": true + }, + { + "label": "Use Mongo Connection String URI", + "configProperty": "datasourceConfiguration.properties[0].value", + "controlType": "DROP_DOWN", + "initialValue": "No", + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + }, + { + "label": "Connection String URI Key", + "configProperty": "datasourceConfiguration.properties[1].key", + "controlType": "INPUT_TEXT", + "initialValue": "Connection String URI", + "hidden": true + }, + { + "label": "Connection String URI", + "placeholderText": "mongodb+srv://:@test-db.swrsq.mongodb.net/myDatabase", + "configProperty": "datasourceConfiguration.properties[1].value", + "controlType": "INPUT_TEXT", + "hidden": { + "path": "datasourceConfiguration.properties[0].value", + "comparison": "NOT_EQUALS", + "value": "Yes" + } + }, { "label": "Connection Mode", "configProperty": "datasourceConfiguration.connection.mode", @@ -17,7 +58,12 @@ "label": "Read / Write", "value": "READ_WRITE" } - ] + ], + "hidden": { + "path": "datasourceConfiguration.properties[0].value", + "comparison": "EQUALS", + "value": "Yes" + } }, { "label": "Connection Type", @@ -33,7 +79,12 @@ "label": "Replica set", "value": "REPLICA_SET" } - ] + ], + "hidden": { + "path": "datasourceConfiguration.properties[0].value", + "comparison": "EQUALS", + "value": "Yes" + } }, { "sectionName": null, @@ -44,13 +95,23 @@ "controlType": "KEYVALUE_ARRAY", "validationMessage": "Please enter a valid host", "validationRegex": "^((?![/:]).)*$", - "placeholderText": "myapp.abcde.mongodb.net" + "placeholderText": "myapp.abcde.mongodb.net", + "hidden": { + "path": "datasourceConfiguration.properties[0].value", + "comparison": "EQUALS", + "value": "Yes" + } }, { "label": "Port", "configProperty": "datasourceConfiguration.endpoints[*].port", "dataType": "NUMBER", - "controlType": "KEYVALUE_ARRAY" + "controlType": "KEYVALUE_ARRAY", + "hidden": { + "path": "datasourceConfiguration.properties[0].value", + "comparison": "EQUALS", + "value": "Yes" + } } ] }, @@ -58,12 +119,22 @@ "label": "Default Database Name", "placeholderText": "(Optional)", "configProperty": "datasourceConfiguration.connection.defaultDatabaseName", - "controlType": "INPUT_TEXT" + "controlType": "INPUT_TEXT", + "hidden": { + "path": "datasourceConfiguration.properties[0].value", + "comparison": "EQUALS", + "value": "Yes" + } } ] }, { "sectionName": "Authentication", + "hidden": { + "path": "datasourceConfiguration.properties[0].value", + "comparison": "EQUALS", + "value": "Yes" + }, "children": [ { "label": "Database Name", @@ -108,13 +179,18 @@ "controlType": "INPUT_TEXT", "placeholderText": "Password", "encrypted": true - } + } ] } ] }, { "sectionName": "SSL (optional)", + "hidden": { + "path": "datasourceConfiguration.properties[0].value", + "comparison": "EQUALS", + "value": "Yes" + }, "children": [ { "label": "SSL Mode", diff --git a/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java b/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java index 7013c2f308..1709486140 100644 --- a/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java +++ b/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java @@ -473,16 +473,129 @@ public class MongoPluginTest { } @Test - public void testErrorMessageOnSrvUrl() { + public void testErrorMessageOnSrvUriWithFormInterface() { DatasourceConfiguration dsConfig = createDatasourceConfiguration(); - dsConfig.getEndpoints().get(0).setHost("mongodb+srv:://url.net"); + dsConfig.getEndpoints().get(0).setHost("mongodb+srv://user:pass@url.net/dbName"); + dsConfig.setProperties(List.of(new Property("Import from URI", "No"))); Mono> invalidsMono = Mono.just(pluginExecutor.validateDatasource(dsConfig)); StepVerifier.create(invalidsMono) .assertNext(invalids -> { assertTrue(invalids .stream() - .anyMatch(error -> error.contains("MongoDb SRV URLs are not yet supported"))); + .anyMatch(error -> error.contains("It seems that you are trying to use a mongo connection" + + " string URI. Please extract relevant fields and fill the form with extracted " + + "values. For details, please check out the Appsmith's documentation for Mongo " + + "database. Alternatively, you may use 'Import from Connection String URI' option " + + "from the dropdown labelled 'Use Mongo Connection String URI' to use the URI " + + "connection string directly."))); + }) + .verifyComplete(); + } + + @Test + public void testErrorMessageOnNonSrvUri() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + dsConfig.getEndpoints().get(0).setHost("mongodb://user:pass@url.net:1234,url.net:1234/dbName"); + dsConfig.setProperties(List.of(new Property("Import from URI", "No"))); + Mono> invalidsMono = Mono.just(pluginExecutor.validateDatasource(dsConfig)); + + StepVerifier.create(invalidsMono) + .assertNext(invalids -> { + assertTrue(invalids + .stream() + .anyMatch(error -> error.contains("It seems that you are trying to use a mongo connection" + + " string URI. Please extract relevant fields and fill the form with extracted " + + "values. For details, please check out the Appsmith's documentation for Mongo " + + "database. Alternatively, you may use 'Import from Connection String URI' option " + + "from the dropdown labelled 'Use Mongo Connection String URI' to use the URI " + + "connection string directly."))); + }) + .verifyComplete(); + } + + @Test + public void testInvalidsOnMissingUri() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + dsConfig.setProperties(List.of(new Property("Import from URI", "Yes"))); + Mono> invalidsMono = Mono.just(pluginExecutor.validateDatasource(dsConfig)); + + StepVerifier.create(invalidsMono) + .assertNext(invalids -> { + assertTrue(invalids + .stream() + .anyMatch(error -> error.contains("'Mongo Connection String URI' field is empty. Please " + + "edit the 'Mongo Connection URI' field to provide a connection uri to connect with."))); + }) + .verifyComplete(); + } + + @Test + public void testInvalidsOnBadSrvUriFormat() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + List properties = new ArrayList<>(); + properties.add(new Property("Import from URI", "Yes")); + properties.add(new Property("Srv Url", "mongodb+srv::username:password//url.net")); + dsConfig.setProperties(properties); + Mono> invalidsMono = Mono.just(pluginExecutor.validateDatasource(dsConfig)); + + StepVerifier.create(invalidsMono) + .assertNext(invalids -> { + assertTrue(invalids + .stream() + .anyMatch(error -> error.contains("Mongo Connection String URI does not seem to be in the" + + " correct format. Please check the URI once."))); + }) + .verifyComplete(); + } + + @Test + public void testInvalidsOnBadNonSrvUriFormat() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + List properties = new ArrayList<>(); + properties.add(new Property("Import from URI", "Yes")); + properties.add(new Property("Srv Url", "mongodb::username:password//url.net")); + dsConfig.setProperties(properties); + Mono> invalidsMono = Mono.just(pluginExecutor.validateDatasource(dsConfig)); + + StepVerifier.create(invalidsMono) + .assertNext(invalids -> { + assertTrue(invalids + .stream() + .anyMatch(error -> error.contains("Mongo Connection String URI does not seem to be in the" + + " correct format. Please check the URI once."))); + }) + .verifyComplete(); + } + + @Test + public void testInvalidsEmptyOnCorrectSrvUriFormat() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + List properties = new ArrayList<>(); + properties.add(new Property("Import from URI", "Yes")); + properties.add(new Property("Srv Url", "mongodb+srv://username:password@url.net/dbname")); + dsConfig.setProperties(properties); + Mono> invalidsMono = Mono.just(pluginExecutor.validateDatasource(dsConfig)); + + StepVerifier.create(invalidsMono) + .assertNext(invalids -> { + assertTrue(invalids.isEmpty()); + }) + .verifyComplete(); + } + + @Test + public void testInvalidsEmptyOnCorrectNonSrvUriFormat() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + List properties = new ArrayList<>(); + properties.add(new Property("Import from URI", "Yes")); + properties.add(new Property("Srv Url", "mongodb://username:password@url-1.net:1234,url-2:1234/dbname")); + dsConfig.setProperties(properties); + Mono> invalidsMono = Mono.just(pluginExecutor.validateDatasource(dsConfig)); + + StepVerifier.create(invalidsMono) + .assertNext(invalids -> { + assertTrue(invalids.isEmpty()); }) .verifyComplete(); } diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/APIConnectionFactory.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/APIConnectionFactory.java index ccc0a8bf30..d7fc95c761 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/APIConnectionFactory.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/APIConnectionFactory.java @@ -3,6 +3,7 @@ package com.external.connections; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; import com.appsmith.external.models.AuthenticationDTO; +import com.appsmith.external.models.BasicAuth; import com.appsmith.external.models.OAuth2; import reactor.core.publisher.Mono; @@ -22,6 +23,8 @@ public class APIConnectionFactory { } else { return Mono.empty(); } + } else if (authenticationType instanceof BasicAuth) { + return Mono.from(BasicAuthentication.create((BasicAuth) authenticationType)); } else { return Mono.empty(); } diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/BasicAuthentication.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/BasicAuthentication.java new file mode 100644 index 0000000000..490858e373 --- /dev/null +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/BasicAuthentication.java @@ -0,0 +1,45 @@ +package com.external.connections; + +import com.appsmith.external.models.BasicAuth; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFunction; +import reactor.core.publisher.Mono; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +@Setter +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class BasicAuthentication extends APIConnection { + + private String encodedAuthorizationHeader; + final private static String HEADER_PREFIX = "Basic "; + + public static Mono create(BasicAuth basicAuth) { + final BasicAuthentication basicAuthentication = new BasicAuthentication(); + final String decodedAuthorizationHeader = basicAuth.getUsername() + ":" + basicAuth.getPassword(); + + basicAuthentication.setEncodedAuthorizationHeader( + Base64.getEncoder().encodeToString(decodedAuthorizationHeader.getBytes(StandardCharsets.UTF_8))); + + return Mono.just(basicAuthentication); + } + + + @Override + public Mono filter(ClientRequest request, ExchangeFunction next) { + return Mono.justOrEmpty(ClientRequest.from(request) + .headers(headers -> headers.set("Authorization", HEADER_PREFIX + this.getEncodedAuthorizationHeader())) + .build()) + // Carry on to next exchange function + .flatMap(next::exchange) + // Default to next exchange function if something went wrong + .switchIfEmpty(next.exchange(request)); + } +} diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2AuthorizationCode.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2AuthorizationCode.java index aa28d35856..d9f19f9419 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2AuthorizationCode.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2AuthorizationCode.java @@ -6,7 +6,9 @@ import com.appsmith.external.models.AuthenticationDTO; import com.appsmith.external.models.AuthenticationResponse; import com.appsmith.external.models.OAuth2; import com.appsmith.external.models.UpdatableConnection; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -31,6 +33,7 @@ import java.util.Map; @Setter @Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) public class OAuth2AuthorizationCode extends APIConnection implements UpdatableConnection { private final Clock clock = Clock.systemUTC(); @@ -42,9 +45,6 @@ public class OAuth2AuthorizationCode extends APIConnection implements UpdatableC private Object tokenResponse; private static final int MAX_IN_MEMORY_SIZE = 10 * 1024 * 1024; // 10 MB - private OAuth2AuthorizationCode() { - } - public static Mono create(OAuth2 oAuth2) { if (oAuth2 == null) { return Mono.empty(); diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2ClientCredentials.java b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2ClientCredentials.java index 33e54ebad3..ab132a4f53 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2ClientCredentials.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/java/com/external/connections/OAuth2ClientCredentials.java @@ -6,7 +6,9 @@ import com.appsmith.external.models.AuthenticationDTO; import com.appsmith.external.models.AuthenticationResponse; import com.appsmith.external.models.OAuth2; import com.appsmith.external.models.UpdatableConnection; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -31,6 +33,7 @@ import java.util.Map; @Setter @Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) public class OAuth2ClientCredentials extends APIConnection implements UpdatableConnection { private final Clock clock = Clock.systemUTC(); @@ -41,9 +44,6 @@ public class OAuth2ClientCredentials extends APIConnection implements UpdatableC private Object tokenResponse; private static final int MAX_IN_MEMORY_SIZE = 10 * 1024 * 1024; // 10 MB - private OAuth2ClientCredentials() { - } - public static Mono create(OAuth2 oAuth2) { if (oAuth2 == null) { return Mono.empty(); diff --git a/app/server/appsmith-plugins/restApiPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/restApiPlugin/src/main/resources/form.json index 9945c475ca..45e5258fb0 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/main/resources/form.json +++ b/app/server/appsmith-plugins/restApiPlugin/src/main/resources/form.json @@ -65,6 +65,10 @@ "label": "None", "value": "dbAuth" }, + { + "label": "Basic", + "value": "basic" + }, { "label": "OAuth2 (Client credentials)", "value": "oAuth2" diff --git a/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/connections/BasicAuthenticationTest.java b/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/connections/BasicAuthenticationTest.java new file mode 100644 index 0000000000..2ed3f751c2 --- /dev/null +++ b/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/connections/BasicAuthenticationTest.java @@ -0,0 +1,26 @@ +package com.external.connections; + +import com.appsmith.external.models.BasicAuth; +import org.junit.Assert; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Base64; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BasicAuthenticationTest { + + @Test + public void testCreate_validCredentials_ReturnsWithEncodedValue() { + BasicAuth basicAuth = new BasicAuth(); + basicAuth.setUsername("test"); + basicAuth.setPassword("password"); + BasicAuthentication connection = BasicAuthentication.create(basicAuth).block(Duration.ofMillis(100)); + assertThat(connection).isNotNull(); + Assert.assertEquals( + Base64.getEncoder().encodeToString("test:password".getBytes(StandardCharsets.UTF_8)), + connection.getEncodedAuthorizationHeader()); + } +} \ No newline at end of file diff --git a/app/server/appsmith-server/pom.xml b/app/server/appsmith-server/pom.xml index 584eba9d7c..55a1bbe62c 100644 --- a/app/server/appsmith-server/pom.xml +++ b/app/server/appsmith-server/pom.xml @@ -66,7 +66,7 @@ org.springframework.boot spring-boot-starter-mail - 2.2.1.RELEASE + 2.4.4 org.springframework.boot @@ -121,7 +121,7 @@ commons-io commons-io - 2.6 + 2.7 commons-validator @@ -131,7 +131,7 @@ org.springframework.boot spring-boot-starter-actuator - 2.3.4.RELEASE + 2.4.4 io.micrometer diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/CommentController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/CommentController.java index 591bb634da..d11e22af62 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/CommentController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/CommentController.java @@ -10,9 +10,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -60,7 +60,7 @@ public class CommentController extends BaseController new ResponseDTO<>(HttpStatus.OK.value(), threads, null)); } - @PatchMapping("/threads/{threadId}") + @PutMapping("/threads/{threadId}") public Mono> updateThread( @Valid @RequestBody CommentThread resource, @PathVariable String threadId @@ -78,4 +78,11 @@ public class CommentController extends BaseController new ResponseDTO<>(HttpStatus.OK.value(), deletedResource, null)); } + @DeleteMapping("/threads/{threadId}") + public Mono> deleteThread(@PathVariable String threadId) { + log.debug("Going to delete thread with id: {}", threadId); + return service.deleteThread(threadId) + .map(deletedResource -> new ResponseDTO<>(HttpStatus.OK.value(), deletedResource, null)); + } + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java index b214257337..c2a6a750e9 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java @@ -2242,4 +2242,20 @@ public class DatabaseChangelog { NewAction.class ); } + + @ChangeSet(order = "067", id = "update-mongo-import-from-srv-field", author = "") + public void updateMongoImportFromSrvField(MongoTemplate mongoTemplate) { + Plugin mongoPlugin = mongoTemplate + .findOne(query(where("packageName").is("mongo-plugin")), Plugin.class); + + List mongoDatasources = mongoTemplate + .find(query(where("pluginId").is(mongoPlugin.getId())), Datasource.class); + + mongoDatasources.stream() + .forEach(datasource -> { + datasource.getDatasourceConfiguration().setProperties(List.of(new Property("Use Mongo Connection " + + "String URI", "No"))); + mongoTemplate.save(datasource); + }); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentService.java index 757a7f9794..452b7ac4e8 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentService.java @@ -18,4 +18,6 @@ public interface CommentService extends CrudService { Mono deleteComment(String id); + Mono deleteThread(String threadId); + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java index cea8dc2952..c3a3b2d82f 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java @@ -258,10 +258,17 @@ public class CommentServiceImpl extends BaseService deleteComment(String id) { - return repository.findById(id, AclPermission.MANAGE_COMMENT) .switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.COMMENT, id))) - .flatMap(comment -> repository.archive(comment)); + .flatMap(repository::archive) + .flatMap(analyticsService::sendDeleteEvent); + } + + @Override + public Mono deleteThread(String threadId) { + return threadRepository.findById(threadId, AclPermission.MANAGE_THREAD) + .flatMap(threadRepository::archive) + .flatMap(analyticsService::sendDeleteEvent); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java index f87b372dc2..31c51aeb28 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/UserServiceImpl.java @@ -398,19 +398,6 @@ public class UserServiceImpl extends BaseService i return Mono.just(user) .flatMap(this::validateObject) .flatMap(repository::save) - .zipWith(configService.getTemplateOrganizationId().defaultIfEmpty("")) - .flatMap(tuple -> { - final String templateOrganizationId = tuple.getT2(); - - if (!StringUtils.hasText(templateOrganizationId)) { - // Since template organization is not configured, we create an empty default organization. - final User savedUser = tuple.getT1(); - log.debug("Creating blank default organization for user '{}'.", savedUser.getEmail()); - return organizationService.createDefault(new Organization(), savedUser); - } - - return Mono.empty(); - }) .then(repository.findByEmail(user.getUsername())) .flatMap(analyticsService::trackNewUser); } @@ -456,7 +443,23 @@ public class UserServiceImpl extends BaseService i } return Mono.error(new AppsmithException(AppsmithError.USER_ALREADY_EXISTS_SIGNUP, user.getUsername())); }) - .switchIfEmpty(Mono.defer(() -> signupIfAllowed(user))) + .switchIfEmpty(Mono.defer(() -> { + return signupIfAllowed(user) + .zipWith(configService.getTemplateOrganizationId().defaultIfEmpty("")) + .flatMap(tuple -> { + final User savedUser = tuple.getT1(); + final String templateOrganizationId = tuple.getT2(); + + if (!StringUtils.hasText(templateOrganizationId)) { + // Since template organization is not configured, we create an empty default organization. + log.debug("Creating blank default organization for user '{}'.", savedUser.getEmail()); + return organizationService.createDefault(new Organization(), savedUser).thenReturn(savedUser); + } + + return Mono.just(savedUser); + }) + .flatMap(savedUser -> findByEmail(savedUser.getEmail())); + })) .flatMap(savedUser -> emailConfig.isWelcomeEmailEnabled() ? sendWelcomeEmail(savedUser, finalOriginHeader) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java index 74c861374e..221541a3ff 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ExamplesOrganizationCloner.java @@ -80,8 +80,9 @@ public class ExamplesOrganizationCloner { * @return Empty Mono. */ private Mono cloneExamplesOrganization(User user) { - if (user.getExamplesOrganizationId() != null) { - // This user already has an examples organization, don't have to do anything. + if (!CollectionUtils.isEmpty(user.getOrganizationIds())) { + // Don't create an examples organization if the user already has some organizations, perhaps because they + // were invited to some. return Mono.empty(); } diff --git a/app/server/appsmith-server/src/main/resources/application.properties b/app/server/appsmith-server/src/main/resources/application.properties index 1b86904aa7..867f87d1aa 100644 --- a/app/server/appsmith-server/src/main/resources/application.properties +++ b/app/server/appsmith-server/src/main/resources/application.properties @@ -66,7 +66,7 @@ admin.emails = ${APPSMITH_ADMIN_EMAILS:} emails.welcome.enabled = ${APPSMITH_EMAILS_WELCOME_ENABLED:true} # Appsmith Cloud Services -appsmith.cloud_services.base_url = ${APPSMITH_CLOUD_SERVICES_BASE_URL:https://cs.appsmith.com} +appsmith.cloud_services.base_url = ${APPSMITH_CLOUD_SERVICES_BASE_URL:} appsmith.cloud_services.username = ${APPSMITH_CLOUD_SERVICES_USERNAME:} appsmith.cloud_services.password = ${APPSMITH_CLOUD_SERVICES_PASSWORD:} github_repo = ${APPSMITH_GITHUB_REPO:} diff --git a/contributions/ClientSetup.md b/contributions/ClientSetup.md index 08b13f654a..13da4c58cf 100644 --- a/contributions/ClientSetup.md +++ b/contributions/ClientSetup.md @@ -10,6 +10,7 @@ On your development machine, please ensure that: 1. You have `mkcert` installed. Please visit: [https://github.com/FiloSottile/mkcert#installation](https://github.com/FiloSottile/mkcert#installation) for details. For `mkcert` to work with Firefox you may require the `nss` utility to be installed. Details are in the link above. 1. You have `envsubst` installed. use `brew install gettext` on MacOS. Linux machines usually have this installed. 1. You have cloned the repo in your local machine. +1. You have yarn installed as a global npm package i.e. `npm install -g yarn` ### Create local HTTPS certificates: @@ -50,7 +51,7 @@ On your development machine, please ensure that: ### Steps to build & run the code: -1. Run `yarn` +1. Run `yarn install` Note: diff --git a/office_hours.md b/office_hours.md index d020c0d34d..d4b42be753 100644 --- a/office_hours.md +++ b/office_hours.md @@ -28,6 +28,15 @@ You can find the archives of the calls below with a brief summary of each sessio ## Archives +Appsmith Live Demo #2, 6th May 2021: Building a support helpdesk using Gmail and Postgres + +Video Link + +#### Summary + +Nikhil shows the community how to build a ticket support dashboard to assign emails to various org members using the Gmail API and Postgres. Questions from members of our community was also discussed. + +------------------ 29th April 2021: List widget, Release roadmap and more