diff --git a/client/src/app/pages/applications/applications-table/applications-table.tsx b/client/src/app/pages/applications/applications-table/applications-table.tsx index e1592ccdc9..66fd402119 100644 --- a/client/src/app/pages/applications/applications-table/applications-table.tsx +++ b/client/src/app/pages/applications/applications-table/applications-table.tsx @@ -43,11 +43,7 @@ import { import { ToolbarBulkSelector } from "@app/components/ToolbarBulkSelector"; import { ConfirmDialog } from "@app/components/ConfirmDialog"; import { NotificationsContext } from "@app/components/NotificationsContext"; -import { - dedupeFunction, - formatPath, - getAxiosErrorMessage, -} from "@app/utils/utils"; +import { formatPath, getAxiosErrorMessage } from "@app/utils/utils"; import { Paths } from "@app/Paths"; import keycloak from "@app/keycloak"; import { @@ -74,7 +70,7 @@ import { // Queries import { getArchetypeById, getAssessmentsByItemId } from "@app/api/rest"; -import { Application, Assessment, Ref, Task } from "@app/api/models"; +import { Application, Assessment, Ref } from "@app/api/models"; import { useBulkDeleteApplicationMutation, useFetchApplications, @@ -85,7 +81,6 @@ import { useFetchAssessments, } from "@app/queries/assessments"; import { useDeleteReviewMutation } from "@app/queries/reviews"; -import { useFetchIdentities } from "@app/queries/identities"; import { useFetchTagsWithTagItems } from "@app/queries/tags"; import { useFetchArchetypes } from "@app/queries/archetypes"; @@ -107,6 +102,11 @@ import { KebabDropdown } from "@app/components/KebabDropdown"; import { ManageColumnsToolbar } from "./components/manage-columns-toolbar"; import { NoDataEmptyState } from "@app/components/NoDataEmptyState"; import { TaskGroupProvider } from "../analysis-wizard/components/TaskGroupContext"; +import { ColumnApplicationName } from "./components/column-application-name"; +import { + DecoratedApplication, + useDecoratedApplications, +} from "./useDecoratedApplications"; export const ApplicationsTable: React.FC = () => { const { t } = useTranslation(); @@ -181,16 +181,9 @@ export const ApplicationsTable: React.FC = () => { useState(false); // ----- Table data fetches and mutations - const { identities } = useFetchIdentities(); const { tagItems } = useFetchTagsWithTagItems(); - const { tasks, hasActiveTasks } = useFetchTasks( - { kind: "analyzer", addon: "analyzer" }, - isAnalyzeModalOpen - ); - - const getTask = (application: Application) => - tasks.find((task: Task) => task.application?.id === application.id); + const { tasks, hasActiveTasks } = useFetchTasks(isAnalyzeModalOpen); const completedCancelTask = () => { pushNotification({ @@ -213,24 +206,32 @@ export const ApplicationsTable: React.FC = () => { failedCancelTask ); - const cancelAnalysis = (row: Application) => { - const task = tasks.find((task) => task.application?.id === row.id); + const cancelAnalysis = (application: DecoratedApplication) => { + const task = application.tasks.currentAnalyzer; if (task?.id) cancelTask(task.id); }; - const isTaskCancellable = (application: Application) => { - const task = getTask(application); + const isTaskCancellable = (application: DecoratedApplication) => { + const task = application.tasks.currentAnalyzer; return task?.state && !["Succeeded", "Failed"].includes(task.state); }; + // TODO: Review the refetchInterval calculation for the application list const { - data: applications, + data: baseApplications, isFetching: isFetchingApplications, error: applicationsFetchError, } = useFetchApplications(() => hasActiveTasks || dayjs().isBefore(endOfAppImportPeriod) ? 5000 : false ); + const { + applications, + applicationNames, + referencedArchetypeRefs, + referencedBusinessServiceRefs, + } = useDecoratedApplications(baseApplications, tasks); + const { assessments, isFetching: isFetchingAssessments } = useFetchAssessments(); @@ -363,12 +364,11 @@ export const ApplicationsTable: React.FC = () => { t("actions.filterBy", { what: t("terms.name").toLowerCase(), }) + "...", - matcher: (filter: string, item: Application) => item.name === filter, - selectOptions: [ - ...new Set( - applications.map((application) => application.name).filter(Boolean) - ), - ].map((name) => ({ key: name, value: name })), + selectOptions: applicationNames.map((name) => ({ + key: name, + value: name, + })), + matcher: (filter: string, app: Application) => app.name === filter, }, { categoryKey: "archetypes", @@ -378,90 +378,76 @@ export const ApplicationsTable: React.FC = () => { t("actions.filterBy", { what: t("terms.archetypes").toLowerCase(), }) + "...", - getItemValue: (item) => { - const archetypeNames = item?.archetypes + selectOptions: referencedArchetypeRefs.map(({ name }) => ({ + key: name, + value: name, + })), + logicOperator: "OR", + getItemValue: (app) => { + const archetypeNames = app?.archetypes ?.map((archetype) => archetype.name) .join(""); return archetypeNames || ""; }, - selectOptions: [ - ...new Set( - applications - .flatMap( - (application) => - application?.archetypes?.map((archetype) => archetype.name) - ) - .filter(Boolean) - ), - ].map((archetypeName) => ({ - key: archetypeName, - value: archetypeName, - })), - logicOperator: "OR", }, { categoryKey: "businessService", title: t("terms.businessService"), + type: FilterType.multiselect, placeholderText: t("actions.filterBy", { what: t("terms.businessService").toLowerCase(), }) + "...", - type: FilterType.multiselect, - selectOptions: dedupeFunction( - applications - .filter((app) => !!app.businessService?.name) - .map((app) => app.businessService?.name) - .map((name) => ({ key: name, value: name })) - ), - getItemValue: (item) => item.businessService?.name || "", + selectOptions: referencedBusinessServiceRefs.map(({ name }) => ({ + key: name, + value: name, + })), + getItemValue: (app) => app.businessService?.name ?? "", }, { categoryKey: "identities", title: t("terms.credentialType"), + type: FilterType.multiselect, placeholderText: t("actions.filterBy", { what: t("terms.credentialType").toLowerCase(), }) + "...", - type: FilterType.multiselect, selectOptions: [ { value: "source", label: "Source" }, { value: "maven", label: "Maven" }, { value: "proxy", label: "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; + getItemValue: (app) => { + const identityKinds = app.identities + ?.map(({ kind }) => kind as string) + ?.filter(Boolean) + ?.join("^"); + + return identityKinds ?? ""; }, }, { categoryKey: "repository", title: t("terms.repositoryType"), + type: FilterType.select, placeholderText: t("actions.filterBy", { what: t("terms.repositoryType").toLowerCase(), }) + "...", - type: FilterType.select, selectOptions: [ { value: "git", label: "Git" }, { value: "subversion", label: "Subversion" }, ], - getItemValue: (item) => item?.repository?.kind || "", + getItemValue: (item) => item?.repository?.kind ?? "", }, { categoryKey: "binary", title: t("terms.artifact"), + type: FilterType.select, placeholderText: t("actions.filterBy", { what: t("terms.artifact").toLowerCase(), }) + "...", - type: FilterType.select, selectOptions: [ { value: "binary", label: t("terms.artifactAssociated") }, { value: "none", label: t("terms.artifactNotAssociated") }, @@ -471,7 +457,6 @@ export const ApplicationsTable: React.FC = () => { item.binary !== "::" && item.binary?.match(/.+:.+:.+/) ? "binary" : "none"; - return hasBinary; }, }, @@ -519,7 +504,7 @@ export const ApplicationsTable: React.FC = () => { { value: "red", label: "High" }, { value: "unknown", label: "Unknown" }, ], - getItemValue: (item) => item.risk || "", + getItemValue: (item) => item.risk ?? "", }, ], initialItemsPerPage: 10, @@ -603,7 +588,10 @@ export const ApplicationsTable: React.FC = () => { key="manage-applications-credentials" isDisabled={selectedRows.length < 1} onClick={() => { - setSaveApplicationsCredentialsModalState(selectedRows); + const selectedApps: Application[] = selectedRows.map( + ({ _ }) => _ + ); + setSaveApplicationsCredentialsModalState(selectedApps); }} > {t("actions.manageCredentials")} @@ -625,28 +613,22 @@ export const ApplicationsTable: React.FC = () => { return false; } - if (tasks.length === 0) { - return true; - } + const currentAnalyzerTasksForSelected = selectedRows + .flatMap((app) => app.tasks.currentAnalyzer) + .filter(Boolean); - const selectedAppIds = selectedRows.map(({ id }) => id); - const tasksForSelected = tasks.filter( - (task) => - (task.kind ?? task.addon) === "analyzer" && - selectedAppIds.includes(task.application.id) - ); const terminalStates = ["Succeeded", "Failed", "Canceled", ""]; return ( - tasksForSelected.length === 0 || - tasksForSelected.every(({ state }) => + currentAnalyzerTasksForSelected.length === 0 || + currentAnalyzerTasksForSelected.every(({ state }) => terminalStates.includes(state ?? "") ) ); }; - const hasExistingAnalysis = selectedRows.some((app) => - tasks.some((task) => task.application?.id === app.id) + const selectedRowsHaveExistingAnalysis = selectedRows.some( + (app) => !!app.tasks.currentAnalyzer ); const handleNavToAssessment = (application: Application) => { @@ -758,7 +740,9 @@ export const ApplicationsTable: React.FC = () => { - {...filterToolbarProps} /> + + {...filterToolbarProps} + /> { >