diff --git a/client/public/locales/en/translation.json b/client/public/locales/en/translation.json index 4ba92d42a9..6bdace9ef6 100644 --- a/client/public/locales/en/translation.json +++ b/client/public/locales/en/translation.json @@ -27,6 +27,7 @@ "createTag": "Create tag", "createTagCategory": "Create tag category", "createTags": "Create tags", + "cancelAnalysis": "Cancel analysis", "delete": "Delete", "discardAssessment": "Discard assessment/review", "downloadCsvTemplate": "Download CSV template", diff --git a/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx b/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx deleted file mode 100644 index b57a170f21..0000000000 --- a/client/src/app/pages/applications/applications-table-analyze/applications-table-analyze.tsx +++ /dev/null @@ -1,873 +0,0 @@ -// External libraries -import * as React from "react"; -import { useState } from "react"; -import { AxiosError } from "axios"; -import { useHistory } from "react-router-dom"; -import { useTranslation } from "react-i18next"; - -// @patternfly -import { - Toolbar, - ToolbarContent, - ToolbarItem, - Button, - ToolbarGroup, - ButtonVariant, - DropdownItem, - Dropdown, - MenuToggle, - MenuToggleElement, - Modal, - DropdownList, -} from "@patternfly/react-core"; -import { - PencilAltIcon, - TagIcon, - EllipsisVIcon, - WarningTriangleIcon, -} from "@patternfly/react-icons"; -import { Table, Thead, Tr, Th, Td, Tbody } from "@patternfly/react-table"; - -// @app components and utilities -import { AppPlaceholder } from "@app/components/AppPlaceholder"; -import { - FilterType, - FilterToolbar, -} from "@app/components/FilterToolbar/FilterToolbar"; -import { SimplePagination } from "@app/components/SimplePagination"; -import { - TableHeaderContentWithControls, - ConditionalTableBody, - TableRowContentWithControls, -} from "@app/components/TableControls"; -import { ToolbarBulkSelector } from "@app/components/ToolbarBulkSelector"; -import { SimpleDocumentViewerModal } from "@app/components/SimpleDocumentViewer"; -import { getTaskById } from "@app/api/rest"; -import { ConfirmDialog } from "@app/components/ConfirmDialog"; -import { NotificationsContext } from "@app/components/NotificationsContext"; -import { dedupeFunction, getAxiosErrorMessage } from "@app/utils/utils"; -import { Paths } from "@app/Paths"; -import keycloak from "@app/keycloak"; -import { - RBAC, - RBAC_TYPE, - applicationsWriteScopes, - tasksWriteScopes, - importsWriteScopes, - tasksReadScopes, -} from "@app/rbac"; -import { checkAccess } from "@app/utils/rbac-utils"; - -// Hooks -import { useLocalTableControls } from "@app/hooks/table-controls"; - -// Queries -import { Application, Task } from "@app/api/models"; -import { - useBulkDeleteApplicationMutation, - useFetchApplications, -} from "@app/queries/applications"; -import { useCancelTaskMutation, useFetchTasks } from "@app/queries/tasks"; -import { useFetchIdentities } from "@app/queries/identities"; -import { useFetchTagCategories } from "@app/queries/tags"; - -// Relative components -import { ApplicationBusinessService } from "../components/application-business-service"; -import { ApplicationDetailDrawerAnalysis } from "../components/application-detail-drawer"; -import { ApplicationForm } from "../components/application-form"; -import { ApplicationIdentityForm } from "../components/application-identity-form/application-identity-form"; -import { ImportApplicationsForm } from "../components/import-applications-form"; -import { ConditionalRender } from "@app/components/ConditionalRender"; -import { NoDataEmptyState } from "@app/components/NoDataEmptyState"; -import { ConditionalTooltip } from "@app/components/ConditionalTooltip"; -import { TaskGroupProvider } from "../analysis-wizard/components/TaskGroupContext"; -import { AnalysisWizard } from "../analysis-wizard/analysis-wizard"; -import { ApplicationAnalysisStatus } from "../components/application-analysis-status"; - -export const ApplicationsTableAnalyze: React.FC = () => { - const { t } = useTranslation(); - const history = useHistory(); - const token = keycloak.tokenParsed; - - const { pushNotification } = React.useContext(NotificationsContext); - - const [isToolbarKebabOpen, setIsToolbarKebabOpen] = - React.useState(false); - const [isRowDropdownOpen, setIsRowDropdownOpen] = React.useState< - number | null - >(null); - - const [saveApplicationModalState, setSaveApplicationModalState] = - React.useState<"create" | Application | null>(null); - - const isCreateUpdateApplicationsModalOpen = - saveApplicationModalState !== null; - - const createUpdateApplications = - saveApplicationModalState !== "create" ? saveApplicationModalState : null; - - const [isAnalyzeModalOpen, setAnalyzeModalOpen] = useState(false); - - const [applicationsToDelete, setApplicationsToDelete] = useState< - Application[] - >([]); - - const getTask = (application: Application) => - tasks.find((task: Task) => task.application?.id === application.id); - - const { tasks } = useFetchTasks({ addon: "analyzer" }, isAnalyzeModalOpen); - - const { tagCategories: tagCategories } = useFetchTagCategories(); - - const { identities } = useFetchIdentities(); - - const { - data: applications, - isFetching: isFetchingApplications, - error: applicationsFetchError, - refetch: fetchApplications, - } = useFetchApplications(isAnalyzeModalOpen); - - const onDeleteApplicationSuccess = (appIDCount: number) => { - pushNotification({ - title: t("toastr.success.applicationDeleted", { - appIDCount: appIDCount, - }), - variant: "success", - }); - clearActiveItem(); - setApplicationsToDelete([]); - }; - - const onDeleteApplicationError = (error: AxiosError) => { - pushNotification({ - title: getAxiosErrorMessage(error), - variant: "danger", - }); - setApplicationsToDelete([]); - }; - - const { mutate: bulkDeleteApplication } = useBulkDeleteApplicationMutation( - onDeleteApplicationSuccess, - onDeleteApplicationError - ); - - const isTaskCancellable = (application: Application) => { - const task = getTask(application); - if (task?.state && task.state.match(/(Created|Running|Ready|Pending)/)) - return true; - return false; - }; - - const cancelAnalysis = (row: Application) => { - const task = tasks.find((task) => task.application?.id === row.id); - if (task?.id) cancelTask(task.id); - }; - - const completedCancelTask = () => { - pushNotification({ - title: "Task", - message: "Canceled", - variant: "info", - }); - }; - - const failedCancelTask = () => { - pushNotification({ - title: "Task", - message: "Cancelation failed.", - variant: "danger", - }); - }; - - const { mutate: cancelTask } = useCancelTaskMutation( - completedCancelTask, - failedCancelTask - ); - - const tableControls = useLocalTableControls({ - idProperty: "id", - items: applications || [], - columnNames: { - name: "Name", - description: "Description", - businessService: "Business Service", - analysis: "Analysis", - tags: "Tags", - effort: "Effort", - }, - isFilterEnabled: true, - isSortEnabled: true, - isPaginationEnabled: true, - isSelectionEnabled: true, - isActiveItemEnabled: true, - sortableColumns: [ - "name", - "description", - "businessService", - "tags", - "effort", - ], - initialSort: { columnKey: "name", direction: "asc" }, - getSortValues: (app) => ({ - name: app.name, - description: app.description || "", - businessService: app.businessService?.name || "", - tags: app.tags?.length || 0, - effort: app.effort || 0, - }), - filterCategories: [ - { - key: "name", - title: t("terms.name"), - type: FilterType.search, - placeholderText: - t("actions.filterBy", { - what: t("terms.name").toLowerCase(), - }) + "...", - getItemValue: (item) => item?.name || "", - }, - { - key: "description", - title: t("terms.description"), - type: FilterType.search, - placeholderText: - t("actions.filterBy", { - what: t("terms.description").toLowerCase(), - }) + "...", - getItemValue: (item) => item.description || "", - }, - { - key: "businessService", - title: t("terms.businessService"), - placeholderText: - t("actions.filterBy", { - what: t("terms.businessService").toLowerCase(), - }) + "...", - type: FilterType.select, - selectOptions: dedupeFunction( - applications - .filter((app) => !!app.businessService?.name) - .map((app) => app.businessService?.name) - .map((name) => ({ key: name, value: name })) - ), - getItemValue: (item) => item.businessService?.name || "", - }, - { - key: "identities", - title: t("terms.credentialType"), - placeholderText: - t("actions.filterBy", { - what: t("terms.credentialType").toLowerCase(), - }) + "...", - type: FilterType.multiselect, - selectOptions: [ - { key: "source", value: "Source" }, - { key: "maven", value: "Maven" }, - { key: "proxy", value: "Proxy" }, - ], - getItemValue: (item) => { - const searchStringArr: string[] = []; - item.identities?.forEach((appIdentity) => { - const matchingIdentity = identities.find( - (identity) => identity.id === appIdentity.id - ); - searchStringArr.push(matchingIdentity?.kind || ""); - }); - const searchString = searchStringArr.join(""); - return searchString; - }, - }, - { - key: "repository", - title: t("terms.repositoryType"), - placeholderText: - t("actions.filterBy", { - what: t("terms.repositoryType").toLowerCase(), - }) + "...", - type: FilterType.select, - selectOptions: [ - { key: "git", value: "Git" }, - { key: "subversion", value: "Subversion" }, - ], - getItemValue: (item) => item?.repository?.kind || "", - }, - { - key: "binary", - title: t("terms.artifact"), - placeholderText: - t("actions.filterBy", { - what: t("terms.artifact").toLowerCase(), - }) + "...", - type: FilterType.select, - selectOptions: [ - { key: "binary", value: t("terms.artifactAssociated") }, - { key: "none", value: t("terms.artifactNotAssociated") }, - ], - getItemValue: (item) => { - const hasBinary = - item.binary !== "::" && item.binary?.match(/.+:.+:.+/) - ? "binary" - : "none"; - - return hasBinary; - }, - }, - { - key: "tags", - title: t("terms.tags"), - type: FilterType.multiselect, - placeholderText: - t("actions.filterBy", { - what: t("terms.tagName").toLowerCase(), - }) + "...", - getItemValue: (item) => { - const tagNames = item?.tags?.map((tag) => tag.name).join(""); - return tagNames || ""; - }, - selectOptions: dedupeFunction( - tagCategories - ?.map((tagCategory) => tagCategory?.tags) - .flat() - .filter((tag) => tag && tag.name) - .map((tag) => ({ key: tag?.name, value: tag?.name })) - ), - }, - ], - initialItemsPerPage: 10, - hasActionsColumn: true, - }); - - const { - currentPageItems, - numRenderedColumns, - propHelpers: { - toolbarProps, - filterToolbarProps, - paginationToolbarItemProps, - paginationProps, - tableProps, - getThProps, - getTrProps, - getTdProps, - toolbarBulkSelectorProps, - }, - activeItemDerivedState: { activeItem, clearActiveItem }, - - selectionState: { selectedItems: selectedRows }, - } = tableControls; - - const [ - saveApplicationsCredentialsModalState, - setSaveApplicationsCredentialsModalState, - ] = useState<"create" | Application[] | null>(null); - const isCreateUpdateCredentialsModalOpen = - saveApplicationsCredentialsModalState !== null; - const applicationsCredentialsToUpdate = - saveApplicationsCredentialsModalState !== "create" - ? saveApplicationsCredentialsModalState - : null; - - const [isApplicationImportModalOpen, setIsApplicationImportModalOpen] = - useState(false); - - const [taskToView, setTaskToView] = useState<{ - name: string; - task: number | undefined; - }>(); - - const userScopes: string[] = token?.scope.split(" ") || [], - importWriteAccess = checkAccess(userScopes, importsWriteScopes), - applicationWriteAccess = checkAccess(userScopes, applicationsWriteScopes), - tasksReadAccess = checkAccess(userScopes, tasksReadScopes), - tasksWriteAccess = checkAccess(userScopes, tasksWriteScopes); - - const areAppsInWaves = selectedRows.some( - (application) => application.migrationWave !== null - ); - - const importDropdownItems = importWriteAccess - ? [ - setIsApplicationImportModalOpen(true)} - > - {t("actions.import")} - , - { - history.push(Paths.applicationsImports); - }} - > - {t("actions.manageImports")} - , - ] - : []; - const applicationDropdownItems = applicationWriteAccess - ? [ - { - setSaveApplicationsCredentialsModalState(selectedRows); - }} - > - {t("actions.manageCredentials")} - , - ] - : []; - const applicationDeleteDropdown = applicationWriteAccess - ? [ - - { - setApplicationsToDelete(selectedRows); - }} - > - {t("actions.delete")} - - , - ] - : []; - const dropdownItems = [ - ...importDropdownItems, - ...applicationDropdownItems, - ...applicationDeleteDropdown, - ]; - - const isAnalyzingAllowed = () => { - const candidateTasks = selectedRows.filter( - (app) => - !tasks.some( - (task) => - task.application?.id === app.id && - task.state?.match(/(Created|Running|Ready|Pending)/) - ) - ); - - if (candidateTasks.length === selectedRows.length) return true; - return false; - }; - const hasExistingAnalysis = selectedRows.some((app) => - tasks.some((task) => task.application?.id === app.id) - ); - - return ( - } - > -
- - - - - - - - - - - - - - - - - - - - {dropdownItems.length ? ( - - setIsToolbarKebabOpen(false)} - onOpenChange={(_isOpen) => setIsToolbarKebabOpen(false)} - toggle={(toggleRef: React.Ref) => ( - - setIsToolbarKebabOpen(!isToolbarKebabOpen) - } - isExpanded={isToolbarKebabOpen} - > - - - )} - shouldFocusToggleOnSelect - > - {dropdownItems} - - - ) : ( - <> - )} - - - - - - - - - - - - - - - } - numRenderedColumns={numRenderedColumns} - > - - {currentPageItems.map((application, rowIndex) => ( - - - - - - - - - - - - - ))} - - -
- - - - - - - -
- {application.name} - - {application.description} - - {application.businessService && ( - - )} - - - - - {application.tags ? application.tags.length : 0} - - {application?.effort ?? "-"} - - - setIsRowDropdownOpen(null)} - onOpenChange={(_isOpen) => setIsRowDropdownOpen(null)} - popperProps={{ position: "right" }} - toggle={(toggleRef: React.Ref) => ( - { - isRowDropdownOpen - ? setIsRowDropdownOpen(null) - : setIsRowDropdownOpen(application.id); - }} - isExpanded={isRowDropdownOpen === rowIndex} - > - - - )} - shouldFocusToggleOnSelect - > - - {applicationWriteAccess && ( - <> - { - setSaveApplicationsCredentialsModalState([ - application, - ]); - }} - > - Manage credentials - - - setApplicationsToDelete([application]) - } - > - {t("actions.delete")} - - - )} - - {tasksReadAccess && ( - - { - const task = getTask(application); - if (task) - setTaskToView({ - name: application.name, - task: task.id, - }); - }} - > - {t("actions.analysisDetails")} - - - )} - - {tasksWriteAccess && ( - - cancelAnalysis(application)} - > - Cancel analysis - - - )} - - -
- - { - setSaveApplicationModalState(activeItem); - }} - task={activeItem ? getTask(activeItem) : null} - /> - - 1 - ? "dialog.title.delete" - : "dialog.title.deleteWithName", - { - what: - applicationsToDelete.length > 1 - ? t("terms.application(s)").toLowerCase() - : t("terms.application").toLowerCase(), - name: - applicationsToDelete.length === 1 && - applicationsToDelete[0].name, - } - )} - titleIconVariant={"warning"} - isOpen={applicationsToDelete.length > 0} - message={`${ - applicationsToDelete.length > 1 - ? t("dialog.message.applicationsBulkDelete") - : "" - } ${t("dialog.message.delete")}`} - aria-label="Applications bulk delete" - confirmBtnVariant={ButtonVariant.danger} - confirmBtnLabel={t("actions.delete")} - cancelBtnLabel={t("actions.cancel")} - onCancel={() => setApplicationsToDelete([])} - onClose={() => setApplicationsToDelete([])} - onConfirm={() => { - const ids = applicationsToDelete - .filter((application) => application.id) - .map((application) => application.id); - if (ids) bulkDeleteApplication({ ids: ids }); - }} - /> - - { - setAnalyzeModalOpen(false); - }} - /> - - setSaveApplicationsCredentialsModalState(null)} - > - {applicationsCredentialsToUpdate && ( - setSaveApplicationsCredentialsModalState(null)} - /> - )} - - setSaveApplicationModalState(null)} - > - setSaveApplicationModalState(null)} - /> - - - title={`Analysis details for ${taskToView?.name}`} - fetch={getTaskById} - documentId={taskToView?.task} - onClose={() => setTaskToView(undefined)} - /> - setIsApplicationImportModalOpen((current) => !current)} - > - { - setIsApplicationImportModalOpen(false); - fetchApplications(); - }} - /> - -
-
- ); -}; diff --git a/client/src/app/pages/applications/applications-table-analyze/index.ts b/client/src/app/pages/applications/applications-table-analyze/index.ts deleted file mode 100644 index dcc5944d4e..0000000000 --- a/client/src/app/pages/applications/applications-table-analyze/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ApplicationsTableAnalyze as default } from "./applications-table-analyze"; diff --git a/client/src/app/pages/applications/applications-table-assessment/applications-table-assessment.tsx b/client/src/app/pages/applications/applications-table/applications-table.tsx similarity index 78% rename from client/src/app/pages/applications/applications-table-assessment/applications-table-assessment.tsx rename to client/src/app/pages/applications/applications-table/applications-table.tsx index 29c7cffc02..71eac890f8 100644 --- a/client/src/app/pages/applications/applications-table-assessment/applications-table-assessment.tsx +++ b/client/src/app/pages/applications/applications-table/applications-table.tsx @@ -57,8 +57,11 @@ import { RBAC_TYPE, applicationsWriteScopes, importsWriteScopes, + tasksReadScopes, + tasksWriteScopes, } from "@app/rbac"; import { checkAccess } from "@app/utils/rbac-utils"; +import WarningTriangleIcon from "@patternfly/react-icons/dist/esm/icons/warning-triangle-icon"; // Hooks import { useQueryClient } from "@tanstack/react-query"; @@ -71,7 +74,7 @@ import { useBulkDeleteApplicationMutation, useFetchApplications, } from "@app/queries/applications"; -import { useFetchTasks } from "@app/queries/tasks"; +import { useCancelTaskMutation, useFetchTasks } from "@app/queries/tasks"; import { useDeleteAssessmentMutation } from "@app/queries/assessments"; import { useDeleteReviewMutation } from "@app/queries/reviews"; import { useFetchIdentities } from "@app/queries/identities"; @@ -80,15 +83,21 @@ import { useFetchTagCategories } from "@app/queries/tags"; // Relative components import { ApplicationAssessmentStatus } from "../components/application-assessment-status"; import { ApplicationBusinessService } from "../components/application-business-service"; -import { ApplicationDetailDrawerAssessment } from "../components/application-detail-drawer"; import { ApplicationForm } from "../components/application-form"; import { ImportApplicationsForm } from "../components/import-applications-form"; import { ConditionalRender } from "@app/components/ConditionalRender"; import { NoDataEmptyState } from "@app/components/NoDataEmptyState"; import { ConditionalTooltip } from "@app/components/ConditionalTooltip"; -import { getAssessmentsByItemId } from "@app/api/rest"; +import { getAssessmentsByItemId, getTaskById } from "@app/api/rest"; import { ApplicationDependenciesForm } from "@app/components/ApplicationDependenciesFormContainer/ApplicationDependenciesForm"; import { useFetchArchetypes } from "@app/queries/archetypes"; +import { useState } from "react"; +import { ApplicationAnalysisStatus } from "../components/application-analysis-status"; +import { ApplicationDetailDrawer } from "../components/application-detail-drawer/application-detail-drawer"; +import { SimpleDocumentViewerModal } from "@app/components/SimpleDocumentViewer"; +import { AnalysisWizard } from "../analysis-wizard/analysis-wizard"; +import { TaskGroupProvider } from "../analysis-wizard/components/TaskGroupContext"; +import { ApplicationIdentityForm } from "../components/application-identity-form/application-identity-form"; export const ApplicationsTable: React.FC = () => { const { t } = useTranslation(); @@ -97,6 +106,7 @@ export const ApplicationsTable: React.FC = () => { const { pushNotification } = React.useContext(NotificationsContext); + const { identities } = useFetchIdentities(); const [isToolbarKebabOpen, setIsToolbarKebabOpen] = React.useState(false); @@ -116,6 +126,51 @@ export const ApplicationsTable: React.FC = () => { const [applicationToAssess, setApplicationToAssess] = React.useState(null); + /*** Analysis */ + + const [isAnalyzeModalOpen, setAnalyzeModalOpen] = useState(false); + + const getTask = (application: Application) => + tasks.find((task: Task) => task.application?.id === application.id); + + const { tasks } = useFetchTasks({ addon: "analyzer" }, isAnalyzeModalOpen); + + const isTaskCancellable = (application: Application) => { + const task = getTask(application); + if (task?.state && task.state.match(/(Created|Running|Ready|Pending)/)) + return true; + return false; + }; + + const cancelAnalysis = (row: Application) => { + const task = tasks.find((task) => task.application?.id === row.id); + if (task?.id) cancelTask(task.id); + }; + + const completedCancelTask = () => { + pushNotification({ + title: "Task", + message: "Canceled", + variant: "info", + }); + }; + + const failedCancelTask = () => { + pushNotification({ + title: "Task", + message: "Cancelation failed.", + variant: "danger", + }); + }; + + const { mutate: cancelTask } = useCancelTaskMutation( + completedCancelTask, + failedCancelTask + ); + /*** Analysis */ + + const { tagCategories: tagCategories } = useFetchTagCategories(); + const [applicationDependenciesToManage, setApplicationDependenciesToManage] = React.useState(null); const isDependenciesModalOpen = applicationDependenciesToManage !== null; @@ -132,15 +187,6 @@ export const ApplicationsTable: React.FC = () => { const [assessmentOrReviewToDiscard, setAssessmentOrReviewToDiscard] = React.useState(null); - const getTask = (application: Application) => - tasks.find((task: Task) => task.application?.id === application.id); - - const { tasks } = useFetchTasks({ addon: "analyzer" }); - - const { tagCategories: tagCategories } = useFetchTagCategories(); - - const { identities } = useFetchIdentities(); - const { data: applications, isFetching: isFetchingApplications, @@ -148,12 +194,7 @@ export const ApplicationsTable: React.FC = () => { refetch: fetchApplications, } = useFetchApplications(); - const { - archetypes, - isFetching: isFetchingArchetypes, - error: archetypesFetchError, - refetch: fetchArchetypes, - } = useFetchArchetypes(); + const { archetypes } = useFetchArchetypes(); const onDeleteApplicationSuccess = (appIDCount: number) => { pushNotification({ @@ -245,23 +286,24 @@ export const ApplicationsTable: React.FC = () => { items: applications || [], columnNames: { name: "Name", - description: "Description", businessService: "Business Service", assessment: "Assessment", review: "Review", + analysis: "Analysis", tags: "Tags", + effort: "Effort", }, isFilterEnabled: true, isSortEnabled: true, isPaginationEnabled: true, isActiveItemEnabled: true, - sortableColumns: ["name", "description", "businessService", "tags"], + sortableColumns: ["name", "businessService", "tags", "effort"], initialSort: { columnKey: "name", direction: "asc" }, getSortValues: (app) => ({ name: app.name, - description: app.description || "", businessService: app.businessService?.name || "", tags: app.tags?.length || 0, + effort: app.effort || 0, }), filterCategories: [ { @@ -303,16 +345,6 @@ export const ApplicationsTable: React.FC = () => { })), logicOperator: "OR", }, - { - key: "description", - title: t("terms.description"), - type: FilterType.search, - placeholderText: - t("actions.filterBy", { - what: t("terms.description").toLowerCase(), - }) + "...", - getItemValue: (item) => item.description || "", - }, { key: "businessService", title: t("terms.businessService"), @@ -436,12 +468,30 @@ export const ApplicationsTable: React.FC = () => { selectionState: { selectedItems: selectedRows }, } = tableControls; + const [ + saveApplicationsCredentialsModalState, + setSaveApplicationsCredentialsModalState, + ] = useState<"create" | Application[] | null>(null); + const isCreateUpdateCredentialsModalOpen = + saveApplicationsCredentialsModalState !== null; + const applicationsCredentialsToUpdate = + saveApplicationsCredentialsModalState !== "create" + ? saveApplicationsCredentialsModalState + : null; + const [isApplicationImportModalOpen, setIsApplicationImportModalOpen] = - React.useState(false); + useState(false); + + const [taskToView, setTaskToView] = useState<{ + name: string; + task: number | undefined; + }>(); const userScopes: string[] = token?.scope.split(" ") || [], importWriteAccess = checkAccess(userScopes, importsWriteScopes), - applicationWriteAccess = checkAccess(userScopes, applicationsWriteScopes); + applicationWriteAccess = checkAccess(userScopes, applicationsWriteScopes), + tasksReadAccess = checkAccess(userScopes, tasksReadScopes), + tasksWriteAccess = checkAccess(userScopes, tasksWriteScopes); const areAppsInWaves = selectedRows.some( (application) => application.migrationWave !== null @@ -466,7 +516,7 @@ export const ApplicationsTable: React.FC = () => { , ] : []; - const applicationDeleteDropdown = applicationWriteAccess + const applicationDropdownItems = applicationWriteAccess ? [ { {t("actions.delete")} , + { + setSaveApplicationsCredentialsModalState(selectedRows); + }} + > + {t("actions.manageCredentials")} + , ] : []; - const dropdownItems = [...importDropdownItems, ...applicationDeleteDropdown]; + const dropdownItems = [...importDropdownItems, ...applicationDropdownItems]; + + const isAnalyzingAllowed = () => { + const candidateTasks = selectedRows.filter( + (app) => + !tasks.some( + (task) => + task.application?.id === app.id && + task.state?.match(/(Created|Running|Ready|Pending)/) + ) + ); + + if (candidateTasks.length === selectedRows.length) return true; + return false; + }; + const hasExistingAnalysis = selectedRows.some((app) => + tasks.some((task) => task.application?.id === app.id) + ); const handleNavToAssessment = (application: Application) => { application?.id && @@ -585,6 +661,39 @@ export const ApplicationsTable: React.FC = () => { + + + + + + + + + {dropdownItems.length ? ( { - - - - - - + + + + + + + @@ -678,21 +791,14 @@ export const ApplicationsTable: React.FC = () => { rowIndex={rowIndex} > {application.name} - {application.description} - - @@ -724,6 +830,15 @@ export const ApplicationsTable: React.FC = () => { } /> + + + { {application.tags ? application.tags.length : 0} + + {application?.effort ?? "-"} + - - - Download - - - - HTML - - - {" | "} - - - YAML - - - - - - - - ) : task?.state === "Failed" ? ( - task ? ( - <> - - - ) : ( - - - Failed - - ) - ) : ( - <> - {task ? ( - - ) : ( - notAvailable - )} - - )} - - title={`Analysis details for ${application?.name}`} - fetch={getTaskById} - documentId={taskIdToView} - onClose={() => { - setTaskIdToView(undefined); - }} - /> - - } - factsTabContent={ - !isFetching && !!facts.length && - } - /> - ); -}; diff --git a/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer-assessment.tsx b/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer-assessment.tsx deleted file mode 100644 index acd1a79ed4..0000000000 --- a/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer-assessment.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import * as React from "react"; -import { useTranslation } from "react-i18next"; -import { - DescriptionList, - DescriptionListDescription, - DescriptionListGroup, - DescriptionListTerm, - TextContent, - Text, - Title, -} from "@patternfly/react-core"; -import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; - -import { EmptyTextMessage } from "@app/components/EmptyTextMessage"; -import { Ref, Task } from "@app/api/models"; -import { - ApplicationDetailDrawer, - IApplicationDetailDrawerProps, -} from "./application-detail-drawer"; -import { ReviewedArchetypeItem } from "./reviewed-archetype-item"; -import { ReviewFields } from "./review-fields"; -import { RiskLabel } from "@app/components/RiskLabel"; -import { LabelsFromItems } from "@app/components/labels-from-items/labels-from-items"; - -export interface IApplicationDetailDrawerAssessmentProps - extends Pick< - IApplicationDetailDrawerProps, - "application" | "onCloseClick" | "onEditClick" - > { - task: Task | undefined | null; -} - -export const ApplicationDetailDrawerAssessment: React.FC< - IApplicationDetailDrawerAssessmentProps -> = ({ application, onCloseClick, task, onEditClick }) => { - const { t } = useTranslation(); - - return ( - - - {t("terms.archetypes")} - - - - - {t("terms.associatedArchetypes")} - - - {application?.archetypes?.length ?? 0 > 0 ? ( - - ) : ( - - )} - - - - - {t("terms.archetypesReviewed")} - - - {application?.archetypes?.length ?? 0 > 0 ? ( - application?.archetypes?.map((archetypeRef) => ( - - )) - ) : ( - - )} - - - - - - {t("terms.riskFromApplication")} - - - - - - - } - reviewsTabContent={} - /> - ); -}; - -const ArchetypeLabels: React.FC<{ archetypeRefs?: Ref[] }> = ({ - archetypeRefs, -}) => ; diff --git a/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer.tsx b/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer.tsx index 7050a0f029..4b98a5636f 100644 --- a/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer.tsx +++ b/client/src/app/pages/applications/components/application-detail-drawer/application-detail-drawer.tsx @@ -13,9 +13,16 @@ import { Bullseye, List, ListItem, + Button, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Divider, + Tooltip, } from "@patternfly/react-core"; import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; -import { Application, Task } from "@app/api/models"; +import { Application, Identity, Task, MimeType, Ref } from "@app/api/models"; import { IPageDrawerContentProps, PageDrawerContent, @@ -25,6 +32,23 @@ import { getIssuesSingleAppSelectedLocation, } from "@app/pages/issues/helpers"; import { ApplicationTags } from "../application-tags"; +import { COLOR_HEX_VALUES_BY_NAME } from "@app/Constants"; +import { getTaskById } from "@app/api/rest"; +import { EmptyTextMessage } from "@app/components/EmptyTextMessage"; +import { SimpleDocumentViewerModal } from "@app/components/SimpleDocumentViewer"; +import { useFetchFacts } from "@app/queries/facts"; +import { useFetchIdentities } from "@app/queries/identities"; +import { useSetting } from "@app/queries/settings"; +import { getKindIdByRef } from "@app/utils/model-utils"; +import DownloadButton from "./components/download-button"; +import { useFetchApplications } from "@app/queries/applications"; +import CheckCircleIcon from "@patternfly/react-icons/dist/esm/icons/check-circle-icon"; +import ExclamationCircleIcon from "@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon"; +import { ApplicationFacts } from "./application-facts"; +import { ReviewFields } from "./review-fields"; +import { LabelsFromItems } from "@app/components/labels-from-items/labels-from-items"; +import { ReviewedArchetypeItem } from "./reviewed-archetype-item"; +import { RiskLabel } from "@app/components/RiskLabel"; import { ApplicationDetailFields } from "./application-detail-fields"; export interface IApplicationDetailDrawerProps @@ -32,10 +56,6 @@ export interface IApplicationDetailDrawerProps application: Application | null; task: Task | undefined | null; applications?: Application[]; - detailTabContent?: React.ReactNode; - reportsTabContent?: React.ReactNode; - factsTabContent?: React.ReactNode; - reviewsTabContent?: React.ReactNode; onEditClick: () => void; } @@ -49,16 +69,7 @@ enum TabKey { export const ApplicationDetailDrawer: React.FC< IApplicationDetailDrawerProps -> = ({ - onCloseClick, - onEditClick, - application, - task, - detailTabContent = null, - reportsTabContent = null, - factsTabContent = null, - reviewsTabContent = null, -}) => { +> = ({ onCloseClick, application, task, onEditClick }) => { const { t } = useTranslation(); const [activeTabKey, setActiveTabKey] = React.useState( TabKey.Details @@ -66,6 +77,25 @@ export const ApplicationDetailDrawer: React.FC< const isTaskRunning = task?.state === "Running"; + const { identities } = useFetchIdentities(); + const { data: applications } = useFetchApplications(); + const { facts, isFetching } = useFetchFacts(application?.id); + const [taskIdToView, setTaskIdToView] = React.useState(); + + let matchingSourceCredsRef: Identity | undefined; + let matchingMavenCredsRef: Identity | undefined; + if (application && identities) { + matchingSourceCredsRef = getKindIdByRef(identities, application, "source"); + matchingMavenCredsRef = getKindIdByRef(identities, application, "maven"); + } + + const notAvailable = ; + + const updatedApplication = applications?.find( + (app) => app.id === application?.id + ); + const enableDownloadSetting = useSetting("download.html.enabled"); + return ( - - {detailTabContent} - + <> + + {t("terms.archetypes")} + + + + + {t("terms.associatedArchetypes")} + + + {application?.archetypes?.length ?? 0 > 0 ? ( + + ) : ( + + )} + + + + + {t("terms.archetypesReviewed")} + + + {application?.archetypes?.length ?? 0 > 0 ? ( + application?.archetypes?.map((archetypeRef) => ( + + )) + ) : ( + + )} + + + + + + {t("terms.riskFromApplication")} + + + + + + + Tags}> @@ -152,32 +231,176 @@ export const ApplicationDetailDrawer: React.FC< {application ? : null} - {reportsTabContent && task ? ( - {t("terms.reports")}} - > - {reportsTabContent} - - ) : null} - - {factsTabContent ? ( - {t("terms.facts")}} - > - {factsTabContent} - - ) : null} - {reviewsTabContent ? ( - {t("terms.reviews")}} - > - {reviewsTabContent} - - ) : null} + {t("terms.reports")}} + > + + + Credentials + + {matchingSourceCredsRef && matchingMavenCredsRef ? ( + + + Source and Maven + + ) : matchingMavenCredsRef ? ( + + + Maven + + ) : matchingSourceCredsRef ? ( + + + Source + + ) : ( + notAvailable + )} + + Analysis + + {task?.state === "Succeeded" && application ? ( + <> + + + Details + + + + + + Download + + + + HTML + + + {" | "} + + + YAML + + + + + + + + ) : task?.state === "Failed" ? ( + task ? ( + <> + + + ) : ( + + + Failed + + ) + ) : ( + <> + {task ? ( + + ) : ( + notAvailable + )} + + )} + + title={`Analysis details for ${application?.name}`} + fetch={getTaskById} + documentId={taskIdToView} + onClose={() => { + setTaskIdToView(undefined); + }} + /> + + {!isFetching && !!facts.length && } + + {t("terms.reviews")}} + > + + ); }; +const ArchetypeLabels: React.FC<{ archetypeRefs?: Ref[] }> = ({ + archetypeRefs, +}) => ; diff --git a/client/src/app/pages/applications/components/application-detail-drawer/index.ts b/client/src/app/pages/applications/components/application-detail-drawer/index.ts deleted file mode 100644 index 9747b88ce7..0000000000 --- a/client/src/app/pages/applications/components/application-detail-drawer/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./application-detail-drawer-analysis"; -export * from "./application-detail-drawer-assessment";