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