From 3b5b3be68ab0d02594f4c72bcbb78799981c4347 Mon Sep 17 00:00:00 2001 From: David Zager Date: Wed, 13 Dec 2023 15:38:42 -0500 Subject: [PATCH 1/4] :seedling: parallelize image build (#1618) Signed-off-by: David Zager --- .github/workflows/image-build.yaml | 26 +++++++++++++++ .github/workflows/march-image-build-push.yml | 33 -------------------- 2 files changed, 26 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/image-build.yaml delete mode 100644 .github/workflows/march-image-build-push.yml diff --git a/.github/workflows/image-build.yaml b/.github/workflows/image-build.yaml new file mode 100644 index 0000000000..261d01674a --- /dev/null +++ b/.github/workflows/image-build.yaml @@ -0,0 +1,26 @@ +name: Multiple Architecture Image Build + +on: + workflow_dispatch: + push: + branches: + - 'main' + - 'release-*' + tags: + - 'v*' + +concurrency: + group: build-${{ github.ref }} + cancel-in-progress: true + +jobs: + image-build: + uses: konveyor/release-tools/.github/workflows/build-push-images.yaml@main + with: + registry: "quay.io/konveyor" + image_name: "tackle2-ui" + containerfile: "./Dockerfile" + architectures: '[ "amd64", "arm64", "ppc64le", "s390x" ]' + secrets: + registry_username: ${{ secrets.QUAY_PUBLISH_ROBOT }} + registry_password: ${{ secrets.QUAY_PUBLISH_TOKEN }} diff --git a/.github/workflows/march-image-build-push.yml b/.github/workflows/march-image-build-push.yml deleted file mode 100644 index f93448352c..0000000000 --- a/.github/workflows/march-image-build-push.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: 'Build and Push Multi-Arch Image' - -on: - workflow_dispatch: - push: - branches: - - 'main' - - 'release-*' - tags: - - 'v*' - -concurrency: - group: march-build-${{ github.ref }} - cancel-in-progress: true - -jobs: - push-quay: - name: Build and Push Manifest - runs-on: ubuntu-22.04 - strategy: - fail-fast: false - steps: - - name: Checkout Push to Registry action - uses: konveyor/release-tools/build-push-quay@main - with: - architectures: "amd64, arm64, ppc64le, s390x" - containerfile: "./Dockerfile" - image_name: "tackle2-ui" - image_namespace: "konveyor" - image_registry: "quay.io" - quay_publish_robot: ${{ secrets.QUAY_PUBLISH_ROBOT }} - quay_publish_token: ${{ secrets.QUAY_PUBLISH_TOKEN }} - ref: ${{ github.ref }} From 1a2f1832f0de993bdaddf01f37e72b07de85abe7 Mon Sep 17 00:00:00 2001 From: Ian Bolton Date: Thu, 14 Dec 2023 09:05:50 -0500 Subject: [PATCH 2/4] :sparkles: Add assessed archetypes section in drawer (#1610) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves: https://issues.redhat.com/browse/MTA-1834 [MTA-1878](https://issues.redhat.com/browse/MTA-1878) [Inconsistent hover text message for Assessment and Review columns](https://issues.redhat.com/browse/MTA-1835) Screenshot 2023-12-13 at 12 15 39 PM Screenshot 2023-12-13 at 12 15 32 PM --------- Signed-off-by: ibolton336 --- client/public/locales/en/translation.json | 6 ++ client/src/app/components/IconedStatus.tsx | 40 +++++++++-- .../applications-table/applications-table.tsx | 71 +++---------------- .../application-assessment-status.tsx | 55 ++++++++------ .../application-detail-drawer.tsx | 62 ++++++++++++++-- .../reviewed-archetype-item.tsx | 15 ---- .../application-review-status.tsx | 51 +++++++++++++ 7 files changed, 192 insertions(+), 108 deletions(-) delete mode 100644 client/src/app/pages/applications/components/application-detail-drawer/reviewed-archetype-item.tsx create mode 100644 client/src/app/pages/applications/components/application-review-status/application-review-status.tsx diff --git a/client/public/locales/en/translation.json b/client/public/locales/en/translation.json index 64d2901eab..e4d1bf749d 100644 --- a/client/public/locales/en/translation.json +++ b/client/public/locales/en/translation.json @@ -184,6 +184,10 @@ "duplicateWave": "The migration wave could not be created due to a conflict with an existing wave. Make sure the name and start/end dates are unique and try again.", "importErrorCheckDocumentation": "For status Error imports, check the documentation to ensure your file is structured correctly.", "insecureTracker": "Insecure mode deactivates certificate verification. Use insecure mode for instances that have self-signed certificates.", + "inheritedReviewTooltip": "This application is inheriting a review from an archetype.", + "inheritedReviewTooltip_plural": "This application is inheriting reviews from {{count}} archetypes.", + "inheritedAssessmentTooltip": "This application is inheriting an assessment from an archetype.", + "inheritedAssessmentTooltip_plural": "This application is inheriting assessments from {{count}} archetypes.", "jiraInstanceNotConnected": "Jira instance {{name}} is not connected.", "manageDependenciesInstructions": "Add northbound and southbound dependencies for the selected application here. Note that any selections made will be saved automatically. To undo any changes, you must manually delete the applications from the dropdowns.", "noDataAvailableBody": "No data available to be shown here.", @@ -237,6 +241,7 @@ "associatedApplications": "Associated applications", "associatedArchetypes": "Associated archetypes", "archetypesReviewed": "Archetypes reviewed", + "archetypesAssessed": "Archetypes assessed", "add": "Add", "additionalNotesOrComments": "Additional notes or comments", "adoptionCandidateDistribution": "Application confidence and risk", @@ -327,6 +332,7 @@ "inProgress": "In-progress", "instanceType": "Instance type", "instance": "Instance", + "inherited": "Inherited", "issueType": "Issue type", "jiraConfig": "Jira configuration", "issue": "Issue", diff --git a/client/src/app/components/IconedStatus.tsx b/client/src/app/components/IconedStatus.tsx index 665d4b0581..ff347cb3ac 100644 --- a/client/src/app/components/IconedStatus.tsx +++ b/client/src/app/components/IconedStatus.tsx @@ -1,13 +1,16 @@ import React from "react"; -import { Flex, FlexItem, Icon } from "@patternfly/react-core"; +import { Flex, FlexItem, Icon, Tooltip } from "@patternfly/react-core"; import { useTranslation } from "react-i18next"; import CheckCircleIcon from "@patternfly/react-icons/dist/esm/icons/check-circle-icon"; import TimesCircleIcon from "@patternfly/react-icons/dist/esm/icons/times-circle-icon"; import InProgressIcon from "@patternfly/react-icons/dist/esm/icons/in-progress-icon"; import ExclamationCircleIcon from "@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon"; import UnknownIcon from "@patternfly/react-icons/dist/esm/icons/unknown-icon"; +import QuestionCircleIcon from "@patternfly/react-icons/dist/esm/icons/question-circle-icon"; export type IconedStatusPreset = + | "InheritedReviews" + | "InheritedAssessments" | "Canceled" | "Completed" | "Error" @@ -35,6 +38,8 @@ export interface IIconedStatusProps { icon?: React.ReactNode; className?: string; label?: React.ReactNode | string; + tooltipMessage?: string; + tooltipCount?: number; } export const IconedStatus: React.FC = ({ @@ -43,9 +48,26 @@ export const IconedStatus: React.FC = ({ icon, className = "", label, + tooltipCount = 0, }: IIconedStatusProps) => { const { t } = useTranslation(); const presets: IconedStatusPresetType = { + InheritedReviews: { + icon: , + status: "info", + label: t("terms.inherited"), + tooltipMessage: t("message.inheritedReviewTooltip", { + count: tooltipCount, + }), + }, + InheritedAssessments: { + icon: , + status: "info", + label: t("terms.inherited"), + tooltipMessage: t("message.inheritedAssessmentTooltip", { + count: tooltipCount, + }), + }, Canceled: { icon: , status: "info", @@ -89,6 +111,14 @@ export const IconedStatus: React.FC = ({ }, }; const presetProps = preset && presets[preset]; + const IconWithOptionalTooltip: React.FC<{ children: React.ReactElement }> = ({ + children, + }) => + presetProps?.tooltipMessage ? ( + {children} + ) : ( + <>{children} + ); return ( = ({ spaceItems={{ default: "spaceItemsSm" }} > - - {icon || presetProps?.icon || } - + + + {icon || presetProps?.icon || } + + {label || presetProps?.label} 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 2189169a08..96ab5b594d 100644 --- a/client/src/app/pages/applications/applications-table/applications-table.tsx +++ b/client/src/app/pages/applications/applications-table/applications-table.tsx @@ -17,8 +17,6 @@ import { MenuToggle, MenuToggleElement, Modal, - Flex, - FlexItem, } from "@patternfly/react-core"; import { PencilAltIcon, TagIcon, EllipsisVIcon } from "@patternfly/react-icons"; import { @@ -30,7 +28,6 @@ import { ActionsColumn, Tbody, } from "@patternfly/react-table"; -import { QuestionCircleIcon } from "@patternfly/react-icons/dist/esm/icons/question-circle-icon"; // @app components and utilities import { AppPlaceholder } from "@app/components/AppPlaceholder"; @@ -44,7 +41,6 @@ import { ConditionalTableBody, TableRowContentWithControls, } from "@app/components/TableControls"; -import { IconedStatus } from "@app/components/IconedStatus"; import { ToolbarBulkSelector } from "@app/components/ToolbarBulkSelector"; import { ConfirmDialog } from "@app/components/ConfirmDialog"; import { NotificationsContext } from "@app/components/NotificationsContext"; @@ -108,7 +104,6 @@ import { 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"; @@ -116,6 +111,7 @@ 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"; +import { ApplicationReviewStatus } from "../components/application-review-status/application-review-status"; export const ApplicationsTable: React.FC = () => { const { t } = useTranslation(); @@ -221,8 +217,6 @@ export const ApplicationsTable: React.FC = () => { refetch: fetchApplications, } = useFetchApplications(); - const { archetypes } = useFetchArchetypes(); - const onDeleteApplicationSuccess = (appIDCount: number) => { pushNotification({ title: t("toastr.success.applicationDeleted", { @@ -870,23 +864,6 @@ export const ApplicationsTable: React.FC = () => { > {currentPageItems?.map((application, rowIndex) => { - const isAppReviewed = !!application.review; - const applicationArchetypes = application.archetypes?.map( - (archetypeRef) => { - return archetypes.find( - (archetype) => archetype.id === archetypeRef.id - ); - } - ); - - const hasReviewedArchetype = applicationArchetypes?.some( - (archetype) => !!archetype?.review - ); - - const hasAssessedArchetype = applicationArchetypes?.some( - (archetype) => !!archetype?.assessments?.length - ); - return ( { modifier="truncate" {...getTdProps({ columnKey: "assessment" })} > - - - - - - {hasAssessedArchetype ? ( - - - - ) : null} - - + - - - - - - {hasReviewedArchetype ? ( - - - - ) : null} - - + = ({ application, isLoading = false }) => { +> = ({ application }) => { const { t } = useTranslation(); + const { archetypes, isFetching } = useFetchArchetypes(); + + const applicationArchetypes = application.archetypes?.map((archetypeRef) => { + return archetypes?.find((archetype) => archetype.id === archetypeRef.id); + }); + + const hasAssessedArchetype = applicationArchetypes?.some( + (archetype) => !!archetype?.assessments?.length ?? 0 > 0 + ); + const { assessments, isFetching: isFetchingAssessmentsById, fetchError, } = useFetchAssessmentsByItemId(false, application.id); - const { questionnaires } = useFetchQuestionnaires(); - const requiredQuestionnaireExists = questionnaires?.some( - (q) => q.required === true - ); - //NOTE: Application.assessed is true if an app is assigned to an archetype and no required questionnaires exist - if (application?.assessed && requiredQuestionnaireExists) { - return ; - } if (fetchError) { return ; } - if (isLoading || isFetchingAssessmentsById) { + if (isFetching || isFetchingAssessmentsById) { return ; } - if ( - assessments?.some((a) => a.status === "started" || a.status === "complete") + let statusPreset: IconedStatusPreset = "NotStarted"; // Default status + let tooltipCount: number = 0; + const isDirectlyAssessed = + application.assessed && (application.assessments?.length ?? 0) > 0; + if (isDirectlyAssessed) { + statusPreset = "Completed"; + } else if (hasAssessedArchetype) { + statusPreset = "InheritedAssessments"; + const assessedArchetypeCount = + applicationArchetypes?.filter( + (archetype) => archetype?.assessments?.length ?? 0 > 0 + ).length || 0; + tooltipCount = assessedArchetypeCount; + } else if ( + assessments?.some( + (assessment) => + assessment.status === "started" || assessment.status === "complete" + ) ) { - return ; + statusPreset = "InProgress"; } - - return ; + return ; }; 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 050925b54f..12887de1d3 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 @@ -20,9 +20,17 @@ import { DescriptionListTerm, Divider, Tooltip, + Label, } from "@patternfly/react-core"; import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; -import { Application, Identity, Task, MimeType, Ref } from "@app/api/models"; +import { + Application, + Identity, + Task, + MimeType, + Ref, + Archetype, +} from "@app/api/models"; import { IPageDrawerContentProps, PageDrawerContent, @@ -46,9 +54,9 @@ import ExclamationCircleIcon from "@patternfly/react-icons/dist/esm/icons/exclam import { ApplicationFacts } from "./application-facts"; import { ReviewFields } from "./review-fields"; import { LabelsFromItems } from "@app/components/labels/labels-from-items/labels-from-items"; -import { ReviewedArchetypeItem } from "./reviewed-archetype-item"; import { RiskLabel } from "@app/components/RiskLabel"; import { ApplicationDetailFields } from "./application-detail-fields"; +import { useFetchArchetypes } from "@app/queries/archetypes"; export interface IApplicationDetailDrawerProps extends Pick { @@ -77,7 +85,9 @@ export const ApplicationDetailDrawer: React.FC< const isTaskRunning = task?.state === "Running"; const { identities } = useFetchIdentities(); + const { archetypes } = useFetchArchetypes(); const { facts, isFetching } = useFetchFacts(application?.id); + const [taskIdToView, setTaskIdToView] = React.useState(); let matchingSourceCredsRef: Identity | undefined; @@ -91,6 +101,22 @@ export const ApplicationDetailDrawer: React.FC< const enableDownloadSetting = useSetting("download.html.enabled"); + const assessedArchetypes = + application?.archetypes + ?.map((archetypeRef) => + archetypes.find((archetype) => archetype.id === archetypeRef.id) + ) + .filter((fullArchetype) => fullArchetype?.assessed) + .filter(Boolean) || []; + + const reviewedArchetypes = + application?.archetypes + ?.map((archetypeRef) => + archetypes.find((archetype) => archetype.id === archetypeRef.id) + ) + .filter((fullArchetype) => fullArchetype?.review) + .filter(Boolean) || []; + return ( + + + {t("terms.archetypesAssessed")} + + + {assessedArchetypes?.length ? ( + assessedArchetypes.map((assessedArchetype) => ( + + )) + ) : ( + + )} + + + {t("terms.archetypesReviewed")} - {application?.archetypes?.length ?? 0 > 0 ? ( - application?.archetypes?.map((archetypeRef) => ( - ( + )) ) : ( @@ -408,3 +452,7 @@ export const ApplicationDetailDrawer: React.FC< const ArchetypeLabels: React.FC<{ archetypeRefs?: Ref[] }> = ({ archetypeRefs, }) => ; + +const ArchetypeItem: React.FC<{ archetype: Archetype }> = ({ archetype }) => { + return ; +}; diff --git a/client/src/app/pages/applications/components/application-detail-drawer/reviewed-archetype-item.tsx b/client/src/app/pages/applications/components/application-detail-drawer/reviewed-archetype-item.tsx deleted file mode 100644 index 912d8588dc..0000000000 --- a/client/src/app/pages/applications/components/application-detail-drawer/reviewed-archetype-item.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useFetchArchetypeById } from "@app/queries/archetypes"; -import { Label } from "@patternfly/react-core"; -import React from "react"; - -export const ReviewedArchetypeItem = ({ id }: { id: number }) => { - const { archetype } = useFetchArchetypeById(id); - - if (!archetype?.review) return null; - - return ( - - ); -}; diff --git a/client/src/app/pages/applications/components/application-review-status/application-review-status.tsx b/client/src/app/pages/applications/components/application-review-status/application-review-status.tsx new file mode 100644 index 0000000000..fccb89c46f --- /dev/null +++ b/client/src/app/pages/applications/components/application-review-status/application-review-status.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { Application } from "@app/api/models"; +import { IconedStatus, IconedStatusPreset } from "@app/components/IconedStatus"; +import { Spinner } from "@patternfly/react-core"; +import { EmptyTextMessage } from "@app/components/EmptyTextMessage"; +import { useTranslation } from "react-i18next"; +import { useFetchArchetypes } from "@app/queries/archetypes"; + +export interface ApplicationReviewStatusProps { + application: Application; + isLoading?: boolean; +} + +export const ApplicationReviewStatus: React.FC< + ApplicationReviewStatusProps +> = ({ application }) => { + const { t } = useTranslation(); + + const { archetypes, isFetching } = useFetchArchetypes(); + const isAppReviewed = !!application.review; + + const applicationArchetypes = application.archetypes?.map((archetypeRef) => { + return archetypes?.find((archetype) => archetype.id === archetypeRef.id); + }); + + const reviewedArchetypeCount = + applicationArchetypes?.filter((archetype) => !!archetype?.review).length || + 0; + + if (isFetching) { + return ; + } + + let statusPreset: IconedStatusPreset; + let tooltipCount = 0; + + if (isAppReviewed) { + statusPreset = "Completed"; + } else if (reviewedArchetypeCount > 0) { + statusPreset = "InheritedReviews"; + tooltipCount = reviewedArchetypeCount; + } else { + statusPreset = "NotStarted"; + } + + if (!applicationArchetypes || applicationArchetypes.length === 0) { + return ; + } + + return ; +}; From b654645c56e8d0f66f41d9deb78dc115c2e18d0e Mon Sep 17 00:00:00 2001 From: Scott Dickerson Date: Thu, 14 Dec 2023 12:50:02 -0500 Subject: [PATCH 3/4] :seedling: Refactor `WithUiId` handling to use hook `useWithUiId()` (#1555) Following up on #1554, create hook `useWithUiId()` to handle injecting UI id to objects. Any object `T` going will to the hook will come out as a `WithUiId` object. Tables using the UI id have been adjusted to use the Constant `UI_UNIQUE_ID` as the `WithUiId` table data `idProperty`. All uses of `WithUiId` are now handled the same way. --------- Signed-off-by: Scott J Dickerson --- client/src/app/Constants.ts | 6 ++ client/src/app/api/models.ts | 8 ++- client/src/app/hooks/table-controls/DOCS.md | 4 +- .../app/pages/dependencies/dependencies.tsx | 4 +- client/src/app/pages/issues/issues-table.tsx | 4 +- client/src/app/queries/dependencies.ts | 60 ++++++------------- client/src/app/queries/issues.ts | 59 ++++++++++-------- client/src/app/utils/query-utils.ts | 33 ++++++++++ 8 files changed, 104 insertions(+), 74 deletions(-) create mode 100644 client/src/app/utils/query-utils.ts diff --git a/client/src/app/Constants.ts b/client/src/app/Constants.ts index 59d8974062..81954b6c0f 100644 --- a/client/src/app/Constants.ts +++ b/client/src/app/Constants.ts @@ -24,6 +24,12 @@ export const isRWXSupported = ENV.RWX_SUPPORTED === "true"; export const DEFAULT_SELECT_MAX_HEIGHT = 200; +/** + * The name of the client generated id field inserted in a object marked with mixin type + * `WithUiId`. + */ +export const UI_UNIQUE_ID = "_ui_unique_id"; + // Colors // t('colors.red') diff --git a/client/src/app/api/models.ts b/client/src/app/api/models.ts index 8ae4ffcaa1..e3107eff72 100644 --- a/client/src/app/api/models.ts +++ b/client/src/app/api/models.ts @@ -578,8 +578,14 @@ export interface BaseAnalysisIssueReport extends AnalysisIssuesCommonFields { files: number; } -// After fetching from the hub, we inject a unique id composed of ruleset+rule for convenience +/** + * Mark an object as having a unique client generated id field. Use this type if + * an objects from hub does not have a single field with a unique key AND the object + * is to be used in a table. Our table handlers assume a single field with a unique + * value across all objects in a set to properly handle row selections. + */ export type WithUiId = T & { _ui_unique_id: string }; + export type AnalysisRuleReport = WithUiId; export type AnalysisIssueReport = WithUiId; diff --git a/client/src/app/hooks/table-controls/DOCS.md b/client/src/app/hooks/table-controls/DOCS.md index 1b409e1b12..ab19bef9fe 100644 --- a/client/src/app/hooks/table-controls/DOCS.md +++ b/client/src/app/hooks/table-controls/DOCS.md @@ -514,7 +514,9 @@ Table columns are identified by unique keys which are statically inferred from t #### Item IDs -Item objects must contain some unique identifier which is either a string or number. The property key of this identifier is a required config argument called `idProperty`, which will usually be `"id"`. If no unique identifier is present in the API data, an artificial one can be injected before passing the data into these hooks, which can be done in the useQuery `select` callback (see instances where we have used `"_ui_unique_id"`). Any state which keeps track of something by item (i.e. by row) makes use of `item[idProperty]` as an identifier. Examples of this include selected rows, expanded rows and active rows. Valid `idProperty` values are also enforced by TypeScript generics; if an `idProperty` is provided that is not a property on the `TItem` type, you should get a type error. +Item objects must contain some unique identifier which is either a string or number. The property key of this identifier is a required config argument called `idProperty`, which will usually be `"id"`. If no unique identifier is present in the API data, an artificial one can be injected before passing the data into these hooks. This can be done in the useQuery `select` callback (see instances where we have used `"_ui_unique_id"`). Another option is to use the query hook `useWithUiId()` on the react-query fetched data. Since `select` modified data is not part of the query cache, it does not matter if transforms are done in react-query, `useWithUiId` hook, or other means. + +Any state which keeps track of something by item (i.e. by row) makes use of `item[idProperty]` as an identifier. Examples of this include selected rows, expanded rows and active rows. Valid `idProperty` values are also enforced by TypeScript generics. If an `idProperty` is provided that is not a property on the `TItem` type, you should get a type error. > ⚠️ TECH DEBT NOTE: Things specific to `useQuery` and `_ui_unique_id` here are Konveyor-specific notes that should be removed after moving this to table-batteries. diff --git a/client/src/app/pages/dependencies/dependencies.tsx b/client/src/app/pages/dependencies/dependencies.tsx index 0c61a698cb..4c4d347656 100644 --- a/client/src/app/pages/dependencies/dependencies.tsx +++ b/client/src/app/pages/dependencies/dependencies.tsx @@ -18,7 +18,7 @@ import { useTableControlProps, getHubRequestParams, } from "@app/hooks/table-controls"; -import { TablePersistenceKeyPrefix } from "@app/Constants"; +import { TablePersistenceKeyPrefix, UI_UNIQUE_ID } from "@app/Constants"; import { SimplePagination } from "@app/components/SimplePagination"; import { ConditionalTableBody, @@ -97,7 +97,7 @@ export const Dependencies: React.FC = () => { const tableControls = useTableControlProps({ ...tableControlState, // Includes filterState, sortState and paginationState - idProperty: "_ui_unique_id", + idProperty: UI_UNIQUE_ID, currentPageItems, totalItemCount, isLoading: isFetching, diff --git a/client/src/app/pages/issues/issues-table.tsx b/client/src/app/pages/issues/issues-table.tsx index db2e487fc8..401e724eba 100644 --- a/client/src/app/pages/issues/issues-table.tsx +++ b/client/src/app/pages/issues/issues-table.tsx @@ -33,7 +33,7 @@ import { useSelectionState } from "@migtools/lib-ui"; import { AppPlaceholder } from "@app/components/AppPlaceholder"; import { OptionWithValue, SimpleSelect } from "@app/components/SimpleSelect"; -import { TablePersistenceKeyPrefix } from "@app/Constants"; +import { TablePersistenceKeyPrefix, UI_UNIQUE_ID } from "@app/Constants"; import { useFetchIssueReports, useFetchRuleReports } from "@app/queries/issues"; import { FilterType, @@ -226,7 +226,7 @@ export const IssuesTable: React.FC = ({ mode }) => { const tableControls = useTableControlProps({ ...tableControlState, // Includes filterState, sortState and paginationState - idProperty: "_ui_unique_id", + idProperty: UI_UNIQUE_ID, currentPageItems: currentPageReports, totalItemCount: totalReportCount, isLoading, diff --git a/client/src/app/queries/dependencies.ts b/client/src/app/queries/dependencies.ts index 6fecde893f..24b155f672 100644 --- a/client/src/app/queries/dependencies.ts +++ b/client/src/app/queries/dependencies.ts @@ -1,70 +1,46 @@ -import { useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import { - AnalysisAppDependency, AnalysisDependency, HubPaginatedResult, HubRequestParams, WithUiId, } from "@app/api/models"; import { getAppDependencies, getDependencies } from "@app/api/rest"; - -export interface IDependenciesFetchState { - result: HubPaginatedResult>; - isFetching: boolean; - fetchError: unknown; - refetch: () => void; -} -export interface IAppDependenciesFetchState { - result: HubPaginatedResult; - isFetching: boolean; - fetchError: unknown; - refetch: () => void; -} +import { useWithUiId } from "@app/utils/query-utils"; export const DependenciesQueryKey = "dependencies"; export const AppDependenciesQueryKey = "appDependencies"; -export const useFetchDependencies = ( - params: HubRequestParams = {} -): IDependenciesFetchState => { - const { data, isLoading, error, refetch } = useQuery({ +export const useFetchDependencies = (params: HubRequestParams = {}) => { + const { + data: dependencies, + isLoading, + error, + refetch, + } = useQuery({ queryKey: [DependenciesQueryKey, params], queryFn: async () => await getDependencies(params), onError: (error) => console.log("error, ", error), keepPreviousData: true, }); - const result = useMemo(() => { - if (!data) { - return { data: [], total: 0, params }; - } - - const syntheticData: WithUiId[] = data.data.map( - (dep) => ({ - ...dep, - _ui_unique_id: `${dep.name}/${dep.provider}`, - }) - ); - - return { - data: syntheticData, - total: data.total, - params: data.params, - }; - }, [data, params]); - + const withUiId = useWithUiId( + dependencies?.data, + (d) => `${d.name}/${d.provider}` + ); return { - result, + result: { + data: withUiId, + total: dependencies?.total ?? 0, + params: dependencies?.params ?? params, + } as HubPaginatedResult>, isFetching: isLoading, fetchError: error, refetch, }; }; -export const useFetchAppDependencies = ( - params: HubRequestParams = {} -): IAppDependenciesFetchState => { +export const useFetchAppDependencies = (params: HubRequestParams = {}) => { const { data, isLoading, error, refetch } = useQuery({ queryKey: [AppDependenciesQueryKey, params], queryFn: async () => await getAppDependencies(params), diff --git a/client/src/app/queries/issues.ts b/client/src/app/queries/issues.ts index ac66630ee5..4a0cf53755 100644 --- a/client/src/app/queries/issues.ts +++ b/client/src/app/queries/issues.ts @@ -2,8 +2,6 @@ import { useQuery } from "@tanstack/react-query"; import { AnalysisIssueReport, AnalysisRuleReport, - BaseAnalysisIssueReport, - BaseAnalysisRuleReport, HubPaginatedResult, HubRequestParams, WithUiId, @@ -17,6 +15,7 @@ import { getIssueReports, getIssue, } from "@app/api/rest"; +import { useWithUiId } from "@app/utils/query-utils"; export const RuleReportsQueryKey = "rulereports"; export const AppReportsQueryKey = "appreports"; @@ -26,37 +25,33 @@ export const IssuesQueryKey = "issues"; export const IssueQueryKey = "issue"; export const IncidentsQueryKey = "incidents"; -const injectUiUniqueIds = < - T extends BaseAnalysisRuleReport | BaseAnalysisIssueReport, ->( - result: HubPaginatedResult -): HubPaginatedResult> => { - // There is no single unique id property on some of the hub's composite report objects. - // We need to create one for table hooks to work. - const processedData = result.data.map( - (baseReport): WithUiId => ({ - ...baseReport, - _ui_unique_id: `${baseReport.ruleset}/${baseReport.rule}`, - }) - ); - return { ...result, data: processedData }; -}; - export const useFetchRuleReports = ( enabled: boolean, params: HubRequestParams = {} ) => { - const { data, isLoading, error, refetch } = useQuery({ + const { + data: ruleReport, + isLoading, + error, + refetch, + } = useQuery({ queryKey: [RuleReportsQueryKey, params], queryFn: () => getRuleReports(params), onError: (error) => console.log("error, ", error), keepPreviousData: true, - select: (result): HubPaginatedResult => - injectUiUniqueIds(result), enabled, }); + + const withUiId = useWithUiId( + ruleReport?.data, + (r) => `${r.ruleset}/${r.rule}` + ); return { - result: data || { data: [], total: 0, params }, + result: { + data: withUiId, + total: ruleReport?.total ?? 0, + params: ruleReport?.params ?? params, + } as HubPaginatedResult>, isFetching: isLoading, fetchError: error, refetch, @@ -82,17 +77,29 @@ export const useFetchIssueReports = ( applicationId?: number, params: HubRequestParams = {} ) => { - const { data, isLoading, error, refetch } = useQuery({ + const { + data: issueReport, + isLoading, + error, + refetch, + } = useQuery({ enabled: applicationId !== undefined, queryKey: [IssueReportsQueryKey, applicationId, params], queryFn: () => getIssueReports(applicationId, params), onError: (error) => console.log("error, ", error), keepPreviousData: true, - select: (result): HubPaginatedResult => - injectUiUniqueIds(result), }); + + const withUiId = useWithUiId( + issueReport?.data, + (r) => `${r.ruleset}/${r.rule}` + ); return { - result: data || { data: [], total: 0, params }, + result: { + data: withUiId, + total: issueReport?.total ?? 0, + params: issueReport?.params ?? params, + } as HubPaginatedResult, isFetching: isLoading, fetchError: error, refetch, diff --git a/client/src/app/utils/query-utils.ts b/client/src/app/utils/query-utils.ts new file mode 100644 index 0000000000..84c1590aba --- /dev/null +++ b/client/src/app/utils/query-utils.ts @@ -0,0 +1,33 @@ +import { useMemo } from "react"; +import { UI_UNIQUE_ID } from "@app/Constants"; +import { WithUiId } from "@app/api/models"; + +/** + * Make a shallow copy of `data` and insert a new `UI_UNIQUE_ID` field in each element + * with the output of the `generator` function. This hook allows generating the needed + * UI id field for any object that does not already have a unique id field so the object + * can be used with our table selection handlers. + * + * @returns A shallow copy of `T` with an added `UI_UNIQUE_ID` field. + */ +export const useWithUiId = ( + /** Source data to modify. */ + data: T[] | undefined, + /** Generate the unique id for a specific `T`. */ + generator: (item: T) => string +): WithUiId[] => { + const result = useMemo(() => { + if (!data || data.length === 0) { + return []; + } + + const dataWithUiId: WithUiId[] = data.map((item) => ({ + ...item, + [UI_UNIQUE_ID]: generator(item), + })); + + return dataWithUiId; + }, [data, generator]); + + return result; +}; From 1dd15d7fedb772ebb27351182c7ece1f53d24afc Mon Sep 17 00:00:00 2001 From: Scott Dickerson Date: Thu, 14 Dec 2023 13:25:44 -0500 Subject: [PATCH 4/4] :seedling: Update github action versions (#1592) Time marches on and github action version need to be updated because old versions get deprecated. This change updates the actions to the current stable versions available. --------- Signed-off-by: Scott J Dickerson --- .github/workflows/ci-actions.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-actions.yml b/.github/workflows/ci-actions.yml index cc0f3ae185..eeed592731 100644 --- a/.github/workflows/ci-actions.yml +++ b/.github/workflows/ci-actions.yml @@ -4,12 +4,10 @@ on: push: branches: - main - - "v[0-9]+.[0-9]+.[0-9]+" - "release-*" pull_request: branches: - main - - "v[0-9]+.[0-9]+.[0-9]+" - "release-*" jobs: @@ -17,13 +15,14 @@ jobs: runs-on: ubuntu-latest strategy: matrix: + # Note: This should match the node version(s) used in the base Dockerfile node-version: [18.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} @@ -43,13 +42,13 @@ jobs: run: npm run test -- --coverage --watchAll=false - name: Upload to codecov (client) - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 with: flags: client directory: ./*/coverage - name: Upload to codecov (server) - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 with: flags: server directory: ./*/coverage @@ -57,7 +56,7 @@ jobs: build-and-upload-for-global-ci: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: save tackle2-ui image run: |