diff --git a/openapi-config.ts b/openapi-config.ts
index 8c2d5455d..30bd4a978 100644
--- a/openapi-config.ts
+++ b/openapi-config.ts
@@ -35,10 +35,13 @@ const config: ConfigFile = {
})),
filterEndpoints: [
/About/,
+ /Agentic/,
+ /AI/,
/Assets/,
/Authentication/,
/CodeAnalytics/,
/Dashboard/,
+ /Diagnostic/,
/Environments/,
/Errors/,
/Graphs/,
diff --git a/package-lock.json b/package-lock.json
index e28d615a2..261211b73 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "digma-ui",
- "version": "16.3.9",
+ "version": "16.4.0-alpha.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "digma-ui",
- "version": "16.3.9",
+ "version": "16.4.0-alpha.2",
"license": "MIT",
"dependencies": {
"@codemirror/lang-json": "^6.0.2",
@@ -27,6 +27,7 @@
"react": "^18.2.0",
"react-cool-dimensions": "^3.0.1",
"react-dom": "^18.2.0",
+ "react-dropzone": "^14.3.8",
"react-error-boundary": "^5.0.0",
"react-helmet": "^6.1.0",
"react-hook-form": "^7.48.2",
@@ -6929,6 +6930,15 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
+ "node_modules/attr-accept": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
+ "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -10621,6 +10631,18 @@
"node": ">=16"
}
},
+ "node_modules/file-selector": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
+ "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.7.0"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/filelist": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@@ -17246,6 +17268,23 @@
"react": "^18.3.1"
}
},
+ "node_modules/react-dropzone": {
+ "version": "14.3.8",
+ "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz",
+ "integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==",
+ "license": "MIT",
+ "dependencies": {
+ "attr-accept": "^2.2.4",
+ "file-selector": "^2.1.0",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">= 10.13"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8 || 18.0.0"
+ }
+ },
"node_modules/react-error-boundary": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-5.0.0.tgz",
@@ -20316,8 +20355,7 @@
"node_modules/tslib": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz",
- "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==",
- "dev": true
+ "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="
},
"node_modules/turbo-stream": {
"version": "2.4.0",
diff --git a/package.json b/package.json
index 7c42c657d..9d82c77b8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "digma-ui",
- "version": "16.3.9",
+ "version": "16.4.0-alpha.2",
"description": "Digma UI",
"scripts": {
"lint:eslint": "eslint --cache .",
@@ -136,6 +136,7 @@
"react": "^18.2.0",
"react-cool-dimensions": "^3.0.1",
"react-dom": "^18.2.0",
+ "react-dropzone": "^14.3.8",
"react-error-boundary": "^5.0.0",
"react-helmet": "^6.1.0",
"react-hook-form": "^7.48.2",
diff --git a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServerIcon/index.tsx b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServerIcon/index.tsx
index ebf103aff..7c861ab92 100644
--- a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServerIcon/index.tsx
+++ b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServerIcon/index.tsx
@@ -3,21 +3,24 @@ import { MCPLogoIcon } from "../../../../common/icons/24px/MCPLogoIcon";
import { KubernetesLogoIcon } from "../../../../common/icons/25px/KubernetesLogoIcon";
import { PostgresLogoIcon } from "../../../../common/icons/25px/PostgresLogoIcon";
import { GitHubLogoIcon } from "../../../../common/icons/28px/GitHubLogoIcon";
+import * as s from "./styles";
import type { MCPServerIconProps } from "./types";
export const DEFAULT_SIZE = 12; // in pixels
export const MCPServerIcon = ({
- type,
- isCustom,
- isActive,
- size = DEFAULT_SIZE
+ size = DEFAULT_SIZE,
+ server
}: MCPServerIconProps) => {
- if (isCustom) {
+ if (server.icon) {
+ return ;
+ }
+
+ if (server.isEditable) {
return ;
}
- switch (type) {
+ switch (server.name) {
case "github":
return ;
case "postgres":
@@ -29,7 +32,7 @@ export const MCPServerIcon = ({
);
default:
diff --git a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServerIcon/styles.ts b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServerIcon/styles.ts
new file mode 100644
index 000000000..bd0405c28
--- /dev/null
+++ b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServerIcon/styles.ts
@@ -0,0 +1,8 @@
+import styled from "styled-components";
+import type { CustomImageProps } from "./types";
+
+export const CustomImage = styled.img`
+ width: ${({ $size }) => $size}px;
+ height: ${({ $size }) => $size}px;
+ object-fit: contain;
+`;
diff --git a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServerIcon/types.ts b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServerIcon/types.ts
index d505a963d..d533ac4c9 100644
--- a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServerIcon/types.ts
+++ b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServerIcon/types.ts
@@ -1,6 +1,10 @@
+import type { ExtendedAgentMCPServer } from "../types";
+
export interface MCPServerIconProps {
- type: string;
- isActive?: boolean;
- isCustom?: boolean;
+ server: ExtendedAgentMCPServer;
size?: number;
}
+
+export interface CustomImageProps {
+ $size: number;
+}
diff --git a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersContainer/index.tsx b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersContainer/index.tsx
index e381116a9..cb82f1734 100644
--- a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersContainer/index.tsx
+++ b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersContainer/index.tsx
@@ -20,9 +20,7 @@ export const MCPServersContainer = ({ servers }: MCPServersContainerProps) => {
diff --git a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersToolbar/index.tsx b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersToolbar/index.tsx
index bd5f1f56c..dc677b808 100644
--- a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersToolbar/index.tsx
+++ b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersToolbar/index.tsx
@@ -99,12 +99,7 @@ export const MCPServersToolbar = ({
-
+
diff --git a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersToolbar/styles.ts b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersToolbar/styles.ts
index c3a6efd1a..e57c7cadf 100644
--- a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersToolbar/styles.ts
+++ b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersToolbar/styles.ts
@@ -22,7 +22,7 @@ export const MCPServerIconContainer = styled.div`
display: flex;
align-items: center;
justify-content: center;
- cursor: ${({ $isEditable }) => ($isEditable ? "pointer" : "default")};
+ cursor: ${({ $isEditable }) => ($isEditable ? "pointer" : "auto")};
`;
export const KebabMenuButton = styled.button`
diff --git a/src/components/Agentic/IncidentTemplate/MCPServerDialog/ToolsStep/index.tsx b/src/components/Agentic/IncidentTemplate/MCPServerDialog/ToolsStep/index.tsx
index 65b89928c..df067da60 100644
--- a/src/components/Agentic/IncidentTemplate/MCPServerDialog/ToolsStep/index.tsx
+++ b/src/components/Agentic/IncidentTemplate/MCPServerDialog/ToolsStep/index.tsx
@@ -1,12 +1,27 @@
-import { useState, type ChangeEvent } from "react";
+import axios from "axios";
+import { useEffect, useState, type ChangeEvent } from "react";
+import { useDropzone } from "react-dropzone";
+import { useFormDataRequest } from "../../../../../hooks/useFormDataRequest";
+import type { MCPServerIcon } from "../../../../../redux/services/types";
+import { isString } from "../../../../../typeGuards/isString";
import { sendUserActionTrackingEvent } from "../../../../../utils/actions/sendUserActionTrackingEvent";
+import { roundTo } from "../../../../../utils/roundTo";
+import { DownloadIcon } from "../../../../common/icons/12px/DownloadIcon";
+import { TrashBinIcon } from "../../../../common/icons/16px/TrashBinIcon";
+import { PageIcon } from "../../../../common/icons/32px/PageIcon";
+import { Direction } from "../../../../common/icons/types";
import { NewButton } from "../../../../common/v3/NewButton";
+import { Tooltip } from "../../../../common/v3/Tooltip";
import { SearchInput } from "../../../common/SearchInput";
import { trackingEvents } from "../../../tracking";
import { Footer } from "../Footer";
import * as s from "./styles";
import type { ToolsStepProps } from "./types";
+const MAX_ICON_FILE_SIZE = 1024 * 1024; // in bytes
+const MAX_ICON_WIDTH = 500; // in pixels
+const MAX_ICON_HEIGHT = 500; // in pixels
+
export const ToolsStep = ({
onCancel,
onSave,
@@ -14,12 +29,114 @@ export const ToolsStep = ({
selectedTools: initialSelectedTools = [],
isLoading,
instructions = "",
+ icon,
error
}: ToolsStepProps) => {
const [instructionsTextAreaValue, setInstructionsTextAreaValue] =
useState(instructions);
const [selectedTools, setSelectedTools] = useState(initialSelectedTools);
const [searchInputValue, setSearchInputValue] = useState("");
+ const [dropzoneError, setDropzoneError] = useState();
+ const [iconId, setIconId] = useState(icon?.id ?? null);
+ const [fileToUpload, setFileToUpload] = useState(null);
+ const {
+ send: upload,
+ progress,
+ isSending,
+ abort
+ } = useFormDataRequest({
+ url: `${
+ isString(window.digmaApiProxyPrefix)
+ ? window.digmaApiProxyPrefix
+ : "/api/"
+ }/Agentic/mcp/icon`,
+ onSuccess: (response) => {
+ setIconId(response.id);
+ setDropzoneError(undefined);
+ },
+ onError: (error) => {
+ const errorMessage = axios.isAxiosError(error)
+ ? error.response
+ ? String(error.response.data)
+ : error.message
+ : error instanceof Error
+ ? error.message
+ : "Unknown error";
+ setDropzoneError(`Failed to upload icon: ${errorMessage}`);
+ }
+ });
+
+ const fileDetails =
+ iconId && icon?.id === iconId
+ ? {
+ name: icon.fileName,
+ size: icon.fileSize,
+ type: icon.fileName.split(".")[1] ?? ""
+ }
+ : fileToUpload
+ ? {
+ name: fileToUpload.name,
+ size: fileToUpload.size,
+ type: fileToUpload.type.split("/")[1]
+ }
+ : null;
+
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
+ accept: {
+ "image/jpeg": [".jpg", ".jpeg"],
+ "image/png": [".png"]
+ },
+ disabled: Boolean(fileDetails),
+ multiple: false,
+ maxSize: MAX_ICON_FILE_SIZE,
+ onDrop: (acceptedFiles, rejectedFiles) => {
+ if (rejectedFiles.length > 0) {
+ const rejection = rejectedFiles[0];
+ if (rejection.errors.some((error) => error.code === "too-many-files")) {
+ setDropzoneError("Too many files. Only one file is allowed.");
+ return;
+ }
+
+ if (
+ rejection.errors.some((error) => error.code === "file-invalid-type")
+ ) {
+ setDropzoneError("Invalid file type. Only JPG and PNG are allowed.");
+ return;
+ }
+
+ if (rejection.errors.some((error) => error.code === "file-too-large")) {
+ setDropzoneError("File too large. Maximum size is 1MB.");
+ }
+ } else {
+ setDropzoneError(undefined);
+
+ if (acceptedFiles.length > 0) {
+ const file = acceptedFiles[0];
+ const image = new Image();
+
+ image.onload = () => {
+ if (
+ image.width > MAX_ICON_WIDTH ||
+ image.height > MAX_ICON_HEIGHT
+ ) {
+ setDropzoneError(
+ `Image dimensions should not exceed ${MAX_ICON_WIDTH}x${MAX_ICON_HEIGHT}px.`
+ );
+ return;
+ }
+
+ setFileToUpload(file);
+ const formData = new FormData();
+ formData.append("icon_image", file);
+
+ void upload(formData);
+ };
+
+ image.src = window.URL.createObjectURL(file);
+ }
+ }
+ }
+ });
const handleInstructionsTextAreaChange = (
e: ChangeEvent
@@ -31,7 +148,7 @@ export const ToolsStep = ({
sendUserActionTrackingEvent(
trackingEvents.INCIDENT_TEMPLATE_EDIT_MCP_DIALOG_SAVE_BUTTON_CLICKED
);
- onSave(selectedTools, instructionsTextAreaValue);
+ onSave(selectedTools, instructionsTextAreaValue, iconId);
};
const handleCancelButtonClick = () => {
@@ -69,6 +186,32 @@ export const ToolsStep = ({
const isSaveButtonEnabled = selectedTools.length > 0 && !isLoading;
+ const handleRemoveFile = () => {
+ if (isSending) {
+ abort();
+ }
+ setFileToUpload(null);
+ setIconId(null);
+ setDropzoneError(undefined);
+ };
+
+ useEffect(() => {
+ if (icon?.id) {
+ setFileToUpload(null);
+ setIconId(icon.id);
+ } else {
+ setIconId(null);
+ }
+ }, [icon?.id]);
+
+ const formattedFileSize = fileDetails
+ ? fileDetails.size >= MAX_ICON_FILE_SIZE
+ ? `${roundTo(fileDetails.size / 1024 / 1024, 0)} MB`
+ : `${roundTo(fileDetails.size / 1024, 0)} KB`
+ : "";
+
+ const footerError = dropzoneError ?? error;
+
return (
@@ -106,9 +249,61 @@ export const ToolsStep = ({
"Describe any specific instructions on how this MCP should be used"
}
/>
+
+
+
+ {fileDetails ? (
+ <>
+
+
+ {fileDetails.type}
+
+
+
+ {fileDetails.name}
+
+
+ {formattedFileSize}{" "}
+ {isSending || progress > 0 ? `– ${progress}% uploaded` : ""}
+
+
+ {fileDetails && (
+
+
+
+ )}
+ >
+ ) : (
+ <>
+
+
+
+
+
+ Click to upload logo or
+ drag and drop
+
+ PNG or JPG (max. 500x500px)
+
+ >
+ )}
+
+