Merge branch 'release' into fix/dropdown-overlap-modal

This commit is contained in:
Tolulope Adetula 2021-05-10 14:43:38 +01:00
commit 23367fe3be
54 changed files with 896 additions and 135 deletions

View File

@ -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"
}

View File

@ -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);
});
});

View File

@ -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));
});
});

View File

@ -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']"
}

View File

@ -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) => {

View File

@ -14,7 +14,6 @@ export type TabProp = {
};
const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
user-select: none;
border-radius: 0px;
height: 100%;
.react-tabs {

View File

@ -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}
<div>
<Text type={TextType.P1}>{props.text}</Text>
{props.variant === Variant.danger ? (
{props.variant === Variant.danger && props.showDebugButton ? (
<StyledDebugButton source={"TOAST"} />
) : null}
</div>

View File

@ -185,7 +185,9 @@ class ChartComponent extends React.Component<ChartComponentProps> {
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<ChartComponentProps> {
const seriesChartData: Array<Record<
string,
unknown
>> = this.getSeriesChartData(item.data, categories);
>> = this.getSeriesChartData(get(item, "data", []), categories);
return {
seriesName: item.seriesName,
data: seriesChartData,

View File

@ -337,7 +337,7 @@ class DropDownComponent extends React.Component<DropDownComponentProps> {
};
renderTag = (option: DropdownOption) => {
return option.label;
return option?.label;
};
isOptionSelected = (selectedOption: DropdownOption) => {

View File

@ -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};

View File

@ -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;

View File

@ -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}
>
<Icon name="bug" size={IconSize.XXXL} />
<Icon name="bug" size={IconSize.XL} />
<div className="debugger-count">{errorCount}</div>
</Container>
);

View File

@ -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 = (
<SettingsIcon
color={
props.activity === Activities.HOVERING
!!props.errorCount
? Colors.WHITE
: props.activity === Activities.HOVERING
? Colors.BLACK_PEARL
: Colors.WHITE
}
@ -77,6 +103,13 @@ export function SettingsControl(props: SettingsControlProps) {
width={12}
/>
);
const errorIcon = (
<StyledErrorIcon
fillColor={Colors.WHITE}
name="warning"
size={IconSize.SMALL}
/>
);
return (
<StyledTooltip
@ -87,8 +120,14 @@ export function SettingsControl(props: SettingsControlProps) {
<SettingsWrapper
className="t--widget-propertypane-toggle"
onClick={props.toggleSettings}
style={getStyles(props.activity)}
style={getStyles(props.activity, props.errorCount)}
>
{!!props.errorCount && (
<>
{errorIcon}
<span className="t--widget-error-count">{props.errorCount}</span>
</>
)}
<WidgetName className="t--widget-name">{props.name}</WidgetName>
{settingsIcon}
</SettingsWrapper>

View File

@ -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) {
<ControlGroup>
<SettingsControl
activity={currentActivity}
errorCount={props.errorCount}
name={props.widgetName}
toggleSettings={togglePropertyEditor}
/>

View File

@ -173,7 +173,7 @@ class EmbeddedDatasourcePathComponent extends React.Component<Props> {
hint: () => {
const list = datasourceList
.filter((datasource) =>
datasource.datasourceConfiguration.url.includes(
(datasource.datasourceConfiguration?.url || "").includes(
parsed.datasourceUrl,
),
)

View File

@ -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",

View File

@ -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;
}

View File

@ -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,

View File

@ -36,7 +36,7 @@ const WidgetSidebarResponse: WidgetCardProps[] = [
},
{
type: "DROP_DOWN_WIDGET",
widgetCardName: "Dropdown",
widgetCardName: "Select",
key: generateReactKey(),
},
{

View File

@ -206,7 +206,7 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
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<Props> {
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<Props> {
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<Props> {
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<Props> {
}
};
renderBasic = () => {
return (
<>
<FormInputContainer>
<InputTextControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.username"
label="Username"
placeholderText="Username"
/>
</FormInputContainer>
<FormInputContainer>
<InputTextControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.password"
dataType="PASSWORD"
encrypted
label="Password"
placeholderText="Password"
/>
</FormInputContainer>
</>
);
};
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<Props> {
]}
/>
</FormInputContainer>
{formData.authentication?.isTokenHeader && (
{_.get(formData.authentication, "isTokenHeader") && (
<FormInputContainer>
<InputTextControl
{...COMMON_INPUT_PROPS}

View File

@ -84,6 +84,7 @@ const TabbedViewContainer = styled.div`
height: 50%;
// Minimum height of bottom tabs as it can be resized
min-height: 36px;
width: 100%;
.react-tabs__tab-panel {
overflow: hidden;
}

View File

@ -58,8 +58,7 @@ const DrawerWrapper = styled.div<{
isActionPath: any;
}>`
background-color: white;
width: ${(props) =>
!props.isVisible ? "0px" : props.isActionPath ? "100%" : "75%"};
width: ${(props) => (!props.isVisible ? "0" : "100%")};
height: 100%;
`;

View File

@ -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 {

View File

@ -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;
}

View File

@ -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 = (

View File

@ -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: {

View File

@ -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),
);
});
});

View File

@ -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<string, any>) => {
const result: Record<string, any> = {};
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;
};

View File

@ -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 && (
<WidgetNameComponent
errorCount={this.getErrorCount(this.props.invalidProps)}
parentId={this.props.parentId}
showControls={showControls}
type={this.props.type}

View File

@ -34,7 +34,7 @@ class DatePickerWidget extends BaseWidget<DatePickerWidget2Props, WidgetState> {
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<DatePickerWidget2Props, WidgetState> {
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"),

View File

@ -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

View File

@ -107,7 +107,7 @@
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
<version>2.7</version>
<scope>compile</scope>
</dependency>
<dependency>

View File

@ -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";

View File

@ -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

View File

@ -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;
}

View File

@ -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<Property> 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<Property> 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<Property> 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<Endpoint> endpoints = datasourceConfiguration.getEndpoints();
@ -483,52 +593,89 @@ public class MongoPlugin extends BasePlugin {
@Override
public Set<String> validateDatasource(DatasourceConfiguration datasourceConfiguration) {
Set<String> invalids = new HashSet<>();
List<Property> 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<Endpoint> 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<Endpoint> 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<DatasourceStructure.Table> tables = new ArrayList<>();
structure.setTables(tables);
final MongoDatabase database = mongoClient.getDatabase(getDatabaseName(datasourceConfiguration));
return Flux.from(database.listCollectionNames())

View File

@ -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://<username>:<password>@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",

View File

@ -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<Set<String>> 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<Set<String>> 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<Set<String>> 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<Property> 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<Set<String>> 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<Property> 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<Set<String>> 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<Property> 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<Set<String>> invalidsMono = Mono.just(pluginExecutor.validateDatasource(dsConfig));
StepVerifier.create(invalidsMono)
.assertNext(invalids -> {
assertTrue(invalids.isEmpty());
})
.verifyComplete();
}
@Test
public void testInvalidsEmptyOnCorrectNonSrvUriFormat() {
DatasourceConfiguration dsConfig = createDatasourceConfiguration();
List<Property> 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<Set<String>> invalidsMono = Mono.just(pluginExecutor.validateDatasource(dsConfig));
StepVerifier.create(invalidsMono)
.assertNext(invalids -> {
assertTrue(invalids.isEmpty());
})
.verifyComplete();
}

View File

@ -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();
}

View File

@ -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<BasicAuthentication> 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<ClientResponse> 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));
}
}

View File

@ -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<OAuth2AuthorizationCode> create(OAuth2 oAuth2) {
if (oAuth2 == null) {
return Mono.empty();

View File

@ -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<OAuth2ClientCredentials> create(OAuth2 oAuth2) {
if (oAuth2 == null) {
return Mono.empty();

View File

@ -65,6 +65,10 @@
"label": "None",
"value": "dbAuth"
},
{
"label": "Basic",
"value": "basic"
},
{
"label": "OAuth2 (Client credentials)",
"value": "oAuth2"

View File

@ -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());
}
}

View File

@ -66,7 +66,7 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<version>2.2.1.RELEASE</version>
<version>2.4.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@ -121,7 +121,7 @@
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
<version>2.7</version>
</dependency>
<dependency>
<groupId>commons-validator</groupId>
@ -131,7 +131,7 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.3.4.RELEASE</version>
<version>2.4.4</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>

View File

@ -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<CommentService, Comment, S
.map(threads -> new ResponseDTO<>(HttpStatus.OK.value(), threads, null));
}
@PatchMapping("/threads/{threadId}")
@PutMapping("/threads/{threadId}")
public Mono<ResponseDTO<CommentThread>> updateThread(
@Valid @RequestBody CommentThread resource,
@PathVariable String threadId
@ -78,4 +78,11 @@ public class CommentController extends BaseController<CommentService, Comment, S
.map(deletedResource -> new ResponseDTO<>(HttpStatus.OK.value(), deletedResource, null));
}
@DeleteMapping("/threads/{threadId}")
public Mono<ResponseDTO<CommentThread>> 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));
}
}

View File

@ -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<Datasource> 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);
});
}
}

View File

@ -18,4 +18,6 @@ public interface CommentService extends CrudService<Comment, String> {
Mono<Comment> deleteComment(String id);
Mono<CommentThread> deleteThread(String threadId);
}

View File

@ -258,10 +258,17 @@ public class CommentServiceImpl extends BaseService<CommentRepository, Comment,
*/
@Override
public Mono<Comment> 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<CommentThread> deleteThread(String threadId) {
return threadRepository.findById(threadId, AclPermission.MANAGE_THREAD)
.flatMap(threadRepository::archive)
.flatMap(analyticsService::sendDeleteEvent);
}
}

View File

@ -398,19 +398,6 @@ public class UserServiceImpl extends BaseService<UserRepository, User, String> 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<UserRepository, User, String> 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)

View File

@ -80,8 +80,9 @@ public class ExamplesOrganizationCloner {
* @return Empty Mono.
*/
private Mono<Organization> 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();
}

View File

@ -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:}

View File

@ -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:

View File

@ -28,6 +28,15 @@ You can find the archives of the calls below with a brief summary of each sessio
## Archives
<strong>Appsmith Live Demo #2, 6th May 2021: Building a support helpdesk using Gmail and Postgres</strong>
<a href = "https://youtu.be/X_x_nRZf418">Video Link</a>
#### 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.
------------------
<strong>29th April 2021: List widget, Release roadmap and more</strong>