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