diff --git a/.nvmrc b/.nvmrc index 8fdd954df..cabf43b5d 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22 \ No newline at end of file +24 \ No newline at end of file diff --git a/README.md b/README.md index b83bb2748..d5fe12ff2 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Run tests: npm run test ``` -Start dev server: +Start dev server (with Web distribution): ```shell npm run start @@ -42,21 +42,28 @@ Supported `platform` values: Build of the package will be in the `./dist` directory -## Environment variables +## Environment variables for Web distribution To set environment variables use .env file -| Name | Type | Default | Description | -| ----------------- | ------ | ------- | --------------------------------------------------------- | -| PORT | number | 3000 | Port (for dev server) | -| UI_BASE_URL | string | - | Base URL to proxy requests to ingress (for dev server) | -| JAEGER_API_PATH | string | - | URL path to proxy requests to Jaeger UI (for dev server ) | -| API_BASE_URL | string | - | Base URL to proxy Digma API requests (for dev server) | -| AUTH_API_BASE_URL | string | - | Base URL to proxy auth API requests (for dev server) | -| API_TOKEN | string | - | API token (for dev server) | -| USERNAME | string | - | User login (for dev server) | -| PASSWORD | string | - | User password (for dev server) | -| JAEGER_UI_PATH | string | - | Path to custom Jaeger UI build | +| Name | Type | Default | Description | +| ------------------------------- | ------- | ------- | -------------------------------------------------------- | +| PORT | number | 3000 | Port (for dev server) | +| UI_BASE_URL | string | - | Base URL to proxy requests to ingress (for dev server) | +| JAEGER_API_PATH | string | - | URL path to proxy requests to Jaeger UI (for dev server) | +| API_BASE_URL | string | - | Base URL to proxy Digma API requests (for dev server) | +| AUTH_API_BASE_URL | string | - | Base URL to proxy auth API requests (for dev server) | +| API_TOKEN | string | - | API token (for dev server) | +| LOGIN | string | - | User login (for dev server) | +| PASSWORD | string | - | User password (for dev server) | +| IS_JAEGER_ENABLED | boolean | false | Enable links to Jaeger | +| JAEGER_UI_PATH | string | - | Path to custom Jaeger UI build | +| IS_SANDBOX_ENABLED | boolean | false | Enable Sandbox (demo) mode | +| ARE_INSIGHT_SUGGESTIONS_ENABLED | boolean | false | Enable insight suggestions | +| GOOGLE_CLIENT_ID | string | - | Google client ID | +| POSTHOG_API_KEY | string | - | PostHog API key | +| POSTHOG_URL | string | - | PostHog URL | +| PRODUCT_FRUITS_WORKSPACE_CODE | string | - | Product Fruits workspace code | ## Jaeger UI diff --git a/package-lock.json b/package-lock.json index 2ae4adfdf..6c2e80cc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "digma-ui", - "version": "15.2.1", + "version": "15.3.0-alpha.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "digma-ui", - "version": "15.2.1", + "version": "15.3.0-alpha.3", "license": "MIT", "dependencies": { "@floating-ui/react": "^0.25.1", diff --git a/package.json b/package.json index 236b9c429..5f08fdbed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "digma-ui", - "version": "15.2.1", + "version": "15.3.0-alpha.3", "description": "Digma UI", "scripts": { "lint:eslint": "eslint --cache .", diff --git a/src/components/Admin/Environments/ActionsMenuButton/index.tsx b/src/components/Admin/Environments/ActionsMenuButton/index.tsx index ce52255e8..d7c8cea00 100644 --- a/src/components/Admin/Environments/ActionsMenuButton/index.tsx +++ b/src/components/Admin/Environments/ActionsMenuButton/index.tsx @@ -1,50 +1,25 @@ -import { useState } from "react"; import { useAdminDispatch } from "../../../../containers/Admin/hooks"; import { setEnvironmentToDelete } from "../../../../redux/slices/environmentsManagerSlice"; import { TrashBinIcon } from "../../../common/icons/16px/TrashBinIcon"; -import { ThreeDotsVerticalIcon } from "../../../common/icons/ThreeDotsVerticalIcon"; -import { NewPopover } from "../../../common/NewPopover"; -import { NewIconButton } from "../../../common/v3/NewIconButton"; -import { MenuList } from "../../../Navigation/common/MenuList"; -import * as s from "./styles"; +import { KebabMenu } from "../../../common/KebabMenu"; +import type { MenuItem } from "../../../Navigation/common/MenuList/types"; import type { ActionMenuButtonProps } from "./types"; export const ActionsMenuButton = ({ environment }: ActionMenuButtonProps) => { - const [isKebabButtonMenuOpen, setIsKebabButtonMenuOpen] = useState(false); const dispatch = useAdminDispatch(); const handleDeleteMenuItemClick = () => { dispatch(setEnvironmentToDelete(environment.id)); }; - const handleKebabMenuOpenChange = (isOpen: boolean) => { - setIsKebabButtonMenuOpen(isOpen); - }; + const items: MenuItem[] = [ + { + id: "delete", + icon: , + label: "Delete", + onClick: handleDeleteMenuItemClick + } + ]; - return ( - - , - label: "Delete", - onClick: handleDeleteMenuItemClick - } - ]} - /> - - } - onOpenChange={handleKebabMenuOpenChange} - isOpen={isKebabButtonMenuOpen} - placement={"bottom-end"} - > - - - ); + return ; }; diff --git a/src/components/Agentic/IncidentDetails/AgentEvents/index.tsx b/src/components/Agentic/IncidentDetails/AgentEvents/index.tsx index c0efcffbc..b15e90d1a 100644 --- a/src/components/Agentic/IncidentDetails/AgentEvents/index.tsx +++ b/src/components/Agentic/IncidentDetails/AgentEvents/index.tsx @@ -80,7 +80,10 @@ export const AgentEvents = () => { ); const isAgentRunning = useMemo( - () => Boolean(agentsData?.agents.find((x) => x.name === agentId)?.running), + () => + Boolean( + agentsData?.agents.find((x) => x.name === agentId)?.status === "running" + ), [agentsData, agentId] ); @@ -104,15 +107,22 @@ export const AgentEvents = () => { speed={shouldShowTypingForEvent(i) ? TYPING_SPEED : undefined} /> ); - case "tool": + case "tool": { + let toolName = event.tool_name; + + if (event.mcp_name) { + toolName += ` ${[event.mcp_name, "MCP tool"] + .filter(Boolean) + .join(" ")})`; + } + return ( } /> ); + } default: return null; } diff --git a/src/components/Agentic/IncidentDetails/AgentFlowChart/AgentFlowChartNodeToolbar/index.tsx b/src/components/Agentic/IncidentDetails/AgentFlowChart/AgentFlowChartNodeToolbar/index.tsx index d6895e679..20d2afc90 100644 --- a/src/components/Agentic/IncidentDetails/AgentFlowChart/AgentFlowChartNodeToolbar/index.tsx +++ b/src/components/Agentic/IncidentDetails/AgentFlowChart/AgentFlowChartNodeToolbar/index.tsx @@ -1,6 +1,6 @@ import { Position } from "@xyflow/react"; import { PlusIcon } from "../../../../common/icons/16px/PlusIcon"; -import { MCPServersSideContainer } from "../MCPServersSideContainer"; +import { MCPServersContainer } from "../MCPServersContainer"; import { MCPServersToolbar } from "../MCPServersToolbar"; import * as s from "./styles"; import type { AgentFlowChartNodeToolbarProps } from "./types"; @@ -10,15 +10,20 @@ export const AgentFlowChartNodeToolbar = ({ position, isEditMode, onAddMCPServer, - onEditMCPServers, + onEditMCPServer, + onDeleteMCPServer, showPlusButton }: AgentFlowChartNodeToolbarProps) => { const handleAddMCPServer = () => { - onAddMCPServer(position); + onAddMCPServer(); }; - const handleEditMCPServers = () => { - onEditMCPServers(position); + const handleEditMCPServer = (server: string) => { + onEditMCPServer(server); + }; + + const handleDeleteMCPServer = (server: string) => { + onDeleteMCPServer(server); }; const toolbarItems = [ @@ -27,8 +32,8 @@ export const AgentFlowChartNodeToolbar = ({ ] : []), @@ -50,11 +55,9 @@ export const AgentFlowChartNodeToolbar = ({ return ( <> {isEditMode ? ( - [Position.Top, Position.Bottom].includes(position) && ( - {sortedToolbarItems} - ) + {sortedToolbarItems} ) : ( - + )} ); diff --git a/src/components/Agentic/IncidentDetails/AgentFlowChart/AgentFlowChartNodeToolbar/types.ts b/src/components/Agentic/IncidentDetails/AgentFlowChart/AgentFlowChartNodeToolbar/types.ts index 2d404286f..267c4dd31 100644 --- a/src/components/Agentic/IncidentDetails/AgentFlowChart/AgentFlowChartNodeToolbar/types.ts +++ b/src/components/Agentic/IncidentDetails/AgentFlowChart/AgentFlowChartNodeToolbar/types.ts @@ -5,7 +5,8 @@ export interface AgentFlowChartNodeToolbarProps { isEditMode?: boolean; position: Position; servers: ExtendedAgentMCPServer[]; - onAddMCPServer: (position: Position) => void; - onEditMCPServers: (position: Position) => void; + onAddMCPServer: () => void; + onEditMCPServer: (server: string) => void; + onDeleteMCPServer: (server: string) => void; showPlusButton?: boolean; } diff --git a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServerIcon/index.tsx b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServerIcon/index.tsx index c31713e43..963320f85 100644 --- a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServerIcon/index.tsx +++ b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServerIcon/index.tsx @@ -27,9 +27,7 @@ export const MCPServerIcon = ({ themeKind={isActive ? "light" : "dark"} /> ); - case "mcp": - return ; default: - return null; + return ; } }; diff --git a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersContainer/index.tsx b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersContainer/index.tsx new file mode 100644 index 000000000..303bf8c88 --- /dev/null +++ b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersContainer/index.tsx @@ -0,0 +1,32 @@ +import { useViewport } from "@xyflow/react"; +import { Tooltip } from "../../../../common/v3/Tooltip"; +import { MCPServerIcon } from "../MCPServerIcon"; +import * as s from "./styles"; +import type { MCPServersContainerProps } from "./types"; + +const DEFAULT_ICON_SIZE = 27; // in pixels + +export const MCPServersContainer = ({ servers }: MCPServersContainerProps) => { + const viewport = useViewport(); + const zoomLevel = viewport.zoom; + + if (!servers || servers.length === 0) { + return null; + } + + return ( + + {servers?.map((x) => ( + + + + + + ))} + + ); +}; diff --git a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersSideContainer/styles.ts b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersContainer/styles.ts similarity index 100% rename from src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersSideContainer/styles.ts rename to src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersContainer/styles.ts diff --git a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersSideContainer/types.ts b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersContainer/types.ts similarity index 83% rename from src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersSideContainer/types.ts rename to src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersContainer/types.ts index d09e89417..dbd06fb09 100644 --- a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersSideContainer/types.ts +++ b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersContainer/types.ts @@ -1,6 +1,6 @@ import type { ExtendedAgentMCPServer } from "../types"; -export interface MCPServersSideContainerProps { +export interface MCPServersContainerProps { servers: ExtendedAgentMCPServer[]; } diff --git a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersSideContainer/index.tsx b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersSideContainer/index.tsx deleted file mode 100644 index 6e5505b79..000000000 --- a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersSideContainer/index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useViewport } from "@xyflow/react"; -import { MCPServerIcon } from "../MCPServerIcon"; -import * as s from "./styles"; -import type { MCPServersSideContainerProps } from "./types"; - -const DEFAULT_ICON_SIZE = 27; // in pixels - -export const MCPServersSideContainer = ({ - servers -}: MCPServersSideContainerProps) => { - const viewport = useViewport(); - const zoomLevel = viewport.zoom; - - if (!servers || servers.length === 0) { - return null; - } - - return ( - - {servers?.map((x) => ( - - - - ))} - - ); -}; diff --git a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersToolbar/index.tsx b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersToolbar/index.tsx index 99a74fc0f..b0953aae3 100644 --- a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersToolbar/index.tsx +++ b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersToolbar/index.tsx @@ -1,10 +1,10 @@ import { useNodeId, useViewport } from "@xyflow/react"; import { useState } from "react"; import { sendUserActionTrackingEvent } from "../../../../../utils/actions/sendUserActionTrackingEvent"; -import { PlusIcon } from "../../../../common/icons/16px/PlusIcon"; +import { TrashBinIcon } from "../../../../common/icons/16px/TrashBinIcon"; import { WrenchIcon } from "../../../../common/icons/16px/WrenchIcon"; -import { ThreeDotsVerticalIcon } from "../../../../common/icons/ThreeDotsVerticalIcon"; import { NewPopover } from "../../../../common/NewPopover"; +import { Tooltip } from "../../../../common/v3/Tooltip"; import { MenuList } from "../../../../Navigation/common/MenuList"; import type { MenuItem } from "../../../../Navigation/common/MenuList/types"; import { Popup } from "../../../../Navigation/common/Popup"; @@ -15,40 +15,51 @@ import type { MCPServersToolbarProps } from "./types"; export const MCPServersToolbar = ({ servers, - onAddMCPServer, - onEditMCPServers + onEditMCPServer, + onDeleteMCPServer }: MCPServersToolbarProps) => { const [isKebabMenuOpen, setIsKebabMenuOpen] = useState(false); + const [selectedMCPServer, setSelectedMCPServer] = useState(); const id = useNodeId(); const viewport = useViewport(); const zoomLevel = viewport.zoom; - const handleKebabMenuOpenChange = (isOpen: boolean) => { - sendUserActionTrackingEvent( - trackingEvents.FLOW_CHART_NODE_MCP_TOOLBAR_MENU_CLICKED, - { - id, - isOpen - } - ); + const handleKebabMenuOpenChange = (server: string) => (isOpen: boolean) => { + if (isOpen) { + sendUserActionTrackingEvent( + trackingEvents.FLOW_CHART_NODE_MCP_TOOLBAR_SERVER_ICON_CLICKED, + { + id + } + ); + setSelectedMCPServer(server); + } else { + setSelectedMCPServer(undefined); + } setIsKebabMenuOpen(isOpen); }; const handleKebabMenuItemClick = (id: string) => { sendUserActionTrackingEvent( - trackingEvents.FLOW_CHART_NODE_MCP_TOOLBAR_MENU_ITEM_CLICKED, + trackingEvents.FLOW_CHART_NODE_MCP_TOOLBAR_SERVER_ICON_MENU_ITEM_CLICKED, { id } ); switch (id) { - case "edit": - onEditMCPServers?.(); + case "edit": { + if (selectedMCPServer) { + onEditMCPServer(selectedMCPServer); + } break; - case "add": - onAddMCPServer?.(); + } + case "delete": { + if (selectedMCPServer) { + onDeleteMCPServer(selectedMCPServer); + } break; + } } setIsKebabMenuOpen(false); @@ -58,41 +69,42 @@ export const MCPServersToolbar = ({ { id: "edit", icon: , - label: "Edit MCPs", + label: "Edit", onClick: () => handleKebabMenuItemClick("edit") }, { - id: "add", - icon: , - label: "Add new MCP", - onClick: () => handleKebabMenuItemClick("add") + id: "delete", + icon: , + label: "Delete", + onClick: () => handleKebabMenuItemClick("delete") } ]; return ( {servers.map((x) => ( - + placement={"bottom-end"} + content={ + + + + } + isOpen={Boolean( + isKebabMenuOpen && selectedMCPServer === x.name && x.isEditable + )} + onOpenChange={handleKebabMenuOpenChange(x.name)} + > +
+ + + + + +
+ ))} - - - - } - isOpen={isKebabMenuOpen} - onOpenChange={handleKebabMenuOpenChange} - > - - - -
); }; diff --git a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersToolbar/styles.ts b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersToolbar/styles.ts index ef0a895db..c3a6efd1a 100644 --- a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersToolbar/styles.ts +++ b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersToolbar/styles.ts @@ -1,5 +1,5 @@ import styled from "styled-components"; -import type { ContainerProps } from "./types"; +import type { ContainerProps, MCPServerIconContainerProps } from "./types"; export const Container = styled.div` display: flex; @@ -18,6 +18,13 @@ export const Container = styled.div` ); `; +export const MCPServerIconContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + cursor: ${({ $isEditable }) => ($isEditable ? "pointer" : "default")}; +`; + export const KebabMenuButton = styled.button` border: none; display: flex; diff --git a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersToolbar/types.ts b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersToolbar/types.ts index 01a22108d..cdbe0e81b 100644 --- a/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersToolbar/types.ts +++ b/src/components/Agentic/IncidentDetails/AgentFlowChart/MCPServersToolbar/types.ts @@ -2,10 +2,14 @@ import type { ExtendedAgentMCPServer } from "../types"; export interface MCPServersToolbarProps { servers: ExtendedAgentMCPServer[]; - onAddMCPServer: () => void; - onEditMCPServers: () => void; + onEditMCPServer: (server: string) => void; + onDeleteMCPServer: (server: string) => void; } export interface ContainerProps { $zoomLevel?: number; } + +export interface MCPServerIconContainerProps { + $isEditable?: boolean; +} diff --git a/src/components/Agentic/IncidentDetails/AgentFlowChart/index.tsx b/src/components/Agentic/IncidentDetails/AgentFlowChart/index.tsx index b623bb37c..96db408be 100644 --- a/src/components/Agentic/IncidentDetails/AgentFlowChart/index.tsx +++ b/src/components/Agentic/IncidentDetails/AgentFlowChart/index.tsx @@ -1,6 +1,5 @@ import { Position, type Edge } from "@xyflow/react"; import { sendUserActionTrackingEvent } from "../../../../utils/actions/sendUserActionTrackingEvent"; -import { groupBy } from "../../../../utils/groupBy"; import { FlowChart } from "../../common/FlowChart"; import type { FlowChartNode, @@ -16,21 +15,22 @@ const getFlowChartNodeData = ({ isInteractive, isEditMode, onAddMCPServer, - onEditMCPServers + onEditMCPServer, + onDeleteMCPServer }: { agent?: ExtendedAgent; isInteractive?: boolean; isSelected?: boolean; isEditMode?: boolean; - onAddMCPServer?: (agentName: string, position: Position) => void; - onEditMCPServers?: (agentName: string, position: Position) => void; + onAddMCPServer?: (agentName: string) => void; + onEditMCPServer?: (agentName: string, server: string) => void; + onDeleteMCPServer?: (agentName: string, server: string) => void; }): Partial => { - const handleAddMCPServer = (position: Position) => () => { + const handleAddMCPServer = () => { sendUserActionTrackingEvent( trackingEvents.AGENT_FLOW_CHART_NODE_ADD_MCP_SERVER_BUTTON_CLICKED, { - agentName: agent?.name, - position + agentName: agent?.name } ); @@ -38,54 +38,54 @@ const getFlowChartNodeData = ({ return; } - onAddMCPServer?.(agent.name, position); + onAddMCPServer?.(agent.name); }; - const handleEditMCPServers = (position: Position) => () => { + const handleEditMCPServer = (server: string) => { if (!agent) { return; } - onEditMCPServers?.(agent.name, position); + onEditMCPServer?.(agent.name, server); }; - const serverGroups = groupBy( - agent?.mcp_servers ?? [], - (server) => server.position ?? Position.Top - ); + const handleDeleteMCPServer = (server: string) => { + if (!agent) { + return; + } + + onDeleteMCPServer?.(agent.name, server); + }; + + const sideContainerPosition = + agent?.name === "code_resolver" ? Position.Bottom : Position.Top; return agent ? { label: agent.display_name, isActive: isSelected, - isRunning: agent.running, + isRunning: agent.status === "running", + isPending: agent.status === "pending", isInteractive, - isDisabled: agent.status === "inactive", - sideContainers: Object.values(Position).map((position) => ({ - isVisible: Boolean( - serverGroups[position]?.length > 0 || - (isEditMode && [Position.Top, Position.Bottom].includes(position)) - ), - position, - element: ( - - ), - isKebabMenuVisible: isEditMode - })) + isDisabled: agent.status === "skipped", + sideContainers: [ + { + isVisible: Boolean(agent.mcp_servers.length > 0 || isEditMode), + position: sideContainerPosition, + element: ( + + ) + } + ], + isKebabMenuVisible: isEditMode } : {}; }; @@ -97,33 +97,28 @@ export const AgentFlowChart = ({ className, isEditMode, onAddMCPServer, - onEditMCPServers + onEditMCPServer, + onDeleteMCPServer }: AgentFlowChartProps) => { const extendedAgents: ExtendedAgent[] = [ { name: "digma", display_name: "Digma", description: "Digma", - running: false, - status: "active", + status: "waiting", mcp_servers: [] }, ...agents.map((agent) => ({ ...agent, mcp_servers: agent.mcp_servers.map((server) => ({ - ...server, - position: - agent.name === "code_resolver" && !isEditMode - ? Position.Bottom - : server.position + ...server })) })), { name: "validator", display_name: "Validator", description: "Validator", - running: false, - status: "active", + status: "waiting", mcp_servers: [] } ]; @@ -142,7 +137,7 @@ export const AgentFlowChart = ({ case "code_resolver": { if ( - extendedAgents?.find((a) => a.name === id)?.status === "inactive" + extendedAgents?.find((a) => a.name === id)?.status === "skipped" ) { break; } @@ -179,10 +174,11 @@ export const AgentFlowChart = ({ isSelected: "watchman" === selectedAgentId, isInteractive: extendedAgents?.find((a) => a.name === "watchman")?.status !== - "inactive", + "skipped", isEditMode, onAddMCPServer, - onEditMCPServers + onEditMCPServer, + onDeleteMCPServer }) } }, @@ -195,10 +191,11 @@ export const AgentFlowChart = ({ isSelected: "triager" === selectedAgentId, isInteractive: extendedAgents?.find((a) => a.name === "triager")?.status !== - "inactive", + "skipped", isEditMode, onAddMCPServer, - onEditMCPServers + onEditMCPServer, + onDeleteMCPServer }) } }, @@ -211,10 +208,11 @@ export const AgentFlowChart = ({ isSelected: "infra_resolver" === selectedAgentId, isInteractive: extendedAgents?.find((a) => a.name === "infra_resolver")?.status !== - "inactive", + "skipped", isEditMode, onAddMCPServer, - onEditMCPServers + onEditMCPServer, + onDeleteMCPServer }) } }, @@ -227,10 +225,11 @@ export const AgentFlowChart = ({ isSelected: "code_resolver" === selectedAgentId, isInteractive: extendedAgents?.find((a) => a.name === "code_resolver")?.status !== - "inactive", + "skipped", isEditMode, onAddMCPServer, - onEditMCPServers + onEditMCPServer, + onDeleteMCPServer }) } }, diff --git a/src/components/Agentic/IncidentDetails/AgentFlowChart/mockData.ts b/src/components/Agentic/IncidentDetails/AgentFlowChart/mockData.ts index 412c36ac3..e5f345a52 100644 --- a/src/components/Agentic/IncidentDetails/AgentFlowChart/mockData.ts +++ b/src/components/Agentic/IncidentDetails/AgentFlowChart/mockData.ts @@ -5,16 +5,14 @@ export const mockedAgents: Agent[] = [ name: "digma", display_name: "Digma", description: "Digma", - running: false, - status: "active", + status: "waiting", mcp_servers: [] }, { name: "watchman", display_name: "Watchman", description: "Watchman", - running: true, - status: "active", + status: "waiting", mcp_servers: [ { name: "github", @@ -37,7 +35,6 @@ export const mockedAgents: Agent[] = [ name: "triager", display_name: "Triage", description: "Triage", - running: false, status: "pending", mcp_servers: [ { @@ -61,7 +58,6 @@ export const mockedAgents: Agent[] = [ name: "infra_resolver", display_name: "Infra Resolution", description: "Infra Resolution", - running: false, status: "pending", mcp_servers: [ { @@ -85,8 +81,7 @@ export const mockedAgents: Agent[] = [ name: "code_resolver", display_name: "Code Resolution", description: "Code Resolution", - running: false, - status: "inactive", + status: "skipped", mcp_servers: [ { name: "github", @@ -109,7 +104,6 @@ export const mockedAgents: Agent[] = [ name: "validator", display_name: "Validator", description: "Validator", - running: false, status: "pending", mcp_servers: [ { diff --git a/src/components/Agentic/IncidentDetails/AgentFlowChart/types.ts b/src/components/Agentic/IncidentDetails/AgentFlowChart/types.ts index 8d81155ff..316bac055 100644 --- a/src/components/Agentic/IncidentDetails/AgentFlowChart/types.ts +++ b/src/components/Agentic/IncidentDetails/AgentFlowChart/types.ts @@ -1,4 +1,3 @@ -import type { Position } from "@xyflow/react"; import type { Agent, AgentMCPServer } from "../../../../redux/services/types"; export interface AgentFlowChartProps { @@ -7,12 +6,13 @@ export interface AgentFlowChartProps { selectedAgentId: string | null; className?: string; isEditMode?: boolean; - onAddMCPServer?: (agentId: string, position: Position) => void; - onEditMCPServers?: (agentId: string) => void; + onAddMCPServer?: (agentId: string) => void; + onEditMCPServer?: (agentId: string, server: string) => void; + onDeleteMCPServer?: (agentId: string, server: string) => void; } export interface ExtendedAgentMCPServer extends AgentMCPServer { - position?: Position; + isEditable?: boolean; } export interface ExtendedAgent extends Agent { diff --git a/src/components/Agentic/IncidentDetails/IncidentMetaData/index.tsx b/src/components/Agentic/IncidentDetails/IncidentMetaData/index.tsx index f76bff834..cc0ebeeb0 100644 --- a/src/components/Agentic/IncidentDetails/IncidentMetaData/index.tsx +++ b/src/components/Agentic/IncidentDetails/IncidentMetaData/index.tsx @@ -1,7 +1,9 @@ import { format } from "date-fns"; import { useMemo } from "react"; import { useParams } from "react-router"; +import { useAgenticDispatch } from "../../../../containers/Agentic/hooks"; import { useGetIncidentQuery } from "../../../../redux/services/digma"; +import { setIncidentToClose } from "../../../../redux/slices/incidentsSlice"; import { Tooltip } from "../../../common/v3/Tooltip"; import { Divider } from "./Divider"; import * as s from "./styles"; @@ -13,6 +15,7 @@ const REFRESH_INTERVAL = 10 * 1000; // in milliseconds export const IncidentMetaData = () => { const params = useParams(); const incidentId = params.id; + const dispatch = useAgenticDispatch(); const { data } = useGetIncidentQuery( { id: incidentId ?? "" }, @@ -32,52 +35,83 @@ export const IncidentMetaData = () => { [data] ); + const handleCloseButtonClick = () => { + if (!incidentId) { + return; + } + + dispatch(setIncidentToClose(incidentId)); + }; + if (!data) { return ; } return ( - - Incident start time: - - {format(data.created_at, DATE_FORMAT)} - - - {data.closed_at && ( - <> - - - - - Incident close time: - - {format(data.closed_at, DATE_FORMAT)} + + {data.status_timestamps.active && ( + + Incident start time: + + + {format(data.status_timestamps.active, DATE_FORMAT)} + - - - )} - {data.affected_services.length > 0 && ( - <> - - - - - Affected services: - {serviceTagsToShow.map((x) => ( - - {x} - - ))} - {hiddenServices.length > 0 && ( - - - +{hiddenServices.length} - + + )} + {data.status_timestamps.closed && ( + <> + + + + + Incident close time: + + + {format(data.status_timestamps.closed, DATE_FORMAT)} + - )} - - + + + )} + {data.affected_services.length > 0 && ( + <> + + + + + Affected services: + {serviceTagsToShow.map((x) => ( + + {x} + + ))} + {hiddenServices.length > 0 && ( + + + +{hiddenServices.length} + + + )} + + + )} + + + + + Status: + {data.status} + + + {data.status === "pending" && ( + )} ); diff --git a/src/components/Agentic/IncidentDetails/IncidentMetaData/styles.ts b/src/components/Agentic/IncidentDetails/IncidentMetaData/styles.ts index c00192f1a..40a4ef50a 100644 --- a/src/components/Agentic/IncidentDetails/IncidentMetaData/styles.ts +++ b/src/components/Agentic/IncidentDetails/IncidentMetaData/styles.ts @@ -1,12 +1,19 @@ import styled from "styled-components"; import { subheading1RegularTypography } from "../../../common/App/typographies"; +import { NewButton } from "../../../common/v3/NewButton"; export const Container = styled.div` display: flex; - align-items: center; padding-top: 24px; - height: 51px; + min-height: 51px; flex-shrink: 0; + align-items: start; +`; + +export const AttributesList = styled.div` + display: flex; + flex-wrap: wrap; + align-items: center; `; export const DividerContainer = styled.div` @@ -15,7 +22,14 @@ export const DividerContainer = styled.div` display: flex; `; -export const DateAttribute = styled.div` +export const CloseIncidentButton = styled(NewButton)` + flex-shrink: 0; + margin-top: 12px; + margin-left: auto; + margin-right: 16px; +`; + +export const Attribute = styled.div` ${subheading1RegularTypography} display: flex; gap: 12px; @@ -23,14 +37,18 @@ export const DateAttribute = styled.div` padding: 16px; `; -export const DateLabel = styled.span` +export const AttributeLabel = styled.span` color: ${({ theme }) => theme.colors.v3.text.tertiary}; `; -export const DateValue = styled.span` +export const AttributeValue = styled.span` color: ${({ theme }) => theme.colors.v3.text.primary}; `; +export const StatusAttributeValue = styled(AttributeValue)` + text-transform: capitalize; +`; + export const ServicesContainer = styled.div` ${subheading1RegularTypography} display: flex; diff --git a/src/components/Agentic/IncidentDetails/index.tsx b/src/components/Agentic/IncidentDetails/index.tsx index 4d8957a4d..75d738ed8 100644 --- a/src/components/Agentic/IncidentDetails/index.tsx +++ b/src/components/Agentic/IncidentDetails/index.tsx @@ -91,7 +91,7 @@ export const IncidentDetails = () => { return null; } - const incidentStatus = incidentData?.status; + const incidentStatus = incidentData?.status_description; const agentName = agentsData?.agents.find( (agent) => agent.name === agentId diff --git a/src/components/Agentic/IncidentDetails/mockData.ts b/src/components/Agentic/IncidentDetails/mockData.ts index f8f0f24e5..66de72034 100644 --- a/src/components/Agentic/IncidentDetails/mockData.ts +++ b/src/components/Agentic/IncidentDetails/mockData.ts @@ -1,16 +1,18 @@ -import type { GetIncidentResponse } from "../../../redux/services/types"; +import { type GetIncidentResponse } from "../../../redux/services/types"; import { mockedArtifacts } from "./AdditionalInfo/Artifacts/mockData"; import { mockedIncidentIssues } from "./AdditionalInfo/RelatedIssues/mockData"; export const mockedIncident: GetIncidentResponse = { id: "incident-123", name: "Sample Incident", - active_status: "active", + status_description: "active", status: "active", - created_at: "2023-10-01T12:00:00Z", - closed_at: "2023-10-01T12:30:00Z", affected_services: ["service-1", "service-2", "service-3", "service-4"], summary: "This is a summary of the incident.", related_issues: mockedIncidentIssues, - related_artifacts: mockedArtifacts + related_artifacts: mockedArtifacts, + status_timestamps: { + active: "2023-10-01T12:00:00Z", + closed: "2023-10-01T12:30:00Z" + } }; diff --git a/src/components/Agentic/IncidentDetails/styles.ts b/src/components/Agentic/IncidentDetails/styles.ts index 7ecb8e2b0..100a88115 100644 --- a/src/components/Agentic/IncidentDetails/styles.ts +++ b/src/components/Agentic/IncidentDetails/styles.ts @@ -69,10 +69,12 @@ export const SummaryContainer = styled.div` padding: 24px; flex-direction: column; flex-grow: 1; + flex-shrink: 1; + min-width: 0; + overflow: hidden; gap: 24px; border-radius: 16px; background: ${({ theme }) => theme.colors.v3.surface.primary}; - width: 60%; `; export const SummaryContainerToolbar = styled.div` @@ -161,7 +163,8 @@ export const AdditionalInfoContainer = styled.div` display: flex; flex-direction: column; gap: 8px; - width: 40%; + width: 30%; + min-width: 401px; border-radius: 16px; background: ${({ theme }) => theme.colors.v3.surface.primary}; flex-shrink: 0; diff --git a/src/components/Agentic/IncidentDirectives/index.tsx b/src/components/Agentic/IncidentDirectives/index.tsx new file mode 100644 index 000000000..1e4d92417 --- /dev/null +++ b/src/components/Agentic/IncidentDirectives/index.tsx @@ -0,0 +1,337 @@ +import { + createColumnHelper, + flexRender, + getCoreRowModel, + getSortedRowModel, + useReactTable +} from "@tanstack/react-table"; +import { useMemo, useState } from "react"; +import { + useDeleteIncidentAgentDirectiveMutation, + useGetIncidentAgentDirectivesQuery +} from "../../../redux/services/digma"; +import { CancelConfirmation } from "../../common/CancelConfirmation"; +import { SortIcon } from "../../common/icons/16px/SortIcon"; +import { TrashBinIcon } from "../../common/icons/16px/TrashBinIcon"; +import { Direction } from "../../common/icons/types"; +import { KebabMenu } from "../../common/KebabMenu"; +import { Checkmark } from "../../common/v3/Checkmark"; +import type { MenuItem } from "../../Navigation/common/MenuList/types"; +import * as s from "./styles"; +import type { ColumnMeta, ExtendedDirective } from "./types"; + +const REFRESH_INTERVAL = 10 * 1000; // in milliseconds + +const columnHelper = createColumnHelper(); + +export const IncidentDirectives = () => { + const [searchInputValue, setSearchInputValue] = useState(""); + const [selectedConditions, setSelectedConditions] = useState([]); + const [directiveToDelete, setDirectiveToDelete] = useState(); + + const { data } = useGetIncidentAgentDirectivesQuery( + { + search_term: searchInputValue || undefined + }, + { + pollingInterval: REFRESH_INTERVAL + } + ); + + const [deleteIncidentAgentDirective] = + useDeleteIncidentAgentDirectiveMutation(); + + const handleSearchInputChange = (value: string) => { + setSearchInputValue(value); + }; + + const handleCheckboxChange = (id: string) => (value: boolean) => { + setSelectedConditions((prev) => + value ? [...prev, id] : prev.filter((x) => x !== id) + ); + }; + + const handleDeleteDirectiveDialogConfirm = () => { + if (directiveToDelete) { + void deleteIncidentAgentDirective({ + id: directiveToDelete + }); + } + setDirectiveToDelete(undefined); + }; + + const handleDeleteDirectiveDialogClose = () => { + setDirectiveToDelete(undefined); + }; + + const handleMessageSend = () => { + // TODO: implement + }; + + const handleSelectedConditionTagClick = (id: string) => () => { + setSelectedConditions((prev) => prev.filter((x) => x !== id)); + }; + + const items = useMemo( + () => + data?.directives?.map((item, index) => ({ + ...item, + number: index + 1, + isSelected: selectedConditions.includes(item.id) + })) ?? [], + [selectedConditions, data] + ); + + const columns = [ + columnHelper.accessor((x) => x, { + id: "selector", + header: "", + meta: { + width: "5%", + minWidth: 60, + textAlign: "center" + }, + cell: (info) => { + const value = info.getValue(); + + return ( + + ); + } + }), + columnHelper.accessor("number", { + header: "#", + meta: { + width: "5%", + minWidth: 60, + textAlign: "center" + }, + cell: (info) => { + const value = info.getValue(); + return {value}; + }, + enableSorting: true, + sortingFn: (rowA, rowB) => { + return rowA.original.number - rowB.original.number; + } + }), + columnHelper.accessor("condition", { + header: "Condition", + meta: { + width: "35%", + minWidth: 100 + }, + cell: (info) => { + const value = info.getValue(); + return {value}; + }, + enableSorting: true, + sortingFn: (rowA, rowB) => { + const a = rowA.original.condition.toLowerCase(); + const b = rowB.original.condition.toLowerCase(); + return a.localeCompare(b); + } + }), + columnHelper.accessor("directive", { + header: "Directive", + meta: { + width: "35%", + minWidth: 100 + }, + cell: (info) => { + const value = info.getValue(); + return {value}; + }, + enableSorting: true, + sortingFn: (rowA, rowB) => { + const a = rowA.original.directive.toLowerCase(); + const b = rowB.original.directive.toLowerCase(); + return a.localeCompare(b); + } + }), + columnHelper.accessor("agents", { + header: "Agents", + meta: { + width: "15%", + minWidth: 60 + }, + cell: (info) => { + const value = info.getValue(); + return {value.join(", ")}; + }, + enableSorting: true, + sortingFn: (rowA, rowB) => { + const a = rowA.original.agents.join(", ").toLowerCase(); + const b = rowB.original.agents.join(", ").toLowerCase(); + return a.localeCompare(b); + } + }), + columnHelper.accessor((x) => x, { + header: "Actions", + meta: { + width: "5%", + minWidth: 100, + textAlign: "center" + }, + cell: (info) => { + const value = info.getValue(); + + const handleDeleteMenuItemClick = () => { + setDirectiveToDelete(value.id); + }; + + const items: MenuItem[] = [ + { + id: "delete", + icon: , + label: "Delete", + onClick: handleDeleteMenuItemClick + } + ]; + + return ; + } + }) + ]; + + const table = useReactTable({ + data: items, + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + enableSortingRemoval: false + }); + + return ( + + + Directives + + + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const meta = header.column.columnDef.meta as ColumnMeta; + + return ( + + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + {header.column.columnDef.enableSorting && + { + asc: ( + + + + ), + desc: ( + + + + ) + }[header.column.getIsSorted() as string]} + + + ); + })} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => { + const meta = cell.column.columnDef.meta as ColumnMeta; + + return ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ); + })} + + ))} + + + + 0 && ( + + {selectedConditions.map((x) => ( + + #{x} + + ))} + + ) + } + /> + {directiveToDelete && ( + + + + )} + + ); +}; diff --git a/src/components/Agentic/IncidentDirectives/styles.ts b/src/components/Agentic/IncidentDirectives/styles.ts new file mode 100644 index 000000000..e3fd68406 --- /dev/null +++ b/src/components/Agentic/IncidentDirectives/styles.ts @@ -0,0 +1,179 @@ +import styled from "styled-components"; +import { + bodyRegularTypography, + heading2BoldTypography, + subheading1BoldTypography, + subheading1RegularTypography, + subscriptRegularTypography +} from "../../common/App/typographies"; +import { Overlay } from "../../common/Overlay"; +import { AgentChat } from "../common/AgentChat"; +import { Form, TextArea } from "../common/PromptInput/styles"; +import { SearchInput } from "../common/SearchInput"; +import type { TableCellContentProps } from "./types"; + +export const Container = styled.div` + padding: 24px; + display: flex; + flex-direction: column; + height: 100%; + gap: 24px; +`; + +export const Header = styled.header` + ${heading2BoldTypography} + display: flex; + align-items: center; + justify-content: space-between; + padding: 32px 24px 24px; + gap: 24px; + color: ${({ theme }) => theme.colors.v3.text.primary}; +`; + +export const StyledSearchInput = styled(SearchInput)` + width: 251px; + flex-grow: 0; +`; + +export const TableContainer = styled.div` + display: flex; + flex-direction: column; + flex-grow: 1; + overflow-y: auto; +`; + +export const Table = styled.div` + width: 100%; + display: flex; + flex-direction: column; +`; + +export const TableHead = styled.div` + ${subheading1BoldTypography} + display: flex; + color: ${({ theme }) => theme.colors.v3.text.tertiary}; + height: 70px; +`; + +export const TableHeadRow = styled.div` + display: flex; + align-items: center; + width: 100%; +`; + +export const TableHeaderCell = styled.div` + box-sizing: border-box; + padding: 0 16px; +`; + +export const TableHeaderCellContent = styled.div` + display: flex; + align-items: center; + justify-content: ${({ $align }) => $align ?? "left"}; + gap: 4px; + ${({ onClick }) => (onClick ? "cursor: pointer;" : "")} +`; + +export const SortingOrderIconContainer = styled.div` + display: flex; +`; + +export const TableBody = styled.div` + ${bodyRegularTypography} + color: ${({ theme }) => theme.colors.v3.text.primary}; + display: flex; + flex-direction: column; +`; + +export const TableBodyRow = styled.div` + display: flex; + height: 70px; + box-sizing: border-box; + border-top: 1px solid + ${({ theme }) => theme.colors.v3.surface.sidePanelHeader}; + + &:nth-child(even) { + background: ${({ theme }) => theme.colors.v3.surface.primary}; + } +`; + +export const TableBodyCell = styled.div` + display: flex; + align-items: center; + overflow: hidden; + box-sizing: border-box; + padding: 0 16px; + border-left: 1px solid + ${({ theme }) => theme.colors.v3.surface.sidePanelHeader}; + border-right: 1px solid + ${({ theme }) => theme.colors.v3.surface.sidePanelHeader}; + + &:first-child { + border-left: none; + } + + &:last-child { + border-right: none; + } +`; + +export const RecordNumber = styled.span` + ${subscriptRegularTypography} + color: ${({ theme }) => theme.colors.v3.text.tertiary}; +`; + +export const Condition = styled.span` + ${subheading1BoldTypography} + color: ${({ theme }) => theme.colors.v3.text.primary}; +`; + +export const Directive = styled.span` + ${subheading1RegularTypography} + color: ${({ theme }) => theme.colors.v3.text.secondary}; +`; + +export const SelectedConditionsContainer = styled.div` + display: flex; + gap: 6px; + flex-wrap: wrap; +`; + +export const SelectedConditionTag = styled.div` + display: flex; + align-items: center; + justify-content: center; + padding: 6px 8px; + background: ${({ theme }) => theme.colors.v3.surface.primary}; + border: 1px solid ${({ theme }) => theme.colors.v3.stroke.dark}; + border-radius: 5px; + color: ${({ theme }) => theme.colors.v3.text.primary}; + font-size: 16px; + line-height: 20px; + box-shadow: 0 3px 5px 0 rgb(0 0 0 / 13%); + cursor: pointer; +`; + +export const StyledAgentChat = styled(AgentChat)` + ${/* TODO: change to color from the theme */ ""} + background: #000; + border-radius: 8px; + padding: 24px; + gap: 12px; + max-height: 306px; + + & ${Form} { + height: 117px; + ${/* TODO: change to color from the theme */ ""} + border: 1px solid #6063f6; + + & ${TextArea} { + ${subheading1RegularTypography} + color: ${({ theme }) => theme.colors.v3.text.primary}; + height: 100%; + } + } +`; + +export const StyledOverlay = styled(Overlay)` + align-items: center; +`; diff --git a/src/components/Agentic/IncidentDirectives/types.ts b/src/components/Agentic/IncidentDirectives/types.ts new file mode 100644 index 000000000..0e56ac949 --- /dev/null +++ b/src/components/Agentic/IncidentDirectives/types.ts @@ -0,0 +1,18 @@ +import type { Directive } from "../../../redux/services/types"; + +export interface ExtendedDirective extends Directive { + number: number; + isSelected: boolean; +} + +export type ContentAlignment = "left" | "center" | "right"; + +export interface ColumnMeta { + width: string | number; + minWidth?: string | number; + textAlign?: ContentAlignment; +} + +export interface TableCellContentProps { + $align?: ContentAlignment; +} diff --git a/src/components/Agentic/IncidentTemplate/AddMCPServerDialog/ToolsStep/index.tsx b/src/components/Agentic/IncidentTemplate/AddMCPServerDialog/ToolsStep/index.tsx deleted file mode 100644 index bba3e7d49..000000000 --- a/src/components/Agentic/IncidentTemplate/AddMCPServerDialog/ToolsStep/index.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { useState, type ChangeEvent, type MouseEvent } from "react"; -import { sendUserActionTrackingEvent } from "../../../../../utils/actions/sendUserActionTrackingEvent"; -import { CrossIcon } from "../../../../common/icons/12px/CrossIcon"; -import { MagnifierIcon } from "../../../../common/icons/MagnifierIcon"; -import { NewButton } from "../../../../common/v3/NewButton"; -import { trackingEvents } from "../../../tracking"; -import * as s from "./styles"; -import type { ToolsStepProps } from "./types"; - -const initialTools = [ - "create_issue", - "list_issues", - "update_issue", - "list_commits", - "get_me", - "search_users", - "list_secret_scanning_alerts", - "push_files", - "get_file_contents", - "get_commit", - "fork_repository" -]; - -export const ToolsStep = ({ onCancel, onSave }: ToolsStepProps) => { - const [textAreaValue, setTextAreaValue] = useState(""); - const [tools, setTools] = useState(initialTools); - const [selectedTools, setSelectedTools] = useState([]); - const [searchInputValue, setSearchInputValue] = useState(""); - - const handleTextAreaChange = (e: ChangeEvent) => { - setTextAreaValue(e.target.value); - }; - - const handleSaveButtonClick = () => { - sendUserActionTrackingEvent( - trackingEvents.INCIDENT_TEMPLATE_EDIT_MCP_DIALOG_SAVE_BUTTON_CLICKED - ); - onSave(selectedTools, textAreaValue); - }; - - const handleCancelButtonClick = () => { - sendUserActionTrackingEvent( - trackingEvents.INCIDENT_TEMPLATE_EDIT_MCP_DIALOG_CANCEL_BUTTON_CLICKED - ); - onCancel(); - }; - - const handleSearchInputChange = (e: ChangeEvent) => { - setSearchInputValue(e.target.value); - }; - - const handleSelectAllToggleChange = (value: boolean) => { - sendUserActionTrackingEvent( - trackingEvents.INCIDENT_TEMPLATE_EDIT_MCP_DIALOG_SELECT_ALL_TOGGLE_CHANGED, - { value } - ); - setSelectedTools(value ? [...tools] : []); - }; - - const handleToolTagClick = (tool: string) => () => { - sendUserActionTrackingEvent( - trackingEvents.INCIDENT_TEMPLATE_EDIT_MCP_DIALOG_TOOL_TAG_CLICKED - ); - setSelectedTools((prev) => - prev.includes(tool) ? prev.filter((x) => x !== tool) : [...prev, tool] - ); - }; - - const handleToolTagDeleteButtonClick = - (tool: string) => (e: MouseEvent) => { - sendUserActionTrackingEvent( - trackingEvents.INCIDENT_TEMPLATE_EDIT_MCP_DIALOG_TOOL_TAG_DELETE_BUTTON_CLICKED - ); - - e.stopPropagation(); - - setTools((prev) => prev.filter((x) => x !== tool)); - }; - - const filteredTools = tools.filter((tool) => - tool.toLowerCase().includes(searchInputValue.toLowerCase()) - ); - const areAllSelected = tools.every((x) => selectedTools.includes(x)); - - const isSaveButtonEnabled = selectedTools.length > 0; - - return ( - - - - Tools - - - - - - - - - {filteredTools.length > 0 && ( - - {filteredTools.map((x) => ( - - {x} - - - - - ))} - - )} - - - - - - - - ); -}; diff --git a/src/components/Agentic/IncidentTemplate/AddMCPServerDialog/index.tsx b/src/components/Agentic/IncidentTemplate/AddMCPServerDialog/index.tsx deleted file mode 100644 index 30bd5936f..000000000 --- a/src/components/Agentic/IncidentTemplate/AddMCPServerDialog/index.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useState } from "react"; -import { sendUserActionTrackingEvent } from "../../../../utils/actions/sendUserActionTrackingEvent"; -import { Dialog } from "../../common/Dialog"; -import { trackingEvents } from "../../tracking"; -import { ServerStep } from "./ServerStep"; -import { ToolsStep } from "./ToolsStep"; -import type { AddMCPServerDialogProps } from "./types"; - -export const AddMCPServerDialog = ({ - onClose, - onComplete -}: AddMCPServerDialogProps) => { - const [currentStep, setCurrentStep] = useState(0); - const [connectionSettings, setConnectionSettings] = useState(""); - - const handleServerStepConnectionSettingsChange = (settings: string) => { - setConnectionSettings(settings); - }; - - const handleServerStepConnect = (settings: string) => { - setConnectionSettings(settings); - setCurrentStep((prev) => prev + 1); - }; - - const handleToolsStepSave = (tools: string[], instructions: string) => { - onComplete(connectionSettings, tools, instructions); - }; - - const handleToolsStepCancel = () => { - setCurrentStep((prev) => prev - 1); - }; - - const steps = [ - , - - ]; - - const handleDialogClose = () => { - sendUserActionTrackingEvent( - trackingEvents.INCIDENT_TEMPLATE_ADD_MCP_DIALOG_CLOSED - ); - onClose(); - }; - - return ( - - {steps[currentStep]} - - ); -}; diff --git a/src/components/Agentic/IncidentTemplate/AddMCPServerDialog/types.ts b/src/components/Agentic/IncidentTemplate/AddMCPServerDialog/types.ts deleted file mode 100644 index ff4e98891..000000000 --- a/src/components/Agentic/IncidentTemplate/AddMCPServerDialog/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface AddMCPServerDialogProps { - onClose: () => void; - onComplete: (text: string, tools: string[], instructions: string) => void; -} diff --git a/src/components/Agentic/IncidentTemplate/MCPServerDialog/Footer/index.tsx b/src/components/Agentic/IncidentTemplate/MCPServerDialog/Footer/index.tsx new file mode 100644 index 000000000..af9133f5b --- /dev/null +++ b/src/components/Agentic/IncidentTemplate/MCPServerDialog/Footer/index.tsx @@ -0,0 +1,21 @@ +import { Spinner } from "../../../../common/v3/Spinner"; +import * as s from "./styles"; +import type { FooterProps } from "./types"; + +export const Footer = ({ + isLoading, + loadingMessage, + errorMessage, + buttons +}: FooterProps) => ( + + {isLoading && ( + + + {loadingMessage} + + )} + {errorMessage && {errorMessage}} + {buttons} + +); diff --git a/src/components/Agentic/IncidentTemplate/MCPServerDialog/Footer/styles.ts b/src/components/Agentic/IncidentTemplate/MCPServerDialog/Footer/styles.ts new file mode 100644 index 000000000..8bb3051ea --- /dev/null +++ b/src/components/Agentic/IncidentTemplate/MCPServerDialog/Footer/styles.ts @@ -0,0 +1,24 @@ +import styled from "styled-components"; + +export const Container = styled.div` + display: flex; + align-items: center; + justify-content: space-between; +`; + +export const LoadingMessage = styled.div` + display: flex; + align-items: center; + gap: 8px; +`; + +export const ErrorMessage = styled.span` + color: ${({ theme }) => theme.colors.v3.status.high}; +`; + +export const ButtonsContainer = styled.div` + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; +`; diff --git a/src/components/Agentic/IncidentTemplate/MCPServerDialog/Footer/types.ts b/src/components/Agentic/IncidentTemplate/MCPServerDialog/Footer/types.ts new file mode 100644 index 000000000..b7f88e73d --- /dev/null +++ b/src/components/Agentic/IncidentTemplate/MCPServerDialog/Footer/types.ts @@ -0,0 +1,8 @@ +import type { ReactNode } from "react"; + +export interface FooterProps { + isLoading?: boolean; + loadingMessage?: string; + errorMessage?: string; + buttons: ReactNode; +} diff --git a/src/components/Agentic/IncidentTemplate/AddMCPServerDialog/ServerStep/index.tsx b/src/components/Agentic/IncidentTemplate/MCPServerDialog/ServerStep/index.tsx similarity index 64% rename from src/components/Agentic/IncidentTemplate/AddMCPServerDialog/ServerStep/index.tsx rename to src/components/Agentic/IncidentTemplate/MCPServerDialog/ServerStep/index.tsx index d2e309f29..4f62e5777 100644 --- a/src/components/Agentic/IncidentTemplate/AddMCPServerDialog/ServerStep/index.tsx +++ b/src/components/Agentic/IncidentTemplate/MCPServerDialog/ServerStep/index.tsx @@ -2,13 +2,16 @@ import { type ChangeEvent } from "react"; import { sendUserActionTrackingEvent } from "../../../../../utils/actions/sendUserActionTrackingEvent"; import { NewButton } from "../../../../common/v3/NewButton"; import { trackingEvents } from "../../../tracking"; +import { Footer } from "../Footer"; import * as s from "./styles"; import type { ServerStepProps } from "./types"; export const ServerStep = ({ onConnect, connectionSettings, - onConnectionSettingsChange + onConnectionSettingsChange, + isLoading, + error }: ServerStepProps) => { const handleTextAreaChange = (e: ChangeEvent) => { onConnectionSettingsChange(e.target.value); @@ -21,18 +24,24 @@ export const ServerStep = ({ onConnect(connectionSettings); }; - const isConnectButtonEnabled = connectionSettings.trim().length > 0; + const isConnectButtonEnabled = + connectionSettings.trim().length > 0 && !isLoading; return ( - - - +