This is an automated email from the ASF dual-hosted git repository.

yasith pushed a commit to branch resource-mgmt
in repository https://gitbox.apache.org/repos/asf/airavata-portals.git

commit a5690bd0ff8d010c854d0620df15b3e6eb57f92e
Author: yasithdev <[email protected]>
AuthorDate: Thu Nov 20 13:56:06 2025 -0600

    initial storage and compute resource management functionality
---
 airavata-research-portal/src/App.tsx               |   2 +
 .../components/resources/ComputeResourceForm.tsx   | 283 +++++++++++++++
 .../src/components/resources/QueueManagement.tsx   | 381 +++++++++++++++++++++
 .../components/resources/ResourceManagement.tsx    | 151 ++++++++
 .../components/resources/StorageResourceForm.tsx   | 218 ++++++++++++
 .../src/interfaces/ComputeResourceType.ts          |  42 +++
 .../src/interfaces/StorageResourceType.ts          |  11 +
 airavata-research-portal/src/lib/resourceApi.ts    | 109 ++++++
 8 files changed, 1197 insertions(+)

diff --git a/airavata-research-portal/src/App.tsx 
b/airavata-research-portal/src/App.tsx
index 4df295780..99850962a 100644
--- a/airavata-research-portal/src/App.tsx
+++ b/airavata-research-portal/src/App.tsx
@@ -41,6 +41,7 @@ import {AddRepoMaster} from "./components/add/AddRepoMaster";
 import {Add} from "./components/add";
 import {AddProjectMaster} from "./components/add/AddProjectMaster";
 import {StarredResourcesPage} from 
