diff --git a/packages/server-admin-ui/src/views/appstore/Grid/cell-renderers/ActionCellRenderer.js b/packages/server-admin-ui/src/views/appstore/Grid/cell-renderers/ActionCellRenderer.js index be4cf01b0..bec72ad48 100644 --- a/packages/server-admin-ui/src/views/appstore/Grid/cell-renderers/ActionCellRenderer.js +++ b/packages/server-admin-ui/src/views/appstore/Grid/cell-renderers/ActionCellRenderer.js @@ -1,12 +1,28 @@ -import React from 'react' -import { Button, Progress } from 'reactstrap' +import React, { useState } from 'react' +import { + Button, + Progress, + Modal, + ModalHeader, + ModalBody, + ModalFooter, + ListGroup, + ListGroupItem +} from 'reactstrap' import { connect } from 'react-redux' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faTrashCan, faCloudArrowDown } from '@fortawesome/free-solid-svg-icons' +import { + faTrashCan, + faCloudArrowDown, + faHistory +} from '@fortawesome/free-solid-svg-icons' function ActionCellRenderer(props) { const app = props.data + const [showVersionsModal, setShowVersionsModal] = useState(false) + const [versions, setVersions] = useState([]) + const [loadingVersions, setLoadingVersions] = useState(false) const handleInstallClick = () => { fetch( @@ -18,6 +34,59 @@ function ActionCellRenderer(props) { ) } + const handleInstallVersionClick = (version) => { + fetch( + `${window.serverRoutesPrefix}/appstore/install/${props.data.name}/${version}`, + { + method: 'POST', + credentials: 'include' + } + ) + setShowVersionsModal(false) + } + + const handleVersionsClick = async () => { + setLoadingVersions(true) + setShowVersionsModal(true) + + try { + // Fetch versions from npm registry + const response = await fetch( + `https://registry.npmjs.org/${props.data.name}` + ) + const packageData = await response.json() + + if (packageData.versions) { + // Get all versions and sort them by semver (newest first) + const versionList = Object.keys(packageData.versions) + .filter((version) => version !== props.data.version) // Exclude current version + .sort((a, b) => { + // Simple version comparison - newer versions first + const aParts = a.split('.').map(Number) + const bParts = b.split('.').map(Number) + + for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { + const aPart = aParts[i] || 0 + const bPart = bParts[i] || 0 + + if (bPart !== aPart) { + return bPart - aPart + } + } + return 0 + }) + .slice(0, 20) // Limit to 20 versions + + setVersions(versionList) + } + } catch (error) { + console.error('Failed to fetch versions:', error) + setVersions([]) + } finally { + setLoadingVersions(false) + } + } + const handleRemoveClick = () => { if (confirm(`Are you sure you want to uninstall ${props.data.name}?`)) { fetch(`${window.serverRoutesPrefix}/appstore/remove/${props.data.name}`, { @@ -86,20 +155,95 @@ function ActionCellRenderer(props) { icon={faTrashCan} onClick={handleRemoveClick} /> + + ) : ( - + <> + + + + ) } return (
{content}
+ + {/* Versions Modal */} + setShowVersionsModal(!showVersionsModal)} + size="md" + > + setShowVersionsModal(!showVersionsModal)}> + Older Versions - {props.data.name} + + + {loadingVersions ? ( +
+
+ Loading... +
+

Loading versions...

+
+ ) : versions.length > 0 ? ( + + {versions.map((version) => ( + + + {version} + {props.data.installedVersion === version && ( + + Installed + + )} + + + + ))} + + ) : ( +

+ No older versions available or failed to load versions. +

+ )} +
+ + + +
) } diff --git a/packages/server-admin-ui/src/views/appstore/appStore.scss b/packages/server-admin-ui/src/views/appstore/appStore.scss index de5f39d22..70cb19441 100644 --- a/packages/server-admin-ui/src/views/appstore/appStore.scss +++ b/packages/server-admin-ui/src/views/appstore/appStore.scss @@ -257,6 +257,22 @@ color: #00cd79; } } + + .icon__remove { + path { + color: #df6c42; + } + } + + .icon__versions { + path { + color: #007bff; + } + + &:hover path { + color: #0056b3; + } + } } .cell__renderer.center {