Merge branch 'release' into fix/filepicker
This commit is contained in:
commit
d9bc97cf39
|
|
@ -1,10 +1,36 @@
|
|||
import { ThemeProvider, theme } from "../../src/constants/DefaultTheme";
|
||||
import { light, dark } from "constants/DefaultTheme";
|
||||
|
||||
export const contexts = [
|
||||
{
|
||||
icon: "box",
|
||||
title: "Themes",
|
||||
components: [ThemeProvider],
|
||||
params: [{ name: "default", props: { theme: theme } }],
|
||||
},
|
||||
];
|
||||
{
|
||||
icon: "box",
|
||||
title: "Themes",
|
||||
components: [ThemeProvider],
|
||||
params: [
|
||||
{
|
||||
name: "lightTheme",
|
||||
props: {
|
||||
theme: {
|
||||
...theme,
|
||||
colors: {
|
||||
...theme.colors,
|
||||
...light,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "darkTheme",
|
||||
props: {
|
||||
theme: {
|
||||
...theme,
|
||||
colors: {
|
||||
...theme.colors,
|
||||
...dark,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/// <reference types="Cypress" />
|
||||
|
||||
describe("Create app same name in differnt org", function() {
|
||||
describe("Create app same name in different org", function() {
|
||||
let orgid;
|
||||
let appid;
|
||||
|
||||
|
|
@ -26,6 +26,8 @@ describe("Create app same name in differnt org", function() {
|
|||
200,
|
||||
);
|
||||
cy.reload();
|
||||
cy.CreateApp(appid);
|
||||
const newOrgName = orgid + "1";
|
||||
cy.createOrg(newOrgName);
|
||||
cy.CreateAppForOrg(newOrgName, appid);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ describe("Page Load tests", () => {
|
|||
cy.get(".t--page-switch-tab")
|
||||
.contains("Page2")
|
||||
.parent()
|
||||
.parent()
|
||||
.parent()
|
||||
.should("have.class", "is-active");
|
||||
// Assert active page DSL
|
||||
cy.get(commonlocators.headingTextStyle).should(
|
||||
|
|
@ -33,6 +35,8 @@ describe("Page Load tests", () => {
|
|||
cy.get(".t--page-switch-tab")
|
||||
.contains("Page2")
|
||||
.parent()
|
||||
.parent()
|
||||
.parent()
|
||||
.should("have.class", "is-active");
|
||||
// Assert active page DSL
|
||||
cy.get(commonlocators.headingTextStyle).should(
|
||||
|
|
@ -47,6 +51,8 @@ describe("Page Load tests", () => {
|
|||
cy.get(".t--page-switch-tab")
|
||||
.contains("Page1")
|
||||
.parent()
|
||||
.parent()
|
||||
.parent()
|
||||
.should("have.class", "is-active");
|
||||
// Assert active page DSL
|
||||
cy.get(commonlocators.headingTextStyle).should(
|
||||
|
|
|
|||
|
|
@ -61,4 +61,4 @@ describe("adding role without Email Id", function() {
|
|||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -31,4 +31,4 @@ describe("Onboarding flow", function()
|
|||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -74,4 +74,4 @@ describe("Checking for error message on Organisation Name ", function() {
|
|||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ Cypress.Commands.add("CreateAppForOrg", (orgName, appname) => {
|
|||
);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("CreateApp", (appname) => {
|
||||
Cypress.Commands.add("CreateAppInFirstListedOrg", (appname) => {
|
||||
cy.get(homePage.createNew)
|
||||
.first()
|
||||
.click({ force: true });
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ before(function() {
|
|||
|
||||
cy.generateUUID().then((id) => {
|
||||
appId = id;
|
||||
cy.CreateApp(id);
|
||||
cy.CreateAppInFirstListedOrg(id);
|
||||
localStorage.setItem("AppName", appId);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -69,3 +69,9 @@ export const getAllApplications = () => {
|
|||
type: ReduxActionTypes.GET_ALL_APPLICATION_INIT,
|
||||
};
|
||||
};
|
||||
|
||||
export const resetCurrentApplication = () => {
|
||||
return {
|
||||
type: ReduxActionTypes.RESET_CURRENT_APPLICATION,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
3
app/client/src/assets/icons/ads/arrow-left.svg
Normal file
3
app/client/src/assets/icons/ads/arrow-left.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="11" height="8" viewBox="0 0 11 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.183313 4.56741L3.44963 7.818C3.69428 8.06147 4.09 8.06056 4.33353 7.81588C4.57703 7.57122 4.57609 7.1755 4.33144 6.932L2.13881 4.75001L10.375 4.75001C10.7202 4.75001 11 4.47019 11 4.12501C11 3.77982 10.7202 3.50001 10.375 3.50001L2.13884 3.50001L4.33141 1.31801C4.57606 1.07451 4.577 0.678793 4.3335 0.434137C4.08997 0.189418 3.69422 0.188575 3.44959 0.432012L0.183844 3.68201C-0.0609375 3.92632 -0.0607182 4.32391 0.183313 4.56741Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 562 B |
3
app/client/src/assets/icons/ads/chevron_left.svg
Normal file
3
app/client/src/assets/icons/ads/chevron_left.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="8" height="14" viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 13L1 7L7 1" stroke="#090707" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 208 B |
3
app/client/src/assets/icons/ads/chevron_right.svg
Normal file
3
app/client/src/assets/icons/ads/chevron_right.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="8" height="14" viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 13L7 7L1 1" stroke="#090707" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 208 B |
1
app/client/src/assets/icons/ads/fork.svg
Normal file
1
app/client/src/assets/icons/ads/fork.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg data-icon="fork" width="16" height="16" viewBox="0 0 16 16"><desc>fork</desc><path d="M13.7 9.29a1.003 1.003 0 00-1.42 1.42l.29.29H11.4l-5-5h6.17l-.29.29a1.003 1.003 0 001.42 1.42l2-2c.18-.18.29-.43.29-.71s-.11-.53-.29-.71l-2-2a1.003 1.003 0 00-1.42 1.42l.29.29H.99c-.55 0-1 .45-1 1s.45 1 1 1h2.59l6.71 6.71c.18.18.43.29.71.29h1.59l-.29.29a1.003 1.003 0 001.42 1.42l2-2c.18-.18.29-.43.29-.71s-.11-.53-.29-.71l-2.02-2z" fill-rule="evenodd"></path></svg>
|
||||
|
After Width: | Height: | Size: 457 B |
BIN
app/client/src/assets/images/appsmith_logo_square.png
Normal file
BIN
app/client/src/assets/images/appsmith_logo_square.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
|
|
@ -193,6 +193,11 @@ const appSizeHandler = (size: Size): cssAttributes => {
|
|||
height = 50;
|
||||
padding = 50;
|
||||
break;
|
||||
default:
|
||||
width = 20;
|
||||
height = 20;
|
||||
padding = 5;
|
||||
break;
|
||||
}
|
||||
return { width, height, padding };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ export enum Category {
|
|||
}
|
||||
|
||||
export enum Size {
|
||||
xxs = "xxs",
|
||||
xs = "xs",
|
||||
small = "small",
|
||||
medium = "medium",
|
||||
large = "large",
|
||||
|
|
@ -59,128 +61,148 @@ type ButtonProps = CommonComponentProps & {
|
|||
href?: string;
|
||||
tag?: "a" | "button";
|
||||
type?: "submit" | "reset" | "button";
|
||||
target?: string;
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
category: Category.primary,
|
||||
variant: Variant.info,
|
||||
size: Size.small,
|
||||
isLoading: false,
|
||||
disabled: false,
|
||||
fill: false,
|
||||
tag: "a",
|
||||
};
|
||||
|
||||
const getDisabledStyles = (props: ThemeProp & ButtonProps) => {
|
||||
const variant = props.variant || defaultProps.variant;
|
||||
const category = props.category || defaultProps.category;
|
||||
|
||||
const stylesByCategory = {
|
||||
[Category.primary]: {
|
||||
txtColorPrimary: props.theme.colors.button.disabledText,
|
||||
bgColorPrimary: props.theme.colors[variant].darkest,
|
||||
borderColorPrimary: props.theme.colors[variant].darkest,
|
||||
},
|
||||
[Category.secondary]: {
|
||||
txtColorSecondary: props.theme.colors.button.disabledText,
|
||||
bgColorSecondary: props.theme.colors[variant].darkest,
|
||||
borderColorSecondary: props.theme.colors[variant].darker,
|
||||
},
|
||||
[Category.tertiary]: {
|
||||
txtColorTertiary: props.theme.colors.button.disabledText,
|
||||
bgColorTertiary: props.theme.colors.tertiary.darker,
|
||||
borderColorTertiary: props.theme.colors.tertiary.dark,
|
||||
},
|
||||
};
|
||||
|
||||
return stylesByCategory[category];
|
||||
};
|
||||
|
||||
const getMainStateStyles = (props: ThemeProp & ButtonProps) => {
|
||||
const variant = props.variant || defaultProps.variant;
|
||||
const category = props.category || defaultProps.category;
|
||||
|
||||
const stylesByCategory = {
|
||||
[Category.primary]: {
|
||||
bgColorPrimary: props.theme.colors[variant].main,
|
||||
borderColorPrimary: props.theme.colors[variant].main,
|
||||
txtColorPrimary: "#fff",
|
||||
},
|
||||
[Category.secondary]: {
|
||||
borderColorSecondary: props.theme.colors[variant].main,
|
||||
txtColorSecondary: props.theme.colors[variant].main,
|
||||
bgColorSecondary: "transparent",
|
||||
},
|
||||
[Category.tertiary]: {
|
||||
bgColorTertiary: "transparent",
|
||||
borderColorTertiary: props.theme.colors.tertiary.main,
|
||||
txtColorTertiary: props.theme.colors.tertiary.main,
|
||||
},
|
||||
};
|
||||
|
||||
return stylesByCategory[category];
|
||||
};
|
||||
|
||||
const getHoverStateStyles = (props: ThemeProp & ButtonProps) => {
|
||||
const variant = props.variant || defaultProps.variant;
|
||||
const category = props.category || defaultProps.category;
|
||||
|
||||
const stylesByCategory = {
|
||||
[Category.primary]: {
|
||||
bgColorPrimary: props.theme.colors[variant].dark,
|
||||
borderColorPrimary: props.theme.colors[variant].dark,
|
||||
txtColorPrimary: "#fff",
|
||||
},
|
||||
[Category.secondary]: {
|
||||
bgColorSecondary: hexToRgba(props.theme.colors[variant].main, 0.1),
|
||||
txtColorSecondary: props.theme.colors[variant].main,
|
||||
borderColorSecondary: props.theme.colors[variant].main,
|
||||
},
|
||||
[Category.tertiary]: {
|
||||
bgColorTertiary: hexToRgba(props.theme.colors.tertiary.main, 0.1),
|
||||
borderColorTertiary: props.theme.colors.tertiary.main,
|
||||
txtColorTertiary: props.theme.colors.tertiary.main,
|
||||
},
|
||||
};
|
||||
|
||||
return stylesByCategory[category];
|
||||
};
|
||||
|
||||
const getActiveStateStyles = (props: ThemeProp & ButtonProps) => {
|
||||
const variant = props.variant || defaultProps.variant;
|
||||
const category = props.category || defaultProps.category;
|
||||
|
||||
const stylesByCategory = {
|
||||
[Category.primary]: {
|
||||
bgColorPrimary: props.theme.colors[variant].dark,
|
||||
borderColorPrimary: props.theme.colors[variant].main,
|
||||
txtColorPrimary: "#fff",
|
||||
},
|
||||
[Category.secondary]: {
|
||||
bgColorSecondary: hexToRgba(props.theme.colors[variant].main, 0.1),
|
||||
txtColorSecondary: props.theme.colors[variant].light,
|
||||
borderColorSecondary: props.theme.colors[variant].light,
|
||||
},
|
||||
[Category.tertiary]: {
|
||||
bgColorTertiary: hexToRgba(props.theme.colors.tertiary.main, 0.1),
|
||||
borderColorTertiary: props.theme.colors.tertiary.light,
|
||||
txtColorTertiary: props.theme.colors.tertiary.light,
|
||||
},
|
||||
};
|
||||
|
||||
return stylesByCategory[category];
|
||||
};
|
||||
|
||||
const stateStyles = (
|
||||
props: ThemeProp & ButtonProps,
|
||||
state: string,
|
||||
stateArg: string,
|
||||
): stateStyleType => {
|
||||
let bgColorPrimary,
|
||||
borderColorPrimary,
|
||||
txtColorPrimary,
|
||||
bgColorSecondary,
|
||||
borderColorSecondary,
|
||||
txtColorSecondary,
|
||||
bgColorTertiary,
|
||||
borderColorTertiary,
|
||||
txtColorTertiary;
|
||||
|
||||
if (props.isLoading || props.disabled) {
|
||||
switch (props.category) {
|
||||
case Category.primary:
|
||||
if (props.variant) {
|
||||
bgColorPrimary = props.theme.colors[props.variant].darkest;
|
||||
borderColorPrimary = props.theme.colors[props.variant].darkest;
|
||||
}
|
||||
txtColorPrimary = props.theme.colors.button.disabledText;
|
||||
break;
|
||||
case Category.secondary:
|
||||
if (props.variant) {
|
||||
bgColorSecondary = props.theme.colors[props.variant].darkest;
|
||||
borderColorSecondary = props.theme.colors[props.variant].darker;
|
||||
}
|
||||
txtColorSecondary = props.theme.colors.button.disabledText;
|
||||
break;
|
||||
case Category.tertiary:
|
||||
bgColorTertiary = props.theme.colors.tertiary.darker;
|
||||
borderColorTertiary = props.theme.colors.tertiary.dark;
|
||||
txtColorTertiary = props.theme.colors.button.disabledText;
|
||||
break;
|
||||
}
|
||||
} else if (state === "main") {
|
||||
switch (props.category) {
|
||||
case Category.primary:
|
||||
if (props.variant) {
|
||||
bgColorPrimary = props.theme.colors[props.variant].main;
|
||||
borderColorPrimary = props.theme.colors[props.variant].main;
|
||||
}
|
||||
txtColorPrimary = "#fff";
|
||||
break;
|
||||
case Category.secondary:
|
||||
if (props.variant) {
|
||||
borderColorSecondary = props.theme.colors[props.variant].main;
|
||||
txtColorSecondary = props.theme.colors[props.variant].main;
|
||||
}
|
||||
bgColorSecondary = "transparent";
|
||||
break;
|
||||
case Category.tertiary:
|
||||
bgColorTertiary = "transparent";
|
||||
borderColorTertiary = props.theme.colors.tertiary.main;
|
||||
txtColorTertiary = props.theme.colors.tertiary.main;
|
||||
break;
|
||||
}
|
||||
} else if (state === "hover") {
|
||||
switch (props.category) {
|
||||
case Category.primary:
|
||||
if (props.variant) {
|
||||
bgColorPrimary = props.theme.colors[props.variant].dark;
|
||||
borderColorPrimary = props.theme.colors[props.variant].dark;
|
||||
}
|
||||
txtColorPrimary = "#fff";
|
||||
break;
|
||||
case Category.secondary:
|
||||
if (props.variant) {
|
||||
bgColorSecondary = hexToRgba(
|
||||
props.theme.colors[props.variant].main,
|
||||
0.1,
|
||||
);
|
||||
txtColorSecondary = props.theme.colors[props.variant].main;
|
||||
borderColorSecondary = props.theme.colors[props.variant].main;
|
||||
}
|
||||
break;
|
||||
case Category.tertiary:
|
||||
bgColorTertiary = hexToRgba(props.theme.colors.tertiary.main, 0.1);
|
||||
borderColorTertiary = props.theme.colors.tertiary.main;
|
||||
txtColorTertiary = props.theme.colors.tertiary.main;
|
||||
break;
|
||||
}
|
||||
} else if (state === "active") {
|
||||
switch (props.category) {
|
||||
case Category.primary:
|
||||
if (props.variant) {
|
||||
bgColorPrimary = props.theme.colors[props.variant].dark;
|
||||
borderColorPrimary = props.theme.colors[props.variant].main;
|
||||
}
|
||||
txtColorPrimary = "#fff";
|
||||
break;
|
||||
case Category.secondary:
|
||||
if (props.variant) {
|
||||
bgColorSecondary = hexToRgba(
|
||||
props.theme.colors[props.variant].main,
|
||||
0.1,
|
||||
);
|
||||
txtColorSecondary = props.theme.colors[props.variant].light;
|
||||
borderColorSecondary = props.theme.colors[props.variant].light;
|
||||
}
|
||||
break;
|
||||
case Category.tertiary:
|
||||
bgColorTertiary = hexToRgba(props.theme.colors.tertiary.main, 0.1);
|
||||
borderColorTertiary = props.theme.colors.tertiary.light;
|
||||
txtColorTertiary = props.theme.colors.tertiary.light;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const styles = {
|
||||
bgColorPrimary: "",
|
||||
borderColorPrimary: "",
|
||||
txtColorPrimary: "",
|
||||
bgColorSecondary: "",
|
||||
borderColorSecondary: "",
|
||||
txtColorSecondary: "",
|
||||
bgColorTertiary: "",
|
||||
borderColorTertiary: "",
|
||||
txtColorTertiary: "",
|
||||
};
|
||||
const state =
|
||||
props.isLoading || props.disabled
|
||||
? "disabled"
|
||||
: (stateArg as keyof typeof stylesByState);
|
||||
const stylesByState = {
|
||||
disabled: getDisabledStyles(props),
|
||||
main: getMainStateStyles(props),
|
||||
hover: getHoverStateStyles(props),
|
||||
active: getActiveStateStyles(props),
|
||||
};
|
||||
|
||||
return {
|
||||
bgColorPrimary,
|
||||
borderColorPrimary,
|
||||
txtColorPrimary,
|
||||
bgColorSecondary,
|
||||
borderColorSecondary,
|
||||
txtColorSecondary,
|
||||
bgColorTertiary,
|
||||
borderColorTertiary,
|
||||
txtColorTertiary,
|
||||
...styles,
|
||||
...stylesByState[state],
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -211,36 +233,61 @@ const btnColorStyles = (
|
|||
return { bgColor, txtColor, border };
|
||||
};
|
||||
|
||||
const getPaddingBySize = (props: ThemeProp & ButtonProps) => {
|
||||
const paddingBySize = {
|
||||
[Size.small]: `0px ${props.theme.spaces[3]}px`,
|
||||
[Size.medium]: `0px ${props.theme.spaces[7]}px`,
|
||||
[Size.large]: `0px ${props.theme.spaces[12] - 4}px`,
|
||||
};
|
||||
const paddingBySizeForJustIcon = {
|
||||
[Size.small]: `0px ${props.theme.spaces[1]}px`,
|
||||
[Size.medium]: `0px ${props.theme.spaces[2]}px`,
|
||||
[Size.large]: `0px ${props.theme.spaces[3]}px`,
|
||||
};
|
||||
|
||||
const isIconOnly = !props.text && props.icon;
|
||||
const paddingConfig = isIconOnly ? paddingBySizeForJustIcon : paddingBySize;
|
||||
|
||||
const iSizeInConfig =
|
||||
Object.keys(paddingConfig).indexOf(props.size || "") !== -1;
|
||||
const size: any = props.size && iSizeInConfig ? props.size : Size.small;
|
||||
|
||||
return paddingConfig[size as keyof typeof paddingConfig];
|
||||
};
|
||||
|
||||
const getHeightBySize = (props: ThemeProp & ButtonProps) => {
|
||||
const heightBySize = {
|
||||
[Size.small]: 20,
|
||||
[Size.medium]: 30,
|
||||
[Size.large]: 38,
|
||||
};
|
||||
|
||||
const iSizeInConfig =
|
||||
Object.keys(heightBySize).indexOf(props.size || "") !== -1;
|
||||
const size: any = props.size && iSizeInConfig ? props.size : Size.small;
|
||||
|
||||
return heightBySize[size as keyof typeof heightBySize];
|
||||
};
|
||||
|
||||
const getBtnFontBySize = (props: ThemeProp & ButtonProps) => {
|
||||
const fontBySize = {
|
||||
[Size.small]: smallButton,
|
||||
[Size.medium]: mediumButton,
|
||||
[Size.large]: largeButton,
|
||||
};
|
||||
|
||||
const iSizeInConfig =
|
||||
Object.keys(fontBySize).indexOf(props.size || "") !== -1;
|
||||
const size: any = props.size && iSizeInConfig ? props.size : Size.small;
|
||||
|
||||
return fontBySize[size as keyof typeof fontBySize];
|
||||
};
|
||||
|
||||
const btnFontStyles = (props: ThemeProp & ButtonProps): BtnFontType => {
|
||||
let buttonFont,
|
||||
padding = "",
|
||||
height = 0;
|
||||
switch (props.size) {
|
||||
case Size.small:
|
||||
buttonFont = smallButton;
|
||||
height = 20;
|
||||
padding =
|
||||
!props.text && props.icon
|
||||
? `0px ${props.theme.spaces[1]}px`
|
||||
: `0px ${props.theme.spaces[3]}px`;
|
||||
break;
|
||||
case Size.medium:
|
||||
buttonFont = mediumButton;
|
||||
height = 30;
|
||||
padding =
|
||||
!props.text && props.icon
|
||||
? `0px ${props.theme.spaces[2]}px`
|
||||
: `0px ${props.theme.spaces[7]}px`;
|
||||
break;
|
||||
case Size.large:
|
||||
buttonFont = largeButton;
|
||||
height = 38;
|
||||
padding =
|
||||
!props.text && props.icon
|
||||
? `0px ${props.theme.spaces[3]}px`
|
||||
: `0px ${props.theme.spaces[12] - 4}px`;
|
||||
break;
|
||||
}
|
||||
const padding = getPaddingBySize(props);
|
||||
const height = getHeightBySize(props);
|
||||
const buttonFont = getBtnFontBySize(props);
|
||||
|
||||
return { buttonFont, padding, height };
|
||||
};
|
||||
|
||||
|
|
@ -318,78 +365,85 @@ export const VisibilityWrapper = styled.div`
|
|||
`;
|
||||
|
||||
const IconSizeProp = (size?: Size) => {
|
||||
if (size === Size.small) {
|
||||
return IconSize.SMALL;
|
||||
} else if (size === Size.medium) {
|
||||
return IconSize.MEDIUM;
|
||||
} else if (size === Size.large) {
|
||||
return IconSize.LARGE;
|
||||
} else {
|
||||
return IconSize.SMALL;
|
||||
}
|
||||
const sizeMapping = {
|
||||
[Size.xxs]: IconSize.XXS,
|
||||
[Size.xs]: IconSize.XS,
|
||||
[Size.small]: IconSize.SMALL,
|
||||
[Size.medium]: IconSize.MEDIUM,
|
||||
[Size.large]: IconSize.LARGE,
|
||||
};
|
||||
|
||||
return size ? sizeMapping[size] : IconSize.SMALL;
|
||||
};
|
||||
|
||||
Button.defaultProps = {
|
||||
category: Category.primary,
|
||||
variant: Variant.info,
|
||||
size: Size.small,
|
||||
isLoading: false,
|
||||
disabled: false,
|
||||
fill: false,
|
||||
tag: "a",
|
||||
};
|
||||
const TextLoadingState = ({ text }: { text?: string }) => (
|
||||
<VisibilityWrapper>{text}</VisibilityWrapper>
|
||||
);
|
||||
|
||||
function Button(props: ButtonProps) {
|
||||
const IconLoadingState = (
|
||||
<Icon name={props.icon} size={IconSizeProp(props.size)} invisible={true} />
|
||||
const IconLoadingState = ({ size, icon }: { size?: Size; icon?: IconName }) => (
|
||||
<Icon name={icon} size={IconSizeProp(size)} invisible={true} />
|
||||
);
|
||||
|
||||
const getIconContent = (props: ButtonProps) =>
|
||||
props.icon ? (
|
||||
props.isLoading ? (
|
||||
<IconLoadingState {...props} />
|
||||
) : (
|
||||
<Icon name={props.icon} size={IconSizeProp(props.size)} />
|
||||
)
|
||||
) : null;
|
||||
|
||||
const getTextContent = (props: ButtonProps) =>
|
||||
props.text ? (
|
||||
props.isLoading ? (
|
||||
<TextLoadingState text={props.text} />
|
||||
) : (
|
||||
props.text
|
||||
)
|
||||
) : null;
|
||||
|
||||
const getButtonContent = (props: ButtonProps) => (
|
||||
<>
|
||||
{getIconContent(props)}
|
||||
{getTextContent(props)}
|
||||
{props.isLoading ? <Spinner size={IconSizeProp(props.size)} /> : null}
|
||||
</>
|
||||
);
|
||||
|
||||
const ButtonComponent = (props: ButtonProps) => (
|
||||
<StyledButton
|
||||
className={props.className}
|
||||
data-cy={props.cypressSelector}
|
||||
{...props}
|
||||
onClick={(e: React.MouseEvent<HTMLElement>) =>
|
||||
props.onClick && props.onClick(e)
|
||||
}
|
||||
>
|
||||
{getButtonContent(props)}
|
||||
</StyledButton>
|
||||
);
|
||||
|
||||
const LinkButtonComponent = (props: ButtonProps) => (
|
||||
<StyledLinkButton
|
||||
href={props.href}
|
||||
className={props.className}
|
||||
data-cy={props.cypressSelector}
|
||||
{...props}
|
||||
onClick={(e: React.MouseEvent<HTMLElement>) =>
|
||||
props.onClick && props.onClick(e)
|
||||
}
|
||||
>
|
||||
{getButtonContent(props)}
|
||||
</StyledLinkButton>
|
||||
);
|
||||
|
||||
const Button = (props: ButtonProps) =>
|
||||
props.tag === "button" ? (
|
||||
<ButtonComponent {...props} />
|
||||
) : (
|
||||
<LinkButtonComponent {...props} />
|
||||
);
|
||||
|
||||
const TextLoadingState = <VisibilityWrapper>{props.text}</VisibilityWrapper>;
|
||||
|
||||
const buttonContent = (
|
||||
<>
|
||||
{props.icon ? (
|
||||
props.isLoading ? (
|
||||
IconLoadingState
|
||||
) : (
|
||||
<Icon name={props.icon} size={IconSizeProp(props.size)} />
|
||||
)
|
||||
) : null}
|
||||
|
||||
{props.text ? (props.isLoading ? TextLoadingState : props.text) : null}
|
||||
|
||||
{props.isLoading ? <Spinner size={IconSizeProp(props.size)} /> : null}
|
||||
</>
|
||||
);
|
||||
|
||||
if (props.tag === "button") {
|
||||
return (
|
||||
<StyledButton
|
||||
className={props.className}
|
||||
data-cy={props.cypressSelector}
|
||||
{...props}
|
||||
onClick={(e: React.MouseEvent<HTMLElement>) =>
|
||||
props.onClick && props.onClick(e)
|
||||
}
|
||||
>
|
||||
{buttonContent}
|
||||
</StyledButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<StyledLinkButton
|
||||
href={props.href}
|
||||
className={props.className}
|
||||
data-cy={props.cypressSelector}
|
||||
{...props}
|
||||
onClick={(e: React.MouseEvent<HTMLElement>) =>
|
||||
props.onClick && props.onClick(e)
|
||||
}
|
||||
>
|
||||
{buttonContent}
|
||||
</StyledLinkButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Button;
|
||||
|
||||
Button.defaultProps = defaultProps;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ import { ReactComponent as ContextMenuIcon } from "assets/icons/ads/context-menu
|
|||
import { ReactComponent as DuplicateIcon } from "assets/icons/ads/duplicate.svg";
|
||||
import { ReactComponent as LogoutIcon } from "assets/icons/ads/logout.svg";
|
||||
import { ReactComponent as ManageIcon } from "assets/icons/ads/manage.svg";
|
||||
import { ReactComponent as ArrowLeft } from "assets/icons/ads/arrow-left.svg";
|
||||
import { ReactComponent as Fork } from "assets/icons/ads/fork.svg";
|
||||
import { ReactComponent as ChevronLeft } from "assets/icons/ads/chevron_left.svg";
|
||||
import { ReactComponent as ChevronRight } from "assets/icons/ads/chevron_right.svg";
|
||||
import styled from "styled-components";
|
||||
import { CommonComponentProps, Classes } from "./common";
|
||||
import { noop } from "lodash";
|
||||
|
|
@ -95,11 +99,15 @@ export const IconCollection = [
|
|||
"duplicate",
|
||||
"logout",
|
||||
"manage",
|
||||
"arrow-left",
|
||||
"fork",
|
||||
"chevron-left",
|
||||
"chevron-right",
|
||||
] as const;
|
||||
|
||||
export type IconName = typeof IconCollection[number];
|
||||
|
||||
const IconWrapper = styled.span<IconProps>`
|
||||
export const IconWrapper = styled.span<IconProps>`
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
|
@ -208,6 +216,18 @@ const Icon = forwardRef(
|
|||
case "warning":
|
||||
returnIcon = <WarningIcon />;
|
||||
break;
|
||||
case "arrow-left":
|
||||
returnIcon = <ArrowLeft />;
|
||||
break;
|
||||
case "fork":
|
||||
returnIcon = <Fork />;
|
||||
break;
|
||||
case "chevron-left":
|
||||
returnIcon = <ChevronLeft />;
|
||||
break;
|
||||
case "chevron-right":
|
||||
returnIcon = <ChevronRight />;
|
||||
break;
|
||||
default:
|
||||
returnIcon = null;
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { CommonComponentProps } from "./common";
|
|||
import styled from "styled-components";
|
||||
import { Popover } from "@blueprintjs/core/lib/esm/components/popover/popover";
|
||||
import { Position } from "@blueprintjs/core/lib/esm/common/position";
|
||||
import { PopperModifiers } from "@blueprintjs/core";
|
||||
|
||||
type MenuProps = CommonComponentProps & {
|
||||
children?: ReactNode[];
|
||||
|
|
@ -10,6 +11,7 @@ type MenuProps = CommonComponentProps & {
|
|||
position?: Position;
|
||||
onOpening?: (node: HTMLElement) => void;
|
||||
onClosing?: (node: HTMLElement) => void;
|
||||
modifiers?: PopperModifiers;
|
||||
};
|
||||
|
||||
const MenuWrapper = styled.div`
|
||||
|
|
@ -33,6 +35,7 @@ const Menu = (props: MenuProps) => {
|
|||
portalClassName={props.className}
|
||||
data-cy={props.cypressSelector}
|
||||
disabled={props.disabled}
|
||||
modifiers={props.modifiers}
|
||||
>
|
||||
{props.target}
|
||||
<MenuWrapper>
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ const Text = styled.span.attrs((props: TextProps) => ({
|
|||
letter-spacing: ${(props) =>
|
||||
props.theme.typography[props.type].letterSpacing}px;
|
||||
color: ${(props) =>
|
||||
props.highlight ? props.theme.colors.text.hightlight : typeSelector(props)};
|
||||
props.highlight ? props.theme.colors.text.highlight : typeSelector(props)};
|
||||
text-transform: ${(props) => (props.case ? props.case : "none")};
|
||||
`;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
import { CommonComponentProps } from "./common";
|
||||
import styled from "styled-components";
|
||||
import { Position, Tooltip, Classes } from "@blueprintjs/core";
|
||||
import { Position, Tooltip, Classes, PopperBoundary } from "@blueprintjs/core";
|
||||
import { Classes as CsClasses } from "./common";
|
||||
|
||||
type Variant = "dark" | "light";
|
||||
|
|
@ -12,6 +12,8 @@ type TooltipProps = CommonComponentProps & {
|
|||
children: JSX.Element;
|
||||
variant?: Variant;
|
||||
maxWidth?: number;
|
||||
usePortal?: boolean;
|
||||
boundary?: PopperBoundary;
|
||||
};
|
||||
|
||||
const TooltipWrapper = styled.div<{ variant?: Variant; maxWidth?: number }>`
|
||||
|
|
@ -24,6 +26,16 @@ const TooltipWrapper = styled.div<{ variant?: Variant; maxWidth?: number }>`
|
|||
: props.theme.colors.tooltip.lightBg};
|
||||
}
|
||||
div.${Classes.POPOVER_ARROW} {
|
||||
path {
|
||||
fill: ${(props) =>
|
||||
props.variant === "dark"
|
||||
? props.theme.colors.tooltip.darkBg
|
||||
: props.theme.colors.tooltip.lightBg};
|
||||
stroke: ${(props) =>
|
||||
props.variant === "dark"
|
||||
? props.theme.colors.tooltip.darkBg
|
||||
: props.theme.colors.tooltip.lightBg};
|
||||
}
|
||||
display: block;
|
||||
}
|
||||
.${Classes.TOOLTIP} {
|
||||
|
|
@ -52,7 +64,8 @@ const TooltipComponent = (props: TooltipProps) => {
|
|||
<Tooltip
|
||||
content={props.content}
|
||||
position={props.position}
|
||||
usePortal={false}
|
||||
usePortal={!!props.usePortal}
|
||||
boundary={props.boundary || "scrollParent"}
|
||||
>
|
||||
{props.children}
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -1,34 +1,14 @@
|
|||
import React, { ReactNode, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import { Icon, Popover, PopoverPosition, Tooltip } from "@blueprintjs/core";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { PopoverInteractionKind } from "@blueprintjs/core/lib/esm/components/popover/popover";
|
||||
|
||||
const IconContainer = styled.div`
|
||||
cursor: pointer;
|
||||
margin: 0 10px;
|
||||
border: 1px solid #bcccd9;
|
||||
border-radius: 50%;
|
||||
min-width: 32px;
|
||||
max-width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
svg {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
`;
|
||||
import styled, { withTheme } from "styled-components";
|
||||
import { Icon, Popover, PopoverPosition } from "@blueprintjs/core";
|
||||
import { Theme } from "constants/DefaultTheme";
|
||||
|
||||
const DeployLinkDialog = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
width: 336px;
|
||||
height: 62px;
|
||||
color: #2e3d49;
|
||||
background-color: ${(props) =>
|
||||
props.theme.colors.header.deployToolTipBackground};
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
|
|
@ -36,17 +16,15 @@ const DeployLink = styled.a`
|
|||
display: flex;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
padding-right: 10px;
|
||||
color: #2e3d49;
|
||||
color: ${(props) => props.theme.colors.header.deployToolTipText};
|
||||
:hover {
|
||||
text-decoration: none;
|
||||
color: #2e3d49;
|
||||
text-decoration: underline;
|
||||
color: ${(props) => props.theme.colors.header.deployToolTipText};
|
||||
}
|
||||
`;
|
||||
|
||||
const DeployUrl = styled.div`
|
||||
flex: 1;
|
||||
width: 222px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
|
@ -57,65 +35,40 @@ const DeployUrl = styled.div`
|
|||
type Props = {
|
||||
trigger: ReactNode;
|
||||
link: string;
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
export const DeployLinkButton = (props: Props) => {
|
||||
export const DeployLinkButton = withTheme((props: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const link = window.location.origin + props.link;
|
||||
|
||||
const onClose = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const copyToClipboard = () => {
|
||||
copy(link);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Popover
|
||||
modifiers={{
|
||||
offset: {
|
||||
enabled: true,
|
||||
offset: "0, 5",
|
||||
},
|
||||
}}
|
||||
modifiers={{ offset: { enabled: true, offset: "0, -3" } }}
|
||||
content={
|
||||
<DeployLinkDialog>
|
||||
<Tooltip
|
||||
content={isCopied ? "Copied!" : "Copy link to published app"}
|
||||
autoFocus={false}
|
||||
interactionKind={PopoverInteractionKind.HOVER_TARGET_ONLY}
|
||||
lazy
|
||||
position={PopoverPosition.BOTTOM}
|
||||
openOnTargetFocus={false}
|
||||
>
|
||||
<IconContainer onClick={copyToClipboard}>
|
||||
<Icon icon="link" color="#BCCCD9" />
|
||||
</IconContainer>
|
||||
</Tooltip>
|
||||
<DeployLink target="_blank" href={props.link}>
|
||||
<DeployUrl>
|
||||
<span>{link}</span>
|
||||
</DeployUrl>
|
||||
<Icon icon="share" color={"rgba(0,0,0,0.5)"} />
|
||||
<DeployUrl>Current deployed version</DeployUrl>
|
||||
<Icon
|
||||
icon="share"
|
||||
color={props.theme.colors.header.deployToolTipText}
|
||||
/>
|
||||
</DeployLink>
|
||||
</DeployLinkDialog>
|
||||
}
|
||||
canEscapeKeyClose={false}
|
||||
onClose={onClose}
|
||||
isOpen={isOpen}
|
||||
position={PopoverPosition.BOTTOM}
|
||||
position={PopoverPosition.BOTTOM_RIGHT}
|
||||
>
|
||||
<div onClick={() => setIsOpen(true)}>{props.trigger}</div>
|
||||
</Popover>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default DeployLinkButton;
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ interface ReduxStateProps {
|
|||
|
||||
type Props = ReduxStateProps & RouteComponentProps<APIEditorRouteParams>;
|
||||
|
||||
const EMPTY_RESPONSE: ActionResponse = {
|
||||
export const EMPTY_RESPONSE: ActionResponse = {
|
||||
statusCode: "",
|
||||
duration: "",
|
||||
body: {},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import { ErrorActionPayload } from "../sagas/ErrorSagas";
|
||||
import { ActionResponse } from "../api/ActionAPI";
|
||||
|
||||
export type ExecuteActionPayloadEvent = {
|
||||
type: EventType;
|
||||
callback?: (result: ExecutionResult) => void;
|
||||
|
|
@ -63,11 +66,10 @@ export interface PageAction {
|
|||
timeoutInMillisecond: number;
|
||||
}
|
||||
|
||||
export interface ExecuteErrorPayload {
|
||||
export interface ExecuteErrorPayload extends ErrorActionPayload {
|
||||
actionId: string;
|
||||
error: any;
|
||||
isPageLoad?: boolean;
|
||||
show?: boolean;
|
||||
data: ActionResponse;
|
||||
}
|
||||
|
||||
// Group 1 = datasource (https://www.domain.com)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,15 @@ export enum Skin {
|
|||
DARK,
|
||||
}
|
||||
|
||||
export const hideScrollbar = css`
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const scrollbarDark = css`
|
||||
scrollbar-color: ${(props) => props.theme.colors.paneCard}
|
||||
${(props) => props.theme.colors.paneBG};
|
||||
|
|
@ -285,6 +294,7 @@ export type Theme = {
|
|||
};
|
||||
propertyPane: PropertyPaneTheme;
|
||||
headerHeight: string;
|
||||
smallHeaderHeight: string;
|
||||
homePage: any;
|
||||
sidebarWidth: string;
|
||||
canvasPadding: string;
|
||||
|
|
@ -511,7 +521,7 @@ type ColorType = {
|
|||
text: {
|
||||
normal: ShadeColor;
|
||||
heading: ShadeColor;
|
||||
hightlight: ShadeColor;
|
||||
highlight: ShadeColor;
|
||||
};
|
||||
icon: {
|
||||
normal: ShadeColor;
|
||||
|
|
@ -742,6 +752,19 @@ type ColorType = {
|
|||
floatingBtn: any;
|
||||
auth: any;
|
||||
formMessage: Record<string, Record<Intent, string>>;
|
||||
header: {
|
||||
separator: string;
|
||||
appName: ShadeColor;
|
||||
background: string;
|
||||
deployToolTipBackground: string;
|
||||
deployToolTipText: ShadeColor;
|
||||
shareBtnHighlight: string;
|
||||
shareBtn: string;
|
||||
tabsHorizontalSeparator: string;
|
||||
tabText: string;
|
||||
activeTabBorderBottom: string;
|
||||
activeTabText: string;
|
||||
};
|
||||
};
|
||||
|
||||
const auth: any = {
|
||||
|
|
@ -772,6 +795,19 @@ const formMessage = {
|
|||
};
|
||||
|
||||
export const dark: ColorType = {
|
||||
header: {
|
||||
separator: darkShades[4],
|
||||
appName: darkShades[7],
|
||||
background: darkShades[2],
|
||||
deployToolTipBackground: lightShades[10],
|
||||
deployToolTipText: darkShades[7],
|
||||
shareBtnHighlight: "#F86A2B",
|
||||
shareBtn: "#fff",
|
||||
tabsHorizontalSeparator: "#EFEFEF",
|
||||
tabText: "#6F6D6D",
|
||||
activeTabBorderBottom: "#FF6D2D",
|
||||
activeTabText: "#000",
|
||||
},
|
||||
button: {
|
||||
disabledText: darkShades[6],
|
||||
},
|
||||
|
|
@ -820,7 +856,7 @@ export const dark: ColorType = {
|
|||
text: {
|
||||
normal: darkShades[6],
|
||||
heading: darkShades[7],
|
||||
hightlight: darkShades[9],
|
||||
highlight: darkShades[9],
|
||||
},
|
||||
icon: {
|
||||
normal: darkShades[6],
|
||||
|
|
@ -1058,6 +1094,19 @@ export const dark: ColorType = {
|
|||
};
|
||||
|
||||
export const light: ColorType = {
|
||||
header: {
|
||||
separator: "#E0DEDE",
|
||||
appName: lightShades[8],
|
||||
background: lightShades[0],
|
||||
deployToolTipText: lightShades[8],
|
||||
deployToolTipBackground: "#FFF",
|
||||
shareBtnHighlight: "#F86A2B",
|
||||
shareBtn: "#4B4848",
|
||||
tabsHorizontalSeparator: "#EFEFEF",
|
||||
tabText: "#6F6D6D",
|
||||
activeTabBorderBottom: "#FF6D2D",
|
||||
activeTabText: "#000",
|
||||
},
|
||||
button: {
|
||||
disabledText: lightShades[6],
|
||||
},
|
||||
|
|
@ -1106,7 +1155,7 @@ export const light: ColorType = {
|
|||
text: {
|
||||
normal: lightShades[8],
|
||||
heading: lightShades[9],
|
||||
hightlight: lightShades[11],
|
||||
highlight: lightShades[11],
|
||||
},
|
||||
icon: {
|
||||
normal: lightShades[4],
|
||||
|
|
@ -1560,6 +1609,7 @@ export const theme: Theme = {
|
|||
},
|
||||
},
|
||||
headerHeight: "48px",
|
||||
smallHeaderHeight: "35px",
|
||||
canvasPadding: "20px 0 200px 0",
|
||||
sideNav: {
|
||||
maxWidth: 220,
|
||||
|
|
@ -1670,7 +1720,6 @@ export const theme: Theme = {
|
|||
|
||||
export const scrollbarLight = css<{ backgroundColor?: Color }>`
|
||||
scrollbar-color: ${(props) => props.theme.colors.paneText}
|
||||
|
||||
scrollbar-width: thin;
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
|
|
|
|||
|
|
@ -320,6 +320,7 @@ export const ReduxActionTypes: { [key: string]: string } = {
|
|||
FETCH_RELEASES_SUCCESS: "FETCH_RELEASES_SUCCESS",
|
||||
RESET_UNREAD_RELEASES_COUNT: "RESET_UNREAD_RELEASES_COUNT",
|
||||
SET_LOADING_ENTITIES: "SET_LOADING_ENTITIES",
|
||||
RESET_CURRENT_APPLICATION: "RESET_CURRENT_APPLICATION",
|
||||
};
|
||||
|
||||
export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTypes];
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ export const AUTOFIT_COLUMN = "Autofit column";
|
|||
export const TIMEZONE = "Timezone";
|
||||
export const ENABLE_TIME = "Enable Time";
|
||||
|
||||
export const EDIT_APP = "Edit App";
|
||||
export const EDIT_APP = "Edit";
|
||||
export const FORK_APP = "Fork App";
|
||||
export const SIGN_IN = "Sign In";
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export const APPLICATIONS_URL = `/applications`;
|
|||
export const BUILDER_URL = "/applications/:applicationId/pages/:pageId/edit";
|
||||
export const USER_AUTH_URL = "/user";
|
||||
export const USERS_URL = "/users";
|
||||
export const VIEWER_URL_REGEX = /applications\/.*?\/pages\/.*/;
|
||||
|
||||
export type BuilderRouteParams = {
|
||||
applicationId: string;
|
||||
|
|
|
|||
|
|
@ -711,6 +711,14 @@ const PropertyPaneConfigResponse: PropertyPaneConfigsResponse["data"] = {
|
|||
controlType: "SWITCH",
|
||||
isJSConvertible: true,
|
||||
},
|
||||
{
|
||||
id: "4.2.0",
|
||||
helpText: "Clears the input value after submit",
|
||||
propertyName: "resetOnSubmit",
|
||||
label: "Reset on submit",
|
||||
controlType: "SWITCH",
|
||||
isJSConvertible: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ import {
|
|||
BuilderRouteParams,
|
||||
getApplicationViewerPageURL,
|
||||
} from "constants/routes";
|
||||
import { ReduxActionTypes } from "constants/ReduxActionConstants";
|
||||
import {
|
||||
PageListPayload,
|
||||
ReduxActionTypes,
|
||||
} from "constants/ReduxActionConstants";
|
||||
import { getIsInitialized } from "selectors/appViewSelectors";
|
||||
import { executeAction } from "actions/widgetActions";
|
||||
import { ExecuteActionPayload } from "constants/ActionConstants";
|
||||
|
|
@ -24,15 +27,19 @@ import {
|
|||
import { editorInitializer } from "utils/EditorUtils";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import log from "loglevel";
|
||||
import { getPageList } from "selectors/editorSelectors";
|
||||
|
||||
const SentryRoute = Sentry.withSentryRouting(Route);
|
||||
|
||||
const AppViewerBody = styled.section`
|
||||
const AppViewerBody = styled.section<{ hasPages: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
height: calc(100vh - ${(props) => props.theme.headerHeight});
|
||||
height: calc(
|
||||
100vh -
|
||||
${(props) => (!props.hasPages ? props.theme.smallHeaderHeight : "72px")}
|
||||
);
|
||||
`;
|
||||
|
||||
export type AppViewerProps = {
|
||||
|
|
@ -51,6 +58,7 @@ export type AppViewerProps = {
|
|||
propertyValue: any,
|
||||
) => void;
|
||||
resetChildrenMetaProperty: (widgetId: string) => void;
|
||||
pages: PageListPayload;
|
||||
} & RouteComponentProps<BuilderRouteParams>;
|
||||
|
||||
class AppViewer extends Component<
|
||||
|
|
@ -85,7 +93,7 @@ class AppViewer extends Component<
|
|||
resetChildrenMetaProperty: this.props.resetChildrenMetaProperty,
|
||||
}}
|
||||
>
|
||||
<AppViewerBody>
|
||||
<AppViewerBody hasPages={this.props.pages.length > 1}>
|
||||
{isInitialized && this.state.registered && (
|
||||
<Switch>
|
||||
<SentryRoute
|
||||
|
|
@ -103,6 +111,7 @@ class AppViewer extends Component<
|
|||
|
||||
const mapStateToProps = (state: AppState) => ({
|
||||
isInitialized: getIsInitialized(state),
|
||||
pages: getPageList(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: any) => ({
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import React, { useRef, useEffect, useState } from "react";
|
||||
import { Link, NavLink, useLocation } from "react-router-dom";
|
||||
import React from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { Helmet } from "react-helmet";
|
||||
import styled from "styled-components";
|
||||
import StyledHeader from "components/designSystems/appsmith/StyledHeader";
|
||||
import AppsmithLogo from "assets/images/appsmith_logo_white.png";
|
||||
import Button from "components/editorComponents/Button";
|
||||
import AppsmithLogo from "assets/images/appsmith_logo.png";
|
||||
import { EDIT_APP, FORK_APP, SIGN_IN } from "constants/messages";
|
||||
import {
|
||||
isPermitted,
|
||||
|
|
@ -17,7 +16,6 @@ import {
|
|||
import {
|
||||
APPLICATIONS_URL,
|
||||
AUTH_LOGIN_URL,
|
||||
getApplicationViewerPageURL,
|
||||
SIGN_UP_URL,
|
||||
} from "constants/routes";
|
||||
import { connect } from "react-redux";
|
||||
|
|
@ -27,27 +25,53 @@ import { getPageList } from "selectors/editorSelectors";
|
|||
import { FormDialogComponent } from "components/editorComponents/form/FormDialogComponent";
|
||||
import AppInviteUsersForm from "pages/organization/AppInviteUsersForm";
|
||||
import { getCurrentOrgId } from "selectors/organizationSelectors";
|
||||
import { HeaderIcons } from "icons/HeaderIcons";
|
||||
import { Colors } from "constants/Colors";
|
||||
|
||||
import { getCurrentUser } from "selectors/usersSelectors";
|
||||
import { ANONYMOUS_USERNAME, User } from "constants/userConstants";
|
||||
import { isEllipsisActive } from "utils/helpers";
|
||||
import TooltipComponent from "components/ads/Tooltip";
|
||||
import Text, { TextType } from "components/ads/Text";
|
||||
import { Classes } from "components/ads/common";
|
||||
import { getTypographyByKey } from "constants/DefaultTheme";
|
||||
import { IconWrapper } from "components/ads/Icon";
|
||||
import Button, { Size } from "components/ads/Button";
|
||||
import ProfileDropdown from "pages/common/ProfileDropdown";
|
||||
import { Profile } from "pages/common/ProfileImage";
|
||||
import PageTabsContainer from "./PageTabsContainer";
|
||||
|
||||
const HeaderWrapper = styled(StyledHeader)<{ hasPages: boolean }>`
|
||||
background: ${Colors.BALTIC_SEA};
|
||||
height: ${(props) => (props.hasPages ? "90px" : "48px")};
|
||||
box-shadow: unset;
|
||||
height: unset;
|
||||
padding: 0;
|
||||
background-color: ${(props) => props.theme.colors.header.background};
|
||||
color: white;
|
||||
flex-direction: column;
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.05);
|
||||
.${Classes.TEXT} {
|
||||
max-width: 194px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #d4d4d4;
|
||||
${(props) => getTypographyByKey(props, "h4")}
|
||||
color: ${(props) => props.theme.colors.header.appName};
|
||||
}
|
||||
|
||||
& .header__application-share-btn {
|
||||
background-color: ${(props) => props.theme.colors.header.background};
|
||||
border-color: ${(props) => props.theme.colors.header.background};
|
||||
color: ${(props) => props.theme.colors.header.shareBtn};
|
||||
${IconWrapper} path {
|
||||
fill: ${(props) => props.theme.colors.header.shareBtn};
|
||||
}
|
||||
}
|
||||
|
||||
& .header__application-share-btn:hover {
|
||||
color: ${(props) => props.theme.colors.header.shareBtnHighlight};
|
||||
${IconWrapper} path {
|
||||
fill: ${(props) => props.theme.colors.header.shareBtnHighlight};
|
||||
}
|
||||
}
|
||||
|
||||
& ${Profile} {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
@ -57,6 +81,7 @@ const HeaderRow = styled.div<{ justify: string }>`
|
|||
flex: 1;
|
||||
flex-direction: row;
|
||||
justify-content: ${(props) => props.justify};
|
||||
height: ${(props) => `calc(${props.theme.smallHeaderHeight})`};
|
||||
`;
|
||||
|
||||
const HeaderSection = styled.div<{ justify: string }>`
|
||||
|
|
@ -67,66 +92,26 @@ const HeaderSection = styled.div<{ justify: string }>`
|
|||
`;
|
||||
|
||||
const AppsmithLogoImg = styled.img`
|
||||
padding-left: ${(props) => props.theme.spaces[7]}px;
|
||||
max-width: 110px;
|
||||
`;
|
||||
|
||||
const BackToEditorButton = styled(Button)`
|
||||
max-width: 200px;
|
||||
height: 32px;
|
||||
margin: 5px 10px;
|
||||
const Cta = styled(Button)`
|
||||
${(props) => getTypographyByKey(props, "btnLarge")}
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const ForkButton = styled(Button)`
|
||||
max-width: 200px;
|
||||
height: 32px;
|
||||
margin: 5px 10px;
|
||||
const ForkButton = styled(Cta)`
|
||||
svg {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const ShareButton = styled(Button)`
|
||||
height: 32px;
|
||||
margin: 5px 10px;
|
||||
color: white !important;
|
||||
`;
|
||||
|
||||
const PageTab = styled(NavLink)`
|
||||
const HeaderRightItemContainer = styled.div`
|
||||
display: flex;
|
||||
height: 30px;
|
||||
max-width: 170px;
|
||||
margin-right: 1px;
|
||||
align-self: flex-end;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
background-color: rgb(49, 48, 51);
|
||||
padding: 0px 10px;
|
||||
&& span {
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0.04em;
|
||||
color: #fff;
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
&&&:hover {
|
||||
text-decoration: none;
|
||||
background-color: #fff;
|
||||
span {
|
||||
color: #2e3d49;
|
||||
}
|
||||
}
|
||||
&&&.is-active {
|
||||
background-color: white;
|
||||
span {
|
||||
color: #2e3d49;
|
||||
}
|
||||
}
|
||||
margin-right: ${(props) => props.theme.spaces[7]}px;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
type AppViewerHeaderProps = {
|
||||
|
|
@ -137,34 +122,17 @@ type AppViewerHeaderProps = {
|
|||
currentUser?: User;
|
||||
};
|
||||
|
||||
const PageTabName: React.FunctionComponent<{ name: string }> = ({ name }) => {
|
||||
const tabNameRef = useRef<HTMLSpanElement>(null);
|
||||
const [ellipsisActive, setEllipsisActive] = useState(false);
|
||||
const tabNameText = <span ref={tabNameRef}>{name}</span>;
|
||||
|
||||
useEffect(() => {
|
||||
if (isEllipsisActive(tabNameRef?.current)) {
|
||||
setEllipsisActive(true);
|
||||
}
|
||||
}, [tabNameRef]);
|
||||
|
||||
return ellipsisActive ? (
|
||||
<TooltipComponent maxWidth={400} content={name}>
|
||||
{tabNameText}
|
||||
</TooltipComponent>
|
||||
) : (
|
||||
<>{tabNameText}</>
|
||||
);
|
||||
};
|
||||
|
||||
export const AppViewerHeader = (props: AppViewerHeaderProps) => {
|
||||
const { currentApplicationDetails, pages, currentOrgId, currentUser } = props;
|
||||
const isExampleApp = currentApplicationDetails?.appIsExample;
|
||||
const userPermissions = currentApplicationDetails?.userPermissions ?? [];
|
||||
const permissionRequired = PERMISSION_TYPE.MANAGE_APPLICATION;
|
||||
const canEdit = isPermitted(userPermissions, permissionRequired);
|
||||
const queryParams = new URLSearchParams(useLocation().search);
|
||||
const hideHeader = !!queryParams.get("embed");
|
||||
const { search } = useLocation();
|
||||
const queryParams = new URLSearchParams(search);
|
||||
const isEmbed = queryParams.get("embed");
|
||||
const hideHeader = !!isEmbed;
|
||||
|
||||
const HtmlTitle = () => {
|
||||
if (!currentApplicationDetails?.name) return null;
|
||||
return (
|
||||
|
|
@ -174,16 +142,6 @@ export const AppViewerHeader = (props: AppViewerHeaderProps) => {
|
|||
);
|
||||
};
|
||||
if (hideHeader) return <HtmlTitle />;
|
||||
// Mark default page as first page
|
||||
const appPages = pages;
|
||||
if (appPages.length > 1) {
|
||||
appPages.forEach(function(item, i) {
|
||||
if (item.isDefault) {
|
||||
appPages.splice(i, 1);
|
||||
appPages.unshift(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const forkAppUrl = `${window.location.origin}${SIGN_UP_URL}?appId=${currentApplicationDetails?.id}`;
|
||||
const loginAppUrl = `${window.location.origin}${AUTH_LOGIN_URL}?appId=${currentApplicationDetails?.id}`;
|
||||
|
|
@ -192,14 +150,11 @@ export const AppViewerHeader = (props: AppViewerHeaderProps) => {
|
|||
|
||||
if (props.url && canEdit) {
|
||||
CTA = (
|
||||
<BackToEditorButton
|
||||
<Cta
|
||||
className="t--back-to-editor"
|
||||
href={props.url}
|
||||
intent="primary"
|
||||
icon="arrow-left"
|
||||
iconAlignment="left"
|
||||
text={EDIT_APP}
|
||||
filled
|
||||
/>
|
||||
);
|
||||
} else if (isExampleApp) {
|
||||
|
|
@ -207,26 +162,15 @@ export const AppViewerHeader = (props: AppViewerHeaderProps) => {
|
|||
<ForkButton
|
||||
className="t--fork-app"
|
||||
href={forkAppUrl}
|
||||
intent="primary"
|
||||
icon="fork"
|
||||
iconAlignment="left"
|
||||
text={FORK_APP}
|
||||
filled
|
||||
icon="fork"
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
currentApplicationDetails?.isPublic &&
|
||||
currentUser?.username === ANONYMOUS_USERNAME
|
||||
) {
|
||||
CTA = (
|
||||
<ForkButton
|
||||
className="t--fork-app"
|
||||
href={loginAppUrl}
|
||||
intent="primary"
|
||||
text={SIGN_IN}
|
||||
filled
|
||||
/>
|
||||
);
|
||||
CTA = <Cta className="t--fork-app" href={loginAppUrl} text={SIGN_IN} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -234,7 +178,7 @@ export const AppViewerHeader = (props: AppViewerHeaderProps) => {
|
|||
<HtmlTitle />
|
||||
<HeaderRow justify={"space-between"}>
|
||||
<HeaderSection justify={"flex-start"}>
|
||||
<Link to={APPLICATIONS_URL}>
|
||||
<Link to={APPLICATIONS_URL} style={{ display: "flex" }}>
|
||||
<AppsmithLogoImg src={AppsmithLogo} alt="Appsmith logo" />
|
||||
</Link>
|
||||
</HeaderSection>
|
||||
|
|
@ -248,19 +192,11 @@ export const AppViewerHeader = (props: AppViewerHeaderProps) => {
|
|||
<>
|
||||
<FormDialogComponent
|
||||
trigger={
|
||||
<ShareButton
|
||||
text="Share"
|
||||
intent="none"
|
||||
outline
|
||||
size="small"
|
||||
className="t--application-share-btn"
|
||||
icon={
|
||||
<HeaderIcons.SHARE
|
||||
color={Colors.WHITE}
|
||||
width={13}
|
||||
height={13}
|
||||
/>
|
||||
}
|
||||
<Button
|
||||
text={"Share"}
|
||||
icon={"share"}
|
||||
size={Size.small}
|
||||
className="t--application-share-btn header__application-share-btn"
|
||||
/>
|
||||
}
|
||||
Form={AppInviteUsersForm}
|
||||
|
|
@ -269,28 +205,31 @@ export const AppViewerHeader = (props: AppViewerHeaderProps) => {
|
|||
title={currentApplicationDetails.name}
|
||||
canOutsideClickClose={true}
|
||||
/>
|
||||
{CTA}
|
||||
{CTA && (
|
||||
<HeaderRightItemContainer>{CTA}</HeaderRightItemContainer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{currentUser && currentUser.username !== ANONYMOUS_USERNAME && (
|
||||
<HeaderRightItemContainer>
|
||||
<ProfileDropdown
|
||||
userName={currentUser?.username || ""}
|
||||
hideThemeSwitch
|
||||
modifiers={{
|
||||
offset: {
|
||||
enabled: true,
|
||||
offset: `0, ${pages.length > 1 ? 35 : 0}`,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</HeaderRightItemContainer>
|
||||
)}
|
||||
</HeaderSection>
|
||||
</HeaderRow>
|
||||
{appPages.length > 1 && (
|
||||
<HeaderRow justify={"flex-start"}>
|
||||
{appPages.map((page) => (
|
||||
<PageTab
|
||||
key={page.pageId}
|
||||
to={getApplicationViewerPageURL(
|
||||
currentApplicationDetails?.id,
|
||||
page.pageId,
|
||||
)}
|
||||
activeClassName="is-active"
|
||||
className="t--page-switch-tab"
|
||||
>
|
||||
<PageTabName name={page.pageName} />
|
||||
</PageTab>
|
||||
))}
|
||||
</HeaderRow>
|
||||
)}
|
||||
<PageTabsContainer
|
||||
pages={pages}
|
||||
currentApplicationDetails={currentApplicationDetails}
|
||||
/>
|
||||
</HeaderWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
177
app/client/src/pages/AppViewer/viewer/PageTabs.tsx
Normal file
177
app/client/src/pages/AppViewer/viewer/PageTabs.tsx
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import React, { useRef, useEffect, useState } from "react";
|
||||
import { NavLink, useLocation } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
ApplicationPayload,
|
||||
PageListPayload,
|
||||
} from "constants/ReduxActionConstants";
|
||||
import { getApplicationViewerPageURL } from "constants/routes";
|
||||
import { isEllipsisActive } from "utils/helpers";
|
||||
import TooltipComponent from "components/ads/Tooltip";
|
||||
import { getTypographyByKey, hideScrollbar } from "constants/DefaultTheme";
|
||||
import { Position } from "@blueprintjs/core";
|
||||
|
||||
const TabsContainer = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
${hideScrollbar}
|
||||
`;
|
||||
|
||||
const PageTab = styled(NavLink)`
|
||||
display: flex;
|
||||
max-width: 170px;
|
||||
align-self: flex-end;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
padding: 0px ${(props) => props.theme.spaces[7]}px;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledBottomBorder = styled.div`
|
||||
position: relative;
|
||||
transition: all 0.3s ease-in-out;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
left: -100%;
|
||||
background-color: ${(props) =>
|
||||
props.theme.colors.header.activeTabBorderBottom};
|
||||
${PageTab}:hover & {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyleTabText = styled.div`
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
${(props) => getTypographyByKey(props, "h6")}
|
||||
color: ${(props) => props.theme.colors.header.tabText};
|
||||
height: ${(props) => `calc(${props.theme.smallHeaderHeight})`};
|
||||
& span {
|
||||
max-width: 138px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
${PageTab}.is-active & {
|
||||
color: ${(props) => props.theme.colors.header.activeTabText};
|
||||
${StyledBottomBorder} {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const CenterTabNameContainer = styled.div`
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const PageTabName: React.FunctionComponent<{ name: string }> = ({ name }) => {
|
||||
const tabNameRef = useRef<HTMLSpanElement>(null);
|
||||
const [ellipsisActive, setEllipsisActive] = useState(false);
|
||||
const tabNameText = (
|
||||
<StyleTabText>
|
||||
<CenterTabNameContainer>
|
||||
<span ref={tabNameRef}>{name}</span>
|
||||
</CenterTabNameContainer>
|
||||
<StyledBottomBorder />
|
||||
</StyleTabText>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEllipsisActive(tabNameRef?.current)) {
|
||||
setEllipsisActive(true);
|
||||
}
|
||||
}, [tabNameRef]);
|
||||
|
||||
return ellipsisActive ? (
|
||||
<TooltipComponent
|
||||
maxWidth={400}
|
||||
content={name}
|
||||
position={Position.BOTTOM}
|
||||
boundary="viewport"
|
||||
>
|
||||
{tabNameText}
|
||||
</TooltipComponent>
|
||||
) : (
|
||||
tabNameText
|
||||
);
|
||||
};
|
||||
|
||||
const PageTabContainer = ({
|
||||
children,
|
||||
isTabActive,
|
||||
tabsScrollable,
|
||||
setShowScrollArrows,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
isTabActive: boolean;
|
||||
tabsScrollable: boolean;
|
||||
setShowScrollArrows: () => void;
|
||||
}) => {
|
||||
const tabContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isTabActive) {
|
||||
tabContainerRef.current?.scrollIntoView(false);
|
||||
setShowScrollArrows();
|
||||
}
|
||||
}, [isTabActive, tabsScrollable]);
|
||||
|
||||
return <div ref={tabContainerRef}>{children}</div>;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
currentApplicationDetails?: ApplicationPayload;
|
||||
appPages: PageListPayload;
|
||||
measuredTabsRef: (ref: HTMLElement | null) => void;
|
||||
tabsScrollable: boolean;
|
||||
setShowScrollArrows: () => void;
|
||||
};
|
||||
|
||||
export const PageTabs = (props: Props) => {
|
||||
const { currentApplicationDetails, appPages } = props;
|
||||
const { pathname } = useLocation();
|
||||
|
||||
return (
|
||||
<TabsContainer ref={props.measuredTabsRef}>
|
||||
{appPages.map((page) => (
|
||||
<PageTabContainer
|
||||
key={page.pageId}
|
||||
isTabActive={
|
||||
pathname ===
|
||||
getApplicationViewerPageURL(
|
||||
currentApplicationDetails?.id,
|
||||
page.pageId,
|
||||
)
|
||||
}
|
||||
tabsScrollable={props.tabsScrollable}
|
||||
setShowScrollArrows={props.setShowScrollArrows}
|
||||
>
|
||||
<PageTab
|
||||
to={getApplicationViewerPageURL(
|
||||
currentApplicationDetails?.id,
|
||||
page.pageId,
|
||||
)}
|
||||
activeClassName="is-active"
|
||||
className="t--page-switch-tab"
|
||||
>
|
||||
<PageTabName name={page.pageName} />
|
||||
</PageTab>
|
||||
</PageTabContainer>
|
||||
))}
|
||||
</TabsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageTabs;
|
||||
150
app/client/src/pages/AppViewer/viewer/PageTabsContainer.tsx
Normal file
150
app/client/src/pages/AppViewer/viewer/PageTabsContainer.tsx
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import React, { useRef, useEffect, useState, useCallback } from "react";
|
||||
import styled from "styled-components";
|
||||
import {
|
||||
ApplicationPayload,
|
||||
PageListPayload,
|
||||
} from "constants/ReduxActionConstants";
|
||||
import Icon from "components/ads/Icon";
|
||||
import PageTabs from "./PageTabs";
|
||||
import useThrottledRAF from "utils/hooks/useThrottledRAF";
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding: 0 ${(props) => props.theme.spaces[7]}px;
|
||||
align-items: center;
|
||||
& {
|
||||
svg path,
|
||||
svg:hover path {
|
||||
fill: transparent;
|
||||
stroke: ${(props) => props.theme.colors.header.tabText};
|
||||
}
|
||||
}
|
||||
border-top: 1px solid
|
||||
${(props) => props.theme.colors.header.tabsHorizontalSeparator};
|
||||
border-bottom: 1px solid
|
||||
${(props) => props.theme.colors.header.tabsHorizontalSeparator};
|
||||
`;
|
||||
|
||||
const ScrollBtnContainer = styled.div<{ visible: boolean }>`
|
||||
padding: ${(props) => props.theme.spaces[2]}px;
|
||||
cursor: pointer;
|
||||
${(props) =>
|
||||
props.visible
|
||||
? `
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: visibility 0s linear 0s, opacity 300ms;
|
||||
`
|
||||
: `
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: visibility 0s linear 300ms, opacity 300ms;
|
||||
`}
|
||||
`;
|
||||
|
||||
type AppViewerHeaderProps = {
|
||||
currentApplicationDetails?: ApplicationPayload;
|
||||
pages: PageListPayload;
|
||||
};
|
||||
|
||||
export const PageTabsContainer = (props: AppViewerHeaderProps) => {
|
||||
const { currentApplicationDetails, pages } = props;
|
||||
|
||||
// Mark default page as first page
|
||||
const appPages = pages;
|
||||
if (appPages.length > 1) {
|
||||
appPages.forEach((item, i) => {
|
||||
if (item.isDefault) {
|
||||
appPages.splice(i, 1);
|
||||
appPages.unshift(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const tabsRef = useRef<HTMLElement | null>(null);
|
||||
const [tabsScrollable, setTabsScrollable] = useState(false);
|
||||
const [shouldShowLeftArrow, setShouldShowLeftArrow] = useState(false);
|
||||
const [shouldShowRightArrow, setShouldShowRightArrow] = useState(true);
|
||||
|
||||
const setShowScrollArrows = useCallback(() => {
|
||||
if (tabsRef.current) {
|
||||
const { scrollWidth, offsetWidth, scrollLeft } = tabsRef.current;
|
||||
setShouldShowLeftArrow(scrollLeft > 0);
|
||||
setShouldShowRightArrow(scrollLeft + offsetWidth < scrollWidth);
|
||||
}
|
||||
}, [tabsRef.current]);
|
||||
|
||||
const measuredTabsRef = useCallback((node) => {
|
||||
tabsRef.current = node;
|
||||
if (node !== null) {
|
||||
const { scrollWidth, offsetWidth } = node;
|
||||
setTabsScrollable(scrollWidth > offsetWidth);
|
||||
setShowScrollArrows();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const [isScrolling, setIsScrolling] = useState(false);
|
||||
const [isScrollingLeft, setIsScrollingLeft] = useState(false);
|
||||
|
||||
const scroll = useCallback(() => {
|
||||
const currentOffset = tabsRef.current?.scrollLeft || 0;
|
||||
|
||||
if (tabsRef.current) {
|
||||
tabsRef.current.scrollLeft = isScrollingLeft
|
||||
? currentOffset - 5
|
||||
: currentOffset + 5;
|
||||
setShowScrollArrows();
|
||||
}
|
||||
}, [tabsRef.current, isScrollingLeft]);
|
||||
// eslint-disable-next-line
|
||||
const [_intervalRef, _rafRef, requestAF] = useThrottledRAF(scroll, 10);
|
||||
|
||||
const stopScrolling = () => {
|
||||
setIsScrolling(false);
|
||||
setIsScrollingLeft(false);
|
||||
};
|
||||
|
||||
const startScrolling = (isLeft: boolean) => {
|
||||
setIsScrolling(true);
|
||||
setIsScrollingLeft(isLeft);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let clear;
|
||||
if (isScrolling) {
|
||||
clear = requestAF();
|
||||
}
|
||||
return clear;
|
||||
}, [isScrolling, isScrollingLeft]);
|
||||
|
||||
return appPages.length > 1 ? (
|
||||
<Container>
|
||||
<ScrollBtnContainer
|
||||
onMouseDown={() => startScrolling(true)}
|
||||
onMouseUp={stopScrolling}
|
||||
onMouseLeave={stopScrolling}
|
||||
visible={shouldShowLeftArrow}
|
||||
>
|
||||
<Icon name="chevron-left" />
|
||||
</ScrollBtnContainer>
|
||||
<PageTabs
|
||||
measuredTabsRef={measuredTabsRef}
|
||||
appPages={appPages}
|
||||
currentApplicationDetails={currentApplicationDetails}
|
||||
tabsScrollable={tabsScrollable}
|
||||
setShowScrollArrows={setShowScrollArrows}
|
||||
/>
|
||||
<ScrollBtnContainer
|
||||
onMouseDown={() => startScrolling(false)}
|
||||
onMouseUp={stopScrolling}
|
||||
onMouseLeave={stopScrolling}
|
||||
visible={shouldShowRightArrow}
|
||||
>
|
||||
<Icon name="chevron-right" />
|
||||
</ScrollBtnContainer>
|
||||
</Container>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default PageTabsContainer;
|
||||
|
|
@ -31,7 +31,7 @@ import { apiActionSettingsConfig } from "mockResponses/ActionSettings";
|
|||
const Form = styled.form`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - ${(props) => props.theme.headerHeight});
|
||||
height: calc(100vh - ${(props) => props.theme.smallHeaderHeight});
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
${FormLabel} {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import React from "react";
|
|||
import styled from "styled-components";
|
||||
import _ from "lodash";
|
||||
import { DATASOURCE_DB_FORM } from "constants/forms";
|
||||
import { Spinner } from "@blueprintjs/core";
|
||||
import { DATA_SOURCES_EDITOR_URL } from "constants/routes";
|
||||
import FormControl from "../FormControl";
|
||||
import Collapsible from "./Collapsible";
|
||||
|
|
@ -10,7 +9,7 @@ import history from "utils/history";
|
|||
import { Icon } from "@blueprintjs/core";
|
||||
import FormTitle from "./FormTitle";
|
||||
import { ControlProps } from "components/formControls/BaseControl";
|
||||
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
|
||||
|
||||
import CollapsibleHelp from "components/designSystems/appsmith/help/CollapsibleHelp";
|
||||
import Connected from "./Connected";
|
||||
|
||||
|
|
@ -45,7 +44,6 @@ interface DatasourceDBEditorProps {
|
|||
pageId: string;
|
||||
formData: Datasource;
|
||||
isTesting: boolean;
|
||||
loadingFormConfigs: boolean;
|
||||
formConfig: any[];
|
||||
isNewDatasource: boolean;
|
||||
pluginImage: string;
|
||||
|
|
@ -64,7 +62,7 @@ const DBForm = styled.div`
|
|||
padding: 20px;
|
||||
margin-left: 10px;
|
||||
margin-right: 0px;
|
||||
height: calc(100vh - ${(props) => props.theme.headerHeight});
|
||||
height: calc(100vh - ${(props) => props.theme.smallHeaderHeight});
|
||||
overflow: auto;
|
||||
.backBtn {
|
||||
padding-bottom: 1px;
|
||||
|
|
@ -117,10 +115,6 @@ const StyledButton = styled(Button)`
|
|||
}
|
||||
`;
|
||||
|
||||
const LoadingContainer = styled(CenteredWrapper)`
|
||||
height: 50%;
|
||||
`;
|
||||
|
||||
const StyledOpenDocsIcon = styled(Icon)`
|
||||
svg {
|
||||
width: 12px;
|
||||
|
|
@ -214,16 +208,8 @@ class DatasourceDBEditor extends React.Component<
|
|||
};
|
||||
|
||||
render() {
|
||||
const { loadingFormConfigs, formConfig } = this.props;
|
||||
const { formConfig } = this.props;
|
||||
const content = this.renderDataSourceConfigForm(formConfig);
|
||||
if (loadingFormConfigs) {
|
||||
return (
|
||||
<LoadingContainer>
|
||||
<Spinner size={30} />
|
||||
</LoadingContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return <DBForm>{content}</DBForm>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,20 +69,21 @@ export const renderDatasourceSection = (
|
|||
return (
|
||||
<FieldWrapper key={reactKey}>
|
||||
<Key>{label}</Key>
|
||||
{value.map((val: { key: string; value: string }) => {
|
||||
return (
|
||||
<div key={val.key}>
|
||||
<div style={{ display: "inline-block" }}>
|
||||
<Key>Key: </Key>
|
||||
<Value>{val.key}</Value>
|
||||
{value &&
|
||||
value.map((val: { key: string; value: string }) => {
|
||||
return (
|
||||
<div key={val.key}>
|
||||
<div style={{ display: "inline-block" }}>
|
||||
<Key>Key: </Key>
|
||||
<Value>{val.key}</Value>
|
||||
</div>
|
||||
<ValueWrapper>
|
||||
<Key>Value: </Key>
|
||||
<Value>{val.value}</Value>
|
||||
</ValueWrapper>
|
||||
</div>
|
||||
<ValueWrapper>
|
||||
<Key>Value: </Key>
|
||||
<Value>{val.value}</Value>
|
||||
</ValueWrapper>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</FieldWrapper>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,442 @@
|
|||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import _ from "lodash";
|
||||
import { DATASOURCE_DB_FORM } from "constants/forms";
|
||||
import { DATA_SOURCES_EDITOR_URL } from "constants/routes";
|
||||
import FormControl from "../FormControl";
|
||||
import Collapsible from "./Collapsible";
|
||||
import history from "utils/history";
|
||||
import FormTitle from "./FormTitle";
|
||||
import { ControlProps } from "components/formControls/BaseControl";
|
||||
import Connected from "./Connected";
|
||||
import Button from "components/editorComponents/Button";
|
||||
import { Datasource } from "entities/Datasource";
|
||||
import { reduxForm, InjectedFormProps } from "redux-form";
|
||||
import { BaseButton } from "components/designSystems/blueprint/ButtonComponent";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import BackButton from "./BackButton";
|
||||
import Boxed from "components/editorComponents/Onboarding/Boxed";
|
||||
import { OnboardingStep } from "constants/OnboardingConstants";
|
||||
import { isHidden } from "components/formControls/utils";
|
||||
import log from "loglevel";
|
||||
|
||||
interface DatasourceDBEditorProps {
|
||||
onSave: (formValues: Datasource) => void;
|
||||
onTest: (formValus: Datasource) => void;
|
||||
handleDelete: (id: string) => void;
|
||||
setDatasourceEditorMode: (id: string, viewMode: boolean) => void;
|
||||
selectedPluginPackage: string;
|
||||
isSaving: boolean;
|
||||
isDeleting: boolean;
|
||||
datasourceId: string;
|
||||
applicationId: string;
|
||||
pageId: string;
|
||||
formData: Datasource;
|
||||
isTesting: boolean;
|
||||
formConfig: any[];
|
||||
isNewDatasource: boolean;
|
||||
pluginImage: string;
|
||||
viewMode: boolean;
|
||||
pluginType: string;
|
||||
}
|
||||
|
||||
interface DatasourceDBEditorState {
|
||||
viewMode: boolean;
|
||||
}
|
||||
|
||||
type Props = DatasourceDBEditorProps &
|
||||
InjectedFormProps<Datasource, DatasourceDBEditorProps>;
|
||||
|
||||
const DBForm = styled.div`
|
||||
padding: 20px;
|
||||
margin-left: 10px;
|
||||
margin-right: 0px;
|
||||
height: calc(100vh - ${(props) => props.theme.headerHeight});
|
||||
overflow: auto;
|
||||
.backBtn {
|
||||
padding-bottom: 1px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.backBtnText {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
const PluginImage = styled.img`
|
||||
height: 40px;
|
||||
width: auto;
|
||||
`;
|
||||
|
||||
export const FormTitleContainer = styled.div`
|
||||
flex-direction: row;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const Header = styled.div`
|
||||
flex-direction: row;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 16px;
|
||||
`;
|
||||
|
||||
const SaveButtonContainer = styled.div`
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
const ActionButton = styled(BaseButton)`
|
||||
&&& {
|
||||
max-width: 72px;
|
||||
margin-right: 9px;
|
||||
min-height: 32px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
&&&& {
|
||||
width: 87px;
|
||||
height: 32px;
|
||||
}
|
||||
`;
|
||||
|
||||
class DatasourceDBEditor extends React.Component<
|
||||
Props,
|
||||
DatasourceDBEditorState
|
||||
> {
|
||||
requiredFields: Record<string, any>;
|
||||
configDetails: Record<string, any>;
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
viewMode: true,
|
||||
};
|
||||
this.requiredFields = {};
|
||||
this.configDetails = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.requiredFields = {};
|
||||
this.configDetails = {};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.datasourceId !== this.props.datasourceId) {
|
||||
this.requiredFields = {};
|
||||
this.configDetails = {};
|
||||
this.props.setDatasourceEditorMode(this.props.datasourceId, true);
|
||||
}
|
||||
}
|
||||
|
||||
validate = () => {
|
||||
const errors = {} as any;
|
||||
const requiredFields = Object.keys(this.requiredFields);
|
||||
const values = this.props.formData;
|
||||
|
||||
requiredFields.forEach((fieldConfigProperty) => {
|
||||
const fieldConfig = this.requiredFields[fieldConfigProperty];
|
||||
if (fieldConfig.controlType === "KEYVALUE_ARRAY") {
|
||||
const configProperty = fieldConfig.configProperty.split("[*].");
|
||||
const arrayValues = _.get(values, configProperty[0]);
|
||||
const keyValueArrayErrors: Record<string, string>[] = [];
|
||||
|
||||
arrayValues.forEach((value: any, index: number) => {
|
||||
const objectKeys = Object.keys(value);
|
||||
const keyValueErrors: Record<string, string> = {};
|
||||
|
||||
if (!value[objectKeys[0]]) {
|
||||
keyValueErrors[objectKeys[0]] = "This field is required";
|
||||
keyValueArrayErrors[index] = keyValueErrors;
|
||||
}
|
||||
if (!value[objectKeys[1]]) {
|
||||
keyValueErrors[objectKeys[1]] = "This field is required";
|
||||
keyValueArrayErrors[index] = keyValueErrors;
|
||||
}
|
||||
});
|
||||
|
||||
if (keyValueArrayErrors.length) {
|
||||
_.set(errors, configProperty[0], keyValueArrayErrors);
|
||||
}
|
||||
} else if (fieldConfig.controlType === "KEY_VAL_INPUT") {
|
||||
const value = _.get(values, fieldConfigProperty, []);
|
||||
|
||||
if (value.length) {
|
||||
const values = Object.values(value[0]);
|
||||
const isNotBlank = values.every((value) => value);
|
||||
|
||||
if (!isNotBlank) {
|
||||
_.set(errors, fieldConfigProperty, "This field is required");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const value = _.get(values, fieldConfigProperty);
|
||||
|
||||
if (!value) {
|
||||
_.set(errors, fieldConfigProperty, "This field is required");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return !_.isEmpty(errors) || this.props.invalid;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { formConfig } = this.props;
|
||||
const content = this.renderDataSourceConfigForm(formConfig);
|
||||
return <DBForm>{content}</DBForm>;
|
||||
}
|
||||
|
||||
isNewDatasource = () => {
|
||||
const { datasourceId } = this.props;
|
||||
|
||||
return datasourceId.includes(":");
|
||||
};
|
||||
|
||||
normalizeValues = () => {
|
||||
let { formData } = this.props;
|
||||
const checked: Record<string, any> = {};
|
||||
const configProperties = Object.keys(this.configDetails);
|
||||
|
||||
for (const configProperty of configProperties) {
|
||||
const controlType = this.configDetails[configProperty];
|
||||
|
||||
if (controlType === "KEYVALUE_ARRAY") {
|
||||
const properties = configProperty.split("[*].");
|
||||
|
||||
if (checked[properties[0]]) continue;
|
||||
|
||||
checked[properties[0]] = 1;
|
||||
const values = _.get(formData, properties[0]);
|
||||
const newValues: ({ [s: string]: unknown } | ArrayLike<unknown>)[] = [];
|
||||
|
||||
values.forEach(
|
||||
(object: { [s: string]: unknown } | ArrayLike<unknown>) => {
|
||||
const isEmpty = Object.values(object).every((x) => x === "");
|
||||
|
||||
if (!isEmpty) {
|
||||
newValues.push(object);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (newValues.length) {
|
||||
formData = _.set(formData, properties[0], newValues);
|
||||
} else {
|
||||
formData = _.set(formData, properties[0], []);
|
||||
}
|
||||
} else if (controlType === "KEY_VAL_INPUT") {
|
||||
if (checked[configProperty]) continue;
|
||||
|
||||
const values = _.get(formData, configProperty);
|
||||
const newValues: ({ [s: string]: unknown } | ArrayLike<unknown>)[] = [];
|
||||
|
||||
values.forEach(
|
||||
(object: { [s: string]: unknown } | ArrayLike<unknown>) => {
|
||||
const isEmpty = Object.values(object).every((x) => x === "");
|
||||
|
||||
if (!isEmpty) {
|
||||
newValues.push(object);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (newValues.length) {
|
||||
formData = _.set(formData, configProperty, newValues);
|
||||
} else {
|
||||
formData = _.set(formData, configProperty, []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return formData;
|
||||
};
|
||||
|
||||
save = () => {
|
||||
const normalizedValues = this.normalizeValues();
|
||||
AnalyticsUtil.logEvent("SAVE_DATA_SOURCE_CLICK", {
|
||||
pageId: this.props.pageId,
|
||||
appId: this.props.applicationId,
|
||||
});
|
||||
this.props.onSave(normalizedValues);
|
||||
};
|
||||
|
||||
test = () => {
|
||||
const normalizedValues = this.normalizeValues();
|
||||
AnalyticsUtil.logEvent("TEST_DATA_SOURCE_CLICK", {
|
||||
pageId: this.props.pageId,
|
||||
appId: this.props.applicationId,
|
||||
});
|
||||
this.props.onTest(normalizedValues);
|
||||
};
|
||||
|
||||
renderDataSourceConfigForm = (sections: any) => {
|
||||
const {
|
||||
isSaving,
|
||||
applicationId,
|
||||
pageId,
|
||||
isTesting,
|
||||
isDeleting,
|
||||
datasourceId,
|
||||
handleDelete,
|
||||
} = this.props;
|
||||
const { viewMode } = this.props;
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<BackButton
|
||||
onClick={() =>
|
||||
history.push(DATA_SOURCES_EDITOR_URL(applicationId, pageId))
|
||||
}
|
||||
/>
|
||||
<br />
|
||||
<Header>
|
||||
<FormTitleContainer>
|
||||
<PluginImage src={this.props.pluginImage} alt="Datasource" />
|
||||
<FormTitle focusOnMount={this.props.isNewDatasource} />
|
||||
</FormTitleContainer>
|
||||
{viewMode && (
|
||||
<Boxed step={OnboardingStep.SUCCESSFUL_BINDING}>
|
||||
<ActionButton
|
||||
className="t--edit-datasource"
|
||||
text="EDIT"
|
||||
accent="secondary"
|
||||
onClick={() => {
|
||||
this.props.setDatasourceEditorMode(
|
||||
this.props.datasourceId,
|
||||
false,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Boxed>
|
||||
)}
|
||||
</Header>
|
||||
{!viewMode ? (
|
||||
<>
|
||||
{!_.isNil(sections)
|
||||
? _.map(sections, this.renderMainSection)
|
||||
: undefined}
|
||||
<SaveButtonContainer>
|
||||
<ActionButton
|
||||
className="t--delete-datasource"
|
||||
text="Delete"
|
||||
accent="error"
|
||||
loading={isDeleting}
|
||||
onClick={() => handleDelete(datasourceId)}
|
||||
/>
|
||||
|
||||
<ActionButton
|
||||
className="t--test-datasource"
|
||||
text="Test"
|
||||
loading={isTesting}
|
||||
accent="secondary"
|
||||
onClick={this.test}
|
||||
/>
|
||||
<StyledButton
|
||||
className="t--save-datasource"
|
||||
onClick={this.save}
|
||||
text="Save"
|
||||
disabled={this.validate()}
|
||||
loading={isSaving}
|
||||
intent="primary"
|
||||
filled
|
||||
size="small"
|
||||
/>
|
||||
</SaveButtonContainer>
|
||||
</>
|
||||
) : (
|
||||
<Connected />
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
renderMainSection = (section: any, index: number) => {
|
||||
if (isHidden(this.props.formData, section.hidden)) return null;
|
||||
return (
|
||||
<Collapsible title={section.sectionName} defaultIsOpen={index === 0}>
|
||||
{this.renderEachConfig(section)}
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
|
||||
renderSingleConfig = (
|
||||
config: ControlProps,
|
||||
multipleConfig?: ControlProps[],
|
||||
) => {
|
||||
multipleConfig = multipleConfig || [];
|
||||
try {
|
||||
this.setupConfig(config);
|
||||
return (
|
||||
<div key={config.configProperty} style={{ marginTop: "16px" }}>
|
||||
<FormControl
|
||||
config={config}
|
||||
formName={DATASOURCE_DB_FORM}
|
||||
multipleConfig={multipleConfig}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
setupConfig = (config: ControlProps) => {
|
||||
const { controlType, isRequired, configProperty } = config;
|
||||
this.configDetails[configProperty] = controlType;
|
||||
|
||||
if (isRequired) {
|
||||
this.requiredFields[configProperty] = config;
|
||||
}
|
||||
};
|
||||
|
||||
isKVArray = (children: Array<ControlProps>) => {
|
||||
if (!Array.isArray(children) || children.length < 2) return false;
|
||||
return (
|
||||
children[0].controlType && children[0].controlType === "KEYVALUE_ARRAY"
|
||||
);
|
||||
};
|
||||
|
||||
renderKVArray = (children: Array<ControlProps>) => {
|
||||
try {
|
||||
// setup config for each child
|
||||
children.forEach((c) => this.setupConfig(c));
|
||||
// We pass last child for legacy reasons, to keep the logic here exactly same as before.
|
||||
return this.renderSingleConfig(children[children.length - 1], children);
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
renderEachConfig = (section: any) => {
|
||||
return (
|
||||
<div key={section.sectionName}>
|
||||
{_.map(section.children, (propertyControlOrSection: ControlProps) => {
|
||||
// If the section is hidden, skip rendering
|
||||
if (isHidden(this.props.formData, section.hidden)) return null;
|
||||
if ("children" in propertyControlOrSection) {
|
||||
const { children } = propertyControlOrSection;
|
||||
if (this.isKVArray(children)) {
|
||||
return this.renderKVArray(children);
|
||||
}
|
||||
return this.renderEachConfig(propertyControlOrSection);
|
||||
} else {
|
||||
return this.renderSingleConfig(propertyControlOrSection);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default reduxForm<Datasource, DatasourceDBEditorProps>({
|
||||
form: DATASOURCE_DB_FORM,
|
||||
})(DatasourceDBEditor);
|
||||
|
|
@ -19,9 +19,18 @@ import {
|
|||
import { DATASOURCE_DB_FORM } from "constants/forms";
|
||||
import DatasourceHome from "./DatasourceHome";
|
||||
import DataSourceEditorForm from "./DBForm";
|
||||
import RestAPIDatasourceForm from "./RestAPIDatasourceForm";
|
||||
import { Datasource } from "entities/Datasource";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import EntityNotFoundPane from "pages/Editor/EntityNotFoundPane";
|
||||
import { PluginType } from "entities/Action";
|
||||
import { Spinner } from "@blueprintjs/core";
|
||||
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
|
||||
import styled from "styled-components";
|
||||
|
||||
export const LoadingContainer = styled(CenteredWrapper)`
|
||||
height: 50%;
|
||||
`;
|
||||
|
||||
interface ReduxStateProps {
|
||||
formData: Datasource;
|
||||
|
|
@ -92,40 +101,49 @@ class DataSourceEditor extends React.Component<Props> {
|
|||
if (!pluginId && datasourceId) {
|
||||
return <EntityNotFoundPane />;
|
||||
}
|
||||
if (!datasourceId) {
|
||||
return (
|
||||
<DatasourceHome
|
||||
isSaving={isSaving}
|
||||
applicationId={this.props.match.params.applicationId}
|
||||
pageId={this.props.match.params.pageId}
|
||||
history={this.props.history}
|
||||
location={this.props.location}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (loadingFormConfigs) {
|
||||
return (
|
||||
<LoadingContainer>
|
||||
<Spinner size={30} />
|
||||
</LoadingContainer>
|
||||
);
|
||||
}
|
||||
const DatasourceForm =
|
||||
pluginType === PluginType.API
|
||||
? RestAPIDatasourceForm
|
||||
: DataSourceEditorForm;
|
||||
return (
|
||||
<React.Fragment>
|
||||
{datasourceId ? (
|
||||
<DataSourceEditorForm
|
||||
pluginImage={pluginImages[pluginId]}
|
||||
applicationId={this.props.match.params.applicationId}
|
||||
pageId={this.props.match.params.pageId}
|
||||
isSaving={isSaving}
|
||||
isTesting={isTesting}
|
||||
isDeleting={isDeleting}
|
||||
isNewDatasource={newDatasource === datasourceId}
|
||||
onSubmit={this.handleSubmit}
|
||||
onSave={this.handleSave}
|
||||
onTest={this.props.testDatasource}
|
||||
selectedPluginPackage={selectedPluginPackage}
|
||||
datasourceId={datasourceId}
|
||||
formData={formData}
|
||||
loadingFormConfigs={loadingFormConfigs}
|
||||
formConfig={formConfig}
|
||||
handleDelete={deleteDatasource}
|
||||
viewMode={viewMode}
|
||||
setDatasourceEditorMode={setDatasourceEditorMode}
|
||||
pluginType={pluginType}
|
||||
/>
|
||||
) : (
|
||||
<DatasourceHome
|
||||
isSaving={isSaving}
|
||||
applicationId={this.props.match.params.applicationId}
|
||||
pageId={this.props.match.params.pageId}
|
||||
history={this.props.history}
|
||||
location={this.props.location}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
<DatasourceForm
|
||||
pluginImage={pluginImages[pluginId]}
|
||||
applicationId={this.props.match.params.applicationId}
|
||||
pageId={this.props.match.params.pageId}
|
||||
isSaving={isSaving}
|
||||
isTesting={isTesting}
|
||||
isDeleting={isDeleting}
|
||||
isNewDatasource={newDatasource === datasourceId}
|
||||
onSubmit={this.handleSubmit}
|
||||
onSave={this.handleSave}
|
||||
onTest={this.props.testDatasource}
|
||||
selectedPluginPackage={selectedPluginPackage}
|
||||
datasourceId={datasourceId}
|
||||
formData={formData}
|
||||
formConfig={formConfig}
|
||||
handleDelete={deleteDatasource}
|
||||
viewMode={viewMode}
|
||||
setDatasourceEditorMode={setDatasourceEditorMode}
|
||||
pluginType={pluginType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
70
app/client/src/pages/Editor/EditableAppName.tsx
Normal file
70
app/client/src/pages/Editor/EditableAppName.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import EditableText, { EditableTextProps } from "components/ads/EditableText";
|
||||
import React, { useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import { Variant } from "components/ads/common";
|
||||
import { Toaster } from "components/ads/Toast";
|
||||
import { Classes } from "@blueprintjs/core";
|
||||
import { getTypographyByKey } from "constants/DefaultTheme";
|
||||
|
||||
type EditableTextWrapperProps = EditableTextProps & {
|
||||
isNewApp: boolean;
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
& .bp3-editable-text-content:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
& .${Classes.EDITABLE_TEXT} {
|
||||
height: ${(props) => props.theme.smallHeaderHeight} !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
&&&& .${Classes.EDITABLE_TEXT}, &&&& .${Classes.EDITABLE_TEXT_EDITING} {
|
||||
padding: 0 ${(props) => props.theme.spaces[0]}px;
|
||||
}
|
||||
&&&& .${Classes.EDITABLE_TEXT_CONTENT}, &&&& .${Classes.EDITABLE_TEXT_INPUT} {
|
||||
display: inline;
|
||||
${(props) => getTypographyByKey(props, "h4")};
|
||||
line-height: 19px !important;
|
||||
padding: 0;
|
||||
height: unset !important;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
width: unset !important;
|
||||
}
|
||||
&&&& .${Classes.EDITABLE_TEXT_CONTENT} {
|
||||
min-width: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default function EditableTextWrapper(props: EditableTextWrapperProps) {
|
||||
const [isEditingDefault, setIsEditingDefault] = useState(props.isNewApp);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<EditableText
|
||||
defaultValue={props.defaultValue}
|
||||
editInteractionKind={props.editInteractionKind}
|
||||
placeholder={props.placeholder}
|
||||
isEditingDefault={isEditingDefault}
|
||||
savingState={props.savingState}
|
||||
fill={props.fill}
|
||||
onBlur={(value) => {
|
||||
props.onBlur(value);
|
||||
setIsEditingDefault(false);
|
||||
}}
|
||||
className={props.className}
|
||||
isInvalid={(value: string) => {
|
||||
if (value.trim() === "") {
|
||||
Toaster.show({
|
||||
text: "Application name can't be empty",
|
||||
variant: Variant.danger,
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}}
|
||||
hideEditIcon
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
@ -9,13 +9,11 @@ import {
|
|||
getApplicationViewerPageURL,
|
||||
} from "constants/routes";
|
||||
import AppInviteUsersForm from "pages/organization/AppInviteUsersForm";
|
||||
import Button from "components/editorComponents/Button";
|
||||
import StyledHeader from "components/designSystems/appsmith/StyledHeader";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import HelpModal from "components/designSystems/appsmith/help/HelpModal";
|
||||
import { FormDialogComponent } from "components/editorComponents/form/FormDialogComponent";
|
||||
import { Colors } from "constants/Colors";
|
||||
import AppsmithLogo from "assets/images/appsmith_logo_white.png";
|
||||
import AppsmithLogo from "assets/images/appsmith_logo_square.png";
|
||||
import { Link } from "react-router-dom";
|
||||
import { AppState } from "reducers";
|
||||
import {
|
||||
|
|
@ -36,19 +34,49 @@ import {
|
|||
getApplicationList,
|
||||
getIsSavingAppName,
|
||||
} from "selectors/applicationSelectors";
|
||||
import EditableTextWrapper from "components/ads/EditableTextWrapper";
|
||||
import EditableAppName from "./EditableAppName";
|
||||
import Boxed from "components/editorComponents/Onboarding/Boxed";
|
||||
import OnboardingToolTip from "components/editorComponents/Onboarding/Tooltip";
|
||||
import { OnboardingStep } from "constants/OnboardingConstants";
|
||||
import { Position } from "@blueprintjs/core";
|
||||
import Indicator from "components/editorComponents/Onboarding/Indicator";
|
||||
import ProfileDropdown from "pages/common/ProfileDropdown";
|
||||
import { getCurrentUser } from "selectors/usersSelectors";
|
||||
import { ANONYMOUS_USERNAME } from "constants/userConstants";
|
||||
import Button, { Size } from "components/ads/Button";
|
||||
import { IconWrapper } from "components/ads/Icon";
|
||||
import { Profile } from "pages/common/ProfileImage";
|
||||
import { getTypographyByKey } from "constants/DefaultTheme";
|
||||
|
||||
const HeaderWrapper = styled(StyledHeader)`
|
||||
background: ${Colors.BALTIC_SEA};
|
||||
height: 48px;
|
||||
color: white;
|
||||
padding-right: 0;
|
||||
padding-left: ${(props) => props.theme.spaces[7]}px;
|
||||
background-color: ${(props) => props.theme.colors.header.background};
|
||||
height: ${(props) => props.theme.smallHeaderHeight};
|
||||
flex-direction: row;
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.05);
|
||||
& .editable-application-name {
|
||||
${(props) => getTypographyByKey(props, "h4")}
|
||||
color: ${(props) => props.theme.colors.header.appName};
|
||||
}
|
||||
|
||||
& .header__application-share-btn {
|
||||
background-color: ${(props) => props.theme.colors.header.background};
|
||||
border-color: ${(props) => props.theme.colors.header.background};
|
||||
// margin-right: ${(props) => props.theme.spaces[1]}px;
|
||||
}
|
||||
|
||||
& .header__application-share-btn:hover {
|
||||
color: ${(props) => props.theme.colors.header.shareBtnHighlight};
|
||||
${IconWrapper} path {
|
||||
fill: ${(props) => props.theme.colors.header.shareBtnHighlight};
|
||||
}
|
||||
}
|
||||
|
||||
& ${Profile} {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
const HeaderSection = styled.div`
|
||||
|
|
@ -59,21 +87,16 @@ const HeaderSection = styled.div`
|
|||
justify-content: flex-start;
|
||||
}
|
||||
:nth-child(2) {
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
:nth-child(3) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
`;
|
||||
|
||||
const AppsmithLogoImg = styled.img`
|
||||
max-width: 110px;
|
||||
margin-right: ${(props) => props.theme.spaces[6]}px;
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
const SaveStatusContainer = styled.div`
|
||||
margin: 0 10px;
|
||||
border: 1px solid rgb(95, 105, 116);
|
||||
border-radius: 50%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
|
@ -83,34 +106,16 @@ const SaveStatusContainer = styled.div`
|
|||
`;
|
||||
const DeploySection = styled.div`
|
||||
display: flex;
|
||||
align-self: flex-end;
|
||||
`;
|
||||
|
||||
const DeployButton = styled(Button)`
|
||||
height: 32px;
|
||||
margin: 5px 10px;
|
||||
margin-right: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
const ProfileDropdownContainer = styled.div`
|
||||
margin: 0 ${(props) => props.theme.spaces[7]}px;
|
||||
`;
|
||||
|
||||
const DeployLinkButton = styled(Button)`
|
||||
height: 32px;
|
||||
margin: 5px 10px;
|
||||
margin-left: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
min-width: 20px !important;
|
||||
width: 20px !important;
|
||||
background-color: rgb(42, 195, 157) !important;
|
||||
border: none !important;
|
||||
`;
|
||||
|
||||
const ShareButton = styled(Button)`
|
||||
height: 32px;
|
||||
margin: 5px 10px;
|
||||
color: white !important;
|
||||
border-color: rgb(95, 105, 116) !important;
|
||||
const StyledDeployButton = styled(Button)`
|
||||
height: ${(props) => props.theme.smallHeaderHeight};
|
||||
${(props) => getTypographyByKey(props, "btnLarge")}
|
||||
padding: ${(props) => props.theme.spaces[2]}px;
|
||||
`;
|
||||
|
||||
type EditorHeaderProps = {
|
||||
|
|
@ -132,7 +137,6 @@ export const EditorHeader = (props: EditorHeaderProps) => {
|
|||
isSaving,
|
||||
pageSaveError,
|
||||
pageId,
|
||||
isPublishing,
|
||||
orgId,
|
||||
applicationId,
|
||||
publishApplication,
|
||||
|
|
@ -141,6 +145,7 @@ export const EditorHeader = (props: EditorHeaderProps) => {
|
|||
const dispatch = useDispatch();
|
||||
const isSavingName = useSelector(getIsSavingAppName);
|
||||
const applicationList = useSelector(getApplicationList);
|
||||
const user = useSelector(getCurrentUser);
|
||||
|
||||
const handlePublish = () => {
|
||||
if (applicationId) {
|
||||
|
|
@ -189,23 +194,19 @@ export const EditorHeader = (props: EditorHeaderProps) => {
|
|||
return (
|
||||
<HeaderWrapper>
|
||||
<HeaderSection>
|
||||
<Link to={APPLICATIONS_URL}>
|
||||
<Link to={APPLICATIONS_URL} style={{ height: 24 }}>
|
||||
<AppsmithLogoImg
|
||||
src={AppsmithLogo}
|
||||
alt="Appsmith logo"
|
||||
className="t--appsmith-logo"
|
||||
/>
|
||||
</Link>
|
||||
</HeaderSection>
|
||||
<Boxed step={OnboardingStep.FINISH}>
|
||||
<HeaderSection flex-direction={"row"}>
|
||||
{currentApplication ? (
|
||||
<EditableTextWrapper
|
||||
variant="UNDERLINE"
|
||||
<Boxed step={OnboardingStep.FINISH}>
|
||||
{currentApplication && (
|
||||
<EditableAppName
|
||||
defaultValue={currentApplication.name || ""}
|
||||
editInteractionKind={EditInteractionKind.SINGLE}
|
||||
hideEditIcon={true}
|
||||
className="t--application-name"
|
||||
className="t--application-name editable-application-name"
|
||||
fill={false}
|
||||
savingState={
|
||||
isSavingName ? SavingState.STARTED : SavingState.NOT_STARTED
|
||||
|
|
@ -221,46 +222,21 @@ export const EditorHeader = (props: EditorHeaderProps) => {
|
|||
})
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{/* <PageName>{pageName} </PageName> */}
|
||||
</HeaderSection>
|
||||
</Boxed>
|
||||
)}
|
||||
</Boxed>
|
||||
</HeaderSection>
|
||||
<HeaderSection>
|
||||
<Boxed step={OnboardingStep.FINISH}>
|
||||
<SaveStatusContainer className={"t--save-status-container"}>
|
||||
{saveStatusIcon}
|
||||
</SaveStatusContainer>
|
||||
<ShareButton
|
||||
target="_blank"
|
||||
href="https://mail.google.com/mail/u/0/?view=cm&fs=1&to=feedback@appsmith.com&tf=1"
|
||||
text="Feedback"
|
||||
intent="none"
|
||||
outline
|
||||
size="small"
|
||||
className="t--application-feedback-btn"
|
||||
icon={
|
||||
<HeaderIcons.FEEDBACK
|
||||
color={Colors.WHITE}
|
||||
width={13}
|
||||
height={13}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<FormDialogComponent
|
||||
trigger={
|
||||
<ShareButton
|
||||
text="Share"
|
||||
intent="none"
|
||||
outline
|
||||
size="small"
|
||||
className="t--application-share-btn"
|
||||
icon={
|
||||
<HeaderIcons.SHARE
|
||||
color={Colors.WHITE}
|
||||
width={13}
|
||||
height={13}
|
||||
/>
|
||||
}
|
||||
<Button
|
||||
text={"Share"}
|
||||
icon={"share"}
|
||||
size={Size.small}
|
||||
className="t--application-share-btn header__application-share-btn"
|
||||
/>
|
||||
}
|
||||
canOutsideClickClose={true}
|
||||
|
|
@ -284,32 +260,28 @@ export const EditorHeader = (props: EditorHeaderProps) => {
|
|||
offset={{ left: 10 }}
|
||||
theme={"light"}
|
||||
>
|
||||
<DeployButton
|
||||
<StyledDeployButton
|
||||
fill
|
||||
onClick={handlePublish}
|
||||
text="Deploy"
|
||||
loading={isPublishing}
|
||||
intent="primary"
|
||||
filled
|
||||
size="small"
|
||||
text={"Deploy"}
|
||||
size={Size.small}
|
||||
className="t--application-publish-btn"
|
||||
icon={
|
||||
<HeaderIcons.DEPLOY
|
||||
color={Colors.WHITE}
|
||||
width={13}
|
||||
height={13}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Indicator>
|
||||
</OnboardingToolTip>
|
||||
<DeployLinkButtonDialog
|
||||
trigger={
|
||||
<DeployLinkButton icon="caret-down" filled intent="primary" />
|
||||
<StyledDeployButton icon={"downArrow"} size={Size.xxs} />
|
||||
}
|
||||
link={getApplicationViewerPageURL(applicationId, pageId)}
|
||||
/>
|
||||
</DeploySection>
|
||||
</Boxed>
|
||||
{user && user.username !== ANONYMOUS_USERNAME && (
|
||||
<ProfileDropdownContainer>
|
||||
<ProfileDropdown userName={user?.username || ""} hideThemeSwitch />
|
||||
</ProfileDropdownContainer>
|
||||
)}
|
||||
</HeaderSection>
|
||||
<HelpModal page={"Editor"} />
|
||||
</HeaderWrapper>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const SentryRoute = Sentry.withSentryRouting(Route);
|
|||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
height: calc(100vh - ${(props) => props.theme.headerHeight});
|
||||
height: calc(100vh - ${(props) => props.theme.smallHeaderHeight});
|
||||
`;
|
||||
|
||||
const EditorContainer = styled.div`
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ const QueryFormContainer = styled.form`
|
|||
flex-direction: column;
|
||||
padding: 20px 0px;
|
||||
width: 100%;
|
||||
height: calc(100vh - ${(props) => props.theme.headerHeight});
|
||||
height: calc(100vh - ${(props) => props.theme.smallHeaderHeight});
|
||||
a {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const QueryHomePage = styled.div`
|
|||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - ${(props) => props.theme.headerHeight});
|
||||
height: calc(100vh - ${(props) => props.theme.smallHeaderHeight});
|
||||
|
||||
.sectionHeader {
|
||||
font-weight: ${(props) => props.theme.fontWeights[2]};
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ const EditorWrapper = styled.div`
|
|||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
overflow: hidden;
|
||||
height: calc(100vh - ${(props) => props.theme.headerHeight});
|
||||
height: calc(100vh - ${(props) => props.theme.smallHeaderHeight});
|
||||
`;
|
||||
|
||||
const CanvasContainer = styled.section`
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ class Editor extends Component<Props> {
|
|||
public render() {
|
||||
if (!this.props.isEditorInitialized || !this.state.registered) {
|
||||
return (
|
||||
<CenteredWrapper style={{ height: "calc(100vh - 48px)" }}>
|
||||
<CenteredWrapper style={{ height: "calc(100vh - 35px)" }}>
|
||||
<Spinner />
|
||||
</CenteredWrapper>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ const Wrapper = styled.div<{ isVisible: boolean }>`
|
|||
top: 0;
|
||||
left: 0;
|
||||
width: ${(props) => (!props.isVisible ? "0px" : "100%")};
|
||||
height: calc(100vh - ${(props) => props.theme.headerHeight});
|
||||
height: calc(100vh - ${(props) => props.theme.smallHeaderHeight});
|
||||
background-color: ${(props) =>
|
||||
props.isVisible ? "rgba(0, 0, 0, 0.26)" : "transparent"};
|
||||
z-index: ${(props) => (props.isVisible ? 2 : -1)};
|
||||
|
|
|
|||
|
|
@ -13,10 +13,13 @@ import {
|
|||
} from "./CustomizedDropdown/dropdownHelpers";
|
||||
import { ReduxActionTypes } from "constants/ReduxActionConstants";
|
||||
import ProfileImage from "./ProfileImage";
|
||||
import { PopperModifiers } from "@blueprintjs/core";
|
||||
|
||||
type TagProps = CommonComponentProps & {
|
||||
onClick?: (text: string) => void;
|
||||
userName?: string;
|
||||
hideThemeSwitch?: boolean;
|
||||
modifiers?: PopperModifiers;
|
||||
};
|
||||
|
||||
const ProfileMenuStyle = createGlobalStyle`
|
||||
|
|
@ -63,6 +66,7 @@ export default function ProfileDropdown(props: TagProps) {
|
|||
className="profile-menu"
|
||||
position={Position.BOTTOM}
|
||||
target={Profile}
|
||||
modifiers={props.modifiers}
|
||||
>
|
||||
<UserInformation>
|
||||
<div className="user-image">{Profile}</div>
|
||||
|
|
@ -73,8 +77,12 @@ export default function ProfileDropdown(props: TagProps) {
|
|||
</div>
|
||||
</UserInformation>
|
||||
<MenuDivider />
|
||||
<ThemeSwitcher />
|
||||
<MenuDivider />
|
||||
{!props.hideThemeSwitch && (
|
||||
<>
|
||||
<ThemeSwitcher />
|
||||
<MenuDivider />
|
||||
</>
|
||||
)}
|
||||
<MenuItem
|
||||
icon="logout"
|
||||
text="Sign Out"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ export const Profile = styled.div<{ backgroundColor?: string }>`
|
|||
justify-content: center;
|
||||
cursor: pointer;
|
||||
background-color: ${(props) => props.backgroundColor};
|
||||
&& span {
|
||||
color: ${(props) => props.theme.colors.text.highlight};
|
||||
}
|
||||
`;
|
||||
|
||||
export default function ProfileImage(props: {
|
||||
|
|
|
|||
|
|
@ -190,9 +190,8 @@ const actionsReducer = createReducer(initialState, {
|
|||
): ActionDataState =>
|
||||
state.map((a) => {
|
||||
if (a.config.id === action.payload.actionId) {
|
||||
return { ...a, isLoading: false, data: action.payload.error };
|
||||
return { ...a, isLoading: false, data: action.payload.data };
|
||||
}
|
||||
|
||||
return a;
|
||||
}),
|
||||
[ReduxActionTypes.RUN_ACTION_REQUEST]: (
|
||||
|
|
|
|||
|
|
@ -271,6 +271,9 @@ const applicationsReducer = createReducer(initialState, {
|
|||
) => {
|
||||
return { ...state, isSavingAppName: false };
|
||||
},
|
||||
[ReduxActionTypes.RESET_CURRENT_APPLICATION]: (
|
||||
state: ApplicationsReduxState,
|
||||
) => ({ ...state, currentApplication: null }),
|
||||
});
|
||||
|
||||
export type creatingApplicationMap = Record<string, boolean>;
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ import {
|
|||
evaluateActionBindings,
|
||||
} from "./EvaluationsSaga";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { EMPTY_RESPONSE } from "../components/editorComponents/ApiResponseView";
|
||||
|
||||
export enum NavigationTargetType {
|
||||
SAME_WINDOW = "SAME_WINDOW",
|
||||
|
|
@ -479,6 +480,10 @@ export function* executeActionSaga(
|
|||
executeActionError({
|
||||
actionId: actionId,
|
||||
error,
|
||||
data: {
|
||||
...EMPTY_RESPONSE,
|
||||
body: "There was an error executing this action",
|
||||
},
|
||||
}),
|
||||
);
|
||||
Toaster.show({
|
||||
|
|
@ -537,20 +542,9 @@ function* executeActionTriggers(
|
|||
yield call(copySaga, trigger.payload, event);
|
||||
break;
|
||||
default:
|
||||
yield put(
|
||||
executeActionError({
|
||||
error: "Trigger type unknown",
|
||||
actionId: "",
|
||||
}),
|
||||
);
|
||||
log.error("Trigger type unknown", trigger.type);
|
||||
}
|
||||
} catch (e) {
|
||||
yield put(
|
||||
executeActionError({
|
||||
error: "Failed to execute action",
|
||||
actionId: "",
|
||||
}),
|
||||
);
|
||||
if (event.callback) event.callback({ success: false });
|
||||
}
|
||||
}
|
||||
|
|
@ -732,10 +726,12 @@ function* executePageLoadAction(pageAction: PageAction) {
|
|||
pageAction.timeoutInMillisecond,
|
||||
);
|
||||
if (isErrorResponse(response)) {
|
||||
const body = _.get(response, "data.body");
|
||||
let body = _.get(response, "data.body");
|
||||
let message = `The action "${pageAction.name}" has failed.`;
|
||||
|
||||
if (body) {
|
||||
if (_.isObject(body)) {
|
||||
body = JSON.stringify(body);
|
||||
}
|
||||
message += `\nERROR: "${body}"`;
|
||||
}
|
||||
|
||||
|
|
@ -746,6 +742,7 @@ function* executePageLoadAction(pageAction: PageAction) {
|
|||
error: _.get(response, "responseMeta.error", {
|
||||
message,
|
||||
}),
|
||||
data: createActionExecutionResponse(response),
|
||||
}),
|
||||
);
|
||||
PerformanceTracker.stopAsyncTracking(
|
||||
|
|
@ -779,6 +776,10 @@ function* executePageLoadAction(pageAction: PageAction) {
|
|||
error: {
|
||||
message: `The action "${pageAction.name}" has failed.`,
|
||||
},
|
||||
data: {
|
||||
...EMPTY_RESPONSE,
|
||||
body: "There was an error executing this action",
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { AppState } from "reducers";
|
|||
import {
|
||||
FetchApplicationPayload,
|
||||
setDefaultApplicationPageSuccess,
|
||||
resetCurrentApplication,
|
||||
} from "actions/applicationActions";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import {
|
||||
|
|
@ -380,6 +381,8 @@ export function* createApplicationSaga(
|
|||
},
|
||||
});
|
||||
} else {
|
||||
yield put(resetCurrentApplication());
|
||||
|
||||
const request: CreateApplicationRequest = {
|
||||
name: applicationName,
|
||||
icon: icon,
|
||||
|
|
|
|||
|
|
@ -127,13 +127,13 @@ enum ErrorEffectTypes {
|
|||
LOG_ERROR = "LOG_ERROR",
|
||||
}
|
||||
|
||||
export function* errorSaga(
|
||||
errorAction: ReduxAction<{
|
||||
error: ErrorPayloadType;
|
||||
show?: boolean;
|
||||
crash?: boolean;
|
||||
}>,
|
||||
) {
|
||||
export interface ErrorActionPayload {
|
||||
error: ErrorPayloadType;
|
||||
show?: boolean;
|
||||
crash?: boolean;
|
||||
}
|
||||
|
||||
export function* errorSaga(errorAction: ReduxAction<ErrorActionPayload>) {
|
||||
const effects = [ErrorEffectTypes.LOG_ERROR];
|
||||
const { type, payload } = errorAction;
|
||||
const { show = true, error } = payload || {};
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ import {
|
|||
SIGN_UP_URL,
|
||||
RESET_PASSWORD_URL,
|
||||
FORGOT_PASSWORD_URL,
|
||||
VIEWER_URL_REGEX,
|
||||
} from "constants/routes";
|
||||
import { theme, dark } from "constants/DefaultTheme";
|
||||
import { theme, dark, light } from "constants/DefaultTheme";
|
||||
|
||||
const enforceDarkThemeRoutes = [
|
||||
AUTH_LOGIN_URL,
|
||||
|
|
@ -21,7 +22,19 @@ const getShouldEnforceDarkTheme = () => {
|
|||
};
|
||||
|
||||
export const getThemeDetails = (state: AppState) => {
|
||||
if (getShouldEnforceDarkTheme()) {
|
||||
const currentPath = window.location.pathname;
|
||||
const isBuilderOrViewerPath = !!currentPath.match(VIEWER_URL_REGEX);
|
||||
const isViewerPath =
|
||||
isBuilderOrViewerPath && currentPath.indexOf("edit") === -1;
|
||||
|
||||
if (isViewerPath) {
|
||||
return {
|
||||
mode: state.ui.theme.mode,
|
||||
theme: { ...theme, colors: { ...theme.colors, ...light } },
|
||||
};
|
||||
}
|
||||
|
||||
if (isBuilderOrViewerPath || getShouldEnforceDarkTheme()) {
|
||||
return {
|
||||
mode: state.ui.theme.mode,
|
||||
theme: { ...theme, colors: { ...theme.colors, ...dark } },
|
||||
|
|
|
|||
38
app/client/src/utils/hooks/useThrottledRAF.ts
Normal file
38
app/client/src/utils/hooks/useThrottledRAF.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import React, { useRef, useCallback } from "react";
|
||||
|
||||
/**
|
||||
* Use requestAnimationFrame + setInterval with Hooks in a declarative way.
|
||||
* @see https://gist.github.com/Danziger/336e75b6675223ad805a88c2dfdcfd4a
|
||||
*/
|
||||
const useThrottledRAF = (
|
||||
callback: React.EffectCallback,
|
||||
delay: number | null,
|
||||
): [
|
||||
React.MutableRefObject<number | null>,
|
||||
React.MutableRefObject<number | null>,
|
||||
React.EffectCallback,
|
||||
] => {
|
||||
const intervalRef = useRef<number | null>(null);
|
||||
const rafRef = useRef<number | null>(null);
|
||||
|
||||
const run = useCallback(() => {
|
||||
if (typeof delay === "number") {
|
||||
intervalRef.current = window.setInterval(() => {
|
||||
rafRef.current = window.requestAnimationFrame(() => {
|
||||
callback();
|
||||
});
|
||||
}, delay);
|
||||
|
||||
// Clear interval and RAF if the components is unmounted or the delay changes:
|
||||
return () => {
|
||||
window.clearInterval(intervalRef.current || 0);
|
||||
window.cancelAnimationFrame(rafRef.current || 0);
|
||||
};
|
||||
}
|
||||
}, [delay, callback]);
|
||||
|
||||
// In case you want to manually clear the interval or RAF from the consuming component...:
|
||||
return [intervalRef, rafRef, run];
|
||||
};
|
||||
|
||||
export default useThrottledRAF;
|
||||
|
|
@ -4,7 +4,7 @@ import { WidgetType } from "constants/WidgetConstants";
|
|||
import InputComponent, {
|
||||
InputComponentProps,
|
||||
} from "components/designSystems/blueprint/InputComponent";
|
||||
import { EventType } from "constants/ActionConstants";
|
||||
import { EventType, ExecutionResult } from "constants/ActionConstants";
|
||||
import {
|
||||
WidgetPropertyValidationType,
|
||||
BASE_WIDGET_VALIDATION,
|
||||
|
|
@ -39,6 +39,7 @@ class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
|
|||
// onTextChanged: VALIDATION_TYPES.ACTION_SELECTOR,
|
||||
isRequired: VALIDATION_TYPES.BOOLEAN,
|
||||
isValid: VALIDATION_TYPES.BOOLEAN,
|
||||
resetOnSubmit: VALIDATION_TYPES.BOOLEAN,
|
||||
};
|
||||
}
|
||||
static getTriggerPropertyMap(): TriggerPropertiesMap {
|
||||
|
|
@ -137,6 +138,17 @@ class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
|
|||
this.props.updateWidgetMetaProperty("isFocused", focusState);
|
||||
};
|
||||
|
||||
onSubmitSuccess = (result: ExecutionResult) => {
|
||||
if (result.success && this.props.resetOnSubmit) {
|
||||
this.props.updateWidgetMetaProperty("text", "", {
|
||||
dynamicString: this.props.onTextChanged,
|
||||
event: {
|
||||
type: EventType.ON_TEXT_CHANGE,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyDown = (
|
||||
e:
|
||||
| React.KeyboardEvent<HTMLTextAreaElement>
|
||||
|
|
@ -148,6 +160,7 @@ class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
|
|||
dynamicString: this.props.onSubmit,
|
||||
event: {
|
||||
type: EventType.ON_SUBMIT,
|
||||
callback: this.onSubmitSuccess,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
package com.appsmith.external.exceptions;
|
||||
|
||||
public enum AppsmithErrorAction {
|
||||
DEFAULT,
|
||||
LOG_EXTERNALLY
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.appsmith.external.exceptions.pluginExceptions;
|
||||
|
||||
import com.appsmith.external.exceptions.AppsmithErrorAction;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
|
||||
@Getter
|
||||
public enum AppsmithPluginError {
|
||||
|
||||
PLUGIN_ERROR(500, 5000, "{0}", AppsmithErrorAction.LOG_EXTERNALLY),
|
||||
PLUGIN_GET_STRUCTURE_ERROR(500, 5001, "Failed to get database structure with error: {0}",
|
||||
AppsmithErrorAction.LOG_EXTERNALLY),
|
||||
PLUGIN_QUERY_TIMEOUT_ERROR(504, 5002, "{0} timed out in {1} milliseconds. " +
|
||||
"Please increase timeout. This can be found in Settings tab of {0}.", AppsmithErrorAction.DEFAULT),
|
||||
PLUGIN_GET_STRUCTURE_TIMEOUT_ERROR(504, 5003, "Plugin timed out when fetching structure.",
|
||||
AppsmithErrorAction.DEFAULT),
|
||||
PLUGIN_DATASOURCE_ARGUMENT_ERROR(500, 5004, "Plugin failed to connect to data source with error: {0}",
|
||||
AppsmithErrorAction.DEFAULT),
|
||||
PLUGIN_EXECUTE_ARGUMENT_ERROR(500, 5005, "Plugin failed to execute query with error: {0}",
|
||||
AppsmithErrorAction.DEFAULT),
|
||||
PLUGIN_JSON_PARSE_ERROR(500, 5006, "Plugin failed to parse JSON \"{0}\" with error: {1}",
|
||||
AppsmithErrorAction.DEFAULT),
|
||||
;
|
||||
|
||||
private final Integer httpErrorCode;
|
||||
private final Integer appErrorCode;
|
||||
private final String message;
|
||||
private final AppsmithErrorAction errorAction;
|
||||
|
||||
AppsmithPluginError(Integer httpErrorCode, Integer appErrorCode, String message, AppsmithErrorAction errorAction,
|
||||
Object... args) {
|
||||
this.httpErrorCode = httpErrorCode;
|
||||
this.appErrorCode = appErrorCode;
|
||||
MessageFormat fmt = new MessageFormat(message);
|
||||
this.message = fmt.format(args);
|
||||
this.errorAction = errorAction;
|
||||
}
|
||||
|
||||
public String getMessage(Object... args) {
|
||||
return new MessageFormat(this.message).format(args);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
package com.appsmith.external.pluginExceptions;
|
||||
package com.appsmith.external.exceptions.pluginExceptions;
|
||||
|
||||
import com.appsmith.external.exceptions.AppsmithErrorAction;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class AppsmithPluginException extends Exception {
|
||||
|
||||
private final AppsmithPluginError error;
|
||||
private final Object[] args;
|
||||
|
||||
|
|
@ -29,4 +29,8 @@ public class AppsmithPluginException extends Exception {
|
|||
return this.error == null ? 0 : this.error.getAppErrorCode();
|
||||
}
|
||||
|
||||
public AppsmithErrorAction getErrorAction() {
|
||||
return this.error.getErrorAction();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.appsmith.external.pluginExceptions;
|
||||
package com.appsmith.external.exceptions.pluginExceptions;
|
||||
|
||||
public class StaleConnectionException extends RuntimeException {
|
||||
public StaleConnectionException() {
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
package com.appsmith.external.pluginExceptions;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
|
||||
@Getter
|
||||
public enum AppsmithPluginError {
|
||||
|
||||
PLUGIN_ERROR(500, 5000, "{0}"),
|
||||
PLUGIN_STRUCTURE_ERROR(500, 5001, "Failed to get database structure with error {0}"),
|
||||
PLUGIN_TIMEOUT_ERROR(504, 5002, "{0} timed out in {1} milliseconds. Please increase timeout. This can be found in Settings tab of {0}."),
|
||||
;
|
||||
|
||||
private final Integer httpErrorCode;
|
||||
private final Integer appErrorCode;
|
||||
private final String message;
|
||||
|
||||
AppsmithPluginError(Integer httpErrorCode, Integer appErrorCode, String message, Object... args) {
|
||||
this.httpErrorCode = httpErrorCode;
|
||||
this.appErrorCode = appErrorCode;
|
||||
MessageFormat fmt = new MessageFormat(message);
|
||||
this.message = fmt.format(args);
|
||||
}
|
||||
|
||||
public String getMessage(Object... args) {
|
||||
return new MessageFormat(this.message).format(args);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -85,5 +85,4 @@ public interface PluginExecutor<C> extends ExtensionPoint {
|
|||
default Mono<DatasourceStructure> getStructure(C connection, DatasourceConfiguration datasourceConfiguration) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import com.appsmith.external.models.DBAuth;
|
|||
import com.appsmith.external.models.DatasourceConfiguration;
|
||||
import com.appsmith.external.models.DatasourceTestResult;
|
||||
import com.appsmith.external.models.Endpoint;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.plugins.BasePlugin;
|
||||
import com.appsmith.external.plugins.PluginExecutor;
|
||||
import lombok.NonNull;
|
||||
|
|
@ -79,7 +79,7 @@ public class DynamoPlugin extends BasePlugin {
|
|||
final String action = actionConfiguration.getPath();
|
||||
if (StringUtils.isEmpty(action)) {
|
||||
return Mono.error(new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_ERROR,
|
||||
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
|
||||
"Missing action name (like `ListTables`, `GetItem` etc.)."
|
||||
));
|
||||
}
|
||||
|
|
@ -93,7 +93,7 @@ public class DynamoPlugin extends BasePlugin {
|
|||
} catch (IOException e) {
|
||||
final String message = "Error parsing the JSON body: " + e.getMessage();
|
||||
log.warn(message, e);
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, message));
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, message));
|
||||
}
|
||||
|
||||
final Class<?> requestClass;
|
||||
|
|
@ -142,7 +142,7 @@ public class DynamoPlugin extends BasePlugin {
|
|||
final DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
|
||||
if (authentication == null || StringUtils.isEmpty(authentication.getDatabaseName())) {
|
||||
return Mono.error(new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_ERROR,
|
||||
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
|
||||
"Missing region in datasource."
|
||||
));
|
||||
}
|
||||
|
|
@ -204,7 +204,6 @@ public class DynamoPlugin extends BasePlugin {
|
|||
)
|
||||
.subscribeOn(scheduler);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static String toLowerCamelCase(String action) {
|
||||
|
|
@ -247,7 +246,7 @@ public class DynamoPlugin extends BasePlugin {
|
|||
});
|
||||
if (setterMethod == null) {
|
||||
throw new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_ERROR,
|
||||
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
|
||||
"Invalid attribute/value by name " + entry.getKey()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import com.appsmith.external.models.DBAuth;
|
|||
import com.appsmith.external.models.DatasourceConfiguration;
|
||||
import com.appsmith.external.models.DatasourceTestResult;
|
||||
import com.appsmith.external.models.Endpoint;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.plugins.BasePlugin;
|
||||
import com.appsmith.external.plugins.PluginExecutor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
|
@ -82,7 +82,7 @@ public class ElasticSearchPlugin extends BasePlugin {
|
|||
} catch (IOException e) {
|
||||
final String message = "Error converting array to ND-JSON: " + e.getMessage();
|
||||
log.warn(message, e);
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, message));
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, message));
|
||||
}
|
||||
body = ndJsonBuilder.toString();
|
||||
}
|
||||
|
|
@ -125,7 +125,8 @@ public class ElasticSearchPlugin extends BasePlugin {
|
|||
try {
|
||||
url = new URL(endpoint.getHost());
|
||||
} catch (MalformedURLException e) {
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Invalid host provided. It should be of the form http(s)://your-es-url.com"));
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
|
||||
"Invalid host provided. It should be of the form http(s)://your-es-url.com"));
|
||||
}
|
||||
String scheme = "http";
|
||||
if (url.getProtocol() != null) {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import com.appsmith.external.models.DatasourceConfiguration;
|
|||
import com.appsmith.external.models.DatasourceStructure;
|
||||
import com.appsmith.external.models.DatasourceTestResult;
|
||||
import com.appsmith.external.models.Property;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.plugins.BasePlugin;
|
||||
import com.appsmith.external.plugins.PluginExecutor;
|
||||
import com.google.api.core.ApiFuture;
|
||||
|
|
@ -79,14 +79,14 @@ public class FirestorePlugin extends BasePlugin {
|
|||
|
||||
if (StringUtils.isBlank(path)) {
|
||||
return Mono.error(new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_ERROR,
|
||||
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
|
||||
"Document/Collection path cannot be empty"
|
||||
));
|
||||
}
|
||||
|
||||
if (path.startsWith("/") || path.endsWith("/")) {
|
||||
return Mono.error(new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_ERROR,
|
||||
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
|
||||
"Firestore paths should not begin or end with `/` character."
|
||||
));
|
||||
}
|
||||
|
|
@ -98,7 +98,7 @@ public class FirestorePlugin extends BasePlugin {
|
|||
|
||||
if (method == null) {
|
||||
return Mono.error(new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_ERROR,
|
||||
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
|
||||
"Missing Firestore method."
|
||||
));
|
||||
}
|
||||
|
|
@ -115,7 +115,7 @@ public class FirestorePlugin extends BasePlugin {
|
|||
return Mono.just(objectMapper.readValue(strBody, HashMap.class));
|
||||
} catch (IOException e) {
|
||||
return Mono.error(new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_ERROR,
|
||||
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
|
||||
e.getMessage()
|
||||
));
|
||||
}
|
||||
|
|
@ -123,7 +123,7 @@ public class FirestorePlugin extends BasePlugin {
|
|||
.flatMap(mapBody -> {
|
||||
if (mapBody.isEmpty() && method.isBodyNeeded()) {
|
||||
return Mono.error(new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_ERROR,
|
||||
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
|
||||
"The method " + method.toString() + " needs a non-empty body to work."
|
||||
));
|
||||
}
|
||||
|
|
@ -278,7 +278,7 @@ public class FirestorePlugin extends BasePlugin {
|
|||
return Mono.just(query1.whereArrayContainsAny(queryFieldPath, parseList(queryValue)));
|
||||
} catch (IOException e) {
|
||||
return Mono.error(new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_ERROR,
|
||||
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
|
||||
"Unable to parse condition value as a JSON list."
|
||||
));
|
||||
}
|
||||
|
|
@ -287,7 +287,7 @@ public class FirestorePlugin extends BasePlugin {
|
|||
return Mono.just(query1.whereIn(queryFieldPath, parseList(queryValue)));
|
||||
} catch (IOException e) {
|
||||
return Mono.error(new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_ERROR,
|
||||
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
|
||||
"Unable to parse condition value as a JSON list."
|
||||
));
|
||||
}
|
||||
|
|
@ -430,7 +430,8 @@ public class FirestorePlugin extends BasePlugin {
|
|||
|
||||
final Set<String> errors = validateDatasource(datasourceConfiguration);
|
||||
if (!CollectionUtils.isEmpty(errors)) {
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, errors.iterator().next()));
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
|
||||
errors.iterator().next()));
|
||||
}
|
||||
|
||||
final String projectId = authentication.getUsername();
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ import com.appsmith.external.models.DatasourceStructure;
|
|||
import com.appsmith.external.models.DatasourceTestResult;
|
||||
import com.appsmith.external.models.Endpoint;
|
||||
import com.appsmith.external.models.SSLDetails;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.plugins.BasePlugin;
|
||||
import com.appsmith.external.plugins.PluginExecutor;
|
||||
import com.mongodb.MongoCommandException;
|
||||
|
|
@ -196,12 +196,23 @@ public class MongoPlugin extends BasePlugin {
|
|||
* Ref: https://api.mongodb.com/java/2.13/com/mongodb/DB.html#setReadOnly-java.lang.Boolean-
|
||||
*/
|
||||
|
||||
try {
|
||||
return Mono.just(MongoClients.create(buildClientURI(datasourceConfiguration)))
|
||||
.subscribeOn(scheduler);
|
||||
} catch (Exception e) {
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e));
|
||||
}
|
||||
return Mono.just(MongoClients.create(buildClientURI(datasourceConfiguration)))
|
||||
.onErrorMap(
|
||||
IllegalArgumentException.class,
|
||||
error ->
|
||||
new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
|
||||
error.getMessage()
|
||||
)
|
||||
)
|
||||
.onErrorMap(e -> {
|
||||
if(!(e instanceof AppsmithPluginException)) {
|
||||
return new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e.getMessage());
|
||||
}
|
||||
|
||||
return e;
|
||||
})
|
||||
.subscribeOn(scheduler);
|
||||
}
|
||||
|
||||
public static String buildClientURI(DatasourceConfiguration datasourceConfiguration) {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import static org.junit.Assert.assertFalse;
|
|||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
|
@ -152,7 +153,10 @@ public class MongoPluginTest {
|
|||
*/
|
||||
MongoPlugin.MongoPluginExecutor mongoPluginExecutor = new MongoPlugin.MongoPluginExecutor();
|
||||
MongoPlugin.MongoPluginExecutor spyMongoPluginExecutor = spy(mongoPluginExecutor);
|
||||
when(spyMongoPluginExecutor.datasourceCreate(any())).thenReturn(Mono.error(mockMongoCommandException));
|
||||
/* Please check this out before modifying this line: https://stackoverflow
|
||||
* .com/questions/11620103/mockito-trying-to-spy-on-method-is-calling-the-original-method
|
||||
*/
|
||||
doReturn(Mono.error(mockMongoCommandException)).when(spyMongoPluginExecutor).datasourceCreate(any());
|
||||
|
||||
/*
|
||||
* 1. Test that MongoCommandException with error code "Unauthorized" is caught and no error is reported.
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import com.appsmith.external.models.DatasourceConfiguration;
|
|||
import com.appsmith.external.models.DatasourceTestResult;
|
||||
import com.appsmith.external.models.Endpoint;
|
||||
import com.appsmith.external.models.SSLDetails;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.plugins.BasePlugin;
|
||||
import com.appsmith.external.plugins.PluginExecutor;
|
||||
import lombok.NonNull;
|
||||
|
|
@ -83,7 +83,8 @@ public class MssqlPlugin extends BasePlugin {
|
|||
String query = actionConfiguration.getBody();
|
||||
|
||||
if (query == null) {
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Missing required parameter: Query."));
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "Missing required " +
|
||||
"parameter: Query."));
|
||||
}
|
||||
|
||||
List<Map<String, Object>> rowsList = new ArrayList<>(50);
|
||||
|
|
@ -152,7 +153,7 @@ public class MssqlPlugin extends BasePlugin {
|
|||
}
|
||||
|
||||
} catch (SQLException e) {
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e.getMessage()));
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, e.getMessage()));
|
||||
|
||||
} finally {
|
||||
if (resultSet != null) {
|
||||
|
|
@ -248,10 +249,9 @@ public class MssqlPlugin extends BasePlugin {
|
|||
|
||||
} catch (SQLException e) {
|
||||
return Mono.error(new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_ERROR,
|
||||
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
|
||||
"Error connecting to MsSQL: " + e.getMessage()
|
||||
));
|
||||
|
||||
}
|
||||
})
|
||||
.flatMap(obj -> obj)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import com.appsmith.external.models.ActionExecutionResult;
|
|||
import com.appsmith.external.models.DBAuth;
|
||||
import com.appsmith.external.models.DatasourceConfiguration;
|
||||
import com.appsmith.external.models.Endpoint;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import com.appsmith.external.models.DatasourceStructure;
|
|||
import com.appsmith.external.models.DatasourceTestResult;
|
||||
import com.appsmith.external.models.Endpoint;
|
||||
import com.appsmith.external.models.Property;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.plugins.BasePlugin;
|
||||
import com.appsmith.external.plugins.PluginExecutor;
|
||||
import io.r2dbc.spi.ColumnMetadata;
|
||||
|
|
@ -179,7 +179,8 @@ public class MySqlPlugin extends BasePlugin {
|
|||
String query = actionConfiguration.getBody().trim();
|
||||
|
||||
if (query == null) {
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Missing required parameter: Query."));
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "Missing required " +
|
||||
"parameter: Query."));
|
||||
}
|
||||
|
||||
boolean isSelectOrShowQuery = getIsSelectOrShowQuery(query);
|
||||
|
|
@ -192,6 +193,7 @@ public class MySqlPlugin extends BasePlugin {
|
|||
return Flux.error(new StaleConnectionException());
|
||||
}
|
||||
});
|
||||
|
||||
Mono<List<Map<String, Object>>> resultMono = null;
|
||||
|
||||
if (isSelectOrShowQuery) {
|
||||
|
|
@ -238,7 +240,6 @@ public class MySqlPlugin extends BasePlugin {
|
|||
@Override
|
||||
public Mono<Connection> datasourceCreate(DatasourceConfiguration datasourceConfiguration) {
|
||||
DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
|
||||
com.appsmith.external.models.Connection configurationConnection = datasourceConfiguration.getConnection();
|
||||
|
||||
StringBuilder urlBuilder = new StringBuilder();
|
||||
if (CollectionUtils.isEmpty(datasourceConfiguration.getEndpoints())) {
|
||||
|
|
@ -278,8 +279,10 @@ public class MySqlPlugin extends BasePlugin {
|
|||
|
||||
return (Mono<Connection>) Mono.from(ConnectionFactories.get(ob.build()).create())
|
||||
.onErrorResume(exception -> {
|
||||
log.debug("Error when creating datasource.", exception);
|
||||
return Mono.error(Exceptions.propagate(exception));
|
||||
return Mono.error(new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
|
||||
exception
|
||||
));
|
||||
})
|
||||
.subscribeOn(scheduler);
|
||||
}
|
||||
|
|
@ -502,7 +505,14 @@ public class MySqlPlugin extends BasePlugin {
|
|||
final Map<String, DatasourceStructure.Table> tablesByName = new LinkedHashMap<>();
|
||||
final Map<String, DatasourceStructure.Key> keyRegistry = new HashMap<>();
|
||||
|
||||
return Flux.from(connection.createStatement(COLUMNS_QUERY).execute())
|
||||
return Mono.from(connection.validate(ValidationDepth.REMOTE))
|
||||
.flatMapMany(isValid -> {
|
||||
if (isValid) {
|
||||
return connection.createStatement(COLUMNS_QUERY).execute();
|
||||
} else {
|
||||
return Flux.error(new StaleConnectionException());
|
||||
}
|
||||
})
|
||||
.flatMap(result -> {
|
||||
return result.map((row, meta) -> {
|
||||
getTableInfo(row, meta, tablesByName);
|
||||
|
|
@ -530,10 +540,15 @@ public class MySqlPlugin extends BasePlugin {
|
|||
|
||||
return structure;
|
||||
})
|
||||
.onErrorResume(error -> {
|
||||
log.debug("In getStructure function error mode.", error);
|
||||
.onErrorMap(e -> {
|
||||
if (!(e instanceof AppsmithPluginException) && !(e instanceof StaleConnectionException)) {
|
||||
return new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_ERROR,
|
||||
e.getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
return Mono.error(Exceptions.propagate(error));
|
||||
return e;
|
||||
})
|
||||
.subscribeOn(scheduler);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import com.appsmith.external.models.DatasourceConfiguration;
|
|||
import com.appsmith.external.models.DatasourceStructure;
|
||||
import com.appsmith.external.models.Endpoint;
|
||||
import com.appsmith.external.models.Property;
|
||||
import com.appsmith.external.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import com.appsmith.external.models.DatasourceStructure;
|
|||
import com.appsmith.external.models.DatasourceTestResult;
|
||||
import com.appsmith.external.models.Endpoint;
|
||||
import com.appsmith.external.models.SSLDetails;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.plugins.BasePlugin;
|
||||
import com.appsmith.external.plugins.PluginExecutor;
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
|
|
@ -118,7 +118,8 @@ public class PostgresPlugin extends BasePlugin {
|
|||
String query = actionConfiguration.getBody();
|
||||
// Check for query parameter before performing the probably expensive fetch connection from the pool op.
|
||||
if (query == null) {
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Missing required parameter: Query."));
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "Missing required " +
|
||||
"parameter: Query."));
|
||||
}
|
||||
|
||||
Connection connectionFromPool = null;
|
||||
|
|
@ -213,7 +214,7 @@ public class PostgresPlugin extends BasePlugin {
|
|||
|
||||
} catch (SQLException e) {
|
||||
System.out.println(Thread.currentThread().getName() + ": In the PostgresPlugin, got action execution error");
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e.getMessage()));
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, e.getMessage()));
|
||||
} finally {
|
||||
idleConnections = poolProxy.getIdleConnections();
|
||||
activeConnections = poolProxy.getActiveConnections();
|
||||
|
|
@ -503,7 +504,10 @@ public class PostgresPlugin extends BasePlugin {
|
|||
}
|
||||
|
||||
} catch (SQLException throwable) {
|
||||
return Mono.error(throwable);
|
||||
return Mono.error(new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_ERROR,
|
||||
throwable.getMessage()
|
||||
));
|
||||
} finally {
|
||||
idleConnections = poolProxy.getIdleConnections();
|
||||
activeConnections = poolProxy.getActiveConnections();
|
||||
|
|
|
|||
|
|
@ -2,12 +2,11 @@ package com.external.plugins;
|
|||
|
||||
import com.appsmith.external.models.ActionConfiguration;
|
||||
import com.appsmith.external.models.ActionExecutionResult;
|
||||
import com.appsmith.external.models.AuthenticationDTO;
|
||||
import com.appsmith.external.models.DBAuth;
|
||||
import com.appsmith.external.models.DatasourceConfiguration;
|
||||
import com.appsmith.external.models.DatasourceStructure;
|
||||
import com.appsmith.external.models.Endpoint;
|
||||
import com.appsmith.external.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import com.appsmith.external.models.ActionExecutionResult;
|
|||
import com.appsmith.external.models.DatasourceConfiguration;
|
||||
import com.appsmith.external.models.DatasourceTestResult;
|
||||
import com.appsmith.external.models.Property;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.plugins.BasePlugin;
|
||||
import com.appsmith.external.plugins.PluginExecutor;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
|
|
@ -62,7 +62,8 @@ public class RapidApiPlugin extends BasePlugin {
|
|||
ActionConfiguration actionConfiguration) {
|
||||
|
||||
if (StringUtils.isEmpty(RAPID_API_KEY_VALUE)) {
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "RapidAPI Key value not set."));
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "RapidAPI Key value " +
|
||||
"not set."));
|
||||
}
|
||||
|
||||
String requestBody = (actionConfiguration.getBody() == null) ? "" : actionConfiguration.getBody();
|
||||
|
|
@ -71,7 +72,8 @@ public class RapidApiPlugin extends BasePlugin {
|
|||
|
||||
HttpMethod httpMethod = actionConfiguration.getHttpMethod();
|
||||
if (httpMethod == null) {
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "HTTPMethod must be set."));
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "HTTPMethod must be " +
|
||||
"set."));
|
||||
}
|
||||
|
||||
WebClient.Builder webClientBuilder = WebClient.builder();
|
||||
|
|
@ -107,8 +109,7 @@ public class RapidApiPlugin extends BasePlugin {
|
|||
uri = createFinalUriWithQueryParams(httpUrl, actionConfiguration.getQueryParameters());
|
||||
log.info("Final URL is : {}", uri);
|
||||
} catch (URISyntaxException e) {
|
||||
e.printStackTrace();
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e));
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, e));
|
||||
}
|
||||
|
||||
// Build the body of the request in case of bodyFormData is not null
|
||||
|
|
@ -191,12 +192,12 @@ public class RapidApiPlugin extends BasePlugin {
|
|||
if (MediaType.APPLICATION_JSON.equals(contentType) ||
|
||||
MediaType.APPLICATION_JSON_UTF8.equals(contentType) ||
|
||||
MediaType.APPLICATION_JSON_UTF8_VALUE.equals(contentType)) {
|
||||
String jsonBody = new String(body);
|
||||
try {
|
||||
String jsonBody = new String(body);
|
||||
result.setBody(objectMapper.readTree(jsonBody));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
throw Exceptions.propagate(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e));
|
||||
throw Exceptions.propagate(new AppsmithPluginException(AppsmithPluginError.PLUGIN_JSON_PARSE_ERROR, jsonBody, e));
|
||||
}
|
||||
} else if (MediaType.IMAGE_GIF.equals(contentType) ||
|
||||
MediaType.IMAGE_JPEG.equals(contentType) ||
|
||||
|
|
@ -276,7 +277,8 @@ public class RapidApiPlugin extends BasePlugin {
|
|||
@Override
|
||||
public Mono<DatasourceTestResult> testDatasource(DatasourceConfiguration datasourceConfiguration) {
|
||||
return StringUtils.isEmpty(RAPID_API_KEY_VALUE)
|
||||
? Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "RapidAPI Key value not set."))
|
||||
? Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, "RapidAPI Key value " +
|
||||
"not set."))
|
||||
: Mono.just(new DatasourceTestResult());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import com.appsmith.external.models.DBAuth;
|
|||
import com.appsmith.external.models.DatasourceConfiguration;
|
||||
import com.appsmith.external.models.DatasourceTestResult;
|
||||
import com.appsmith.external.models.Endpoint;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.plugins.BasePlugin;
|
||||
import com.appsmith.external.plugins.PluginExecutor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
|
@ -51,7 +51,7 @@ public class RedisPlugin extends BasePlugin {
|
|||
return (Mono<ActionExecutionResult>) Mono.fromCallable(() -> {
|
||||
String body = actionConfiguration.getBody();
|
||||
if (StringUtils.isNullOrEmpty(body)) {
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR,
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
|
||||
String.format("Body is null or empty [%s]", body)));
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ public class RedisPlugin extends BasePlugin {
|
|||
// Commands are in upper case
|
||||
command = Protocol.Command.valueOf(bodySplitted[0].toUpperCase());
|
||||
} catch (IllegalArgumentException exc) {
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR,
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
|
||||
String.format("Not a valid Redis command:%s", bodySplitted[0])));
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +106,8 @@ public class RedisPlugin extends BasePlugin {
|
|||
|
||||
return (Mono<Jedis>) Mono.fromCallable(() -> {
|
||||
if (datasourceConfiguration.getEndpoints().isEmpty()) {
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "No endpoint(s) configured"));
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, "No endpoint(s) " +
|
||||
"configured"));
|
||||
}
|
||||
|
||||
Endpoint endpoint = datasourceConfiguration.getEndpoints().get(0);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import com.appsmith.external.models.DBAuth;
|
|||
import com.appsmith.external.models.DatasourceConfiguration;
|
||||
import com.appsmith.external.models.DatasourceTestResult;
|
||||
import com.appsmith.external.models.Endpoint;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import com.appsmith.external.models.DatasourceStructure;
|
|||
import com.appsmith.external.models.DatasourceTestResult;
|
||||
import com.appsmith.external.models.Endpoint;
|
||||
import com.appsmith.external.models.SSLDetails;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.plugins.BasePlugin;
|
||||
import com.appsmith.external.plugins.PluginExecutor;
|
||||
import lombok.NonNull;
|
||||
|
|
@ -220,7 +220,7 @@ public class RedshiftPlugin extends BasePlugin {
|
|||
if (query == null) {
|
||||
return Mono.error(
|
||||
new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_ERROR,
|
||||
AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR,
|
||||
"Missing required parameter: Query."
|
||||
)
|
||||
);
|
||||
|
|
@ -228,16 +228,12 @@ public class RedshiftPlugin extends BasePlugin {
|
|||
|
||||
return (Mono<ActionExecutionResult>) Mono.fromCallable(() -> {
|
||||
/*
|
||||
* 1. StaleConnectionException thrown by checkConnectionValidity(...) needs to be propagated to upper
|
||||
* layers so that a retry can be triggered.
|
||||
* 1. If there is any issue with checking connection validity then assume that the connection is stale.
|
||||
*/
|
||||
try {
|
||||
checkConnectionValidity(connection);
|
||||
} catch (SQLException error) {
|
||||
String error_msg = "Error checking validity of Redshift connection. " + error;
|
||||
System.out.println(Thread.currentThread().getName() + ": " + error_msg);
|
||||
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, error_msg));
|
||||
} catch (SQLException e) {
|
||||
return Mono.error(new StaleConnectionException());
|
||||
}
|
||||
|
||||
List<Map<String, Object>> rowsList = new ArrayList<>(50);
|
||||
|
|
@ -262,8 +258,8 @@ public class RedshiftPlugin extends BasePlugin {
|
|||
);
|
||||
|
||||
}
|
||||
} catch (SQLException | AppsmithPluginException e) {
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e.getMessage()));
|
||||
} catch (SQLException e) {
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, e.getMessage()));
|
||||
} finally {
|
||||
if (resultSet != null) {
|
||||
try {
|
||||
|
|
@ -292,6 +288,13 @@ public class RedshiftPlugin extends BasePlugin {
|
|||
return Mono.just(result);
|
||||
})
|
||||
.flatMap(obj -> obj)
|
||||
.onErrorMap(e -> {
|
||||
if(!(e instanceof AppsmithPluginException) && !(e instanceof StaleConnectionException)) {
|
||||
return new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e.getMessage());
|
||||
}
|
||||
|
||||
return e;
|
||||
})
|
||||
.subscribeOn(scheduler);
|
||||
}
|
||||
|
||||
|
|
@ -348,8 +351,12 @@ public class RedshiftPlugin extends BasePlugin {
|
|||
configurationConnection != null && READ_ONLY.equals(configurationConnection.getMode()));
|
||||
return Mono.just(connection);
|
||||
} catch (SQLException e) {
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Error connecting" +
|
||||
" to Redshift.", e));
|
||||
return Mono.error(
|
||||
new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
|
||||
e.getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
})
|
||||
.flatMap(obj -> obj)
|
||||
|
|
@ -567,16 +574,12 @@ public class RedshiftPlugin extends BasePlugin {
|
|||
@Override
|
||||
public Mono<DatasourceStructure> getStructure(Connection connection, DatasourceConfiguration datasourceConfiguration) {
|
||||
/*
|
||||
* 1. StaleConnectionException thrown by checkConnectionValidity(...) needs to be propagated to upper
|
||||
* layers so that a retry can be triggered.
|
||||
* 1. If there is any issue with checking connection validity then assume that the connection is stale.
|
||||
*/
|
||||
try {
|
||||
checkConnectionValidity(connection);
|
||||
} catch (SQLException error) {
|
||||
String error_msg = "Error checking validity of Redshift connection. " + error;
|
||||
System.out.println(Thread.currentThread().getName() + ": " + error_msg);
|
||||
|
||||
return Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, error_msg));
|
||||
} catch (SQLException e) {
|
||||
return Mono.error(new StaleConnectionException());
|
||||
}
|
||||
|
||||
final DatasourceStructure structure = new DatasourceStructure();
|
||||
|
|
@ -602,8 +605,17 @@ public class RedshiftPlugin extends BasePlugin {
|
|||
|
||||
// Get templates for each table and put those in.
|
||||
getTemplates(tablesByName);
|
||||
} catch (SQLException | AppsmithPluginException throwable) {
|
||||
return Mono.error(throwable);
|
||||
}
|
||||
catch (SQLException e) {
|
||||
return Mono.error(
|
||||
new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_GET_STRUCTURE_ERROR,
|
||||
e.getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
catch (AppsmithPluginException e) {
|
||||
return Mono.error(e);
|
||||
}
|
||||
|
||||
structure.setTables(new ArrayList<>(tablesByName.values()));
|
||||
|
|
@ -615,6 +627,13 @@ public class RedshiftPlugin extends BasePlugin {
|
|||
return structure;
|
||||
})
|
||||
.map(resultStructure -> (DatasourceStructure) resultStructure)
|
||||
.onErrorMap(e -> {
|
||||
if(!(e instanceof AppsmithPluginException)) {
|
||||
return new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e.getMessage());
|
||||
}
|
||||
|
||||
return e;
|
||||
})
|
||||
.subscribeOn(scheduler);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import com.appsmith.external.models.DBAuth;
|
|||
import com.appsmith.external.models.DatasourceConfiguration;
|
||||
import com.appsmith.external.models.DatasourceStructure;
|
||||
import com.appsmith.external.models.Endpoint;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.plugins.PluginExecutor;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
|
@ -87,9 +87,16 @@ public class RedshiftPluginTest {
|
|||
Mono<Connection> dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig);
|
||||
|
||||
StepVerifier.create(dsConnectionMono)
|
||||
.expectErrorMatches(throwable -> throwable instanceof AppsmithPluginException && throwable.getMessage()
|
||||
.equals(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Error connecting" +
|
||||
" to Redshift.").getMessage()))
|
||||
.expectErrorMatches(throwable ->
|
||||
throwable instanceof AppsmithPluginException &&
|
||||
throwable.getMessage().equals(
|
||||
new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
|
||||
"The connection attempt failed."
|
||||
)
|
||||
.getMessage()
|
||||
)
|
||||
)
|
||||
.verify();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.external.connections;
|
|||
import com.appsmith.external.models.AuthenticationDTO;
|
||||
import com.appsmith.external.models.OAuth2;
|
||||
import com.appsmith.external.models.UpdatableConnection;
|
||||
import com.appsmith.external.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import com.appsmith.external.models.ActionExecutionResult;
|
|||
import com.appsmith.external.models.DatasourceConfiguration;
|
||||
import com.appsmith.external.models.DatasourceTestResult;
|
||||
import com.appsmith.external.models.Property;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.plugins.BasePlugin;
|
||||
import com.appsmith.external.plugins.PluginExecutor;
|
||||
import com.external.connections.APIConnection;
|
||||
|
|
@ -202,7 +202,13 @@ public class RestApiPlugin extends BasePlugin {
|
|||
try {
|
||||
result.setHeaders(objectMapper.readTree(headerInJsonString));
|
||||
} catch (IOException e) {
|
||||
throw Exceptions.propagate(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e));
|
||||
throw Exceptions.propagate(
|
||||
new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_JSON_PARSE_ERROR,
|
||||
headerInJsonString,
|
||||
e.getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (body != null) {
|
||||
|
|
@ -216,7 +222,13 @@ public class RestApiPlugin extends BasePlugin {
|
|||
String jsonBody = new String(body);
|
||||
result.setBody(objectMapper.readTree(jsonBody));
|
||||
} catch (IOException e) {
|
||||
throw Exceptions.propagate(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e));
|
||||
throw Exceptions.propagate(
|
||||
new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_JSON_PARSE_ERROR,
|
||||
new String(body),
|
||||
e.getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (MediaType.IMAGE_GIF.equals(contentType) ||
|
||||
MediaType.IMAGE_JPEG.equals(contentType) ||
|
||||
|
|
@ -263,7 +275,7 @@ public class RestApiPlugin extends BasePlugin {
|
|||
if (isSendSessionEnabled) {
|
||||
if (StringUtils.isEmpty(secretKey) || secretKey.length() < 32) {
|
||||
throw new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_ERROR,
|
||||
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
|
||||
"Secret key is required when sending session details is switched on," +
|
||||
" and should be at least 32 characters in length."
|
||||
);
|
||||
|
|
@ -340,7 +352,8 @@ public class RestApiPlugin extends BasePlugin {
|
|||
objectFromJson(requestBodyAsString);
|
||||
} catch (JsonSyntaxException e) {
|
||||
return Mono.error(new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_ERROR,
|
||||
AppsmithPluginError.PLUGIN_JSON_PARSE_ERROR,
|
||||
requestBodyAsString,
|
||||
"Malformed JSON: " + e.getMessage()
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.external.connections;
|
||||
|
||||
import com.appsmith.external.models.OAuth2;
|
||||
import com.appsmith.external.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
|
|
|
|||
|
|
@ -133,6 +133,6 @@ public class User extends BaseDomain implements UserDetails, OidcUser {
|
|||
@Transient
|
||||
@JsonIgnore
|
||||
public String computeFirstName() {
|
||||
return StringUtils.isEmpty(name) ? "" : name.split("[\\s@]+", 2)[0];
|
||||
return (StringUtils.isEmpty(name) ? email : name).split("[\\s@]+", 2)[0];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,8 @@ package com.appsmith.server.exceptions;
|
|||
import lombok.Getter;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import com.appsmith.external.exceptions.AppsmithErrorAction;
|
||||
|
||||
enum AppsmithErrorAction {
|
||||
DEFAULT,
|
||||
LOG_EXTERNALLY
|
||||
}
|
||||
@Getter
|
||||
public enum AppsmithError {
|
||||
INVALID_PARAMETER(400, 4000, "Please enter a valid parameter {0}.", AppsmithErrorAction.DEFAULT),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
package com.appsmith.server.exceptions;
|
||||
import com.appsmith.external.exceptions.AppsmithErrorAction;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.appsmith.server.exceptions;
|
||||
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.exceptions.AppsmithErrorAction;
|
||||
import com.appsmith.server.dtos.ResponseDTO;
|
||||
import io.sentry.Sentry;
|
||||
import io.sentry.SentryLevel;
|
||||
|
|
@ -53,8 +54,14 @@ public class GlobalExceptionHandler {
|
|||
}
|
||||
);
|
||||
|
||||
if (error instanceof AppsmithException) {
|
||||
if (((AppsmithException) error).getErrorAction() == AppsmithErrorAction.LOG_EXTERNALLY) {
|
||||
if (error instanceof AppsmithException || error instanceof AppsmithPluginException) {
|
||||
if (error instanceof AppsmithException
|
||||
&& ((AppsmithException)error).getErrorAction() == AppsmithErrorAction.LOG_EXTERNALLY) {
|
||||
Sentry.captureException(error);
|
||||
}
|
||||
|
||||
if (error instanceof AppsmithPluginException
|
||||
&& ((AppsmithPluginException)error).getErrorAction() == AppsmithErrorAction.LOG_EXTERNALLY) {
|
||||
Sentry.captureException(error);
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package com.appsmith.server.services;
|
||||
|
||||
import com.appsmith.external.models.AuthenticationDTO;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.models.UpdatableConnection;
|
||||
import com.appsmith.external.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.plugins.PluginExecutor;
|
||||
import com.appsmith.server.domains.Datasource;
|
||||
import com.appsmith.server.domains.DatasourceContext;
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ import com.appsmith.external.models.Param;
|
|||
import com.appsmith.external.models.Policy;
|
||||
import com.appsmith.external.models.Property;
|
||||
import com.appsmith.external.models.Provider;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.plugins.PluginExecutor;
|
||||
import com.appsmith.server.acl.AclPermission;
|
||||
import com.appsmith.server.acl.PolicyGenerator;
|
||||
|
|
@ -592,7 +592,7 @@ public class NewActionServiceImpl extends BaseService<NewActionRepository, NewAc
|
|||
.timeout(Duration.ofMillis(timeoutDuration))
|
||||
.onErrorMap(TimeoutException.class,
|
||||
error -> new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_TIMEOUT_ERROR,
|
||||
AppsmithPluginError.PLUGIN_QUERY_TIMEOUT_ERROR,
|
||||
action.getName(), timeoutDuration
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ package com.appsmith.server.solutions;
|
|||
|
||||
import com.appsmith.external.models.AuthenticationDTO;
|
||||
import com.appsmith.external.models.DatasourceStructure;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.plugins.PluginExecutor;
|
||||
import com.appsmith.server.constants.FieldName;
|
||||
import com.appsmith.server.domains.Datasource;
|
||||
|
|
@ -22,8 +22,10 @@ import org.springframework.stereotype.Component;
|
|||
import org.springframework.util.CollectionUtils;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.net.UnknownHostException;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
|
|
@ -43,7 +45,22 @@ public class DatasourceStructureSolution {
|
|||
public Mono<DatasourceStructure> getStructure(String datasourceId, boolean ignoreCache) {
|
||||
return datasourceService.getById(datasourceId)
|
||||
.flatMap(datasource -> getStructure(datasource, ignoreCache))
|
||||
.defaultIfEmpty(new DatasourceStructure());
|
||||
.defaultIfEmpty(new DatasourceStructure())
|
||||
.onErrorMap(
|
||||
IllegalArgumentException.class,
|
||||
error ->
|
||||
new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
|
||||
error.getMessage()
|
||||
)
|
||||
)
|
||||
.onErrorMap(e -> {
|
||||
if(!(e instanceof AppsmithPluginException)) {
|
||||
return new AppsmithPluginException(AppsmithPluginError.PLUGIN_GET_STRUCTURE_ERROR, e.getMessage());
|
||||
}
|
||||
|
||||
return e;
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<DatasourceStructure> getStructure(Datasource datasource, boolean ignoreCache) {
|
||||
|
|
@ -71,16 +88,36 @@ public class DatasourceStructureSolution {
|
|||
)
|
||||
)
|
||||
.timeout(Duration.ofSeconds(GET_STRUCTURE_TIMEOUT_SECONDS))
|
||||
.onErrorMap(
|
||||
TimeoutException.class,
|
||||
error -> new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_GET_STRUCTURE_TIMEOUT_ERROR,
|
||||
"Timed out when fetching structure"
|
||||
)
|
||||
)
|
||||
.onErrorMap(
|
||||
StaleConnectionException.class,
|
||||
error -> new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_ERROR,
|
||||
AppsmithPluginError.PLUGIN_GET_STRUCTURE_ERROR,
|
||||
"Secondary stale connection error."
|
||||
)
|
||||
)
|
||||
.onErrorMap(
|
||||
IllegalArgumentException.class,
|
||||
error ->
|
||||
new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
|
||||
error.getMessage()
|
||||
)
|
||||
)
|
||||
.onErrorMap(e -> {
|
||||
log.error("In the datasource structure error mode.", e);
|
||||
return new AppsmithPluginException(AppsmithPluginError.PLUGIN_STRUCTURE_ERROR, e.getMessage());
|
||||
|
||||
if(!(e instanceof AppsmithPluginException)) {
|
||||
return new AppsmithPluginException(AppsmithPluginError.PLUGIN_GET_STRUCTURE_ERROR, e.getMessage());
|
||||
}
|
||||
|
||||
return e;
|
||||
})
|
||||
.flatMap(structure -> datasource.getId() == null
|
||||
? Mono.empty()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
package com.appsmith.server.domains;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Slf4j
|
||||
public class UserTest {
|
||||
|
||||
@Test
|
||||
public void testFirstNameFromFullName() {
|
||||
User one = new User();
|
||||
one.setName("Sherlock Holmes");
|
||||
assertThat(one.computeFirstName()).isEqualTo("Sherlock");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstNameFromEmail() {
|
||||
User one = new User();
|
||||
one.setEmail("sherlock@gmail.com");
|
||||
assertThat(one.computeFirstName()).isEqualTo("sherlock");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFirstNameFromFullNameAndEmail() {
|
||||
User one = new User();
|
||||
one.setName("Sherlock Holmes");
|
||||
one.setEmail("sherlock@gmail.com");
|
||||
assertThat(one.computeFirstName()).isEqualTo("Sherlock");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -7,9 +7,9 @@ import com.appsmith.external.models.PaginationField;
|
|||
import com.appsmith.external.models.PaginationType;
|
||||
import com.appsmith.external.models.Policy;
|
||||
import com.appsmith.external.models.Property;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException;
|
||||
import com.appsmith.external.exceptions.pluginExceptions.StaleConnectionException;
|
||||
import com.appsmith.external.plugins.PluginExecutor;
|
||||
import com.appsmith.server.acl.AclPermission;
|
||||
import com.appsmith.server.constants.FieldName;
|
||||
|
|
@ -637,7 +637,7 @@ public class ActionServiceTest {
|
|||
StepVerifier.create(executionResultMono)
|
||||
.assertNext(result -> {
|
||||
assertThat(result.getIsExecutionSuccess()).isFalse();
|
||||
assertThat(result.getStatusCode()).isEqualTo(AppsmithPluginError.PLUGIN_TIMEOUT_ERROR.getAppErrorCode().toString());
|
||||
assertThat(result.getStatusCode()).isEqualTo(AppsmithPluginError.PLUGIN_QUERY_TIMEOUT_ERROR.getAppErrorCode().toString());
|
||||
})
|
||||
.verifyComplete();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ check_ports_occupied() {
|
|||
}' > /dev/null
|
||||
echo "+++++++++++ ERROR ++++++++++++++++++++++"
|
||||
echo "Appsmith requires ports 80 & 443 to be open. Please shut down any other service(s) that may be running on these ports."
|
||||
echo "You can run appsmith on another port following this guide https://docs.appsmith.com/v/v1.2.1/troubleshooting-guide/deployment-errors"
|
||||
echo "++++++++++++++++++++++++++++++++++++++++"
|
||||
echo ""
|
||||
exit 1
|
||||
|
|
@ -530,8 +531,7 @@ echo "Installing Appsmith to '$install_dir'."
|
|||
mkdir -p "$install_dir"
|
||||
echo ""
|
||||
|
||||
echo "Appsmith needs a MongoDB instance to run"
|
||||
if confirm y "Initialise a new database? (Recommended)"; then
|
||||
if confirm y "Is this a fresh installation?"; then
|
||||
mongo_host="mongo"
|
||||
mongo_database="appsmith"
|
||||
|
||||
|
|
@ -546,6 +546,7 @@ else
|
|||
read -rp 'Enter your existing appsmith mongo db host: ' mongo_host
|
||||
read -rp 'Enter your existing appsmith mongo root user: ' mongo_root_user
|
||||
read -srp 'Enter your existing appsmith mongo password: ' mongo_root_password
|
||||
echo ""
|
||||
read -rp 'Enter your existing appsmith mongo database name: ' mongo_database
|
||||
# It is possible that this isn't the first installation.
|
||||
echo ""
|
||||
|
|
@ -688,7 +689,8 @@ if [[ $status_code -ne 401 ]]; then
|
|||
echo "The containers didn't seem to start correctly. Please run the following command to check containers that may have errored out:"
|
||||
echo ""
|
||||
echo -e "cd \"$install_dir\" && sudo docker-compose ps -a"
|
||||
echo "For troubleshooting help, please reach out to us via our Discord server: https://discord.com/invite/rBTTVJp"
|
||||
echo "Please read our troubleshooting guide https://docs.appsmith.com/v/v1.2.1/troubleshooting-guide/deployment-errors"
|
||||
echo "or reach us on Discord for support https://discord.com/invite/rBTTVJp"
|
||||
echo "++++++++++++++++++++++++++++++++++++++++"
|
||||
curl -s --location --request POST 'https://hook.integromat.com/dkwb6i52am93pi30ojeboktvj32iw0fa' \
|
||||
--header 'Content-Type: text/plain' \
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user