Repository: keephq/keep
Pull request: feat: manual topology manipulation
<!-- 
Thanks for creating this pull request 🤗

Please make sure that the pull request is limited to one type (docs, feature, etc.) and keep it as small as possible. You can open multiple prs instead of opening a huge one.
-->

<!-- If this pull request closes an issue, please mention the issue number below -->
Closes #2904
Closes #3328

## 📑 Description
<!-- Add a brief description of the pr -->

<!-- You can also choose to add a list of changes and if they have been completed or not by using the markdown to-do list syntax
- [ ] Not Completed
- [x] Completed
-->

## ✅ Checks
<!-- Make sure your pr passes the CI checks and do check the following fields as needed - -->
- [ ] My pull request adheres to the code style of this project
- [ ] My code requires changes to the documentation
- [ ] I have updated the documentation as required
- [ ] All the tests have passed

## ℹ Additional Information
<!-- Any additional information like breaking changes, dependencies added, screenshots, comparisons between new and old behavior, etc. -->


Problem statement:
[➕ Feature]: Manual creation of topology
As a user, I would like to manually be able to add services and create connection with Keep's topology feature

We imagine the Topology map to be dynamic, allowing users to add new components and drawing the connections between them.
If the Topology item is fetched from 3rd party provider, it should be stated on object itself and it should not be able to edit it but only to create new connections.

[➕ Feature]: Topology (services + application) export import from YAML
As a user, I would like to export/import the topology (application/services) as YAML file.

Hints from issue discussion:
/bounty 500
/attempt #2904

<div id="algora-attempt" />

