Merge branch 'release' of https://github.com/appsmithorg/appsmith into feat/reactive-actions-run-behaviour
This commit is contained in:
commit
17bb150532
|
|
@ -263,11 +263,16 @@ export class LightModeTheme implements ColorModeTheme {
|
|||
|
||||
// Colder seeds require a bit more chroma to not seem completely washed out
|
||||
if (this.seedChroma > 0.09 && this.seedIsCold) {
|
||||
color.oklch.c = 0.09;
|
||||
color.oklch.c = 0.06;
|
||||
}
|
||||
|
||||
// Teal is quite intense in the perceived chroma, possibly because of the narrow range on both lighntess and chroma
|
||||
if (color.oklch.h >= 160 && color.oklch.h <= 235) {
|
||||
color.oklch.c = 0.04;
|
||||
}
|
||||
|
||||
if (this.seedChroma > 0.06 && !this.seedIsCold) {
|
||||
color.oklch.c = 0.06;
|
||||
color.oklch.c = 0.03;
|
||||
}
|
||||
|
||||
if (this.seedIsAchromatic) {
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ describe("bgAccentSubtle color", () => {
|
|||
"oklch(0.95 0.09 231)",
|
||||
).getColors();
|
||||
|
||||
expect(bgAccentSubtle).toBe("rgb(80.68% 97.025% 100%)");
|
||||
expect(bgAccentSubtle).toBe("rgb(84.002% 96.389% 100%)");
|
||||
});
|
||||
|
||||
it("should return correct color when seedLightness < 0.93", () => {
|
||||
|
|
@ -162,7 +162,7 @@ describe("bgAccentSubtle color", () => {
|
|||
"oklch(0.92 0.09 231)",
|
||||
).getColors();
|
||||
|
||||
expect(bgAccentSubtle).toBe("rgb(73.159% 94.494% 100%)");
|
||||
expect(bgAccentSubtle).toBe("rgb(80.804% 93.121% 99.673%)");
|
||||
});
|
||||
|
||||
it("should return correct color when seedChroma > 0.09 and hue is between 116 and 165", () => {
|
||||
|
|
@ -170,7 +170,7 @@ describe("bgAccentSubtle color", () => {
|
|||
"oklch(0.95 0.10 120)",
|
||||
).getColors();
|
||||
|
||||
expect(bgAccentSubtle).toBe("rgb(90.964% 97.964% 71.119%)");
|
||||
expect(bgAccentSubtle).toBe("rgb(91.961% 96.786% 79.187%)");
|
||||
});
|
||||
|
||||
it("should return correct color when seedChroma > 0.06 and hue is not between 116 and 165", () => {
|
||||
|
|
@ -178,7 +178,7 @@ describe("bgAccentSubtle color", () => {
|
|||
"oklch(0.95 0.07 170)",
|
||||
).getColors();
|
||||
|
||||
expect(bgAccentSubtle).toBe("rgb(75.944% 100% 91.359%)");
|
||||
expect(bgAccentSubtle).toBe("rgb(84.267% 97.811% 92.543%)");
|
||||
});
|
||||
|
||||
it("should return correct color when seedChroma < 0.04", () => {
|
||||
|
|
@ -196,7 +196,7 @@ describe("bgAccentSubtleHover color", () => {
|
|||
"oklch(0.35 0.09 70)",
|
||||
).getColors();
|
||||
|
||||
expect(bgAccentSubtleHover).toBe("rgb(100% 91.101% 76.695%)");
|
||||
expect(bgAccentSubtleHover).toBe("rgb(98.845% 92.363% 85.26%)");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -206,7 +206,7 @@ describe("bgAccentSubtleActive color", () => {
|
|||
"oklch(0.35 0.09 70)",
|
||||
).getColors();
|
||||
|
||||
expect(bgAccentSubtleActive).toBe("rgb(100% 87.217% 72.911%)");
|
||||
expect(bgAccentSubtleActive).toBe("rgb(94.908% 88.479% 81.43%)");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { Flex } from "@appsmith/ads";
|
|||
import styled from "styled-components";
|
||||
import { noop } from "lodash";
|
||||
import { EditableName, useIsRenaming } from "IDE";
|
||||
|
||||
import ImageAlt from "assets/images/placeholder-image.svg";
|
||||
export interface SaveActionNameParams {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -64,7 +64,7 @@ const PluginActionNameEditor = ({
|
|||
isFeatureEnabled,
|
||||
action?.userPermissions,
|
||||
);
|
||||
const iconUrl = getAssetUrl(plugin?.iconLocation) || "";
|
||||
const iconUrl = getAssetUrl(plugin?.iconLocation ?? ImageAlt);
|
||||
const icon = ActionUrlIcon(iconUrl);
|
||||
|
||||
const handleDoubleClick = isChangePermitted ? enterEditMode : noop;
|
||||
|
|
|
|||
|
|
@ -704,6 +704,8 @@ export const RECONNECT_MISSING_DATASOURCE_CREDENTIALS = () =>
|
|||
"Reconnect missing datasource credentials";
|
||||
export const RECONNECT_MISSING_DATASOURCE_CREDENTIALS_DESCRIPTION = () =>
|
||||
"Fill these with utmost care as the application will not behave normally otherwise";
|
||||
export const RECONNECT_MISSING_DATASOURCE_CREDENTIALS_DESCRIPTION_FOR_AGENTS =
|
||||
() => "Ensure your agent is ready by integrating the required datasources.";
|
||||
export const RECONNECT_DATASOURCE_SUCCESS_MESSAGE1 = () =>
|
||||
"These datasources were imported successfully!";
|
||||
export const RECONNECT_DATASOURCE_SUCCESS_MESSAGE2 = () =>
|
||||
|
|
@ -714,6 +716,7 @@ export const SKIP_TO_APPLICATION_TOOLTIP_HEADER = () =>
|
|||
export const SKIP_TO_APPLICATION_TOOLTIP_DESCRIPTION = () =>
|
||||
`Skip this step to configure datasources later`;
|
||||
export const SKIP_TO_APPLICATION = () => "Go to application";
|
||||
export const SKIP_TO_APPLICATION_FOR_AGENTS = () => "Go to agent";
|
||||
export const SKIP_CONFIGURATION = () => "Skip configuration";
|
||||
export const SELECT_A_METHOD_TO_ADD_CREDENTIALS = () =>
|
||||
"Select a method to add credentials";
|
||||
|
|
@ -2038,6 +2041,7 @@ export const SAVE_BUTTON_TEXT = () => "Save";
|
|||
export const TEST_BUTTON_TEXT = () => "Test configuration";
|
||||
export const SAVE_AND_AUTHORIZE_BUTTON_TEXT = () => "Save & Authorize";
|
||||
export const CONNECT_DATASOURCE_BUTTON_TEXT = () => "Connect Datasource";
|
||||
export const CONNECT_DATASOURCE_BUTTON_TEXT_FOR_AGENTS = () => "Connect";
|
||||
export const SAVE_AND_RE_AUTHORIZE_BUTTON_TEXT = () => "Save & Re-Authorize";
|
||||
export const DISCARD_POPUP_DONT_SAVE_BUTTON_TEXT = () => "Don't save";
|
||||
export const GSHEET_AUTHORISED_FILE_IDS_KEY = () => "userAuthorizedSheetIds";
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ import { IDE_TYPE } from "ee/IDE/Interfaces/IDETypes";
|
|||
import { builderURL } from "ee/RouteBuilder";
|
||||
import {
|
||||
RECONNECT_MISSING_DATASOURCE_CREDENTIALS_DESCRIPTION,
|
||||
RECONNECT_MISSING_DATASOURCE_CREDENTIALS_DESCRIPTION_FOR_AGENTS,
|
||||
SKIP_TO_APPLICATION,
|
||||
SKIP_TO_APPLICATION_FOR_AGENTS,
|
||||
createMessage,
|
||||
} from "ee/constants/messages";
|
||||
import { getIsAiAgentFlowEnabled } from "ee/selectors/aiAgentSelectors";
|
||||
import { getApplicationByIdFromWorkspaces } from "ee/selectors/applicationSelectors";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
|
|
@ -14,6 +17,7 @@ interface UseReconnectModalDataProps {
|
|||
}
|
||||
|
||||
function useReconnectModalData({ appId, pageId }: UseReconnectModalDataProps) {
|
||||
const isAiAgentFlowEnabled = useSelector(getIsAiAgentFlowEnabled);
|
||||
const application = useSelector((state) =>
|
||||
getApplicationByIdFromWorkspaces(state, appId ?? ""),
|
||||
);
|
||||
|
|
@ -26,12 +30,21 @@ function useReconnectModalData({ appId, pageId }: UseReconnectModalDataProps) {
|
|||
builderURL({
|
||||
basePageId,
|
||||
branch,
|
||||
params: {
|
||||
type: isAiAgentFlowEnabled ? "agent" : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
skipMessage: createMessage(SKIP_TO_APPLICATION),
|
||||
skipMessage: createMessage(
|
||||
isAiAgentFlowEnabled
|
||||
? SKIP_TO_APPLICATION_FOR_AGENTS
|
||||
: SKIP_TO_APPLICATION,
|
||||
),
|
||||
missingDsCredentialsDescription: createMessage(
|
||||
RECONNECT_MISSING_DATASOURCE_CREDENTIALS_DESCRIPTION,
|
||||
isAiAgentFlowEnabled
|
||||
? RECONNECT_MISSING_DATASOURCE_CREDENTIALS_DESCRIPTION_FOR_AGENTS
|
||||
: RECONNECT_MISSING_DATASOURCE_CREDENTIALS_DESCRIPTION,
|
||||
),
|
||||
editorURL,
|
||||
editorId: appId,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import type {
|
|||
UploadNavigationLogoRequest,
|
||||
} from "ee/api/ApplicationApi";
|
||||
import ApplicationApi from "ee/api/ApplicationApi";
|
||||
import { all, call, put, select, take } from "redux-saga/effects";
|
||||
import { all, call, fork, put, select, take } from "redux-saga/effects";
|
||||
|
||||
import { validateResponse } from "sagas/ErrorSagas";
|
||||
import {
|
||||
|
|
@ -121,6 +121,7 @@ import type { Page } from "entities/Page";
|
|||
import type { ApplicationPayload } from "entities/Application";
|
||||
import { objectKeys } from "@appsmith/utils";
|
||||
import { findDefaultPage } from "pages/utils";
|
||||
import { getIsAiAgentFlowEnabled } from "ee/selectors/aiAgentSelectors";
|
||||
|
||||
export let windowReference: Window | null = null;
|
||||
|
||||
|
|
@ -1004,6 +1005,7 @@ export function* initDatasourceConnectionDuringImport(
|
|||
}>,
|
||||
) {
|
||||
const workspaceId = action.payload.workspaceId;
|
||||
const isAgentFlowEnabled: boolean = yield select(getIsAiAgentFlowEnabled);
|
||||
|
||||
const pluginsAndDatasourcesCalls: boolean = yield failFastApiCalls(
|
||||
[fetchPlugins({ workspaceId }), fetchDatasources({ workspaceId })],
|
||||
|
|
@ -1032,9 +1034,13 @@ export function* initDatasourceConnectionDuringImport(
|
|||
});
|
||||
|
||||
yield all(
|
||||
datasources.map((datasource: Datasource) =>
|
||||
call(initializeDatasourceWithDefaultValues, datasource),
|
||||
),
|
||||
datasources.map((datasource: Datasource) => {
|
||||
if (isAgentFlowEnabled) {
|
||||
return fork(initializeDatasourceWithDefaultValues, datasource);
|
||||
}
|
||||
|
||||
return call(initializeDatasourceWithDefaultValues, datasource);
|
||||
}),
|
||||
);
|
||||
|
||||
if (!action.payload.isPartialImport) {
|
||||
|
|
|
|||
|
|
@ -598,15 +598,25 @@ export const getDatasourcePlugins = createSelector(getPlugins, (plugins) => {
|
|||
return plugins.filter((plugin) => plugin?.allowUserDatasources ?? true);
|
||||
});
|
||||
|
||||
export const getPluginImages = createSelector(getPlugins, (plugins) => {
|
||||
const pluginImages: Record<string, string> = {};
|
||||
export const getPluginImages = createSelector(
|
||||
getPlugins,
|
||||
getDatasources,
|
||||
(plugins, datasources) => {
|
||||
const pluginImages: Record<string, string> = {};
|
||||
|
||||
plugins.forEach((plugin) => {
|
||||
pluginImages[plugin.id] = plugin?.iconLocation ?? ImageAlt;
|
||||
});
|
||||
plugins.forEach((plugin) => {
|
||||
pluginImages[plugin.id] = plugin?.iconLocation ?? ImageAlt;
|
||||
});
|
||||
|
||||
return pluginImages;
|
||||
});
|
||||
datasources.forEach((datasource) => {
|
||||
if (!pluginImages[datasource.pluginId]) {
|
||||
pluginImages[datasource.pluginId] = ImageAlt;
|
||||
}
|
||||
});
|
||||
|
||||
return pluginImages;
|
||||
},
|
||||
);
|
||||
|
||||
export const getPluginNames = createSelector(getPlugins, (plugins) => {
|
||||
const pluginNames: Record<string, string> = {};
|
||||
|
|
@ -1676,7 +1686,7 @@ export const getQuerySegmentItems = createSelector(
|
|||
const items: EntityItem[] = actions.map((action) => {
|
||||
let group;
|
||||
const iconUrl = getAssetUrl(
|
||||
pluginGroups[action.config.pluginId]?.iconLocation,
|
||||
pluginGroups[action.config.pluginId]?.iconLocation ?? ImageAlt,
|
||||
);
|
||||
|
||||
if (action.config.pluginType === PluginType.API) {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import {
|
|||
getCurrentJSCollections,
|
||||
getQueryModuleInstances,
|
||||
getJSModuleInstancesData,
|
||||
getPluginImages,
|
||||
} from "ee/selectors/entitiesSelector";
|
||||
import {
|
||||
getModalDropdownList,
|
||||
|
|
@ -64,7 +65,7 @@ import { selectEvaluationVersion } from "ee/selectors/applicationSelectors";
|
|||
import { isJSAction } from "ee/workers/Evaluation/evaluationUtils";
|
||||
import type { DataTreeEntity } from "entities/DataTree/dataTreeTypes";
|
||||
import type { ModuleInstanceDataState } from "ee/constants/ModuleInstanceConstants";
|
||||
import { getModuleIcon, getPluginImagesFromPlugins } from "pages/Editor/utils";
|
||||
import { getModuleIcon } from "pages/Editor/utils";
|
||||
import { getAllModules } from "ee/selectors/modulesSelector";
|
||||
import type { Module } from "ee/constants/ModuleConstants";
|
||||
import {
|
||||
|
|
@ -420,6 +421,7 @@ export function getApiQueriesAndJSActionOptionsWithChildren(
|
|||
queryModuleInstances: ModuleInstanceDataState,
|
||||
jsModuleInstances: ReturnType<typeof getJSModuleInstancesData>,
|
||||
modules: Record<string, Module>,
|
||||
pluginImages: Record<string, string>,
|
||||
) {
|
||||
// this function gets a list of all the queries/apis and attaches it to actionList
|
||||
getApiAndQueryOptions(
|
||||
|
|
@ -429,6 +431,7 @@ export function getApiQueriesAndJSActionOptionsWithChildren(
|
|||
handleClose,
|
||||
queryModuleInstances,
|
||||
modules,
|
||||
pluginImages,
|
||||
);
|
||||
|
||||
// this function gets a list of all the JS Objects and attaches it to actionList
|
||||
|
|
@ -446,8 +449,8 @@ function getApiAndQueryOptions(
|
|||
handleClose: () => void,
|
||||
queryModuleInstances: ModuleInstanceDataState,
|
||||
modules: Record<string, Module>,
|
||||
pluginImages: Record<string, string>,
|
||||
) {
|
||||
const pluginImages = getPluginImagesFromPlugins(plugins);
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const pluginGroups: any = keyBy(plugins, "id");
|
||||
|
|
@ -686,6 +689,7 @@ export function useApisQueriesAndJsActionOptions(handleClose: () => void) {
|
|||
) as unknown as ModuleInstanceDataState;
|
||||
const jsModuleInstancesData = useSelector(getJSModuleInstancesData);
|
||||
const modules = useSelector(getAllModules);
|
||||
const pluginImages = useSelector(getPluginImages);
|
||||
|
||||
// this function gets all the Queries/API's/JS Objects and attaches it to actionList
|
||||
return getApiQueriesAndJSActionOptionsWithChildren(
|
||||
|
|
@ -698,5 +702,6 @@ export function useApisQueriesAndJsActionOptions(handleClose: () => void) {
|
|||
queryModuleInstances,
|
||||
jsModuleInstancesData,
|
||||
modules,
|
||||
pluginImages,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@ import { useSelector } from "react-redux";
|
|||
import { keyBy } from "lodash";
|
||||
import type { LogItemProps } from "../ErrorLogItem";
|
||||
import { Colors } from "constants/Colors";
|
||||
import { getPlugins } from "ee/selectors/entitiesSelector";
|
||||
import { getPluginImages, getPlugins } from "ee/selectors/entitiesSelector";
|
||||
import EntityLink from "../../EntityLink";
|
||||
import { DebuggerLinkUI } from "components/editorComponents/Debugger/DebuggerEntityLink";
|
||||
import { getIconForEntity } from "ee/components/editorComponents/Debugger/ErrorLogs/getLogIconForEntity";
|
||||
import { getPluginImagesFromPlugins } from "pages/Editor/utils";
|
||||
|
||||
const EntityLinkWrapper = styled.div`
|
||||
display: flex;
|
||||
|
|
@ -50,7 +49,7 @@ const getIcon = (props: LogItemProps, pluginImages: Record<string, string>) => {
|
|||
export default function LogEntityLink(props: LogItemProps) {
|
||||
const plugins = useSelector(getPlugins);
|
||||
const pluginGroups = useMemo(() => keyBy(plugins, "id"), [plugins]);
|
||||
const pluginImages = getPluginImagesFromPlugins(plugins);
|
||||
const pluginImages = useSelector(getPluginImages);
|
||||
|
||||
const plugin = props.iconId ? pluginGroups[props.iconId] : undefined;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
getAllJSCollections,
|
||||
getJSModuleInstancesData,
|
||||
getModuleInstances,
|
||||
getPluginImages,
|
||||
getPlugins,
|
||||
} from "ee/selectors/entitiesSelector";
|
||||
import store from "store";
|
||||
|
|
@ -115,6 +116,7 @@ class ActionSelectorControl extends BaseControl<ControlProps> {
|
|||
const queryModuleInstances = [] as ModuleInstanceDataState;
|
||||
const jsModuleInstances = getJSModuleInstancesData(state);
|
||||
const modules = getAllModules(state);
|
||||
const pluginImages = getPluginImages(state);
|
||||
|
||||
if (!!moduleInstances) {
|
||||
for (const moduleInstance of Object.values(moduleInstances)) {
|
||||
|
|
@ -174,6 +176,7 @@ class ActionSelectorControl extends BaseControl<ControlProps> {
|
|||
queryModuleInstances,
|
||||
jsModuleInstances,
|
||||
modules,
|
||||
pluginImages,
|
||||
);
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
import { noop } from "lodash";
|
||||
|
||||
export const toggleAISupportModal = noop;
|
||||
|
||||
// just a placeholder action to avoid type errors
|
||||
export const setCreateAgentModalOpen = ({ isOpen }: { isOpen: boolean }) => ({
|
||||
type: "",
|
||||
payload: { isOpen },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ import { useSelector } from "react-redux";
|
|||
import {
|
||||
getDatasources,
|
||||
getDatasourcesGroupedByPluginCategory,
|
||||
getPlugins,
|
||||
getPluginImages,
|
||||
} from "ee/selectors/entitiesSelector";
|
||||
import history from "utils/history";
|
||||
import { datasourcesEditorIdURL, integrationEditorURL } from "ee/RouteBuilder";
|
||||
import { getSelectedDatasourceId } from "ee/navigation/FocusSelectors";
|
||||
import { get, keyBy } from "lodash";
|
||||
import { get } from "lodash";
|
||||
import CreateDatasourceButton from "./CreateDatasourceButton";
|
||||
import { useLocation } from "react-router";
|
||||
import {
|
||||
|
|
@ -53,8 +53,7 @@ export const DataSidePane = (props: DataSidePaneProps) => {
|
|||
>("");
|
||||
const datasources = useSelector(getDatasources);
|
||||
const groupedDatasources = useSelector(getDatasourcesGroupedByPluginCategory);
|
||||
const plugins = useSelector(getPlugins);
|
||||
const groupedPlugins = keyBy(plugins, "id");
|
||||
const pluginImages = useSelector(getPluginImages);
|
||||
const location = useLocation();
|
||||
const goToDatasource = useCallback((id: string) => {
|
||||
history.push(datasourcesEditorIdURL({ datasourceId: id }));
|
||||
|
|
@ -123,9 +122,7 @@ export const DataSidePane = (props: DataSidePaneProps) => {
|
|||
title: data.name,
|
||||
startIcon: (
|
||||
<DatasourceIcon
|
||||
src={getAssetUrl(
|
||||
groupedPlugins[data.pluginId]?.iconLocation || "",
|
||||
)}
|
||||
src={getAssetUrl(pluginImages[data.pluginId])}
|
||||
/>
|
||||
),
|
||||
description: get(dsUsageMap, data.id, ""),
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ import DatasourceTabs from "../DatasourceInfo/DatasorceTabs";
|
|||
import DatasourceInformation, { ViewModeWrapper } from "./DatasourceSection";
|
||||
import { convertToPageIdSelector } from "selectors/pageListSelectors";
|
||||
import { getApplicationByIdFromWorkspaces } from "ee/selectors/applicationSelectors";
|
||||
import { getIsAiAgentFlowEnabled } from "ee/selectors/aiAgentSelectors";
|
||||
|
||||
interface ReduxStateProps {
|
||||
canDeleteDatasource: boolean;
|
||||
|
|
@ -143,6 +144,7 @@ interface ReduxStateProps {
|
|||
featureFlags?: FeatureFlags;
|
||||
isPluginAllowedToPreviewData: boolean;
|
||||
isOnboardingFlow?: boolean;
|
||||
isAiAgentFlowEnabled?: boolean;
|
||||
}
|
||||
|
||||
const Form = styled.div`
|
||||
|
|
@ -160,8 +162,11 @@ type Props = ReduxStateProps &
|
|||
basePageId: string;
|
||||
}>;
|
||||
|
||||
export const DSEditorWrapper = styled.div`
|
||||
height: calc(100vh - ${(props) => props.theme.headerHeight});
|
||||
export const DSEditorWrapper = styled.div<{ isAiAgentFlowEnabled?: boolean }>`
|
||||
height: ${(props) =>
|
||||
props.isAiAgentFlowEnabled
|
||||
? `auto`
|
||||
: `calc(100vh - ${props.theme.headerHeight})`};
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
@ -1022,6 +1027,7 @@ class DatasourceEditorRouter extends React.Component<Props, State> {
|
|||
>
|
||||
<DSEditorWrapper
|
||||
className={!!isOnboardingFlow ? "onboarding-flow" : ""}
|
||||
isAiAgentFlowEnabled={this.props.isAiAgentFlowEnabled}
|
||||
>
|
||||
{viewMode && !isInsideReconnectModal ? (
|
||||
this.renderTabsForViewMode()
|
||||
|
|
@ -1155,6 +1161,7 @@ const mapStateToProps = (state: AppState, props: any): ReduxStateProps => {
|
|||
|
||||
const featureFlags = selectFeatureFlags(state);
|
||||
const isFeatureEnabled = isGACEnabled(featureFlags);
|
||||
const isAiAgentFlowEnabled = getIsAiAgentFlowEnabled(state);
|
||||
|
||||
const canManageDatasource = getHasManageDatasourcePermission(
|
||||
isFeatureEnabled,
|
||||
|
|
@ -1223,6 +1230,7 @@ const mapStateToProps = (state: AppState, props: any): ReduxStateProps => {
|
|||
defaultKeyValueArrayConfig,
|
||||
initialValue,
|
||||
showDebugger,
|
||||
isAiAgentFlowEnabled,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import {
|
|||
ModalHeader,
|
||||
Text,
|
||||
} from "@appsmith/ads";
|
||||
import { useSelector } from "react-redux";
|
||||
import { getIsAiAgentFlowEnabled } from "ee/selectors/aiAgentSelectors";
|
||||
|
||||
const BodyContainer = styled.div`
|
||||
display: flex;
|
||||
|
|
@ -40,6 +42,7 @@ function ImportSuccessModal(props: ImportSuccessModalProps) {
|
|||
const importedAppSuccess = localStorage.getItem("importSuccess");
|
||||
// const isOpen = importedAppSuccess === "true";
|
||||
const [isOpen, setIsOpen] = useState(importedAppSuccess === "true");
|
||||
const isAgentFlowEnabled = useSelector(getIsAiAgentFlowEnabled);
|
||||
|
||||
const onClose = (open: boolean) => {
|
||||
if (!open) {
|
||||
|
|
@ -53,7 +56,7 @@ function ImportSuccessModal(props: ImportSuccessModalProps) {
|
|||
};
|
||||
|
||||
return (
|
||||
<Modal onOpenChange={onClose} open={isOpen}>
|
||||
<Modal onOpenChange={onClose} open={isOpen && !isAgentFlowEnabled}>
|
||||
<StyledModalContent className={"t--import-app-success-modal"}>
|
||||
<ModalHeader>Datasource configured</ModalHeader>
|
||||
<ModalBody>
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ import useReconnectModalData from "ee/pages/Editor/gitSync/useReconnectModalData
|
|||
import { resetImportData } from "ee/actions/workspaceActions";
|
||||
import { getLoadingTokenForDatasourceId } from "selectors/datasourceSelectors";
|
||||
import ReconnectDatasourceForm from "Datasource/components/ReconnectDatasourceForm";
|
||||
import { getIsAiAgentFlowEnabled } from "ee/selectors/aiAgentSelectors";
|
||||
|
||||
const Section = styled.div`
|
||||
display: flex;
|
||||
|
|
@ -86,10 +87,10 @@ const Section = styled.div`
|
|||
width: calc(100% - 206px);
|
||||
`;
|
||||
|
||||
const BodyContainer = styled.div`
|
||||
const BodyContainer = styled.div<{ isAiAgentFlowEnabled?: boolean }>`
|
||||
flex: 3;
|
||||
height: 640px;
|
||||
max-height: 82vh;
|
||||
height: ${(props) => (props.isAiAgentFlowEnabled ? "auto" : "640px")};
|
||||
max-height: ${(props) => (props.isAiAgentFlowEnabled ? "auto" : "82vh")};
|
||||
`;
|
||||
|
||||
// TODO: Removed usage of "t--" classes since they clash with the test classes.
|
||||
|
|
@ -190,6 +191,8 @@ const Message = styled.div`
|
|||
|
||||
const DBFormWrapper = styled.div`
|
||||
width: calc(100% - 206px);
|
||||
min-width: 400px;
|
||||
min-height: 360px;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
|
@ -215,8 +218,17 @@ const DBFormWrapper = styled.div`
|
|||
}
|
||||
`;
|
||||
|
||||
const ModalContentWrapper = styled(ModalContent)`
|
||||
width: 100%;
|
||||
const ModalHeaderWrapper = styled.div<{ isAgentFlowEnabled: boolean }>`
|
||||
& > div {
|
||||
padding-bottom: ${(props) =>
|
||||
props.isAgentFlowEnabled ? "0px" : undefined};
|
||||
}
|
||||
`;
|
||||
|
||||
const ModalContentWrapper = styled(ModalContent)<{
|
||||
isAiAgentFlowEnabled?: boolean;
|
||||
}>`
|
||||
width: ${(props) => (props.isAiAgentFlowEnabled ? "auto" : "100%")};
|
||||
`;
|
||||
const ModalBodyWrapper = styled(ModalBody)`
|
||||
overflow-y: hidden;
|
||||
|
|
@ -330,6 +342,7 @@ function ReconnectDatasourceModal() {
|
|||
parentEntityId, // appId or packageId from query params
|
||||
skipMessage,
|
||||
} = useReconnectModalData({ pageId, appId });
|
||||
const isAgentFlowEnabled = useSelector(getIsAiAgentFlowEnabled);
|
||||
|
||||
// when redirecting from oauth, processing the status
|
||||
if (isImport) {
|
||||
|
|
@ -614,17 +627,28 @@ function ReconnectDatasourceModal() {
|
|||
<Modal open={isModalOpen}>
|
||||
<ModalContentWrapper
|
||||
data-testid="reconnect-datasource-modal"
|
||||
isAiAgentFlowEnabled={isAgentFlowEnabled}
|
||||
onClick={handleClose}
|
||||
onEscapeKeyDown={onClose}
|
||||
onInteractOutside={handleClose}
|
||||
overlayClassName="reconnect-datasource-modal"
|
||||
>
|
||||
<ModalHeader>Reconnect datasources</ModalHeader>
|
||||
<ModalBodyWrapper>
|
||||
<BodyContainer>
|
||||
<Title>
|
||||
{createMessage(RECONNECT_MISSING_DATASOURCE_CREDENTIALS)}
|
||||
</Title>
|
||||
<ModalHeaderWrapper isAgentFlowEnabled={isAgentFlowEnabled}>
|
||||
<ModalHeader>
|
||||
{isAgentFlowEnabled
|
||||
? "Connect Datasources"
|
||||
: "Reconnect datasources"}
|
||||
</ModalHeader>
|
||||
</ModalHeaderWrapper>
|
||||
<ModalBodyWrapper
|
||||
style={{ padding: isAgentFlowEnabled ? "0px" : undefined }}
|
||||
>
|
||||
<BodyContainer isAiAgentFlowEnabled={isAgentFlowEnabled}>
|
||||
{!isAgentFlowEnabled && (
|
||||
<Title>
|
||||
{createMessage(RECONNECT_MISSING_DATASOURCE_CREDENTIALS)}
|
||||
</Title>
|
||||
)}
|
||||
|
||||
<Text>{missingDsCredentialsDescription}</Text>
|
||||
<ContentWrapper>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import styled from "styled-components";
|
|||
import { getAssetUrl } from "ee/utils/airgapHelpers";
|
||||
import { PluginImage } from "pages/Editor/DataSourceEditor/DSFormHeader";
|
||||
import { isEnvironmentConfigured } from "ee/utils/Environments";
|
||||
import ImageAlt from "assets/images/placeholder-image.svg";
|
||||
import type { Plugin } from "entities/Plugin";
|
||||
import {
|
||||
isDatasourceAuthorizedForQueryCreation,
|
||||
|
|
@ -76,7 +77,10 @@ function ListItemWrapper(props: {
|
|||
className={`t--ds-list ${selected ? "active" : ""}`}
|
||||
onClick={() => onClick(ds)}
|
||||
>
|
||||
<PluginImage alt="Datasource" src={getAssetUrl(plugin?.iconLocation)} />
|
||||
<PluginImage
|
||||
alt="Datasource"
|
||||
src={getAssetUrl(plugin?.iconLocation || ImageAlt)}
|
||||
/>
|
||||
<ListLabels>
|
||||
<Tooltip content={ds.name} placement="left">
|
||||
<DsTitle>
|
||||
|
|
|
|||
|
|
@ -28,8 +28,7 @@ import {
|
|||
JsFileIconV2,
|
||||
} from "pages/Editor/Explorer/ExplorerIcons";
|
||||
import { getAssetUrl } from "ee/utils/airgapHelpers";
|
||||
import { type Plugin, PluginType } from "entities/Plugin";
|
||||
import ImageAlt from "assets/images/placeholder-image.svg";
|
||||
import { PluginType } from "entities/Plugin";
|
||||
import { Icon } from "@appsmith/ads";
|
||||
import { objectKeys } from "@appsmith/utils";
|
||||
|
||||
|
|
@ -438,13 +437,3 @@ export function getModuleIcon(
|
|||
</EntityIcon>
|
||||
);
|
||||
}
|
||||
|
||||
export function getPluginImagesFromPlugins(plugins: Plugin[]) {
|
||||
const pluginImages: Record<string, string> = {};
|
||||
|
||||
plugins.forEach((plugin) => {
|
||||
pluginImages[plugin.id] = plugin?.iconLocation ?? ImageAlt;
|
||||
});
|
||||
|
||||
return pluginImages;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,11 +75,13 @@ const TemplateDatasources = styled.div`
|
|||
`;
|
||||
|
||||
export interface TemplateProps {
|
||||
hideForkTemplateButton: boolean;
|
||||
hideForkTemplateButton?: boolean;
|
||||
template: TemplateInterface;
|
||||
size?: string;
|
||||
onClick?: (id: string) => void;
|
||||
onForkTemplateClick?: (template: TemplateInterface) => void;
|
||||
hideFooter?: boolean;
|
||||
hideDescription?: boolean;
|
||||
}
|
||||
|
||||
const Template = (props: TemplateProps) => {
|
||||
|
|
@ -152,42 +154,46 @@ export function TemplateLayout(props: TemplateLayoutProps) {
|
|||
<Text className="categories" kind="heading-s" renderAs="h4">
|
||||
{functions.join(" • ")}
|
||||
</Text>
|
||||
<Text className="description" kind="body-m">
|
||||
{description}
|
||||
</Text>
|
||||
{!props.hideDescription && (
|
||||
<Text className="description" kind="body-m">
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</TemplateContent>
|
||||
|
||||
<TemplateContentFooter>
|
||||
<TemplateDatasources>
|
||||
{datasources.map((pluginPackageName) => {
|
||||
return (
|
||||
<DatasourceChip
|
||||
key={pluginPackageName}
|
||||
pluginPackageName={pluginPackageName}
|
||||
{!props.hideFooter && (
|
||||
<TemplateContentFooter>
|
||||
<TemplateDatasources>
|
||||
{datasources.map((pluginPackageName) => {
|
||||
return (
|
||||
<DatasourceChip
|
||||
key={pluginPackageName}
|
||||
pluginPackageName={pluginPackageName}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TemplateDatasources>
|
||||
{!props.hideForkTemplateButton && (
|
||||
<Tooltip
|
||||
content={createMessage(FORK_THIS_TEMPLATE)}
|
||||
placement={Position.BOTTOM}
|
||||
>
|
||||
<Button
|
||||
className="t--fork-template fork-button"
|
||||
data-testid="t--fork-template-button"
|
||||
isDisabled={isImportingTemplateToApp || !!loadingTemplateId}
|
||||
isIconButton
|
||||
isLoading={
|
||||
props.onForkTemplateClick && loadingTemplateId === id
|
||||
}
|
||||
onClick={onForkButtonTrigger}
|
||||
size="sm"
|
||||
startIcon="plus"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TemplateDatasources>
|
||||
{!props.hideForkTemplateButton && (
|
||||
<Tooltip
|
||||
content={createMessage(FORK_THIS_TEMPLATE)}
|
||||
placement={Position.BOTTOM}
|
||||
>
|
||||
<Button
|
||||
className="t--fork-template fork-button"
|
||||
data-testid="t--fork-template-button"
|
||||
isDisabled={isImportingTemplateToApp || !!loadingTemplateId}
|
||||
isIconButton
|
||||
isLoading={
|
||||
props.onForkTemplateClick && loadingTemplateId === id
|
||||
}
|
||||
onClick={onForkButtonTrigger}
|
||||
size="sm"
|
||||
startIcon="plus"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</TemplateContentFooter>
|
||||
</Tooltip>
|
||||
)}
|
||||
</TemplateContentFooter>
|
||||
)}
|
||||
</TemplateWrapper>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -123,9 +123,13 @@ export function TemplateContent(props: TemplateContentProps) {
|
|||
const isLoading = isFetchingApplications || isFetchingTemplates;
|
||||
|
||||
const filterWithAllowPageImport = props.filterWithAllowPageImport || false;
|
||||
const templates = useSelector(getSearchedTemplateList).filter((template) =>
|
||||
filterWithAllowPageImport ? !!template.allowPageImport : true,
|
||||
);
|
||||
const templates = useSelector(getSearchedTemplateList)
|
||||
.filter((template) =>
|
||||
filterWithAllowPageImport ? !!template.allowPageImport : true,
|
||||
)
|
||||
.filter((template) => {
|
||||
return template.title !== "AI Agent";
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingScreen text="Loading templates" />;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { AuthType, AuthenticationStatus } from "entities/Datasource";
|
|||
import {
|
||||
CANCEL,
|
||||
CONNECT_DATASOURCE_BUTTON_TEXT,
|
||||
CONNECT_DATASOURCE_BUTTON_TEXT_FOR_AGENTS,
|
||||
OAUTH_AUTHORIZATION_APPSMITH_ERROR,
|
||||
OAUTH_AUTHORIZATION_FAILED,
|
||||
SAVE_AND_AUTHORIZE_BUTTON_TEXT,
|
||||
|
|
@ -44,6 +45,7 @@ import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
|
|||
import { getHasManageDatasourcePermission } from "ee/utils/BusinessFeatures/permissionPageHelpers";
|
||||
import { resetCurrentPluginIdForCreateNewApp } from "actions/onboardingActions";
|
||||
import { useParentEntityDetailsFromParams } from "ee/entities/Engine/actionHelpers";
|
||||
import { getIsAiAgentFlowEnabled } from "ee/selectors/aiAgentSelectors";
|
||||
|
||||
interface Props {
|
||||
datasource: Datasource;
|
||||
|
|
@ -198,6 +200,8 @@ function DatasourceAuth({
|
|||
isInsideReconnectModal,
|
||||
);
|
||||
|
||||
const isAgentFlowEnabled = useSelector(getIsAiAgentFlowEnabled);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
authType === AuthType.OAUTH2 &&
|
||||
|
|
@ -450,7 +454,11 @@ function DatasourceAuth({
|
|||
onClick={handleOauthDatasourceSave}
|
||||
size="md"
|
||||
>
|
||||
{createMessage(CONNECT_DATASOURCE_BUTTON_TEXT)}
|
||||
{createMessage(
|
||||
isAgentFlowEnabled
|
||||
? CONNECT_DATASOURCE_BUTTON_TEXT_FOR_AGENTS
|
||||
: CONNECT_DATASOURCE_BUTTON_TEXT,
|
||||
)}
|
||||
</Button>
|
||||
),
|
||||
}[buttonType];
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ import {
|
|||
import { validateResponse } from "./ErrorSagas";
|
||||
import { failFastApiCalls } from "./InitSagas";
|
||||
import { getAllPageIdentities } from "./selectors";
|
||||
import { setCreateAgentModalOpen } from "ee/actions/aiAgentActions";
|
||||
import { getIsAiAgentFlowEnabled } from "ee/selectors/aiAgentSelectors";
|
||||
|
||||
const isAirgappedInstance = isAirgapped();
|
||||
const AI_DATASOURCE_NAME = "AI Datasource";
|
||||
|
|
@ -76,6 +78,8 @@ function* getAllTemplatesSaga() {
|
|||
function* importTemplateToWorkspaceSaga(
|
||||
action: ReduxAction<{ templateId: string; workspaceId: string }>,
|
||||
) {
|
||||
const isAiAgentFlowEnabled: boolean = yield select(getIsAiAgentFlowEnabled);
|
||||
|
||||
try {
|
||||
const response: ImportTemplateResponse = yield call(
|
||||
TemplatesAPI.importTemplate,
|
||||
|
|
@ -97,6 +101,10 @@ function* importTemplateToWorkspaceSaga(
|
|||
payload: response.data.application,
|
||||
});
|
||||
|
||||
if (isAiAgentFlowEnabled) {
|
||||
yield put(setCreateAgentModalOpen({ isOpen: false }));
|
||||
}
|
||||
|
||||
// Temporary fix to remove AI Datasource from the unConfiguredDatasourceList
|
||||
// so we can avoid showing the AI Datasource in reconnect datasource modal
|
||||
const filteredUnConfiguredDatasourceList = (
|
||||
|
|
@ -117,6 +125,9 @@ function* importTemplateToWorkspaceSaga(
|
|||
} else {
|
||||
const pageURL = builderURL({
|
||||
basePageId: application.defaultBasePageId,
|
||||
params: {
|
||||
type: isAiAgentFlowEnabled ? "agent" : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
history.push(pageURL);
|
||||
|
|
|
|||
|
|
@ -169,18 +169,6 @@ export const getExistingActionNames = createSelector(
|
|||
},
|
||||
);
|
||||
|
||||
export const getPluginIdToImageLocation = createSelector(
|
||||
getPlugins,
|
||||
(plugins) =>
|
||||
// TODO: Fix this the next time the file is edited
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
plugins.reduce((acc: any, p: Plugin) => {
|
||||
acc[p.id] = p.iconLocation;
|
||||
|
||||
return acc;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
/**
|
||||
* returns a objects of existing page name in data tree
|
||||
*
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ const fuzzySearchOptions = {
|
|||
};
|
||||
|
||||
const AGENT_TEMPLATES_USE_CASE = "Agent";
|
||||
const AGENT_TEMPLATES_TITLE = "AI Agent";
|
||||
|
||||
export const getTemplatesSelector = (state: AppState) =>
|
||||
state.ui.templates.templates;
|
||||
|
|
@ -36,22 +35,15 @@ export const getTemplatesByFlagSelector = createSelector(
|
|||
getIsAiAgentFlowEnabled,
|
||||
(templates, isAiAgentFlowEnabled) => {
|
||||
// For agents, we only show the templates that have the use case "Agent".
|
||||
// The "Agent" use case acts as a filter for use to just show the templates
|
||||
// The "Agent" use case acts as a filter for us to just show the templates
|
||||
// that are relevant to agents.
|
||||
return (
|
||||
templates
|
||||
.filter((template) => {
|
||||
if (isAiAgentFlowEnabled) {
|
||||
return template.useCases.includes(AGENT_TEMPLATES_USE_CASE);
|
||||
}
|
||||
return templates.filter((template) => {
|
||||
if (isAiAgentFlowEnabled) {
|
||||
return template.useCases.includes(AGENT_TEMPLATES_USE_CASE);
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
// We are using AI Agent template for creating ai agent app,
|
||||
// so we are not showing it in the templates list.
|
||||
// TODO: Once we have a new entity for ai agent, we need to remove this filter.
|
||||
.filter((template) => template.title !== AGENT_TEMPLATES_TITLE)
|
||||
);
|
||||
return template.useCases.includes(AGENT_TEMPLATES_USE_CASE) === false;
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package com.appsmith.external.constants.spans;
|
||||
|
||||
import static com.appsmith.external.constants.spans.BaseSpan.APPSMITH_SPAN_PREFIX;
|
||||
|
||||
public class LoginSpan {
|
||||
public static final String LOGIN_FAILURE = APPSMITH_SPAN_PREFIX + "login_failure";
|
||||
public static final String LOGIN_ATTEMPT = APPSMITH_SPAN_PREFIX + "login_total";
|
||||
}
|
||||
|
|
@ -2,12 +2,14 @@ package com.appsmith.server.authentication.handlers;
|
|||
|
||||
import com.appsmith.server.authentication.handlers.ce.AuthenticationFailureHandlerCE;
|
||||
import com.appsmith.server.authentication.helpers.AuthenticationFailureRetryHandler;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class AuthenticationFailureHandler extends AuthenticationFailureHandlerCE {
|
||||
|
||||
public AuthenticationFailureHandler(AuthenticationFailureRetryHandler authenticationFailureRetryHandler) {
|
||||
super(authenticationFailureRetryHandler);
|
||||
public AuthenticationFailureHandler(
|
||||
AuthenticationFailureRetryHandler authenticationFailureRetryHandler, MeterRegistry meterRegistry) {
|
||||
super(authenticationFailureRetryHandler, meterRegistry);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,52 @@
|
|||
package com.appsmith.server.authentication.handlers.ce;
|
||||
|
||||
import com.appsmith.server.authentication.helpers.AuthenticationFailureRetryHandler;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||
import org.springframework.security.web.server.WebFilterExchange;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static com.appsmith.external.constants.spans.LoginSpan.LOGIN_FAILURE;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class AuthenticationFailureHandlerCE implements ServerAuthenticationFailureHandler {
|
||||
|
||||
private final AuthenticationFailureRetryHandler authenticationFailureRetryHandler;
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
private static final String SOURCE_FORM = "form";
|
||||
|
||||
@Override
|
||||
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
|
||||
String source = exception instanceof OAuth2AuthenticationException
|
||||
? ((OAuth2AuthenticationException) exception).getError().getErrorCode()
|
||||
: SOURCE_FORM;
|
||||
|
||||
String errorMessage = exception.getMessage();
|
||||
|
||||
meterRegistry
|
||||
.counter(LOGIN_FAILURE, "source", source, "message", errorMessage)
|
||||
.increment();
|
||||
return authenticationFailureRetryHandler.retryAndRedirectOnAuthenticationFailure(webFilterExchange, exception);
|
||||
}
|
||||
|
||||
public Mono<Void> handleErrorRedirect(WebFilterExchange webFilterExchange) {
|
||||
String error =
|
||||
webFilterExchange.getExchange().getRequest().getQueryParams().getFirst("error");
|
||||
String message =
|
||||
webFilterExchange.getExchange().getRequest().getQueryParams().getFirst("message");
|
||||
|
||||
if ("true".equals(error)) {
|
||||
meterRegistry
|
||||
.counter(LOGIN_FAILURE, "source", "redirect", "message", message)
|
||||
.increment();
|
||||
}
|
||||
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import io.micrometer.core.instrument.config.MeterFilter;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.appsmith.external.constants.spans.LoginSpan.LOGIN_ATTEMPT;
|
||||
import static com.appsmith.external.constants.spans.LoginSpan.LOGIN_FAILURE;
|
||||
import static com.appsmith.external.constants.spans.ce.ActionSpanCE.*;
|
||||
import static com.appsmith.external.git.constants.ce.GitSpanCE.FS_FETCH_REMOTE;
|
||||
import static com.appsmith.external.git.constants.ce.GitSpanCE.FS_RESET;
|
||||
|
|
@ -28,7 +30,9 @@ public class NoTagsMeterFilter implements MeterFilter {
|
|||
Map.entry(FS_RESET, List.of()),
|
||||
Map.entry(JGIT_RESET_HARD, List.of()),
|
||||
Map.entry(FS_FETCH_REMOTE, List.of()),
|
||||
Map.entry(JGIT_FETCH_REMOTE, List.of()));
|
||||
Map.entry(JGIT_FETCH_REMOTE, List.of()),
|
||||
Map.entry(LOGIN_FAILURE, List.of("source", "message")),
|
||||
Map.entry(LOGIN_ATTEMPT, List.of("source")));
|
||||
|
||||
@Override
|
||||
public Meter.Id map(Meter.Id id) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.appsmith.server.configurations;
|
|||
|
||||
import com.appsmith.external.exceptions.ErrorDTO;
|
||||
import com.appsmith.server.authentication.handlers.AccessDeniedHandler;
|
||||
import com.appsmith.server.authentication.handlers.AuthenticationFailureHandler;
|
||||
import com.appsmith.server.authentication.handlers.CustomServerOAuth2AuthorizationRequestResolver;
|
||||
import com.appsmith.server.authentication.handlers.LogoutSuccessHandler;
|
||||
import com.appsmith.server.authentication.oauth2clientrepositories.CustomOauth2ClientRepositoryManager;
|
||||
|
|
@ -11,6 +12,7 @@ import com.appsmith.server.domains.User;
|
|||
import com.appsmith.server.dtos.ResponseDTO;
|
||||
import com.appsmith.server.exceptions.AppsmithErrorCode;
|
||||
import com.appsmith.server.filters.ConditionalFilter;
|
||||
import com.appsmith.server.filters.LoginMetricsFilter;
|
||||
import com.appsmith.server.filters.LoginRateLimitFilter;
|
||||
import com.appsmith.server.helpers.RedirectHelper;
|
||||
import com.appsmith.server.ratelimiting.RateLimitService;
|
||||
|
|
@ -18,6 +20,7 @@ import com.appsmith.server.services.AnalyticsService;
|
|||
import com.appsmith.server.services.UserService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
|
@ -42,7 +45,6 @@ import org.springframework.security.oauth2.client.registration.ReactiveClientReg
|
|||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
||||
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationEntryPointFailureHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
|
||||
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
|
||||
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
|
||||
|
|
@ -90,7 +92,7 @@ public class SecurityConfig {
|
|||
private ServerAuthenticationSuccessHandler authenticationSuccessHandler;
|
||||
|
||||
@Autowired
|
||||
private ServerAuthenticationFailureHandler authenticationFailureHandler;
|
||||
private AuthenticationFailureHandler authenticationFailureHandler;
|
||||
|
||||
@Autowired
|
||||
private ServerAuthenticationEntryPoint authenticationEntryPoint;
|
||||
|
|
@ -119,21 +121,28 @@ public class SecurityConfig {
|
|||
@Autowired
|
||||
private CsrfConfig csrfConfig;
|
||||
|
||||
@Autowired
|
||||
private MeterRegistry meterRegistry;
|
||||
|
||||
@Value("${appsmith.internal.password}")
|
||||
private String INTERNAL_PASSWORD;
|
||||
|
||||
private static final String INTERNAL = "INTERNAL";
|
||||
|
||||
/**
|
||||
* This routerFunction is required to map /public/** endpoints to the src/main/resources/public folder
|
||||
* This is to allow static resources to be served by the server. Couldn't find an easier way to do this,
|
||||
* This routerFunction is required to map /public/** endpoints to the
|
||||
* src/main/resources/public folder
|
||||
* This is to allow static resources to be served by the server. Couldn't find
|
||||
* an easier way to do this,
|
||||
* hence using RouterFunctions to implement this feature.
|
||||
* <p>
|
||||
* Future folks: Please check out links:
|
||||
* - <a href="https://www.baeldung.com/spring-webflux-static-content">...</a>
|
||||
* - <a href="https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-config-static-resources">...</a>
|
||||
* - <a href=
|
||||
* "https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-config-static-resources">...</a>
|
||||
* - Class ResourceHandlerRegistry
|
||||
* for details. If you figure out a cleaner approach, please modify this function
|
||||
* for details. If you figure out a cleaner approach, please modify this
|
||||
* function
|
||||
*/
|
||||
@Bean
|
||||
public RouterFunction<ServerResponse> publicRouter() {
|
||||
|
|
@ -182,14 +191,17 @@ public class SecurityConfig {
|
|||
// Disabled because we use CSP's `frame-ancestors` instead.
|
||||
.frameOptions(options -> options.disable()))
|
||||
.anonymous(anonymousSpec -> anonymousSpec.principal(createAnonymousUser()))
|
||||
// This returns 401 unauthorized for all requests that are not authenticated but authentication is
|
||||
// This returns 401 unauthorized for all requests that are not authenticated but
|
||||
// authentication is
|
||||
// required
|
||||
// The client will redirect to the login page if we return 401 as Http status response
|
||||
// The client will redirect to the login page if we return 401 as Http status
|
||||
// response
|
||||
.exceptionHandling(exceptionHandlingSpec -> exceptionHandlingSpec
|
||||
.authenticationEntryPoint(authenticationEntryPoint)
|
||||
.accessDeniedHandler(accessDeniedHandler))
|
||||
.authorizeExchange(authorizeExchangeSpec -> authorizeExchangeSpec
|
||||
// The following endpoints are allowed to be accessed without authentication
|
||||
// The following endpoints are allowed to be accessed without
|
||||
// authentication
|
||||
.matchers(
|
||||
ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, Url.HEALTH_CHECK),
|
||||
ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, USER_URL),
|
||||
|
|
@ -226,7 +238,10 @@ public class SecurityConfig {
|
|||
.authenticated())
|
||||
// Add Pre Auth rate limit filter before authentication filter
|
||||
.addFilterBefore(
|
||||
new ConditionalFilter(new LoginRateLimitFilter(rateLimitService), Url.LOGIN_URL),
|
||||
new ConditionalFilter(new LoginMetricsFilter(meterRegistry), Url.LOGIN_URL),
|
||||
SecurityWebFiltersOrder.FORM_LOGIN)
|
||||
.addFilterBefore(
|
||||
new ConditionalFilter(new LoginRateLimitFilter(rateLimitService, meterRegistry), Url.LOGIN_URL),
|
||||
SecurityWebFiltersOrder.FORM_LOGIN)
|
||||
.httpBasic(httpBasicSpec -> httpBasicSpec.authenticationFailureHandler(failureHandler))
|
||||
.formLogin(formLoginSpec -> formLoginSpec
|
||||
|
|
@ -264,7 +279,8 @@ public class SecurityConfig {
|
|||
}
|
||||
|
||||
/**
|
||||
* This bean configures the parameters that need to be set when a Cookie is created for a logged in user
|
||||
* This bean configures the parameters that need to be set when a Cookie is
|
||||
* created for a logged in user
|
||||
*/
|
||||
@Bean
|
||||
public WebSessionIdResolver webSessionIdResolver() {
|
||||
|
|
@ -283,7 +299,8 @@ public class SecurityConfig {
|
|||
private Mono<Void> sanityCheckFilter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
final HttpHeaders headers = exchange.getRequest().getHeaders();
|
||||
|
||||
// 1. Check if the content-type is valid at all. Mostly just checks if it contains a `/`.
|
||||
// 1. Check if the content-type is valid at all. Mostly just checks if it
|
||||
// contains a `/`.
|
||||
MediaType contentType;
|
||||
try {
|
||||
contentType = headers.getContentType();
|
||||
|
|
@ -299,7 +316,8 @@ public class SecurityConfig {
|
|||
return writeErrorResponse(exchange, chain, "Unsupported Content-Type");
|
||||
}
|
||||
|
||||
// 3. Check Appsmith version, if present. Not making this a mandatory check for now, but reconsider later.
|
||||
// 3. Check Appsmith version, if present. Not making this a mandatory check for
|
||||
// now, but reconsider later.
|
||||
final String versionHeaderValue = headers.getFirst(CsrfConfig.VERSION_HEADER);
|
||||
final String serverVersion = projectProperties.getVersion();
|
||||
if (versionHeaderValue != null && !serverVersion.equals(versionHeaderValue)) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
package com.appsmith.server.filters;
|
||||
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.Timer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static com.appsmith.external.constants.spans.LoginSpan.LOGIN_ATTEMPT;
|
||||
import static com.appsmith.external.constants.spans.LoginSpan.LOGIN_FAILURE;
|
||||
|
||||
@Slf4j
|
||||
public class LoginMetricsFilter implements WebFilter {
|
||||
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
public LoginMetricsFilter(MeterRegistry meterRegistry) {
|
||||
this.meterRegistry = meterRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
return Mono.defer(() -> {
|
||||
Timer.Sample sample = Timer.start(meterRegistry);
|
||||
return chain.filter(exchange)
|
||||
.doOnSuccess(aVoid -> {
|
||||
sample.stop(Timer.builder(LOGIN_ATTEMPT).register(meterRegistry));
|
||||
})
|
||||
.doOnError(throwable -> {
|
||||
sample.stop(Timer.builder(LOGIN_ATTEMPT)
|
||||
.tag("message", throwable.getMessage())
|
||||
.register(meterRegistry));
|
||||
|
||||
meterRegistry
|
||||
.counter(
|
||||
LOGIN_FAILURE,
|
||||
"source",
|
||||
"form_login",
|
||||
"errorCode",
|
||||
"AuthenticationFailed",
|
||||
"message",
|
||||
throwable.getMessage())
|
||||
.increment();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.appsmith.server.filters;
|
|||
import com.appsmith.server.constants.FieldName;
|
||||
import com.appsmith.server.constants.RateLimitConstants;
|
||||
import com.appsmith.server.ratelimiting.RateLimitService;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.web.server.DefaultServerRedirectStrategy;
|
||||
import org.springframework.security.web.server.ServerRedirectStrategy;
|
||||
|
|
@ -15,6 +16,7 @@ import java.net.URI;
|
|||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static com.appsmith.external.constants.spans.LoginSpan.LOGIN_FAILURE;
|
||||
import static java.lang.Boolean.FALSE;
|
||||
|
||||
@Slf4j
|
||||
|
|
@ -22,9 +24,11 @@ public class LoginRateLimitFilter implements WebFilter {
|
|||
|
||||
private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();
|
||||
private final RateLimitService rateLimitService;
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
public LoginRateLimitFilter(RateLimitService rateLimitService) {
|
||||
public LoginRateLimitFilter(RateLimitService rateLimitService, MeterRegistry meterRegistry) {
|
||||
this.rateLimitService = rateLimitService;
|
||||
this.meterRegistry = meterRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -60,6 +64,18 @@ public class LoginRateLimitFilter implements WebFilter {
|
|||
// Set the error in the URL query parameter for rate limiting
|
||||
String url = "/user/login?error=true&message="
|
||||
+ URLEncoder.encode(RateLimitConstants.RATE_LIMIT_REACHED_ACCOUNT_SUSPENDED, StandardCharsets.UTF_8);
|
||||
|
||||
meterRegistry
|
||||
.counter(
|
||||
LOGIN_FAILURE,
|
||||
"source",
|
||||
"rate_limit",
|
||||
"errorCode",
|
||||
"RateLimitExceeded",
|
||||
"message",
|
||||
RateLimitConstants.RATE_LIMIT_REACHED_ACCOUNT_SUSPENDED)
|
||||
.increment();
|
||||
|
||||
return redirectWithUrl(exchange, url);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user