"@/components/resources/StarredResourcesPage.tsx";
+import {ResourceManagement} from 
"@/components/resources/ResourceManagement.tsx";
 
 function App() {
   const colorMode = useColorMode();
@@ -120,6 +121,7 @@ function App() {
                 element={<ProtectedComponent Component={NavBarFooterLayout}/>}
             >
               <Route path="/resources/starred" 
element={<StarredResourcesPage/>}/>
+              <Route path="/resources/manage" element={<ResourceManagement/>}/>
               <Route path="/sessions" element={<Home/>}/>
               <Route path="/add" element={<Add/>}/>
               <Route path="/add/repo" element={<AddRepoMaster/>}/>
diff --git 
a/airavata-research-portal/src/components/resources/ComputeResourceForm.tsx 
b/airavata-research-portal/src/components/resources/ComputeResourceForm.tsx
new file mode 100644
index 000000000..9800aa615
--- /dev/null
+++ b/airavata-research-portal/src/components/resources/ComputeResourceForm.tsx
@@ -0,0 +1,283 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {
+  Box,
+  Button,
+  FormControl,
+  FormLabel,
+  Input,
+  Select,
+  Textarea,
+  VStack,
+  HStack,
+  Switch,
+  NumberInput,
+  NumberInputField,
+  Table,
+  Thead,
+  Tbody,
+  Tr,
+  Th,
+  Td,
+  useToast,
+} from "@chakra-ui/react";
+import { useState, useEffect } from "react";
+import { ComputeResource, ResourceName } from 
"@/interfaces/ComputeResourceType";
+import { computeResourceApi } from "@/lib/resourceApi";
+import { QueueManagement } from "./QueueManagement";
+
+interface ComputeResourceFormProps {
+  resources: ResourceName[];
+  selectedResource: ComputeResource | null;
+  onSelect: (id: string) => void;
+  onSave: () => void;
+  loading: boolean;
+}
+
+export const ComputeResourceForm = ({
+  resources,
+  selectedResource,
+  onSelect,
+  onSave,
+  loading,
+}: ComputeResourceFormProps) => {
+  const [formData, setFormData] = useState<ComputeResource>({
+    hostName: "",
+  } as ComputeResource);
+  const [saving, setSaving] = useState(false);
+  const toast = useToast();
+
+  useEffect(() => {
+    if (selectedResource) {
+      setFormData(selectedResource);
+    } else {
+      setFormData({ hostName: "" } as ComputeResource);
+    }
+  }, [selectedResource]);
+
+  const handleChange = (field: keyof ComputeResource, value: any) => {
+    setFormData((prev) => ({ ...prev, [field]: value }));
+  };
+
+  const handleSave = async () => {
+    setSaving(true);
+    try {
+      if (formData.computeResourceId) {
+        await computeResourceApi.update(formData.computeResourceId, formData);
+        toast({
+          title: "Success",
+          description: "Compute resource updated successfully",
+          status: "success",
+          duration: 3000,
+        });
+      } else {
+        await computeResourceApi.create(formData);
+        toast({
+          title: "Success",
+          description: "Compute resource created successfully",
+          status: "success",
+          duration: 3000,
+        });
+      }
+      onSave();
+    } catch (error: any) {
+      toast({
+        title: "Error",
+        description: error.response?.data?.error || "Failed to save compute 
resource",
+        status: "error",
+        duration: 5000,
+      });
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  const handleDelete = async () => {
+    if (!formData.computeResourceId) return;
+    if (!confirm("Are you sure you want to delete this compute resource?")) 
return;
+
+    setSaving(true);
+    try {
+      await computeResourceApi.delete(formData.computeResourceId);
+      toast({
+        title: "Success",
+        description: "Compute resource deleted successfully",
+        status: "success",
+        duration: 3000,
+      });
+      onSave();
+    } catch (error: any) {
+      toast({
+        title: "Error",
+        description: error.response?.data?.error || "Failed to delete compute 
resource",
+        status: "error",
+        duration: 5000,
+      });
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  if (!selectedResource) {
+    return (
+      <Box>
+        {resources.length > 0 ? (
+          <Table variant="simple">
+            <Thead>
+              <Tr>
+                <Th>ID</Th>
+                <Th>Name</Th>
+                <Th>Actions</Th>
+              </Tr>
+            </Thead>
+            <Tbody>
+              {resources.map((resource) => (
+                <Tr key={resource.id}>
+                  <Td>{resource.id}</Td>
+                  <Td>{resource.name}</Td>
+                  <Td>
+                    <Button size="sm" onClick={() => onSelect(resource.id)}>
+                      Edit
+                    </Button>
+                  </Td>
+                </Tr>
+              ))}
+            </Tbody>
+          </Table>
+        ) : (
+          <Box p={4} textAlign="center">
+            No compute resources found. Click "Create New Compute Resource" to 
add one.
+          </Box>
+        )}
+      </Box>
+    );
+  }
+
+  return (
+    <Box>
+      <VStack spacing={4} align="stretch">
+        <FormControl>
+          <FormLabel>Host Name *</FormLabel>
+          <Input
+            value={formData.hostName || ""}
+            onChange={(e) => handleChange("hostName", e.target.value)}
+            placeholder="e.g., login.example.com"
+          />
+        </FormControl>
+
+        <FormControl>
+          <FormLabel>Resource Description</FormLabel>
+          <Textarea
+            value={formData.resourceDescription || ""}
+            onChange={(e) => handleChange("resourceDescription", 
e.target.value)}
+            placeholder="Description of the compute resource"
+          />
+        </FormControl>
+
+        <FormControl>
+          <FormLabel>Enabled</FormLabel>
+          <Switch
+            isChecked={formData.enabled ?? true}
+            onChange={(e) => handleChange("enabled", e.target.checked)}
+          />
+        </FormControl>
+
+        <HStack>
+          <FormControl>
+            <FormLabel>CPUs Per Node</FormLabel>
+            <NumberInput
+              value={formData.cpusPerNode || ""}
+              onChange={(_, value) => handleChange("cpusPerNode", value)}
+            >
+              <NumberInputField />
+            </NumberInput>
+          </FormControl>
+
+          <FormControl>
+            <FormLabel>Max Memory Per Node (MB)</FormLabel>
+            <NumberInput
+              value={formData.maxMemoryPerNode || ""}
+              onChange={(_, value) => handleChange("maxMemoryPerNode", value)}
+            >
+              <NumberInputField />
+            </NumberInput>
+          </FormControl>
+        </HStack>
+
+        <HStack>
+          <FormControl>
+            <FormLabel>Default Node Count</FormLabel>
+            <NumberInput
+              value={formData.defaultNodeCount || ""}
+              onChange={(_, value) => handleChange("defaultNodeCount", value)}
+            >
+              <NumberInputField />
+            </NumberInput>
+          </FormControl>
+
+          <FormControl>
+            <FormLabel>Default CPU Count</FormLabel>
+            <NumberInput
+              value={formData.defaultCPUCount || ""}
+              onChange={(_, value) => handleChange("defaultCPUCount", value)}
+            >
+              <NumberInputField />
+            </NumberInput>
+          </FormControl>
+
+          <FormControl>
+            <FormLabel>Default Walltime (minutes)</FormLabel>
+            <NumberInput
+              value={formData.defaultWalltime || ""}
+              onChange={(_, value) => handleChange("defaultWalltime", value)}
+            >
+              <NumberInputField />
+            </NumberInput>
+          </FormControl>
+        </HStack>
+
+        {formData.computeResourceId && (
+          <QueueManagement
+            computeResourceId={formData.computeResourceId}
+            queues={formData.batchQueues || []}
+            onQueuesChange={(queues) => handleChange("batchQueues", queues)}
+          />
+        )}
+
+        <HStack>
+          <Button
+            colorScheme="blue"
+            onClick={handleSave}
+            isLoading={saving}
+            loadingText="Saving..."
+          >
+            {formData.computeResourceId ? "Update" : "Create"}
+          </Button>
+          {formData.computeResourceId && (
+            <Button colorScheme="red" onClick={handleDelete} 
isLoading={saving}>
+              Delete
+            </Button>
+          )}
+          <Button onClick={() => onSelect("")}>Back to List</Button>
+        </HStack>
+      </VStack>
+    </Box>
+  );
+};
+
diff --git 
a/airavata-research-portal/src/components/resources/QueueManagement.tsx 
b/airavata-research-portal/src/components/resources/QueueManagement.tsx
new file mode 100644
index 000000000..645e68098
--- /dev/null
+++ b/airavata-research-portal/src/components/resources/QueueManagement.tsx
@@ -0,0 +1,381 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {
+  Box,
+  Button,
+  FormControl,
+  FormLabel,
+  Input,
+  VStack,
+  HStack,
+  Switch,
+  NumberInput,
+  NumberInputField,
+  Table,
+  Thead,
+  Tbody,
+  Tr,
+  Th,
+  Td,
+  Textarea,
+  Modal,
+  ModalOverlay,
+  ModalContent,
+  ModalHeader,
+  ModalBody,
+  ModalFooter,
+  ModalCloseButton,
+  useDisclosure,
+  useToast,
+} from "@chakra-ui/react";
+import { useState } from "react";
+import { BatchQueue } from "@/interfaces/ComputeResourceType";
+import { computeResourceApi } from "@/lib/resourceApi";
+
+interface QueueManagementProps {
+  computeResourceId: string;
+  queues: BatchQueue[];
+  onQueuesChange: (queues: BatchQueue[]) => void;
+}
+
+export const QueueManagement = ({
+  computeResourceId,
+  queues,
+  onQueuesChange,
+}: QueueManagementProps) => {
+  const { isOpen, onOpen, onClose } = useDisclosure();
+  const [editingQueue, setEditingQueue] = useState<BatchQueue | null>(null);
+  const [queueForm, setQueueForm] = useState<BatchQueue>({
+    queueName: "",
+  } as BatchQueue);
+  const [saving, setSaving] = useState(false);
+  const toast = useToast();
+
+  const handleAddQueue = () => {
+    setEditingQueue(null);
+    setQueueForm({ queueName: "" } as BatchQueue);
+    onOpen();
+  };
+
+  const handleEditQueue = (queue: BatchQueue) => {
+    setEditingQueue(queue);
+    setQueueForm({ ...queue });
+    onOpen();
+  };
+
+  const handleDeleteQueue = async (queueName: string) => {
+    if (!confirm(`Are you sure you want to delete queue "${queueName}"?`)) 
return;
+
+    setSaving(true);
+    try {
+      await computeResourceApi.deleteQueue(computeResourceId, queueName);
+      const updatedQueues = queues.filter((q) => q.queueName !== queueName);
+      onQueuesChange(updatedQueues);
+      toast({
+        title: "Success",
+        description: "Queue deleted successfully",
+        status: "success",
+        duration: 3000,
+      });
+    } catch (error: any) {
+      toast({
+        title: "Error",
+        description: error.response?.data?.error || "Failed to delete queue",
+        status: "error",
+        duration: 5000,
+      });
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  const handleSaveQueue = async () => {
+    if (!queueForm.queueName) {
+      toast({
+        title: "Error",
+        description: "Queue name is required",
+        status: "error",
+        duration: 3000,
+      });
+      return;
+    }
+
+    setSaving(true);
+    try {
+      if (editingQueue) {
+        await computeResourceApi.updateQueue(computeResourceId, 
editingQueue.queueName, queueForm);
+        const updatedQueues = queues.map((q) =>
+          q.queueName === editingQueue.queueName ? queueForm : q
+        );
+        onQueuesChange(updatedQueues);
+        toast({
+          title: "Success",
+          description: "Queue updated successfully",
+          status: "success",
+          duration: 3000,
+        });
+      } else {
+        await computeResourceApi.addQueue(computeResourceId, queueForm);
+        onQueuesChange([...queues, queueForm]);
+        toast({
+          title: "Success",
+          description: "Queue added successfully",
+          status: "success",
+          duration: 3000,
+        });
+      }
+      onClose();
+    } catch (error: any) {
+      toast({
+        title: "Error",
+        description: error.response?.data?.error || "Failed to save queue",
+        status: "error",
+        duration: 5000,
+      });
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  return (
+    <Box>
+      <HStack mb={4}>
+        <Button size="sm" onClick={handleAddQueue}>
+          Add Queue
+        </Button>
+      </HStack>
+
+      <Table variant="simple">
+        <Thead>
+          <Tr>
+            <Th>Queue Name</Th>
+            <Th>Description</Th>
+            <Th>Max Runtime</Th>
+            <Th>Max Nodes</Th>
+            <Th>Default Queue</Th>
+            <Th>Actions</Th>
+          </Tr>
+        </Thead>
+        <Tbody>
+          {queues.map((queue) => (
+            <Tr key={queue.queueName}>
+              <Td>{queue.queueName}</Td>
+              <Td>{queue.queueDescription || "-"}</Td>
+              <Td>{queue.maxRunTime || "-"}</Td>
+              <Td>{queue.maxNodes || "-"}</Td>
+              <Td>{queue.isDefaultQueue ? "Yes" : "No"}</Td>
+              <Td>
+                <HStack>
+                  <Button size="sm" onClick={() => handleEditQueue(queue)}>
+                    Edit
+                  </Button>
+                  <Button
+                    size="sm"
+                    colorScheme="red"
+                    onClick={() => handleDeleteQueue(queue.queueName)}
+                  >
+                    Delete
+                  </Button>
+                </HStack>
+              </Td>
+            </Tr>
+          ))}
+        </Tbody>
+      </Table>
+
+      <Modal isOpen={isOpen} onClose={onClose} size="xl">
+        <ModalOverlay />
+        <ModalContent>
+          <ModalHeader>{editingQueue ? "Edit Queue" : "Add 
Queue"}</ModalHeader>
+          <ModalCloseButton />
+          <ModalBody>
+            <VStack spacing={4} align="stretch">
+              <FormControl isRequired>
+                <FormLabel>Queue Name</FormLabel>
+                <Input
+                  value={queueForm.queueName || ""}
+                  onChange={(e) => setQueueForm({ ...queueForm, queueName: 
e.target.value })}
+                  isDisabled={!!editingQueue}
+                />
+              </FormControl>
+
+              <FormControl>
+                <FormLabel>Queue Description</FormLabel>
+                <Textarea
+                  value={queueForm.queueDescription || ""}
+                  onChange={(e) =>
+                    setQueueForm({ ...queueForm, queueDescription: 
e.target.value })
+                  }
+                />
+              </FormControl>
+
+              <HStack>
+                <FormControl>
+                  <FormLabel>Max Runtime (hours)</FormLabel>
+                  <NumberInput
+                    value={queueForm.maxRunTime || ""}
+                    onChange={(_, value) =>
+                      setQueueForm({ ...queueForm, maxRunTime: value })
+                    }
+                  >
+                    <NumberInputField />
+                  </NumberInput>
+                </FormControl>
+
+                <FormControl>
+                  <FormLabel>Max Nodes</FormLabel>
+                  <NumberInput
+                    value={queueForm.maxNodes || ""}
+                    onChange={(_, value) =>
+                      setQueueForm({ ...queueForm, maxNodes: value })
+                    }
+                  >
+                    <NumberInputField />
+                  </NumberInput>
+                </FormControl>
+              </HStack>
+
+              <HStack>
+                <FormControl>
+                  <FormLabel>Max Processors</FormLabel>
+                  <NumberInput
+                    value={queueForm.maxProcessors || ""}
+                    onChange={(_, value) =>
+                      setQueueForm({ ...queueForm, maxProcessors: value })
+                    }
+                  >
+                    <NumberInputField />
+                  </NumberInput>
+                </FormControl>
+
+                <FormControl>
+                  <FormLabel>Max Jobs in Queue</FormLabel>
+                  <NumberInput
+                    value={queueForm.maxJobsInQueue || ""}
+                    onChange={(_, value) =>
+                      setQueueForm({ ...queueForm, maxJobsInQueue: value })
+                    }
+                  >
+                    <NumberInputField />
+                  </NumberInput>
+                </FormControl>
+              </HStack>
+
+              <HStack>
+                <FormControl>
+                  <FormLabel>Max Memory (MB)</FormLabel>
+                  <NumberInput
+                    value={queueForm.maxMemory || ""}
+                    onChange={(_, value) =>
+                      setQueueForm({ ...queueForm, maxMemory: value })
+                    }
+                  >
+                    <NumberInputField />
+                  </NumberInput>
+                </FormControl>
+
+                <FormControl>
+                  <FormLabel>CPU Per Node</FormLabel>
+                  <NumberInput
+                    value={queueForm.cpuPerNode || ""}
+                    onChange={(_, value) =>
+                      setQueueForm({ ...queueForm, cpuPerNode: value })
+                    }
+                  >
+                    <NumberInputField />
+                  </NumberInput>
+                </FormControl>
+              </HStack>
+
+              <HStack>
+                <FormControl>
+                  <FormLabel>Default Node Count</FormLabel>
+                  <NumberInput
+                    value={queueForm.defaultNodeCount || ""}
+                    onChange={(_, value) =>
+                      setQueueForm({ ...queueForm, defaultNodeCount: value })
+                    }
+                  >
+                    <NumberInputField />
+                  </NumberInput>
+                </FormControl>
+
+                <FormControl>
+                  <FormLabel>Default CPU Count</FormLabel>
+                  <NumberInput
+                    value={queueForm.defaultCPUCount || ""}
+                    onChange={(_, value) =>
+                      setQueueForm({ ...queueForm, defaultCPUCount: value })
+                    }
+                  >
+                    <NumberInputField />
+                  </NumberInput>
+                </FormControl>
+              </HStack>
+
+              <FormControl>
+                <FormLabel>Default Walltime (minutes)</FormLabel>
+                <NumberInput
+                  value={queueForm.defaultWalltime || ""}
+                  onChange={(_, value) =>
+                    setQueueForm({ ...queueForm, defaultWalltime: value })
+                  }
+                >
+                  <NumberInputField />
+                </NumberInput>
+              </FormControl>
+
+              <FormControl>
+                <FormLabel>Queue Specific Macros</FormLabel>
+                <Textarea
+                  value={queueForm.queueSpecificMacros || ""}
+                  onChange={(e) =>
+                    setQueueForm({ ...queueForm, queueSpecificMacros: 
e.target.value })
+                  }
+                  placeholder="Comma-separated macros"
+                />
+              </FormControl>
+
+              <FormControl>
+                <FormLabel>Is Default Queue</FormLabel>
+                <Switch
+                  isChecked={queueForm.isDefaultQueue || false}
+                  onChange={(e) =>
+                    setQueueForm({ ...queueForm, isDefaultQueue: 
e.target.checked })
+                  }
+                />
+              </FormControl>
+            </VStack>
+          </ModalBody>
+          <ModalFooter>
+            <Button colorScheme="blue" onClick={handleSaveQueue} 
isLoading={saving}>
+              {editingQueue ? "Update" : "Add"}
+            </Button>
+            <Button onClick={onClose} ml={2}>
+              Cancel
+            </Button>
+          </ModalFooter>
+        </ModalContent>
+      </Modal>
+    </Box>
+  );
+};
+
+
+
diff --git 
a/airavata-research-portal/src/components/resources/ResourceManagement.tsx 
b/airavata-research-portal/src/components/resources/ResourceManagement.tsx
new file mode 100644
index 000000000..83388d08c
--- /dev/null
+++ b/airavata-research-portal/src/components/resources/ResourceManagement.tsx
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {
+  Box,
+  Button,
+  Container,
+  Heading,
+  Tab,
+  TabList,
+  TabPanel,
+  TabPanels,
+  Tabs,
+} from "@chakra-ui/react";
+import { useState, useEffect } from "react";
+import { ComputeResourceForm } from "./ComputeResourceForm";
+import { StorageResourceForm } from "./StorageResourceForm";
+import { computeResourceApi, storageResourceApi } from "@/lib/resourceApi";
+import { ComputeResource } from "@/interfaces/ComputeResourceType";
+import { StorageResource } from "@/interfaces/StorageResourceType";
+import { ResourceName } from "@/interfaces/ComputeResourceType";
+
+export const ResourceManagement = () => {
+  const [computeResources, setComputeResources] = useState<ResourceName[]>([]);
+  const [storageResources, setStorageResources] = useState<ResourceName[]>([]);
+  const [selectedComputeResource, setSelectedComputeResource] = 
useState<ComputeResource | null>(null);
+  const [selectedStorageResource, setSelectedStorageResource] = 
useState<StorageResource | null>(null);
+  const [loading, setLoading] = useState(false);
+
+  useEffect(() => {
+    loadResources();
+  }, []);
+
+  const loadResources = async () => {
+    setLoading(true);
+    try {
+      const [compute, storage] = await Promise.all([
+        computeResourceApi.getAll(),
+        storageResourceApi.getAll(),
+      ]);
+      setComputeResources(compute);
+      setStorageResources(storage);
+    } catch (error) {
+      console.error("Error loading resources:", error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleComputeResourceSelect = async (id: string) => {
+    if (!id) {
+      setSelectedComputeResource(null);
+      return;
+    }
+    try {
+      const resource = await computeResourceApi.get(id);
+      setSelectedComputeResource(resource);
+    } catch (error) {
+      console.error("Error loading compute resource:", error);
+    }
+  };
+
+  const handleStorageResourceSelect = async (id: string) => {
+    if (!id) {
+      setSelectedStorageResource(null);
+      return;
+    }
+    try {
+      const resource = await storageResourceApi.get(id);
+      setSelectedStorageResource(resource);
+    } catch (error) {
+      console.error("Error loading storage resource:", error);
+    }
+  };
+
+  const handleComputeResourceSave = async () => {
+    await loadResources();
+    setSelectedComputeResource(null);
+  };
+
+  const handleStorageResourceSave = async () => {
+    await loadResources();
+    setSelectedStorageResource(null);
+  };
+
+  return (
+    <Container maxW="container.xl" mt={8}>
+      <Heading mb={6}>Resource Management</Heading>
+      <Tabs>
+        <TabList>
+          <Tab>Compute Resources</Tab>
+          <Tab>Storage Resources</Tab>
+        </TabList>
+
+        <TabPanels>
+          <TabPanel>
+            <Box>
+              <Button
+                mb={4}
+                onClick={() => setSelectedComputeResource({ hostName: "" } as 
ComputeResource)}
+              >
+                Create New Compute Resource
+              </Button>
+              <ComputeResourceForm
+                resources={computeResources}
+                selectedResource={selectedComputeResource}
+                onSelect={handleComputeResourceSelect}
+                onSave={handleComputeResourceSave}
+                loading={loading}
+              />
+            </Box>
+          </TabPanel>
+
+          <TabPanel>
+            <Box>
+              <Button
+                mb={4}
+                onClick={() => setSelectedStorageResource({ hostName: "" } as 
StorageResource)}
+              >
+                Create New Storage Resource
+              </Button>
+              <StorageResourceForm
+                resources={storageResources}
+                selectedResource={selectedStorageResource}
+                onSelect={handleStorageResourceSelect}
+                onSave={handleStorageResourceSave}
+                loading={loading}
+              />
+            </Box>
+          </TabPanel>
+        </TabPanels>
+      </Tabs>
+    </Container>
+  );
+};
+
diff --git 
a/airavata-research-portal/src/components/resources/StorageResourceForm.tsx 
b/airavata-research-portal/src/components/resources/StorageResourceForm.tsx
new file mode 100644
index 000000000..c78c37a9d
--- /dev/null
+++ b/airavata-research-portal/src/components/resources/StorageResourceForm.tsx
@@ -0,0 +1,218 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {
+  Box,
+  Button,
+  FormControl,
+  FormLabel,
+  Input,
+  Textarea,
+  VStack,
+  HStack,
+  Switch,
+  Table,
+  Thead,
+  Tbody,
+  Tr,
+  Th,
+  Td,
+  useToast,
+} from "@chakra-ui/react";
+import { useState, useEffect } from "react";
+import { StorageResource } from "@/interfaces/StorageResourceType";
+import { ResourceName } from "@/interfaces/ComputeResourceType";
+import { storageResourceApi } from "@/lib/resourceApi";
+
+interface StorageResourceFormProps {
+  resources: ResourceName[];
+  selectedResource: StorageResource | null;
+  onSelect: (id: string) => void;
+  onSave: () => void;
+  loading: boolean;
+}
+
+export const StorageResourceForm = ({
+  resources,
+  selectedResource,
+  onSelect,
+  onSave,
+  loading,
+}: StorageResourceFormProps) => {
+  const [formData, setFormData] = useState<StorageResource>({
+    hostName: "",
+  } as StorageResource);
+  const [saving, setSaving] = useState(false);
+  const toast = useToast();
+
+  useEffect(() => {
+    if (selectedResource) {
+      setFormData(selectedResource);
+    } else {
+      setFormData({ hostName: "" } as StorageResource);
+    }
+  }, [selectedResource]);
+
+  const handleChange = (field: keyof StorageResource, value: any) => {
+    setFormData((prev) => ({ ...prev, [field]: value }));
+  };
+
+  const handleSave = async () => {
+    setSaving(true);
+    try {
+      if (formData.storageResourceId) {
+        await storageResourceApi.update(formData.storageResourceId, formData);
+        toast({
+          title: "Success",
+          description: "Storage resource updated successfully",
+          status: "success",
+          duration: 3000,
+        });
+      } else {
+        await storageResourceApi.create(formData);
+        toast({
+          title: "Success",
+          description: "Storage resource created successfully",
+          status: "success",
+          duration: 3000,
+        });
+      }
+      onSave();
+    } catch (error: any) {
+      toast({
+        title: "Error",
+        description: error.response?.data?.error || "Failed to save storage 
resource",
+        status: "error",
+        duration: 5000,
+      });
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  const handleDelete = async () => {
+    if (!formData.storageResourceId) return;
+    if (!confirm("Are you sure you want to delete this storage resource?")) 
return;
+
+    setSaving(true);
+    try {
+      await storageResourceApi.delete(formData.storageResourceId);
+      toast({
+        title: "Success",
+        description: "Storage resource deleted successfully",
+        status: "success",
+        duration: 3000,
+      });
+      onSave();
+    } catch (error: any) {
+      toast({
+        title: "Error",
+        description: error.response?.data?.error || "Failed to delete storage 
resource",
+        status: "error",
+        duration: 5000,
+      });
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  if (!selectedResource) {
+    return (
+      <Box>
+        {resources.length > 0 ? (
+          <Table variant="simple">
+            <Thead>
+              <Tr>
+                <Th>ID</Th>
+                <Th>Name</Th>
+                <Th>Actions</Th>
+              </Tr>
+            </Thead>
+            <Tbody>
+              {resources.map((resource) => (
+                <Tr key={resource.id}>
+                  <Td>{resource.id}</Td>
+                  <Td>{resource.name}</Td>
+                  <Td>
+                    <Button size="sm" onClick={() => onSelect(resource.id)}>
+                      Edit
+                    </Button>
+                  </Td>
+                </Tr>
+              ))}
+            </Tbody>
+          </Table>
+        ) : (
+          <Box p={4} textAlign="center">
+            No storage resources found. Click "Create New Storage Resource" to 
add one.
+          </Box>
+        )}
+      </Box>
+    );
+  }
+
+  return (
+    <Box>
+      <VStack spacing={4} align="stretch">
+        <FormControl>
+          <FormLabel>Host Name *</FormLabel>
+          <Input
+            value={formData.hostName || ""}
+            onChange={(e) => handleChange("hostName", e.target.value)}
+            placeholder="e.g., storage.example.com"
+          />
+        </FormControl>
+
+        <FormControl>
+          <FormLabel>Storage Resource Description</FormLabel>
+          <Textarea
+            value={formData.storageResourceDescription || ""}
+            onChange={(e) => handleChange("storageResourceDescription", 
e.target.value)}
+            placeholder="Description of the storage resource"
+          />
+        </FormControl>
+
+        <FormControl>
+          <FormLabel>Enabled</FormLabel>
+          <Switch
+            isChecked={formData.enabled ?? true}
+            onChange={(e) => handleChange("enabled", e.target.checked)}
+          />
+        </FormControl>
+
+        <HStack>
+          <Button
+            colorScheme="blue"
+            onClick={handleSave}
+            isLoading={saving}
+            loadingText="Saving..."
+          >
+            {formData.storageResourceId ? "Update" : "Create"}
+          </Button>
+          {formData.storageResourceId && (
+            <Button colorScheme="red" onClick={handleDelete} 
isLoading={saving}>
+              Delete
+            </Button>
+          )}
+          <Button onClick={() => onSelect("")}>Back to List</Button>
+        </HStack>
+      </VStack>
+    </Box>
+  );
+};
+
diff --git a/airavata-research-portal/src/interfaces/ComputeResourceType.ts 
b/airavata-research-portal/src/interfaces/ComputeResourceType.ts
new file mode 100644
index 000000000..3fdef159e
--- /dev/null
+++ b/airavata-research-portal/src/interfaces/ComputeResourceType.ts
@@ -0,0 +1,42 @@
+export interface BatchQueue {
+  queueName: string;
+  queueDescription?: string;
+  maxRunTime?: number;
+  maxNodes?: number;
+  maxProcessors?: number;
+  maxJobsInQueue?: number;
+  maxMemory?: number;
+  cpuPerNode?: number;
+  defaultNodeCount?: number;
+  defaultCPUCount?: number;
+  defaultWalltime?: number;
+  queueSpecificMacros?: string;
+  isDefaultQueue?: boolean;
+}
+
+export interface ComputeResource {
+  computeResourceId?: string;
+  hostName: string;
+  hostAliases?: string[];
+  ipAddresses?: string[];
+  resourceDescription?: string;
+  enabled?: boolean;
+  batchQueues?: BatchQueue[];
+  fileSystems?: Record<string, string>;
+  maxMemoryPerNode?: number;
+  gatewayUsageReporting?: boolean;
+  gatewayUsageModuleLoadCommand?: string;
+  gatewayUsageExecutable?: string;
+  cpusPerNode?: number;
+  defaultNodeCount?: number;
+  defaultCPUCount?: number;
+  defaultWalltime?: number;
+}
+
+export interface ResourceName {
+  id: string;
+  name: string;
+}
+
+
+
diff --git a/airavata-research-portal/src/interfaces/StorageResourceType.ts 
b/airavata-research-portal/src/interfaces/StorageResourceType.ts
new file mode 100644
index 000000000..f6758bc78
--- /dev/null
+++ b/airavata-research-portal/src/interfaces/StorageResourceType.ts
@@ -0,0 +1,11 @@
+export interface StorageResource {
+  storageResourceId?: string;
+  hostName: string;
+  storageResourceDescription?: string;
+  enabled?: boolean;
+  creationTime?: number;
+  updateTime?: number;
+}
+
+
+
diff --git a/airavata-research-portal/src/lib/resourceApi.ts 
b/airavata-research-portal/src/lib/resourceApi.ts
new file mode 100644
index 000000000..9acc6e369
--- /dev/null
+++ b/airavata-research-portal/src/lib/resourceApi.ts
@@ -0,0 +1,109 @@
+import axios, { AxiosInstance } from 'axios';
+import { ComputeResource, BatchQueue, ResourceName } from 
'../interfaces/ComputeResourceType';
+import { StorageResource } from '../interfaces/StorageResourceType';
+
+const REST_API_URL = import.meta.env.VITE_REST_API_URL || 
'http://localhost:8080';
+
+const resourceApi: AxiosInstance = axios.create({
+  baseURL: `${REST_API_URL}/api/v1`,
+  timeout: 30000,
+  headers: {
+    'Content-Type': 'application/json',
+  },
+});
+
+// Request interceptor for adding auth if needed
+resourceApi.interceptors.request.use(
+  (config) => {
+    // Add auth headers if available
+    const token = localStorage.getItem('access_token');
+    if (token) {
+      config.headers.Authorization = `Bearer ${token}`;
+    }
+    return config;
+  },
+  (error) => Promise.reject(error)
+);
+
+// Response interceptor
+resourceApi.interceptors.response.use(
+  (response) => response,
+  (error) => {
+    console.error('Resource API Error:', error.response?.data || 
error.message);
+    return Promise.reject(error);
+  }
+);
+
+// Compute Resource APIs
+export const computeResourceApi = {
+  getAll: async (): Promise<ResourceName[]> => {
+    const response = await resourceApi.get('/compute-resources');
+    return response.data;
+  },
+
+  get: async (id: string): Promise<ComputeResource> => {
+    const response = await resourceApi.get(`/compute-resources/${id}`);
+    return response.data;
+  },
+
+  create: async (resource: ComputeResource): Promise<{ computeResourceId: 
string }> => {
+    const response = await resourceApi.post('/compute-resources', resource);
+    return response.data;
+  },
+
+  update: async (id: string, resource: ComputeResource): Promise<void> => {
+    await resourceApi.put(`/compute-resources/${id}`, resource);
+  },
+
+  delete: async (id: string): Promise<void> => {
+    await resourceApi.delete(`/compute-resources/${id}`);
+  },
+
+  getQueues: async (id: string): Promise<BatchQueue[]> => {
+    const response = await resourceApi.get(`/compute-resources/${id}/queues`);
+    return response.data || [];
+  },
+
+  addQueue: async (id: string, queue: BatchQueue): Promise<void> => {
+    await resourceApi.post(`/compute-resources/${id}/queues`, queue);
+  },
+
+  updateQueue: async (id: string, queueName: string, queue: BatchQueue): 
Promise<void> => {
+    await resourceApi.put(`/compute-resources/${id}/queues/${queueName}`, 
queue);
+  },
+
+  deleteQueue: async (id: string, queueName: string): Promise<void> => {
+    await resourceApi.delete(`/compute-resources/${id}/queues/${queueName}`);
+  },
+};
+
+// Storage Resource APIs
+export const storageResourceApi = {
+  getAll: async (): Promise<ResourceName[]> => {
+    const response = await resourceApi.get('/storage-resources');
+    return response.data;
+  },
+
+  get: async (id: string): Promise<StorageResource> => {
+    const response = await resourceApi.get(`/storage-resources/${id}`);
+    return response.data;
+  },
+
+  create: async (resource: StorageResource): Promise<{ storageResourceId: 
string }> => {
+    const response = await resourceApi.post('/storage-resources', resource);
+    return response.data;
+  },
+
+  update: async (id: string, resource: StorageResource): Promise<void> => {
+    await resourceApi.put(`/storage-resources/${id}`, resource);
+  },
+
+  delete: async (id: string): Promise<void> => {
+    await resourceApi.delete(`/storage-resources/${id}`);
+  },
+};
+
+export default resourceApi;
+
+
+


Reply via email to