diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/organizations/OrganizationVulnerabilitiesTab.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/organizations/OrganizationVulnerabilitiesTab.kt index 442c824d71..ecad6a676b 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/organizations/OrganizationVulnerabilitiesTab.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/organizations/OrganizationVulnerabilitiesTab.kt @@ -31,7 +31,7 @@ private val vulnerabilityTable: FC> = tableComponen Fragment.create { td { Link { - to = "/${FrontendRoutes.VULN}/${cellContext.row.original.identifier}" + to = "/${FrontendRoutes.VULNERABILITY_SINGLE}/${cellContext.row.original.identifier}" +cellContext.value } } diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/userprofile/UserProfileVulnerabilitiesTab.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/userprofile/UserProfileVulnerabilitiesTab.kt index 7c05ded49d..a2ecb66867 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/userprofile/UserProfileVulnerabilitiesTab.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/userprofile/UserProfileVulnerabilitiesTab.kt @@ -25,7 +25,7 @@ val renderVulnerabilityTableForProfileView: FC = FC { props -> + val rejectVulnerabilityWindowOpenness = useWindowOpenness() + val navigate = useNavigate() + val (rejectComment, setRejectComment) = useState(CommentDto.empty) + val deleteVulnerabilityWindowOpenness = useWindowOpenness() + + // ======================= requests ================================================================================ + + val enrollUpdateRequest = useDeferredRequest { + val vulnerabilityUpdate = props.vulnerability.copy(status = VulnerabilityStatus.APPROVED) + val response = post( + url = "$apiUrl/vulnerabilities/approve", + headers = jsonHeaders, + body = Json.encodeToString(vulnerabilityUpdate), + loadingHandler = ::loadingHandler, + ) + if (response.ok) { + navigate(to = "/${FrontendRoutes.VULNERABILITIES}") + } + } + + val enrollRejectRequest = useDeferredRequest { + if (rejectComment.message.isNotEmpty()) { + val commentNew = rejectComment.copy(section = window.location.pathname) + post( + url = "$apiUrl/comments/save", + headers = jsonHeaders, + body = Json.encodeToString(commentNew), + loadingHandler = ::loadingHandler, + responseHandler = ::noopResponseHandler, + ) + } + + val vulnerabilityUpdate = props.vulnerability.copy(status = VulnerabilityStatus.REJECTED) + val response = post( + url = "$apiUrl/vulnerabilities/reject", + headers = jsonHeaders, + body = Json.encodeToString(vulnerabilityUpdate), + loadingHandler = ::loadingHandler, + ) + if (response.ok) { + navigate(to = "/${FrontendRoutes.VULNERABILITIES}") + } + } + + val enrollDeleteRequest = useDeferredRequest { + val response = delete( + url = "$apiUrl/vulnerabilities/delete?identifier=${props.vulnerability.identifier}", + headers = jsonHeaders, + loadingHandler = ::loadingHandler, + ) + if (response.ok) { + navigate(to = "/${FrontendRoutes.VULNERABILITIES}") + } + } + + // ======================= modals ================================================================================== + + displayModal( + deleteVulnerabilityWindowOpenness.isOpen(), + "Deletion of vulnerability", + "Are you sure you want to remove this vulnerability?", + mediumTransparentModalStyle, + deleteVulnerabilityWindowOpenness.closeWindowAction(), + ) { + buttonBuilder("Ok") { + enrollDeleteRequest() + deleteVulnerabilityWindowOpenness.closeWindow() + } + buttonBuilder("Close", "secondary") { + deleteVulnerabilityWindowOpenness.closeWindow() + } + } + + displayModal( + rejectVulnerabilityWindowOpenness.isOpen(), + "Reject of vulnerability", + bodyBuilder = { + div { + h6 { + className = ClassName("modal-title") + +"Are you sure you want to reject this vulnerability?" + } + textarea { + className = ClassName("border-secondary form-control p-3 border-1") + onChange = { event -> setRejectComment { it.copy(message = event.target.value) } } + value = rejectComment.message + ariaDescribedBy = "${InputTypes.COMMENT.name}Span" + rows = 5 + id = InputTypes.COMMENT.name + required = true + placeholder = "Write a comment" + } + } + }, + modalStyle = mediumTransparentModalStyle, + onCloseButtonPressed = rejectVulnerabilityWindowOpenness.closeWindowAction(), + ) { + buttonBuilder("Ok") { + enrollRejectRequest() + rejectVulnerabilityWindowOpenness.closeWindow() + } + buttonBuilder("Close", "secondary") { + rejectVulnerabilityWindowOpenness.closeWindow() + } + } + + // ======================= rendering =============================================================================== + + div { + className = ClassName("col-3 mr-3") + vulnerabilityBadge { + this.vulnerability = props.vulnerability + } + } + div { + className = ClassName("col-6") + div { + className = ClassName("card shadow") + style = jso { + height = HEADER_HEIGHT.unsafeCast() + } + tab( + props.selectedMenu.name, + VulnerabilityTab.values().map { it.name }, + "nav nav-tabs mt-3" + ) { value -> props.setSelectedMenu { VulnerabilityTab.valueOf(value) } } + + val isAbleToEdit = + props.currentUserInfo.isSuperAdmin() || props.currentUserInfo in props.vulnerability.getAllParticipants() + + // separate it to button menu + div { + className = ClassName("row justify-content-center mt-3") + div { + className = ClassName("d-flex justify-content-end my-2") + if (props.selectedMenu == VulnerabilityTab.INFO) { + if (isAbleToEdit) { + buttonBuilder( + faPlus, + classes = "mr-2", + isOutline = true, + title = "Add more info" + ) { + props.addProjectWindowOpenness.openWindow() + } + } + + buttonBuilder( + if (props.isTableView) faImage else faTable, + "secondary", + classes = "mr-2", + isOutline = true, + title = "Change to ${if (props.isTableView) "card" else "table"} mode" + ) { + props.setIsTableView { !it } + } + } + + if (props.permissions.isSuperAdmin || + (props.permissions.isOwner && props.vulnerability.status != VulnerabilityStatus.APPROVED) + ) { + buttonBuilder( + faTrash, + "danger", + isOutline = true, + title = "Delete vulnerability", + classes = "mr-2" + ) { + deleteVulnerabilityWindowOpenness.openWindow() + } + } + + if (props.permissions.isSuperAdmin && props.vulnerability.status != VulnerabilityStatus.APPROVED) { + buttonBuilder(label = "Approve", classes = "mr-2 btn-sm", style = "success") { + enrollUpdateRequest() + } + buttonBuilder(label = "Reject", classes = "mr-2 btn-sm", style = "warning") { + rejectVulnerabilityWindowOpenness.openWindow() + } + } + } + } + } + } +} + +/** + * [Props] for a header with menu buttons on the vulnerability view + */ +@Suppress("MISSING_KDOC_CLASS_ELEMENTS") +internal external interface HeaderMenuProps : Props { + var selectedMenu: VulnerabilityTab + var vulnerability: VulnerabilityDto + var currentUserInfo: UserInfo? + var permissions: Permissions + var isTableView: Boolean + var setSelectedMenu: StateSetter + var setIsTableView: StateSetter + var addProjectWindowOpenness: WindowOpenness +} diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/vuln/VulnerabilityView.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/vuln/VulnerabilityView.kt index 9f4772230d..b203c204d6 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/vuln/VulnerabilityView.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/vuln/VulnerabilityView.kt @@ -6,38 +6,20 @@ package com.saveourtool.save.frontend.components.views.vuln -import com.saveourtool.save.entities.CommentDto import com.saveourtool.save.entities.vulnerability.VulnerabilityDto import com.saveourtool.save.entities.vulnerability.VulnerabilityLanguage -import com.saveourtool.save.entities.vulnerability.VulnerabilityStatus import com.saveourtool.save.frontend.TabMenuBar -import com.saveourtool.save.frontend.components.inputform.InputTypes -import com.saveourtool.save.frontend.components.modal.displayModal -import com.saveourtool.save.frontend.components.modal.mediumTransparentModalStyle -import com.saveourtool.save.frontend.components.views.contests.tab -import com.saveourtool.save.frontend.externals.fontawesome.faImage -import com.saveourtool.save.frontend.externals.fontawesome.faPlus -import com.saveourtool.save.frontend.externals.fontawesome.faTable -import com.saveourtool.save.frontend.externals.fontawesome.faTrash import com.saveourtool.save.frontend.utils.* import com.saveourtool.save.info.UserInfo import com.saveourtool.save.validation.FrontendRoutes import js.core.jso import react.* -import react.dom.aria.ariaDescribedBy import react.dom.html.ReactHTML.div import react.dom.html.ReactHTML.h1 -import react.dom.html.ReactHTML.h6 import react.dom.html.ReactHTML.span -import react.dom.html.ReactHTML.textarea -import react.router.useNavigate import web.cssom.* -import kotlinx.browser.window -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json - const val HEADER_HEIGHT = "9rem" @Suppress( @@ -51,64 +33,11 @@ val vulnerabilityView: FC = FC { props -> useBackground(Style.VULN_LIGHT) useTooltip() - val deleteVulnerabilityWindowOpenness = useWindowOpenness() - val rejectVulnerabilityWindowOpenness = useWindowOpenness() - val navigate = useNavigate() - - val (rejectComment, setRejectComment) = useState(CommentDto.empty) val (vulnerability, setVulnerability) = useState(VulnerabilityDto.empty) val (selectedMenu, setSelectedMenu) = useState(VulnerabilityTab.INFO) val (isTableView, setIsTableView) = useState(false) val addProjectWindowOpenness = useWindowOpenness() - val enrollUpdateRequest = useDeferredRequest { - val vulnerabilityUpdate = vulnerability.copy(status = VulnerabilityStatus.APPROVED) - val response = post( - url = "$apiUrl/vulnerabilities/approve", - headers = jsonHeaders, - body = Json.encodeToString(vulnerabilityUpdate), - loadingHandler = ::loadingHandler, - ) - if (response.ok) { - navigate(to = "/${FrontendRoutes.VULNERABILITIES}") - } - } - - val enrollRejectRequest = useDeferredRequest { - if (rejectComment.message.isNotEmpty()) { - val commentNew = rejectComment.copy(section = window.location.pathname) - post( - url = "$apiUrl/comments/save", - headers = jsonHeaders, - body = Json.encodeToString(commentNew), - loadingHandler = ::loadingHandler, - responseHandler = ::noopResponseHandler, - ) - } - - val vulnerabilityUpdate = vulnerability.copy(status = VulnerabilityStatus.REJECTED) - val response = post( - url = "$apiUrl/vulnerabilities/reject", - headers = jsonHeaders, - body = Json.encodeToString(vulnerabilityUpdate), - loadingHandler = ::loadingHandler, - ) - if (response.ok) { - navigate(to = "/${FrontendRoutes.VULNERABILITIES}") - } - } - - val enrollDeleteRequest = useDeferredRequest { - val response = delete( - url = "$apiUrl/vulnerabilities/delete?identifier=${props.identifier}", - headers = jsonHeaders, - loadingHandler = ::loadingHandler, - ) - if (response.ok) { - navigate(to = "/${FrontendRoutes.VULNERABILITIES}") - } - } - val fetchVulnerability = useDeferredRequest { val vulnerabilityNew: VulnerabilityDto = get( url = "$apiUrl/vulnerabilities/by-identifier-with-description?identifier=${props.identifier}", @@ -122,60 +51,13 @@ val vulnerabilityView: FC = FC { props -> setVulnerability(vulnerabilityNew) } - displayModal( - rejectVulnerabilityWindowOpenness.isOpen(), - "Reject of vulnerability", - bodyBuilder = { - div { - h6 { - className = ClassName("modal-title") - +"Are you sure you want to reject this vulnerability?" - } - textarea { - className = ClassName("border-secondary form-control p-3 border-1") - onChange = { event -> setRejectComment { it.copy(message = event.target.value) } } - value = rejectComment.message - ariaDescribedBy = "${InputTypes.COMMENT.name}Span" - rows = 5 - id = InputTypes.COMMENT.name - required = true - placeholder = "Write a comment" - } - } - }, - modalStyle = mediumTransparentModalStyle, - onCloseButtonPressed = rejectVulnerabilityWindowOpenness.closeWindowAction(), - ) { - buttonBuilder("Ok") { - enrollRejectRequest() - rejectVulnerabilityWindowOpenness.closeWindow() - } - buttonBuilder("Close", "secondary") { - rejectVulnerabilityWindowOpenness.closeWindow() - } - } - - displayModal( - deleteVulnerabilityWindowOpenness.isOpen(), - "Deletion of vulnerability", - "Are you sure you want to remove this vulnerability?", - mediumTransparentModalStyle, - deleteVulnerabilityWindowOpenness.closeWindowAction(), - ) { - buttonBuilder("Ok") { - enrollDeleteRequest() - deleteVulnerabilityWindowOpenness.closeWindow() - } - buttonBuilder("Close", "secondary") { - deleteVulnerabilityWindowOpenness.closeWindow() - } - } - useOnce(fetchVulnerability) - val isSuperAdmin = props.currentUserInfo?.isSuperAdmin() == true - val isOwner = vulnerability.userInfo.id?.let { props.currentUserInfo?.id == it } ?: false - val isParticipant = props.currentUserInfo in vulnerability.participants + val permissions = Permissions( + isSuperAdmin = props.currentUserInfo?.isSuperAdmin() == true, + isOwner = vulnerability.userInfo.id?.let { props.currentUserInfo?.id == it } ?: false, + isParticipant = props.currentUserInfo in vulnerability.participants + ) div { className = ClassName("d-flex align-items-center justify-content-center mb-4") @@ -188,81 +70,16 @@ val vulnerabilityView: FC = FC { props -> div { className = ClassName("row justify-content-center align-items-center") - div { - className = ClassName("col-3 mr-3") - vulnerabilityBadge { - this.vulnerability = vulnerability - } - } - div { - className = ClassName("col-6") - div { - className = ClassName("card shadow") - style = jso { - height = HEADER_HEIGHT.unsafeCast() - } - tab( - selectedMenu.name, - VulnerabilityTab.values().map { it.name }, - "nav nav-tabs mt-3" - ) { value -> setSelectedMenu { VulnerabilityTab.valueOf(value) } } - - val isAbleToEdit = - props.currentUserInfo.isSuperAdmin() || props.currentUserInfo in vulnerability.getAllParticipants() - - // separate it to button menu - div { - className = ClassName("row justify-content-center mt-3") - div { - className = ClassName("d-flex justify-content-end my-2") - if (selectedMenu == VulnerabilityTab.INFO) { - if (isAbleToEdit) { - buttonBuilder( - faPlus, - classes = "mr-2", - isOutline = true, - title = "Add more info" - ) { - addProjectWindowOpenness.openWindow() - } - } - - buttonBuilder( - if (isTableView) faImage else faTable, - "secondary", - classes = "mr-2", - isOutline = true, - title = "Change to ${if (isTableView) "card" else "table"} mode" - ) { - setIsTableView { !it } - } - } - - if (isSuperAdmin || (isOwner && vulnerability.status != VulnerabilityStatus.APPROVED)) { - buttonBuilder( - faTrash, - "danger", - isOutline = true, - title = "Delete vulnerability", - classes = "mr-2" - ) { - deleteVulnerabilityWindowOpenness.openWindow() - } - } - - if (isSuperAdmin && vulnerability.status != VulnerabilityStatus.APPROVED) { - buttonBuilder(label = "Approve", classes = "mr-2 btn-sm", style = "success") { - enrollUpdateRequest() - } - } - if (isSuperAdmin && vulnerability.status != VulnerabilityStatus.APPROVED) { - buttonBuilder(label = "Reject", classes = "mr-2 btn-sm", style = "warning") { - rejectVulnerabilityWindowOpenness.openWindow() - } - } - } - } - } + // ===================== HEADER WITH MENU ================================================================== + headerMenu { + this.selectedMenu = selectedMenu + this.vulnerability = vulnerability + this.currentUserInfo = props.currentUserInfo + this.permissions = permissions + this.isTableView = isTableView + this.addProjectWindowOpenness = addProjectWindowOpenness + this.setIsTableView = setIsTableView + this.setSelectedMenu = setSelectedMenu } } div { @@ -273,7 +90,7 @@ val vulnerabilityView: FC = FC { props -> vulnerabilityGeneralInfo { this.vulnerability = vulnerability this.fetchVulnerability = fetchVulnerability - this.canEditVulnerability = isOwner || isSuperAdmin || isParticipant + this.canEditVulnerability = permissions.isOwner || permissions.isSuperAdmin || permissions.isParticipant this.userInfo = props.currentUserInfo } } @@ -308,6 +125,7 @@ val vulnerabilityView: FC = FC { props -> enum class VulnerabilityTab { INFO, COMMENTS, + // PROPOSED_CHANGES, ; companion object : TabMenuBar { @@ -334,6 +152,17 @@ external interface VulnerabilitiesViewProps : Props { var currentUserInfo: UserInfo? } +/** + * @property isSuperAdmin + * @property isOwner + * @property isParticipant + */ +internal data class Permissions( + val isSuperAdmin: Boolean, + val isOwner: Boolean, + val isParticipant: Boolean +) + private fun ChildrenBuilder.languageSpan(language: VulnerabilityLanguage) { span { className = ClassName("border border-danger text-danger ml-2 pl-1 pr-1")