Merge branch 'release' into fix/dropdown-overlap-modal
This commit is contained in:
commit
23367fe3be
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
|
@ -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']"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ export type TabProp = {
|
|||
};
|
||||
|
||||
const TabsWrapper = styled.div<{ shouldOverflow?: boolean }>`
|
||||
user-select: none;
|
||||
border-radius: 0px;
|
||||
height: 100%;
|
||||
.react-tabs {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -337,7 +337,7 @@ class DropDownComponent extends React.Component<DropDownComponentProps> {
|
|||
};
|
||||
|
||||
renderTag = (option: DropdownOption) => {
|
||||
return option.label;
|
||||
return option?.label;
|
||||
};
|
||||
|
||||
isOptionSelected = (selectedOption: DropdownOption) => {
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ const WidgetSidebarResponse: WidgetCardProps[] = [
|
|||
},
|
||||
{
|
||||
type: "DROP_DOWN_WIDGET",
|
||||
widgetCardName: "Dropdown",
|
||||
widgetCardName: "Select",
|
||||
key: generateReactKey(),
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
`;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
86
app/client/src/utils/helpers.test.ts
Normal file
86
app/client/src/utils/helpers.test.ts
Normal 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),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
26
app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/BasicAuth.java
vendored
Normal file
26
app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/BasicAuth.java
vendored
Normal 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;
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@
|
|||
"label": "None",
|
||||
"value": "dbAuth"
|
||||
},
|
||||
{
|
||||
"label": "Basic",
|
||||
"value": "basic"
|
||||
},
|
||||
{
|
||||
"label": "OAuth2 (Client credentials)",
|
||||
"value": "oAuth2"
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,4 +18,6 @@ public interface CommentService extends CrudService<Comment, String> {
|
|||
|
||||
Mono<Comment> deleteComment(String id);
|
||||
|
||||
Mono<CommentThread> deleteThread(String threadId);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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:}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user