| [Algora profile](https://console.algora.io/@/35C4n0r) | Completed bounties | Tech | Active attempts | Options |
| --- | --- | --- | --- | --- |
| @35C4n0r | &nbsp;&nbsp;&nbsp;18 keephq bounties <br/> + 16 bounties from 6 projects | <div align="center">Python, TypeScript, <br />HTML & more</div> | <div align="center"></div> | [Cancel attempt](https://console.algora.io/api/bounties/cm66depxv000jle03abyi6ff8/cancel-attempt) |
Plan for #2904
- User clicks the +Add Node button
- We open a side panel, where user can input these fields (service & display_name are mandatory)
```
class TopologyServiceRequest(BaseModel):
    repository: Optional[str] = None
    tags: Optional[List[str]] = None
    service: str
    display_name: str
    environment: str = "unknown"
    description: Optional[str] = None
    team: Optional[str] = None
    email: Optional[str] = None
    slack: Optional[str] = None
    ip_address: Optional[str] = None
    mac_address: Optional[str] = None
    category: Optional[str] = None
    manufacturer: Optional[str] = None
    namespace: Optional[str] = None
```
- Clicks Save, node gets added in the backend POST/services, and has field manual=True
    manual: Optional[bool] = False (In the schema)
- User can use the handle on the frontend to connect nodes, services dependencies gets updated via POST/services/dependencies . (handle: visible if manual===true).
- User can click an edge that will open a side panel? (to update protocol)
- Users can delete edge and nodes.

/attempt #2904

<div id="algora-attempt" />

<details>
  <summary>Options</summary>
  <ul>
    <li>
      <a href="https://console.algora.io/api/bounties/cm66depxv000jle03abyi6ff8/cancel-attempt">
        Cancel my attempt
      </a>
    </li>
  </ul>
</details>
@promisingcoder FYI, @35C4n0r is already attempting this issue.
/bounty 250
/attempt #3328

<div id="algora-attempt" />

| [Algora profile](https://console.algora.io/@/35C4n0r) | Completed bounties | Tech | Active attempts | Options |
| --- | --- | --- | --- | --- |
| @35C4n0r | &nbsp;&nbsp;&nbsp;18 keephq bounties <br/> + 16 bounties from 6 projects | <div align="center">Python, TypeScript, <br />HTML & more</div> | <div align="center"></div> | [Cancel attempt](https://console.algora.io/api/bounties/cm6s77wc30004kw03t9jdfco9/cancel-attempt) |

Commit message: fix: CI

Signed-off-by: 35C4n0r <jaykumar20march@gmail.com>

Patch to review:
diff --git a/keep-ui/app/(keep)/topology/model/models.ts b/keep-ui/app/(keep)/topology/model/models.ts
index e8f01188ad..c9b59234de 100644
--- a/keep-ui/app/(keep)/topology/model/models.ts
+++ b/keep-ui/app/(keep)/topology/model/models.ts
@@ -1,14 +1,16 @@
 import { InterfaceToType } from "@/utils/type-utils";
 import type { Node } from "@xyflow/react";
+import { KeyedMutator } from "swr";
 
 export interface TopologyServiceDependency {
+  id: string;
   serviceId: string;
   serviceName: string;
   protocol?: string;
 }
 
 export interface TopologyService {
-  id: number;
+  id: string;
   source_provider_id?: string;
   repository?: string;
   tags?: string[];
@@ -27,16 +29,21 @@ export interface TopologyService {
   // Added on client to optimize rendering
   applications: TopologyApplicationMinimal[];
   incidents?: number;
+  is_manual: boolean;
+}
+
+export interface TopologyServiceWithMutator extends TopologyService {
+  topologyMutator: KeyedMutator<TopologyService[]>;
 }
 
 // We need to convert interface to type because only types are allowed in @xyflow/react
 // https://github.com/xyflow/web/issues/486
-export type ServiceNodeType = Node<InterfaceToType<TopologyService>, string>;
+export type ServiceNodeType = Node<InterfaceToType<TopologyServiceWithMutator>, string>;
 
 export type TopologyNode = ServiceNodeType | Node;
 
 export type TopologyServiceMinimal = {
-  id: number;
+  id: string;
   service: string;
   name: string;
 };
diff --git a/keep-ui/app/(keep)/topology/topology-client.tsx b/keep-ui/app/(keep)/topology/topology-client.tsx
index 27e60dd466..c81faa393b 100644
--- a/keep-ui/app/(keep)/topology/topology-client.tsx
+++ b/keep-ui/app/(keep)/topology/topology-client.tsx
@@ -3,7 +3,7 @@
 import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
 import { TopologyMap } from "./ui/map";
 import { ApplicationsList } from "./ui/applications/applications-list";
-import { useContext, useEffect, useState } from "react";
+import React, { useContext, useEffect, useState } from "react";
 import { TopologySearchContext } from "./TopologySearchContext";
 import { TopologyApplication, TopologyService } from "./model";
 import { Button } from "@/components/ui";
@@ -42,47 +42,49 @@ export function TopologyPageClient({
   }, [selectedObjectId]);
 
   return (
-    <TabGroup
-      id="topology-tabs"
-      className="flex flex-col"
-      index={tabIndex}
-      onIndexChange={setTabIndex}
-    >
-      <TabList className="mb-2">
-        <Tab>
-          <div className="flex items-center gap-2 h-6">
-            <span className="inline-flex">
-              <Button
-                variant="secondary"
-                size="xs"
-                onClick={handlePullTopology}
-                title="Pull latest topology"
-              >
-                <ArrowPathIcon className="h-4 w-4" />
-              </Button>
-            </span>
-            <span>Topology Map</span>
-          </div>
-        </Tab>
-        <Tab>
-          <div className="flex items-center h-6">
-            <span>Applications</span>
-          </div>
-        </Tab>
-      </TabList>
-      <TabPanels className="flex-1 flex flex-col">
-        <TabPanel className="h-[calc(100vh-10rem)]">
-          <TopologyMap
-            standalone
-            topologyApplications={applications}
-            topologyServices={topologyServices}
-            isVisible={tabIndex === 0}
-          />
-        </TabPanel>
-        <TabPanel className="flex-1">
-          <ApplicationsList applications={applications} />
-        </TabPanel>
-      </TabPanels>
-    </TabGroup>
+    <>
+      <TabGroup
+        id="topology-tabs"
+        className="flex flex-col"
+        index={tabIndex}
+        onIndexChange={setTabIndex}
+      >
+        <TabList className="mb-2">
+          <Tab>
+            <div className="flex items-center gap-2 h-6">
+              <span className="inline-flex">
+                <Button
+                  variant="secondary"
+                  size="xs"
+                  onClick={handlePullTopology}
+                  title="Pull latest topology"
+                >
+                  <ArrowPathIcon className="h-4 w-4" />
+                </Button>
+              </span>
+              <span>Topology Map</span>
+            </div>
+          </Tab>
+          <Tab>
+            <div className="flex items-center h-6">
+              <span>Applications</span>
+            </div>
+          </Tab>
+        </TabList>
+        <TabPanels className="flex-1 flex flex-col">
+          <TabPanel className="h-[calc(100vh-10rem)]">
+            <TopologyMap
+              standalone
+              topologyApplications={applications}
+              topologyServices={topologyServices}
+              isVisible={tabIndex === 0}
+            />
+          </TabPanel>
+          <TabPanel className="flex-1">
+            <ApplicationsList applications={applications} />
+          </TabPanel>
+        </TabPanels>
+      </TabGroup>
+    </>
   );
 }
diff --git a/keep-ui/app/(keep)/topology/ui/map/AddEditNodeSidePanel.tsx b/keep-ui/app/(keep)/topology/ui/map/AddEditNodeSidePanel.tsx
new file mode 100644
index 0000000000..d247a0f430
--- /dev/null
+++ b/keep-ui/app/(keep)/topology/ui/map/AddEditNodeSidePanel.tsx
@@ -0,0 +1,281 @@
+import SidePanel from "@/components/SidePanel";
+import { useApi } from "@/shared/lib/hooks/useApi";
+import { Button, TextInput } from "@tremor/react";
+import React, { useState, ChangeEvent } from "react";
+import { toast } from "react-toastify";
+import { KeyedMutator } from "swr";
+import { TopologyService } from "../../model";
+
+interface AddNodeSidePanelProps {
+  isOpen: boolean;
+  handleClose: () => void;
+  editData?: TopologyServiceFormProps;
+  topologyMutator: KeyedMutator<TopologyService[]>;
+}
+
+export type TopologyServiceFormProps = {
+  id?: string;
+  repository?: string;
+  tags?: string;
+  service: string;
+  display_name: string;
+  description?: string;
+  team?: string;
+  email?: string;
+  slack?: string;
+  ip_address?: string;
+  mac_address?: string;
+  category?: string;
+  manufacturer?: string;
+  namespace?: string;
+};
+
+export function AddEditNodeSidePanel({
+  isOpen,
+  handleClose,
+  editData,
+  topologyMutator,
+}: AddNodeSidePanelProps) {
+  const api = useApi();
+
+  const handleSave = async () => {
+    try {
+      const result = await api.post("/topology/service", {
+        ...formData,
+        tags: formData.tags
+          ?.split(",")
+          .map((tag) => tag.trim()) // Trim whitespace from each tag
+          .filter((tag) => tag !== ""),
+      });
+      toast.success(`Service added successfully`, { position: "top-right" });
+    } catch (error) {
+      toast.error(`Failed to add service: ${error}`, { position: "top-right" });
+    }
+    topologyMutator();
+    handleClosePanel();
+  };
+
+  const handleUpdate = async () => {
+    try {
+      const result = await api.put("/topology/service", {
+        ...formData,
+        tags: formData.tags
+          ?.split(",")
+          .map((tag) => tag.trim()) // Trim whitespace from each tag
+          .filter((tag) => tag !== ""),
+        id: formData.id,
+      });
+      toast.success(`Service updated successfully`, { position: "top-right" });
+    } catch (error) {
+      toast.error(`Failed to update service: ${error}`, {
+        position: "top-right",
+      });
+    }
+    topologyMutator();
+    handleClosePanel();
+  };
+
+  const handleClosePanel = () => {
+    setFormData({ ...defaultFormData });
+    handleClose();
+  };
+
+  const defaultFormData: TopologyServiceFormProps = {
+    repository: undefined,
+    tags: undefined,
+    service: "",
+    display_name: "",
+    description: undefined,
+    team: undefined,
+    email: undefined,
+    slack: undefined,
+    ip_address: undefined,
+    mac_address: undefined,
+    category: undefined,
+    manufacturer: undefined,
+    namespace: undefined,
+  };
+
+  const [formData, setFormData] = useState<TopologyServiceFormProps>(
+    editData ?? {
+      ...defaultFormData,
+    }
+  );
+
+  const handleSaveValidation = () => {
+    return formData.display_name.length > 0 && formData.service.length > 0;
+  };
+
+  // Function to handle input changes
+  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
+    const { name, value } = e.target;
+    setFormData((prevState) => ({
+      ...prevState,
+      [name]: value,
+    }));
+  };
+
+  return (
+    <SidePanel isOpen={isOpen} onClose={handleClose} panelWidth={"w-1/3"}>
+      <div className="h-full overflow-y-auto gap-y-3 pr-3">
+        <div className="flex flex-col gap-y-3">
+          <div>
+            <label htmlFor="service">
+              Service<sup className="text-red-500">*</sup>
+            </label>
+            <TextInput
+              id="service"
+              name="service"
+              placeholder="Enter service here..."
+              value={formData.service}
+              onChange={handleChange}
+              required
+            />
+          </div>
+          <div>
+            <label htmlFor="display_name">
+              Display Name<sup className="text-red-500">*</sup>
+            </label>
+            <TextInput
+              id="display_name"
+              name="display_name"
+              placeholder="Enter display name here..."
+              value={formData.display_name}
+              onChange={handleChange}
+              required
+            />
+          </div>
+          <div>
+            <label htmlFor="description">Description</label>
+            <TextInput
+              id="description"
+              name="description"
+              placeholder="Enter description here..."
+              value={formData.description || ""}
+              onChange={handleChange}
+            />
+          </div>
+          <div>
+            <label htmlFor="repository">Repository</label>
+            <TextInput
+              id="repository"
+              name="repository"
+              placeholder="Enter repository here..."
+              value={formData.repository || ""}
+              onChange={handleChange}
+            />
+          </div>
+          <div>
+            <label htmlFor="tags">Tags</label>
+            <TextInput
+              id="tags"
+              name="tags"
+              placeholder="Enter tags here (comma-separated)..."
+              value={formData.tags || ""}
+              onChange={handleChange}
+            />
+          </div>
+          <div>
+            <label htmlFor="team">Team</label>
+            <TextInput
+              id="team"
+              name="team"
+              placeholder="Enter team here..."
+              value={formData.team || ""}
+              onChange={handleChange}
+            />
+          </div>
+          <div>
+            <label htmlFor="email">Email</label>
+            <TextInput
+              id="email"
+              name="email"
+              placeholder="Enter email here..."
+              value={formData.email || ""}
+              onChange={handleChange}
+            />
+          </div>
+          <div>
+            <label htmlFor="slack">Slack</label>
+            <TextInput
+              id="slack"
+              name="slack"
+              placeholder="Enter Slack channel here..."
+              value={formData.slack || ""}
+              onChange={handleChange}
+            />
+          </div>
+          <div>
+            <label htmlFor="ip_address">IP Address</label>
+            <TextInput
+              id="ip_address"
+              name="ip_address"
+              placeholder="Enter IP address here..."
+              value={formData.ip_address || ""}
+              onChange={handleChange}
+            />
+          </div>
+          <div>
+            <label htmlFor="mac_address">MAC Address</label>
+            <TextInput
+              id="mac_address"
+              name="mac_address"
+              placeholder="Enter MAC address here..."
+              value={formData.mac_address || ""}
+              onChange={handleChange}
+            />
+          </div>
+          <div>
+            <label htmlFor="category">Category</label>
+            <TextInput
+              id="category"
+              name="category"
+              placeholder="Enter category here..."
+              value={formData.category || ""}
+              onChange={handleChange}
+            />
+          </div>
+          <div>
+            <label htmlFor="manufacturer">Manufacturer</label>
+            <TextInput
+              id="manufacturer"
+              name="manufacturer"
+              placeholder="Enter manufacturer here..."
+              value={formData.manufacturer || ""}
+              onChange={handleChange}
+            />
+          </div>
+          <div>
+            <label htmlFor="namespace">Namespace</label>
+            <TextInput
+              id="namespace"
+              name="namespace"
+              placeholder="Enter namespace here..."
+              value={formData.namespace || ""}
+              onChange={handleChange}
+            />
+          </div>
+        </div>
+      </div>
+      <div className="sticky bottom-0 p-4 border-t border-gray-200 bg-white flex justify-end gap-2">
+        {editData ? (
+          <Button onClick={handleUpdate} color="orange" variant="primary">
+            Update
+          </Button>
+        ) : (
+          <Button
+            onClick={handleSave}
+            color="orange"
+            variant="primary"
+            disabled={!handleSaveValidation()}
+          >
+            Save
+          </Button>
+        )}
+        <Button onClick={handleClosePanel} color="orange" variant="secondary">
+          Close
+        </Button>
+      </div>
+    </SidePanel>
+  );
+}
diff --git a/keep-ui/app/(keep)/topology/ui/map/getNodesAndEdgesFromTopologyData.ts b/keep-ui/app/(keep)/topology/ui/map/getNodesAndEdgesFromTopologyData.ts
index 2bca3c13ed..e905a73c97 100644
--- a/keep-ui/app/(keep)/topology/ui/map/getNodesAndEdgesFromTopologyData.ts
+++ b/keep-ui/app/(keep)/topology/ui/map/getNodesAndEdgesFromTopologyData.ts
@@ -12,11 +12,13 @@ import {
   edgeMarkerEndNoHover,
 } from "@/app/(keep)/topology/ui/map/styles";
 import { IncidentDto } from "@/entities/incidents/model";
+import { KeyedMutator } from "swr";
 
 export function getNodesAndEdgesFromTopologyData(
   topologyData: TopologyService[],
   applicationsMap: Map<string, TopologyApplication>,
-  allIncidents: IncidentDto[]
+  allIncidents: IncidentDto[],
+  topologyMutator: KeyedMutator<TopologyService[]>
 ) {
   const nodeMap = new Map<string, TopologyNode>();
   const edgeMap = new Map<string, Edge>();
@@ -29,9 +31,13 @@ export function getNodesAndEdgesFromTopologyData(
         incident.services.includes(service.service)
     );
     const node: ServiceNodeType = {
-      id: service.service.toString(),
+      id: service.id.toString(),
       type: "service",
-      data: { ...service, incidents: numIncidentsToService.length },
+      data: {
+        ...service,
+        incidents: numIncidentsToService.length,
+        topologyMutator,
+      },
       position: { x: 0, y: 0 }, // Dagre will handle the actual positioning
       selectable: true,
     };
@@ -49,21 +55,17 @@ export function getNodesAndEdgesFromTopologyData(
         })
         .filter((a) => !!a);
     }
-    nodeMap.set(service.service.toString(), node);
+    nodeMap.set(service.id.toString(), node);
     service.dependencies.forEach((dependency) => {
       const dependencyService = topologyData.find(
-        (s) => s.service === dependency.serviceName
+        (s) => s.id === dependency.serviceId
       );
-      const edgeId = `${service.service}_${dependency.protocol}_${
-        dependencyService
-          ? dependencyService.service
-          : dependency.serviceId.toString()
-      }`;
+      const edgeId = dependency.id.toString();
       if (!edgeMap.has(edgeId)) {
         edgeMap.set(edgeId, {
-          id: edgeId,
-          source: service.service.toString(),
-          target: dependency.serviceName.toString(),
+          id: edgeId.toString(),
+          source: service.id.toString(),
+          target: dependencyService?.id.toString() ?? "",
           label: dependency.protocol === "unknown" ? "" : dependency.protocol,
           animated: false,
           labelBgPadding: edgeLabelBgPaddingNoHover,
diff --git a/keep-ui/app/(keep)/topology/ui/map/manage-selection.tsx b/keep-ui/app/(keep)/topology/ui/map/manage-selection.tsx
index aa8c4394be..07e80649c4 100644
--- a/keep-ui/app/(keep)/topology/ui/map/manage-selection.tsx
+++ b/keep-ui/app/(keep)/topology/ui/map/manage-selection.tsx
@@ -1,37 +1,92 @@
 "use client";
 
-import { useOnSelectionChange } from "@xyflow/react";
-import { useState, useCallback, useContext } from "react";
+import { Edge, useOnSelectionChange } from "@xyflow/react";
+import { useState, useCallback, useContext, useEffect } from "react";
 import { cn } from "@/utils/helpers";
 import { Button } from "@/components/ui";
 import {
   useTopologyApplications,
   TopologyApplication,
   ServiceNodeType,
-  TopologyServiceMinimal,
   TopologyNode,
 } from "@/app/(keep)/topology/model";
 import { TopologySearchContext } from "../../TopologySearchContext";
 import { ApplicationModal } from "@/app/(keep)/topology/ui/applications/application-modal";
 import { showErrorToast } from "@/shared/ui";
+import {
+  TopologyService,
+  TopologyServiceWithMutator,
+} from "../../model/models";
+import {
+  AddEditNodeSidePanel,
+  TopologyServiceFormProps,
+} from "./AddEditNodeSidePanel";
+import { useApi } from "@/shared/lib/hooks/useApi";
+import { toast } from "react-toastify";
+import { KeyedMutator } from "swr";
 
-export function ManageSelection({ className }: { className?: string }) {
+export function ManageSelection({
+  className,
+  topologyMutator,
+  getServiceById,
+}: {
+  className?: string;
+  topologyMutator: KeyedMutator<TopologyService[]>;
+  getServiceById: (_id: string) => TopologyService | undefined;
+}) {
   const { setSelectedObjectId } = useContext(TopologySearchContext);
   const { applications, addApplication, removeApplication, updateApplication } =
     useTopologyApplications();
   const [selectedApplication, setSelectedApplication] =
     useState<TopologyApplication | null>(null);
   const [selectedServices, setSelectedServices] = useState<
-    TopologyServiceMinimal[]
+    TopologyServiceWithMutator[]
   >([]);
   const [isModalOpen, setIsModalOpen] = useState(false);
+  const [isSidePanelOpen, setIsSidePanelOpen] = useState<boolean>(false);
+  const [serviceToEdit, setServiceToEdit] = useState<
+    TopologyServiceWithMutator | undefined
+  >(undefined);
+  const api = useApi();
+  const handleServicesDelete = async () => {
+    try {
+      const response = await api.delete("/topology/services", {
+        service_ids: selectedServices.map((service) => service.id),
+      });
+      selectedServices[0].topologyMutator();
+    } catch (error) {
+      toast.error(
+        `Error while deleting ${selectedServices.length === 1 ? "service" : "services"}: ${error}`
+      );
+    }
+  };
+  const [isDependencyEditable, setIsDependencyEditable] =
+    useState<boolean>(false);
 
-  const updateSelectedServices = useCallback(
-    ({ nodes }: { nodes: TopologyNode[] }) => {
+  const [selectedEdges, setSelectedEdges] = useState<Edge[]>([]);
+
+  useEffect(() => {
+    if (
+      selectedEdges.length === 1 &&
+      getServiceById(selectedEdges[0].source)?.is_manual === true &&
+      getServiceById(selectedEdges[0].target)?.is_manual === true
+    ) {
+      setIsDependencyEditable(true);
+    } else {
+      setIsDependencyEditable(false);
+    }
+  }, [selectedEdges, getServiceById]);
+
+  const updateSelectedServicesAndEdges = useCallback(
+    ({ nodes, edges }: { nodes: TopologyNode[]; edges: Edge[] }) => {
       if (isModalOpen) {
         // Avoid dropping selection when focus is on the modal
         return;
       }
+      setSelectedEdges(edges.map((edge) => ({ ...edge })));
+      if (edges.length > 0) {
+        return;
+      }
       if (nodes.length === 0) {
         setSelectedServices([]);
         setSelectedApplication(null);
@@ -39,11 +94,10 @@ export function ManageSelection({ className }: { className?: string }) {
       }
       const servicesNodes = nodes.filter((node) => node.type === "service");
       setSelectedServices(
-        servicesNodes.map((node: TopologyNode) => ({
-          id: (node as ServiceNodeType).data.id,
-          name: (node as ServiceNodeType).data.display_name as string,
-          service: (node as ServiceNodeType).data.service,
-        }))
+        servicesNodes.map(
+          (node: TopologyNode) =>
+            ({ ...node.data }) as TopologyServiceWithMutator
+        )
       );
       // Setting selected application if all services selected has the same app id in data.application_ids
       const appIds = new Set(
@@ -67,7 +121,7 @@ export function ManageSelection({ className }: { className?: string }) {
   );
 
   useOnSelectionChange({
-    onChange: updateSelectedServices,
+    onChange: updateSelectedServicesAndEdges,
   });
 
   const handleUpdateApplication = async (
@@ -109,6 +163,24 @@ export function ManageSelection({ className }: { className?: string }) {
     [removeApplication]
   );
 
+  const editEdgeProtocol = async (edge: Edge) => {
+    const protocol = prompt(
+      "Please enter the protocol:",
+      edge.label?.toString()
+    );
+    if (protocol !== null) {
+      try {
+        const response = await api.put("/topology/dependency", {
+          id: edge.id,
+          protocol: protocol,
+        });
+        topologyMutator();
+      } catch (error) {
+        toast.error("Failed to update protocol");
+      }
+    }
+  };
+
   const renderManageApplicationForm = () => {
     if (selectedApplication === null) {
       return null;
@@ -139,29 +211,79 @@ export function ManageSelection({ className }: { className?: string }) {
     );
   };
 
-  const renderCreateApplicationForm = () => {
+  const renderCreateApplicationAndManageServicesForm = () => {
     return (
       <>
         <p>
           {selectedServices.length > 0 &&
             `Selected: ${selectedServices
-              .map((service) => service.name)
+              .map((service) => service.display_name)
               .join(", ")}`}
         </p>
-        <Button
-          color="orange"
-          size="xs"
-          variant="primary"
-          onClick={() => setIsModalOpen(true)}
-        >
-          Create Application
-        </Button>
+        <div className="">
+          {selectedServices.length === 1 && selectedServices[0].is_manual && (
+            <Button
+              color="orange"
+              size="xs"
+              variant="secondary"
+              className="mr-3"
+              onClick={() => {
+                setIsSidePanelOpen(true);
+                setServiceToEdit(selectedServices[0]);
+              }}
+            >
+              Update Service
+            </Button>
+          )}
+          {selectedServices.length > 0 &&
+            selectedServices.every((service) => service.is_manual === true) && (
+              <Button
+                color="red"
+                size="xs"
+                variant="primary"
+                className="mr-3"
+                onClick={() => handleServicesDelete()}
+              >
+                Delete {selectedServices.length === 1 ? "Service" : "Services"}
+              </Button>
+            )}
+          <Button
+            color="orange"
+            size="xs"
+            variant="primary"
+            onClick={() => setIsModalOpen(true)}
+          >
+            Create Application
+          </Button>
+        </div>
+        {serviceToEdit && (
+          <AddEditNodeSidePanel
+            isOpen={isSidePanelOpen}
+            editData={
+              {
+                ...serviceToEdit,
+                tags: serviceToEdit.tags?.join(","),
+              } as TopologyServiceFormProps
+            }
+            topologyMutator={serviceToEdit.topologyMutator}
+            handleClose={() => {
+              setIsSidePanelOpen(false);
+              setServiceToEdit(undefined);
+            }}
+          />
+        )}
         <ApplicationModal
           isOpen={isModalOpen}
           onClose={() => setIsModalOpen(false)}
           actionType="create"
           application={{
-            services: selectedServices,
+            services: selectedServices.map(
+              (node: TopologyServiceWithMutator) => ({
+                id: node.id,
+                name: node.display_name as string,
+                service: node.service,
+              })
+            ),
           }}
           onSubmit={createApplication}
         />
@@ -169,7 +291,29 @@ export function ManageSelection({ className }: { className?: string }) {
     );
   };
 
-  if (selectedServices.length === 0 && selectedApplication === null) {
+  const renderEditEdgeToolBar = () => {
+    return (
+      <>
+        <div></div>
+        <div className="flex gap-2">
+          <Button
+            color="orange"
+            size="xs"
+            variant="secondary"
+            onClick={() => editEdgeProtocol(selectedEdges[0])}
+          >
+            Edit Dependency
+          </Button>
+        </div>
+      </>
+    );
+  };
+
+  if (
+    selectedServices.length === 0 &&
+    selectedApplication === null &&
+    !isDependencyEditable
+  ) {
     return null;
   }
 
@@ -182,8 +326,9 @@ export function ManageSelection({ className }: { className?: string }) {
     >
       {selectedApplication !== null ? renderManageApplicationForm() : null}
       {selectedApplication === null && selectedServices.length > 0
-        ? renderCreateApplicationForm()
+        ? renderCreateApplicationAndManageServicesForm()
         : null}
+      {isDependencyEditable ? renderEditEdgeToolBar() : null}
     </div>
   );
 }
diff --git a/keep-ui/app/(keep)/topology/ui/map/service-node.tsx b/keep-ui/app/(keep)/topology/ui/map/service-node.tsx
index ff4f927e26..c0216d3dd6 100644
--- a/keep-ui/app/(keep)/topology/ui/map/service-node.tsx
+++ b/keep-ui/app/(keep)/topology/ui/map/service-node.tsx
@@ -158,8 +158,20 @@ export function ServiceNode({ data, selected }: NodeProps<ServiceNodeType>) {
         <ServiceDetailsTooltip data={data} />
       </NodeToolbar>
 
-      <Handle type="source" position={Position.Right} id="right" />
-      <Handle type="target" position={Position.Left} id="left" />
+      <>
+        <Handle
+          type="source"
+          className={clsx(data.is_manual === true && "!opacity-100")}
+          position={Position.Right}
+          id="right"
+        />
+        <Handle
+          type="target"
+          className={clsx(data.is_manual === true && "!opacity-100")}
+          position={Position.Left}
+          id="left"
+        />
+      </>
     </>
   );
 }
diff --git a/keep-ui/app/(keep)/topology/ui/map/topology-map.tsx b/keep-ui/app/(keep)/topology/ui/map/topology-map.tsx
index fd8dc2677b..bd9b2c203a 100644
--- a/keep-ui/app/(keep)/topology/ui/map/topology-map.tsx
+++ b/keep-ui/app/(keep)/topology/ui/map/topology-map.tsx
@@ -19,9 +19,11 @@ import {
   NodeChange,
   EdgeChange,
   FitViewOptions,
+  addEdge,
+  reconnectEdge,
 } from "@xyflow/react";
 import { ServiceNode } from "./service-node";
-import { Card, MultiSelect, MultiSelectItem } from "@tremor/react";
+import { Button, Card, MultiSelect, MultiSelectItem } from "@tremor/react";
 import { ArrowUpRightIcon } from "@heroicons/react/24/outline";
 import {
   edgeLabelBgStyleNoHover,
@@ -51,6 +53,10 @@ import { areSetsEqual } from "@/utils/helpers";
 import { getLayoutedElements } from "@/app/(keep)/topology/ui/map/getLayoutedElements";
 import { getNodesAndEdgesFromTopologyData } from "@/app/(keep)/topology/ui/map/getNodesAndEdgesFromTopologyData";
 import { useIncidents } from "@/utils/hooks/useIncidents";
+import { EdgeBase, Connection } from "@xyflow/system";
+import { AddEditNodeSidePanel } from "./AddEditNodeSidePanel";
+import { toast } from "react-toastify";
+import { useApi } from "@/shared/lib/hooks/useApi";
 
 const defaultFitViewOptions: FitViewOptions = {
   padding: 0.1,
@@ -80,7 +86,12 @@ export function TopologyMap({
 }: TopologyMapProps) {
   const [initiallyFitted, setInitiallyFitted] = useState(false);
 
-  const { topologyData, isLoading, error } = useTopology({
+  const {
+    topologyData,
+    isLoading,
+    error,
+    mutate: mutateTopologyData,
+  } = useTopology({
     providerIds,
     services,
     environment,
@@ -105,6 +116,8 @@ export function TopologyMap({
     }
   }, [initialSelectedApplicationIds, setSelectedApplicationIds]);
 
+  const [isSidePanelOpen, setIsSidePanelOpen] = useState<boolean>(false);
+
   const applicationMap = useMemo(() => {
     const map = new Map<string, TopologyApplication>();
     applications.forEach((app) => {
@@ -153,6 +166,121 @@ export function TopologyMap({
     setNodes((nds) => applyNodeChanges(changes, nds));
   }, []);
 
+  const getServiceById = useCallback(
+    (_id: string) => {
+      return topologyData?.find((service) => {
+        return service.id === _id;
+      });
+    },
+    [topologyData]
+  );
+
+  const api = useApi();
+  const edgeReconnectSuccessful = useRef(true);
+
+  const onConnect = useCallback(
+    async (params: EdgeBase | Connection) => {
+      const sourceService = getServiceById(params.source);
+      const targetService = getServiceById(params.target);
+      if (sourceService?.is_manual === true && targetService?.is_manual === true) {
+        setEdges((eds) => addEdge(params, eds));
+        try {
+          const response = await api.post("/topology/dependency", {
+            service_id: sourceService.id,
+            depends_on_service_id: targetService.id,
+          });
+          mutateTopologyData();
+        } catch (error) {
+          const edgeIdToRevert = `xy-edge__${sourceService.id}right-${targetService.id}left`;
+          setEdges((eds) => eds.filter((e) => e.id !== edgeIdToRevert));
+          toast.error(
+            `Error while adding connection from ${params.source} to ${params.target}: ${error}`
+          );
+        }
+      }},
+    [api, getServiceById, mutateTopologyData]
+  );
+
+  const onReconnectStart = useCallback(() => {
+    edgeReconnectSuccessful.current = false;
+  }, []);
+
+  const onReconnect = useCallback(
+    async (oldEdge: EdgeBase, newConnection: Connection) => {
+      edgeReconnectSuccessful.current = true;
+      if (
+        getServiceById(oldEdge.source)?.is_manual === false ||
+        getServiceById(oldEdge.target)?.is_manual === false ||
+        getServiceById(newConnection.source)?.is_manual === false ||
+        getServiceById(newConnection.target)?.is_manual === false
+      ) {
+        return;
+      }
+      if (
+        oldEdge.source === newConnection.source &&
+        oldEdge.target === newConnection.target
+      ) {
+        return;
+      } else {
+        setEdges((els) => reconnectEdge(oldEdge, newConnection, els));
+        try {
+          const response = await api.put("/topology/dependency", {
+            id: oldEdge.id,
+            service_id: newConnection.source,
+            depends_on_service_id: newConnection.target,
+          });
+          mutateTopologyData();
+        } catch (error) {
+          setEdges((eds) => eds.filter((e) => e.id !== oldEdge.id));
+          setEdges((eds) => addEdge(oldEdge, eds));
+          toast.error(
+            `Error while adding (re)connection from ${newConnection.source} to ${newConnection.target}: ${error}`
+          );
+        }
+      }
+    },
+    [api, mutateTopologyData, getServiceById]
+  );
+
+  const getEdgeIdBySourceTarget = useCallback(
+    (source: string, target: string) => {
+      const sourceNode = topologyData?.find((node) => node.id === source);
+      const edge = sourceNode?.dependencies.find(
+        (deps) => deps.serviceId === target
+      );
+      return edge?.id;
+    },
+    [topologyData]
+  );
+
+  const onReconnectEnd = useCallback(
+    async (_: MouseEvent | TouchEvent, edge: Edge) => {
+      if (
+        getServiceById(edge.source)?.is_manual === false ||
+        getServiceById(edge.target)?.is_manual === false
+      ) {
+        return;
+      }
+
+      if (!edgeReconnectSuccessful.current) {
+        setEdges((eds) => eds.filter((e) => e.id !== edge.id));
+        try {
+          const edgeId = getEdgeIdBySourceTarget(edge.source, edge.target);
+          const response = await api.delete(`/topology/dependency/${edgeId}`);
+          mutateTopologyData();
+          // setEdges((eds) => eds.filter((e) => e.id !== edge.id));
+        } catch (error) {
+          setEdges((eds) => addEdge(edge, eds));
+          toast.error(
+            `Failed to delete connection from ${edge.source} to ${edge.target}: ${error}`
+          );
+        }
+      }
+      edgeReconnectSuccessful.current = true;
+    },
+    [mutateTopologyData, getEdgeIdBySourceTarget, api, getServiceById]
+  );
+
   const onEdgesChange = useCallback(
     (changes: EdgeChange[]) =>
       setEdges((eds) => applyEdgeChanges(changes, eds)),
@@ -180,7 +308,7 @@ export function TopologyMap({
       value: TopologyServiceMinimal | TopologyApplicationMinimal;
     }) => {
       if ("service" in value) {
-        setSelectedObjectId(value.service);
+        setSelectedObjectId(value.id);
       } else {
         const application = applicationMap.get(value.id);
         if (application) {
@@ -203,6 +331,8 @@ export function TopologyMap({
     [isVisible, initiallyFitted]
   );
 
+  
+
   useEffect(() => {
     if (!isVisible || !selectedObjectId || selectedObjectId === "") {
       return;
@@ -244,7 +374,8 @@ export function TopologyMap({
       const { nodeMap, edgeMap } = getNodesAndEdgesFromTopologyData(
         topologyData,
         applicationMap,
-        allIncidents?.items ?? []
+        allIncidents?.items ?? [],
+        mutateTopologyData
       );
 
       const newNodes = Array.from(nodeMap.values());
@@ -275,7 +406,7 @@ export function TopologyMap({
       setNodes(layoutedElements.nodes);
       setEdges(layoutedElements.edges);
     },
-    [topologyData, applicationMap, allIncidents]
+    [topologyData, applicationMap, allIncidents, mutateTopologyData]
   );
 
   useEffect(
@@ -289,7 +420,7 @@ export function TopologyMap({
       const selectedServiceNodesIds = new Set(
         applications.flatMap((app) =>
           selectedApplicationIds.includes(app.id)
-            ? app.services.map((s) => s.service.toString())
+            ? app.services.map((s) => s.id.toString())
             : []
         )
       );
@@ -351,90 +482,107 @@ export function TopologyMap({
   }
 
   return (
-    <div className="flex flex-col gap-4 h-full">
-      <div className="flex justify-between items-baseline gap-4">
-        <TopologySearchAutocomplete
-          wrapperClassName="w-full flex-1"
-          includeApplications={true}
-          providerIds={providerIds}
-          services={services}
-          environment={environment}
-          placeholder="Search for a service or application"
-          onSelect={handleSelectFromSearch}
-        />
-        {/* Using z-index to overflow the manage selection component */}
-        <div className="basis-1/3 relative z-30">
-          <MultiSelect
-            placeholder="Show application"
-            value={selectedApplicationIds}
-            onValueChange={setSelectedApplicationIds}
-            disabled={!applications.length}
-          >
-            {applications.map((app) => (
-              <MultiSelectItem key={app.id} value={app.id}>
-                {app.name}
-              </MultiSelectItem>
-            ))}
-          </MultiSelect>
-        </div>
-        {!standalone ? (
-          <div>
-            <Link
-              icon={ArrowUpRightIcon}
-              iconPosition="right"
-              className="mr-2"
-              href="/topology"
+    <>
+      <div className="flex flex-col gap-4 h-full">
+        <div className="flex justify-between items-baseline gap-4">
+          <TopologySearchAutocomplete
+            wrapperClassName="w-full flex-1"
+            includeApplications={true}
+            providerIds={providerIds}
+            services={services}
+            environment={environment}
+            placeholder="Search for a service or application"
+            onSelect={handleSelectFromSearch}
+          />
+          {/* Using z-index to overflow the manage selection component */}
+          <div className="basis-1/3 relative z-30">
+            <MultiSelect
+              placeholder="Show application"
+              value={selectedApplicationIds}
+              onValueChange={setSelectedApplicationIds}
+              disabled={!applications.length}
             >
-              Full topology map
-            </Link>
+              {applications.map((app) => (
+                <MultiSelectItem key={app.id} value={app.id}>
+                  {app.name}
+                </MultiSelectItem>
+              ))}
+            </MultiSelect>
           </div>
-        ) : null}
-      </div>
-      <Card className="p-0 h-full mx-auto relative overflow-hidden flex flex-col">
-        <ReactFlowProvider>
-          <ManageSelection />
-          <ReactFlow
-            nodes={nodes}
-            edges={edges}
-            minZoom={0.1}
-            snapToGrid
-            fitView
-            fitViewOptions={defaultFitViewOptions}
-            onNodesChange={onNodesChange}
-            onEdgesChange={onEdgesChange}
-            zoomOnDoubleClick={true}
-            onEdgeMouseEnter={(_event, edge) => onEdgeHover("enter", edge)}
-            onEdgeMouseLeave={(_event, edge) => onEdgeHover("leave", edge)}
-            nodeTypes={{
-              service: ServiceNode,
-              application: ApplicationNode,
-            }}
-            onInit={(instance) => {
-              reactFlowInstanceRef.current = instance;
-            }}
-          >
-            <Background variant={BackgroundVariant.Lines} />
-            <Controls />
-          </ReactFlow>
-        </ReactFlowProvider>
-        {!topologyData ||
-          (topologyData?.length === 0 && (
-            <>
-              <div className="absolute top-0 right-0 bg-gray-200 opacity-30 h-full w-full" />
-              <div className="absolute top-0 right-0 h-full w-full p-4 md:p-10">
-                <div className="relative w-full h-full flex flex-col justify-center mb-20">
-                  <EmptyStateCard
-                    className="mb-20"
-                    title="No Topology Available"
-                    description="Seems like no topology data is available, start by connecting providers that support topology."
-                    buttonText="Connect Providers"
-                    onClick={() => router.push("/providers?labels=topology")}
-                  />
+          <Button onClick={() => setIsSidePanelOpen(true)}>+ Add Node</Button>
+          {!standalone ? (
+            <div>
+              <Link
+                icon={ArrowUpRightIcon}
+                iconPosition="right"
+                className="mr-2"
+                href="/topology"
+              >
+                Full topology map
+              </Link>
+            </div>
+          ) : null}
+        </div>
+        <Card className="p-0 h-full mx-auto relative overflow-hidden flex flex-col">
+          <ReactFlowProvider>
+            <ManageSelection
+              topologyMutator={mutateTopologyData}
+              getServiceById={getServiceById}
+            />
+            <ReactFlow
+              nodes={nodes}
+              edges={edges}
+              minZoom={0.1}
+              snapToGrid
+              fitView
+              fitViewOptions={defaultFitViewOptions}
+              onNodesChange={onNodesChange}
+              onEdgesChange={onEdgesChange}
+              onReconnect={onReconnect}
+              onReconnectStart={onReconnectStart}
+              onReconnectEnd={onReconnectEnd}
+              onConnect={onConnect}
+              zoomOnDoubleClick={true}
+              onEdgeMouseEnter={(_event, edge) => onEdgeHover("enter", edge)}
+              onEdgeMouseLeave={(_event, edge) => onEdgeHover("leave", edge)}
+              nodeTypes={{
+                service: ServiceNode,
+                application: ApplicationNode,
+              }}
+              onInit={(instance) => {
+                reactFlowInstanceRef.current = instance;
+              }}
+            >
+              <Background variant={BackgroundVariant.Lines} />
+              <Controls />
+            </ReactFlow>
+          </ReactFlowProvider>
+          {!topologyData ||
+            (topologyData?.length === 0 && (
+              <>
+                <div className="absolute top-0 right-0 bg-gray-200 opacity-30 h-full w-full" />
+                <div className="absolute top-0 right-0 h-full w-full p-4 md:p-10">
+                  <div className="relative w-full h-full flex flex-col justify-center mb-20">
+                    <EmptyStateCard
+                      className="mb-20"
+                      title="No Topology Available"
+                      description="Seems like no topology data is available, start by connecting providers that support topology."
+                      buttonText="Connect Providers"
+                      onClick={() => router.push("/providers?labels=topology")}
+                    />
+                  </div>
                 </div>
-              </div>
-            </>
-          ))}
-      </Card>
-    </div>
+              </>
+            ))}
+        </Card>
+      </div>
+      <AddEditNodeSidePanel
+        isOpen={isSidePanelOpen}
+        topologyMutator={mutateTopologyData}
+        handleClose={() => {
+          setIsSidePanelOpen(false);
+        }}
+      />
+    </>
   );
 }
diff --git a/keep-ui/components/SidePanel.tsx b/keep-ui/components/SidePanel.tsx
index b57afc2e09..dd2a99ae8c 100644
--- a/keep-ui/components/SidePanel.tsx
+++ b/keep-ui/components/SidePanel.tsx
@@ -18,7 +18,7 @@ const SidePanel: React.FC<SidePanelProps> = ({
 }) => {
   return (
     <Transition appear show={isOpen} as={Fragment}>
-      <Dialog as="div" className="relative z-20" onClose={onClose}>
+      <Dialog as="div" className="relative z-50" onClose={onClose}>
         <Transition.Child
           as={Fragment}
           enter="ease-out duration-300"
diff --git a/keep/api/models/db/migrations/versions/2025-01-26-15-25_8176d7153747.py b/keep/api/models/db/migrations/versions/2025-01-26-15-25_8176d7153747.py
new file mode 100644
index 0000000000..580a3b021b
--- /dev/null
+++ b/keep/api/models/db/migrations/versions/2025-01-26-15-25_8176d7153747.py
@@ -0,0 +1,29 @@
+"""Add manual field in topology-service
+
+Revision ID: 8176d7153747
+Revises: e343054ae740
+Create Date: 2025-01-26 15:25:23.811890
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "8176d7153747"
+down_revision = "e343054ae740"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    with op.batch_alter_table("topologyservice", schema=None) as batch_op:
+        batch_op.add_column(sa.Column("is_manual", sa.Boolean(), nullable=True))
+    # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+    with op.batch_alter_table("topologyservice", schema=None) as batch_op:
+        batch_op.drop_column("is_manual")
+
+    # ### end Alembic commands ###
diff --git a/keep/api/models/db/topology.py b/keep/api/models/db/topology.py
index 720768b031..eb3029ad0a 100644
--- a/keep/api/models/db/topology.py
+++ b/keep/api/models/db/topology.py
@@ -41,6 +41,7 @@ class TopologyService(SQLModel, table=True):
     category: Optional[str] = None
     manufacturer: Optional[str] = None
     namespace: Optional[str] = None
+    is_manual: Optional[bool] = False
 
     updated_at: Optional[datetime] = Field(
         sa_column=Column(
@@ -115,6 +116,7 @@ class TopologyServiceDtoBase(BaseModel, extra="ignore"):
     category: Optional[str] = None
     manufacturer: Optional[str] = None
     namespace: Optional[str] = None
+    is_manual: Optional[bool] = False
 
 
 class TopologyServiceInDto(TopologyServiceDtoBase):
@@ -125,10 +127,20 @@ class TopologyServiceInDto(TopologyServiceDtoBase):
 
 
 class TopologyServiceDependencyDto(BaseModel, extra="ignore"):
-    serviceId: int
+    id: Optional[str] = None
+    serviceId: str
     serviceName: str
     protocol: Optional[str] = "unknown"
 
+    @classmethod
+    def from_orm(cls, db_dependency: TopologyServiceDependency):
+        return TopologyServiceDependencyDto(
+            id=db_dependency.id,
+            serviceId=str(db_dependency.depends_on_service_id),
+            protocol=db_dependency.protocol,
+            serviceName=db_dependency.dependent_service.service,
+        )
+
 
 class TopologyApplicationDto(BaseModel, extra="ignore"):
     id: UUID
@@ -153,14 +165,14 @@ class TopologyApplicationDtoIn(BaseModel, extra="ignore"):
 
 
 class TopologyApplicationServiceDto(BaseModel, extra="ignore"):
-    id: int
+    id: str
     name: str
     service: str
 
     @classmethod
     def from_orm(cls, service: "TopologyService") -> "TopologyApplicationServiceDto":
         return cls(
-            id=service.id,
+            id=str(service.id),
             name=service.display_name,
             service=service.service,
         )
@@ -186,7 +198,7 @@ def from_orm(
 
 
 class TopologyServiceDtoOut(TopologyServiceDtoBase):
-    id: int
+    id: str
     dependencies: List[TopologyServiceDependencyDto]
     application_ids: List[UUID]
     updated_at: Optional[datetime]
@@ -196,7 +208,7 @@ def from_orm(
         cls, service: "TopologyService", application_ids: List[UUID]
     ) -> "TopologyServiceDtoOut":
         return cls(
-            id=service.id,
+            id=str(service.id),
             source_provider_id=service.source_provider_id,
             repository=service.repository,
             tags=service.tags,
@@ -213,7 +225,8 @@ def from_orm(
             category=service.category,
             dependencies=[
                 TopologyServiceDependencyDto(
-                    serviceId=dep.depends_on_service_id,
+                    id=dep.id,
+                    serviceId=str(dep.depends_on_service_id),
                     protocol=dep.protocol,
                     serviceName=dep.dependent_service.service,
                 )
@@ -222,4 +235,44 @@ def from_orm(
             application_ids=application_ids,
             updated_at=service.updated_at,
             namespace=service.namespace,
+            is_manual=service.is_manual if service.is_manual is not None else False,
         )
+
+
+class TopologyServiceCreateRequestDTO(BaseModel, extra="ignore"):
+    repository: Optional[str] = None
+    tags: Optional[List[str]] = None
+    service: str
+    display_name: str
+    environment: str = "unknown"
+    description: Optional[str] = None
+    team: Optional[str] = None
+    email: Optional[str] = None
+    slack: Optional[str] = None
+    ip_address: Optional[str] = None
+    mac_address: Optional[str] = None
+    category: Optional[str] = None
+    manufacturer: Optional[str] = None
+    namespace: Optional[str] = None
+
+
+class TopologyServiceUpdateRequestDTO(TopologyServiceCreateRequestDTO, extra="ignore"):
+    id: int
+
+
+class TopologyServiceDependencyCreateRequestDto(BaseModel, extra="ignore"):
+    service_id: int
+    depends_on_service_id: int
+    protocol: Optional[str] = "unknown"
+
+
+class TopologyServiceDependencyUpdateRequestDto(
+    TopologyServiceDependencyCreateRequestDto, extra="ignore"
+):
+    service_id: Optional[int]
+    depends_on_service_id: Optional[int]
+    id: int
+
+
+class DeleteServicesRequest(BaseModel, extra="ignore"):
+    service_ids: List[int]
diff --git a/keep/api/routes/topology.py b/keep/api/routes/topology.py
index 13dc21841f..db104fcc7b 100644
--- a/keep/api/routes/topology.py
+++ b/keep/api/routes/topology.py
@@ -12,6 +12,13 @@
     TopologyApplicationDtoOut,
     TopologyServiceDtoIn,
     TopologyServiceDtoOut,
+    TopologyServiceCreateRequestDTO,
+    TopologyServiceUpdateRequestDTO,
+    TopologyServiceDependencyCreateRequestDto,
+    TopologyServiceDependencyUpdateRequestDto,
+    TopologyServiceDependencyDto,
+    TopologyService,
+    DeleteServicesRequest,
 )
 from keep.api.tasks.process_topology_task import process_topology
 from keep.identitymanager.authenticatedentity import AuthenticatedEntity
@@ -24,6 +31,8 @@
     InvalidApplicationDataException,
     ServiceNotFoundException,
     TopologiesService,
+    DependencyNotFoundException,
+    ServiceNotManualException,
 )
 
 logger = logging.getLogger(__name__)
@@ -280,3 +289,162 @@ def pull_topology_data(
         raise HTTPException(
             status_code=500, detail=f"Failed to pull topology data: {str(e)}"
         )
+
+
+@router.post("/service", description="Creating a service manually")
+def create_service(
+    service: TopologyServiceCreateRequestDTO,
+    authenticated_entity: AuthenticatedEntity = Depends(
+        IdentityManagerFactory.get_auth_verifier(["write:topology"])
+    ),
+    session: Session = Depends(get_session),
+) -> TopologyService:
+    """
+    Any services created by this endpoint will have manual set to True.
+    """
+    try:
+        return TopologiesService.create_service(
+            service=service, tenant_id=authenticated_entity.tenant_id, session=session
+        )
+    except Exception as e:
+        raise HTTPException(
+            status_code=500, detail=f"Failed to create service: {str(e)}"
+        )
+
+
+@router.put("/service", description="Updating a service manually")
+def update_service(
+    service: TopologyServiceUpdateRequestDTO,
+    authenticated_entity: AuthenticatedEntity = Depends(
+        IdentityManagerFactory.get_auth_verifier(["write:topology"])
+    ),
+    session: Session = Depends(get_session),
+) -> TopologyService:
+    try:
+        return TopologiesService.update_service(
+            service=service, tenant_id=authenticated_entity.tenant_id, session=session
+        )
+
+    except ServiceNotManualException:
+        raise HTTPException(
+            status_code=404,
+            detail="The service you're trying to updated was not created manually.",
+        )
+    except ServiceNotFoundException:
+        raise HTTPException(status_code=404, detail="Service not found")
+    except Exception as e:
+        raise HTTPException(
+            status_code=500, detail=f"Failed to update service: {str(e)}"
+        )
+
+
+@router.delete("/services", description="Delete a list of services manually")
+def delete_services(
+    service_ids: DeleteServicesRequest,
+    authenticated_entity: AuthenticatedEntity = Depends(
+        IdentityManagerFactory.get_auth_verifier(["write:topology"])
+    ),
+    session: Session = Depends(get_session),
+):
+    try:
+        TopologiesService.delete_services(
+            service_ids=service_ids.service_ids,
+            tenant_id=authenticated_entity.tenant_id,
+            session=session,
+        )
+        return JSONResponse(
+            status_code=200, content={"message": "Services deleted successfully"}
+        )
+    except ServiceNotManualException:
+        raise HTTPException(
+            status_code=404,
+            detail="One or more service(s) you're trying to delete was not created manually.",
+        )
+    except ServiceNotFoundException:
+        raise HTTPException(status_code=404, detail="Service not found")
+    except Exception as e:
+        raise HTTPException(
+            status_code=500, detail=f"Failed to delete services: {str(e)}"
+        )
+
+
+@router.post("/dependency", description="Creating a new dependency manually")
+def create_dependencies(
+    dependency: TopologyServiceDependencyCreateRequestDto,
+    authenticated_entity: AuthenticatedEntity = Depends(
+        IdentityManagerFactory.get_auth_verifier(["write:topology"])
+    ),
+    session: Session = Depends(get_session),
+) -> TopologyServiceDependencyDto:
+    try:
+        return TopologiesService.create_dependency(
+            dependency=dependency,
+            session=session,
+            tenant_id=authenticated_entity.tenant_id,
+        )
+    except ServiceNotManualException:
+        raise HTTPException(
+            status_code=404,
+            detail="You're tying to create a dependency between one or more pulled services.",
+        )
+    except Exception as e:
+        raise HTTPException(
+            status_code=500, detail=f"Failed to create Dependency: {str(e)}"
+        )
+
+
+@router.put("/dependency", description="Updating a dependency manually")
+def update_dependency(
+    dependency: TopologyServiceDependencyUpdateRequestDto,
+    authenticated_entity: AuthenticatedEntity = Depends(
+        IdentityManagerFactory.get_auth_verifier(["write:topology"])
+    ),
+    session: Session = Depends(get_session),
+) -> TopologyServiceDependencyDto:
+    try:
+        return TopologiesService.update_dependency(
+            dependency=dependency,
+            session=session,
+            tenant_id=authenticated_entity.tenant_id,
+        )
+    except DependencyNotFoundException:
+        raise HTTPException(status_code=404, detail="Dependency not found")
+    except ServiceNotManualException:
+        raise HTTPException(
+            status_code=404,
+            detail="You're tying to update a dependency between one or more pulled services.",
+        )
+    except Exception as e:
+        raise HTTPException(
+            status_code=500, detail=f"Failed to update Dependency: {str(e)}"
+        )
+
+
+@router.delete(
+    "/dependency/{dependency_id}", description="Deleting a dependency manually"
+)
+def delete_dependency(
+    dependency_id: int,
+    authenticated_entity: AuthenticatedEntity = Depends(
+        IdentityManagerFactory.get_auth_verifier(["write:topology"])
+    ),
+    session: Session = Depends(get_session),
+):
+    try:
+        TopologiesService.delete_dependency(
+            dependency_id=dependency_id, session=session, tenant_id=authenticated_entity.tenant_id
+        )
+        return JSONResponse(
+            status_code=200, content={"message": "Dependency deleted successfully"}
+        )
+    except DependencyNotFoundException:
+        raise HTTPException(status_code=404, detail="Dependency not found")
+    except ServiceNotManualException:
+        raise HTTPException(
+            status_code=404,
+            detail="You're tying to delete a dependency between two or more manual services.",
+        )
+    except Exception as e:
+        raise HTTPException(
+            status_code=500, detail=f"Failed to delete Dependency: {str(e)}"
+        )
diff --git a/keep/topologies/topologies_service.py b/keep/topologies/topologies_service.py
index 6c0b858fde..eff8aaf89d 100644
--- a/keep/topologies/topologies_service.py
+++ b/keep/topologies/topologies_service.py
@@ -4,6 +4,7 @@
 from uuid import UUID
 
 from pydantic import ValidationError
+from sqlalchemy import and_, or_, exists
 from sqlalchemy.orm import joinedload, selectinload
 from sqlmodel import Session, select
 
@@ -16,6 +17,11 @@
     TopologyServiceApplication,
     TopologyServiceDependency,
     TopologyServiceDtoOut,
+    TopologyServiceCreateRequestDTO,
+    TopologyServiceUpdateRequestDTO,
+    TopologyServiceDependencyCreateRequestDto,
+    TopologyServiceDependencyUpdateRequestDto,
+    TopologyServiceDependencyDto,
 )
 
 logger = logging.getLogger(__name__)
@@ -41,6 +47,14 @@ class ServiceNotFoundException(TopologyException):
     """Raised when a service is not found"""
 
 
+class ServiceNotManualException(TopologyException):
+    """Raised when a service is not manual"""
+
+
+class DependencyNotFoundException(TopologyException):
+    """Raised when a dependency is not found"""
+
+
 def get_service_application_ids_dict(
     session: Session, service_ids: List[int]
 ) -> dict[int, List[UUID]]:
@@ -84,6 +98,19 @@ def get_service_application_ids_dict(
     return result
 
 
+def validate_non_manual_exists(
+    service_ids: list[int], session: Session, tenant_id: str
+) -> bool:
+    non_manual_exists = session.query(
+        exists()
+        .where(TopologyService.id.in_(service_ids))
+        .where(TopologyService.tenant_id == tenant_id)
+        .where(TopologyService.is_manual.isnot(True))
+    ).scalar()
+
+    return non_manual_exists
+
+
 class TopologiesService:
     @staticmethod
     def get_all_topology_data(
@@ -324,3 +351,220 @@ def delete_application_by_id(
         session.delete(application)
         session.commit()
         return None
+
+    @staticmethod
+    def get_service_by_id(
+        _id: int, tenant_id: str, session: Session
+    ) -> TopologyService:
+        return session.exec(
+            select(TopologyService)
+            .where(TopologyService.tenant_id == tenant_id)
+            .where(TopologyService.id == _id)
+        ).first()
+
+    @staticmethod
+    def get_dependency_by_id(_id: int, session: Session) -> TopologyServiceDependency:
+        return session.exec(
+            select(TopologyServiceDependency).where(TopologyServiceDependency.id == _id)
+        ).first()
+
+    @staticmethod
+    def create_service(
+        service: TopologyServiceCreateRequestDTO, tenant_id: str, session: Session
+    ) -> TopologyService:
+        """This function is used for creating services manually. services.is_manual=True"""
+
+        try:
+            # Setting is_manual to True since this service is created manually.
+            db_service = TopologyService(
+                **service.dict(), tenant_id=tenant_id, is_manual=True
+            )
+            session.add(db_service)
+            session.commit()
+            session.refresh(db_service)
+            return db_service
+        except Exception as e:
+            session.rollback()
+            logger.error(f"Error while creating/updating the services manually: {e}")
+            raise e
+        finally:
+            session.close()
+
+    @staticmethod
+    def update_service(
+        service: TopologyServiceUpdateRequestDTO, tenant_id: str, session: Session
+    ) -> TopologyService:
+        try:
+            db_service: TopologyService = TopologiesService.get_service_by_id(
+                _id=service.id, tenant_id=tenant_id, session=session
+            )
+
+            # Asserting that the service we're trying to update was created manually
+            if not db_service.is_manual:
+                raise ServiceNotManualException()
+
+            service_dict = service.dict()
+            if db_service is None:
+                raise ServiceNotFoundException()
+            else:  # We update it.
+                for attr in service_dict:
+                    if (
+                        service_dict[attr] is not None
+                        and db_service.__getattribute__(attr) != service_dict[attr]
+                    ):
+                        db_service.__setattr__(attr, service_dict[attr])
+                session.commit()
+                session.refresh(db_service)
+                return db_service
+        except Exception as e:
+            session.rollback()
+            logger.error(f"Error while updating the services manually: {e}")
+            raise e
+        finally:
+            session.close()
+
+    @staticmethod
+    def delete_services(service_ids: list[int], tenant_id: str, session: Session):
+        try:
+
+            # Asserting that all the services that we are trying to delete were created manually, if this assertion
+            # fails we do not proceed with deletion at all
+            if validate_non_manual_exists(
+                service_ids=service_ids,
+                session=session,
+                tenant_id=tenant_id,
+            ):
+                raise ServiceNotManualException()
+
+            # Deleting all the dependencies first
+            session.query(TopologyServiceDependency).filter(
+                TopologyServiceDependency.service.has(
+                    and_(
+                        TopologyService.tenant_id == tenant_id,
+                        or_(
+                            TopologyServiceDependency.service_id.in_(service_ids),
+                            TopologyServiceDependency.depends_on_service_id.in_(
+                                service_ids
+                            ),
+                        ),
+                    )
+                )
+            ).delete(synchronize_session=False)
+
+            deleted_count = (
+                session.query(TopologyService)
+                .filter(
+                    TopologyService.id.in_(service_ids),
+                    TopologyService.tenant_id == tenant_id,
+                )
+                .delete(synchronize_session=False)  # Efficient batch delete
+            )
+
+            if deleted_count == 0:
+                raise ServiceNotFoundException("No services found for the given IDs.")
+
+            session.commit()
+        except Exception as e:
+            session.rollback()
+            logger.error(f"Error while deleting services: {e}")
+            raise e
+        finally:
+            session.close()
+
+    @staticmethod
+    def create_dependency(
+        dependency: TopologyServiceDependencyCreateRequestDto,
+        tenant_id: str,
+        session: Session,
+    ) -> TopologyServiceDependencyDto:
+        try:
+           # Enforcing is_manual on the service_id and depends_on_service_id
+            if validate_non_manual_exists(
+                service_ids=[dependency.service_id, dependency.depends_on_service_id],
+                session=session,
+                tenant_id=tenant_id,
+            ):
+                raise ServiceNotManualException()
+
+            db_dependency = TopologyServiceDependency(**dependency.dict())
+            session.add(db_dependency)
+            session.commit()
+            session.refresh(db_dependency)
+            return TopologyServiceDependencyDto.from_orm(db_dependency)
+        except Exception as e:
+            session.rollback()
+            logger.error(f"Error while creating/updating the Dependency manually: {e}")
+            raise e
+        finally:
+            session.close()
+
+    @staticmethod
+    def update_dependency(
+        dependency: TopologyServiceDependencyUpdateRequestDto,
+        session: Session,
+        tenant_id: str,
+    ) -> TopologyServiceDependencyDto:
+        try:
+            # Enforcing is_manual on the service_id and depends_on_service_id
+            if validate_non_manual_exists(
+                service_ids=[dependency.service_id, dependency.depends_on_service_id],
+                session=session,
+                tenant_id=tenant_id,
+            ):
+                raise ServiceNotManualException()
+
+            db_dependency: TopologyServiceDependency = (
+                TopologiesService.get_dependency_by_id(
+                    _id=dependency.id, session=session
+                )
+            )
+            service_dict = dependency.dict()
+            if db_dependency is None:
+                raise DependencyNotFoundException()
+            else:  # We update it.
+                for attr in service_dict:
+                    if (
+                        service_dict[attr] is not None
+                        and db_dependency.__getattribute__(attr) != service_dict[attr]
+                    ):
+                        db_dependency.__setattr__(attr, service_dict[attr])
+                session.commit()
+                session.refresh(db_dependency)
+                return TopologyServiceDependencyDto.from_orm(db_dependency)
+        except Exception as e:
+            session.rollback()
+            logger.error(f"Error while updating the Dependency manually: {e}")
+            raise e
+        finally:
+            session.close()
+
+    @staticmethod
+    def delete_dependency(dependency_id: int, session: Session, tenant_id: str):
+        try:
+            db_dependency: TopologyServiceDependency = (
+                TopologiesService.get_dependency_by_id(
+                    _id=dependency_id, session=session
+                )
+            )
+            # Enforcing is_manual on the service_id and depends_on_service_id
+            if validate_non_manual_exists(
+                service_ids=[
+                    db_dependency.service_id,
+                    db_dependency.depends_on_service_id,
+                ],
+                session=session,
+                tenant_id=tenant_id,
+            ):
+                raise ServiceNotManualException()
+
+            if db_dependency is None:
+                raise DependencyNotFoundException()
+            session.delete(db_dependency)
+            session.commit()
+            return None
+        except Exception as e:
+            session.rollback()
+            logger.error(f"Error while updating the Dependency manually: {e}")
+            raise e
+        finally:
+            session.close()
diff --git a/tests/test_enrichments.py b/tests/test_enrichments.py
index 13f430e8b1..e826aeaf31 100644
--- a/tests/test_enrichments.py
+++ b/tests/test_enrichments.py
@@ -634,6 +634,7 @@ def test_topology_mapping_rule_enrichment(mock_session, mock_alert_dto):
                     "service": "test-service",
                     "environment": "unknown",
                     "display_name": "Test Service",
+                    "is_manual": False,
                 },
                 action_callee="system",
                 action_type=ActionType.MAPPING_RULE_ENRICH,


Code review comments:
diff_hunk: @@ -42,47 +42,49 @@ export function TopologyPageClient({
   }, [selectedObjectId]);
 
   return (
-    <TabGroup
-      id="topology-tabs"
-      className="flex flex-col"
-      index={tabIndex}
-      onIndexChange={setTabIndex}
-    >
-      <TabList className="mb-2">
-        <Tab>
-          <div className="flex items-center gap-2 h-6">
-            <span className="inline-flex">
-              <Button
-                variant="secondary"
-                size="xs"
-                onClick={handlePullTopology}
-                title="Pull latest topology"
-              >
-                <ArrowPathIcon className="h-4 w-4" />
-              </Button>
-            </span>
-            <span>Topology Map</span>
-          </div>
-        </Tab>
-        <Tab>
-          <div className="flex items-center h-6">
-            <span>Applications</span>
-          </div>
-        </Tab>
-      </TabList>
-      <TabPanels className="flex-1 flex flex-col">
-        <TabPanel className="h-[calc(100vh-10rem)]">
-          <TopologyMap
-            standalone
-            topologyApplications={applications}
-            topologyServices={topologyServices}
-            isVisible={tabIndex === 0}
-          />
-        </TabPanel>
-        <TabPanel className="flex-1">
-          <ApplicationsList applications={applications} />
-        </TabPanel>
-      </TabPanels>
-    </TabGroup>
+    <>
path: keep-ui/app/(keep)/topology/topology-client.tsx
text: @user1:
is that needed?