Merge branch 'release' of https://github.com/appsmithorg/appsmith into feat/reactive-actions-run-behaviour

This commit is contained in:
Ankita Kinger 2025-04-24 15:45:03 +05:30
commit 17bb150532
30 changed files with 358 additions and 145 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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