diff --git a/client/package.json b/client/package.json index de932e6874..dbc3a3fe32 100644 --- a/client/package.json +++ b/client/package.json @@ -34,7 +34,7 @@ "@react-keycloak/web": "^3.4.0", "@tanstack/react-query": "^4.22.0", "@tanstack/react-query-devtools": "^4.22.0", - "axios": "^0.21.2", + "axios": "^1.6.8", "dayjs": "^1.11.7", "ejs": "^3.1.7", "fast-xml-parser": "^4.0.3", @@ -71,7 +71,6 @@ "@types/react-measure": "^2.0.12", "@types/react-router-dom": "^5.1.7", "@types/tinycolor2": "^1.4.6", - "axios-mock-adapter": "^1.19.0", "browserslist": "^4.19.1", "case-sensitive-paths-webpack-plugin": "^2.4.0", "copy-webpack-plugin": "^12.0.2", diff --git a/client/public/locales/en/translation.json b/client/public/locales/en/translation.json index a4de299d5a..82757ccf79 100644 --- a/client/public/locales/en/translation.json +++ b/client/public/locales/en/translation.json @@ -450,6 +450,7 @@ "teamMember": "team member", "ticket": "Ticket", "trivialButMigratable": "Trivial but migratable", + "type": "Type", "unassessedOrUnknown": "Unassessed or unknown", "unassessed": "Unassessed", "unassigned": "Not yet assigned", diff --git a/client/src/app/api/rest.ts b/client/src/app/api/rest.ts index 68c10d1b3b..df92de2d33 100644 --- a/client/src/app/api/rest.ts +++ b/client/src/app/api/rest.ts @@ -1,6 +1,4 @@ -// hub OpenAPI definition: https://github.com/konveyor/tackle2-hub/blob/main/docs/openapi3.json - -import axios, { AxiosPromise } from "axios"; +import axios, { AxiosPromise, RawAxiosRequestHeaders } from "axios"; import { AnalysisDependency, @@ -107,14 +105,18 @@ export const QUESTIONNAIRES = HUB + "/questionnaires"; export const ARCHETYPES = HUB + "/archetypes"; -// PATHFINDER -export const PATHFINDER = "/hub/pathfinder"; export const ASSESSMENTS = HUB + "/assessments"; -const jsonHeaders = { headers: { Accept: "application/json" } }; -const formHeaders = { headers: { Accept: "multipart/form-data" } }; -const fileHeaders = { headers: { Accept: "application/json" } }; -const yamlHeaders = { headers: { Accept: "application/x-yaml" } }; +const jsonHeaders: RawAxiosRequestHeaders = { + Accept: "application/json", +}; +const formHeaders: RawAxiosRequestHeaders = { + Accept: "multipart/form-data", +}; +const fileHeaders: RawAxiosRequestHeaders = { Accept: "application/json" }; +const yamlHeaders: RawAxiosRequestHeaders = { + Accept: "application/x-yaml", +}; type Direction = "asc" | "desc"; @@ -137,7 +139,7 @@ export const getApplicationDependencies = ( return axios .get(`${APPLICATION_DEPENDENCY}`, { params, - headers: jsonHeaders.headers, + headers: jsonHeaders, }) .then((response) => response.data); }; @@ -243,7 +245,7 @@ export const deleteAssessment = (id: number) => { }; export const getIdentities = () => { - return axios.get(`${IDENTITIES}`, jsonHeaders); + return axios.get(`${IDENTITIES}`, { headers: jsonHeaders }); }; export const createIdentity = (obj: New) => { @@ -322,8 +324,7 @@ export function getTaskById( format: string, merged: boolean = false ): Promise { - const headers = - format === "yaml" ? { ...yamlHeaders.headers } : { ...jsonHeaders.headers }; + const headers = format === "yaml" ? { ...yamlHeaders } : { ...jsonHeaders }; const responseType = format === "yaml" ? "text" : "json"; let url = `${TASKS}/${id}`; @@ -371,11 +372,9 @@ export const uploadFileTaskgroup = ({ formData: any; file: any; }) => { - return axios.post( - `${TASKGROUPS}/${id}/bucket/${path}`, - formData, - formHeaders - ); + return axios.post(`${TASKGROUPS}/${id}/bucket/${path}`, formData, { + headers: formHeaders, + }); }; export const removeFileTaskgroup = ({ @@ -430,7 +429,9 @@ export const createFile = ({ file: IReadFile; }) => axios - .post(`${FILES}/${file.fileName}`, formData, fileHeaders) + .post(`${FILES}/${file.fileName}`, formData, { + headers: fileHeaders, + }) .then((response) => { return response.data; }); diff --git a/client/src/app/components/AppTableActionButtons.tsx b/client/src/app/components/AppTableActionButtons.tsx index 550f196568..62df3c1d65 100644 --- a/client/src/app/components/AppTableActionButtons.tsx +++ b/client/src/app/components/AppTableActionButtons.tsx @@ -1,6 +1,6 @@ import React from "react"; import { useTranslation } from "react-i18next"; -import { Button } from "@patternfly/react-core"; +import { Button, Flex, FlexItem } from "@patternfly/react-core"; import { applicationsWriteScopes, RBAC, RBAC_TYPE } from "@app/rbac"; import { ConditionalTooltip } from "./ConditionalTooltip"; import { Td } from "@patternfly/react-table"; @@ -26,30 +26,39 @@ export const AppTableActionButtons: React.FC = ({ rbacType={RBAC_TYPE.Scope} > - - - - - - + + + + + + + + + + ); diff --git a/client/src/app/components/AppTableWithControls.tsx b/client/src/app/components/AppTableWithControls.tsx deleted file mode 100644 index adf6751915..0000000000 --- a/client/src/app/components/AppTableWithControls.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; - -import { - Toolbar, - ToolbarContent, - ToolbarItem, - ToolbarItemVariant, -} from "@patternfly/react-core"; - -import { AppTable, IAppTableProps } from "./AppTable"; -import { PaginationStateProps } from "@app/hooks/useLegacyPaginationState"; -import { SimplePagination } from "./SimplePagination"; - -export interface IAppTableWithControlsProps extends IAppTableProps { - count: number; - withoutTopPagination?: boolean; - withoutBottomPagination?: boolean; - toolbarBulkSelector?: any; - toolbarToggle?: any; - toolbarActions?: any; - toolbarClearAllFilters?: () => void; - paginationProps: PaginationStateProps; - paginationIdPrefix?: string; -} - -export const AppTableWithControls: React.FC = ({ - count, - withoutTopPagination, - withoutBottomPagination, - toolbarBulkSelector, - toolbarToggle, - toolbarActions, - toolbarClearAllFilters, - paginationProps, - paginationIdPrefix, - ...rest -}) => { - const { t } = useTranslation(); - - return ( -
- - - {toolbarBulkSelector} - {toolbarToggle ? toolbarToggle : null} - {toolbarActions} - {!withoutTopPagination && ( - - - - )} - - - - {!withoutBottomPagination && ( - - )} -
- ); -}; diff --git a/client/src/app/components/Icons/IconWithLabel.tsx b/client/src/app/components/Icons/IconWithLabel.tsx new file mode 100644 index 0000000000..3ca28c1f65 --- /dev/null +++ b/client/src/app/components/Icons/IconWithLabel.tsx @@ -0,0 +1,36 @@ +import React, { FC, ReactElement, ReactNode } from "react"; +import { Flex, FlexItem } from "@patternfly/react-core"; +import { OptionalTooltip } from "./OptionalTooltip"; + +export const IconWithLabel: FC<{ + iconTooltipMessage?: string; + icon: ReactElement; + label: ReactNode; + trailingItem?: ReactElement; + trailingItemTooltipMessage?: string; +}> = ({ + iconTooltipMessage, + icon, + label, + trailingItem, + trailingItemTooltipMessage, +}) => ( + + + + {icon} + + + {label} + {!!trailingItem && ( + + + {trailingItem} + + + )} + +); diff --git a/client/src/app/components/IconedStatus.tsx b/client/src/app/components/Icons/IconedStatus.tsx similarity index 66% rename from client/src/app/components/IconedStatus.tsx rename to client/src/app/components/Icons/IconedStatus.tsx index 6dbf102e9c..6179f95b25 100644 --- a/client/src/app/components/IconedStatus.tsx +++ b/client/src/app/components/Icons/IconedStatus.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Flex, FlexItem, Icon, Tooltip } from "@patternfly/react-core"; +import { Icon } 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"; @@ -7,6 +7,8 @@ import InProgressIcon from "@patternfly/react-icons/dist/esm/icons/in-progress-i import ExclamationCircleIcon from "@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon"; import UnknownIcon from "@patternfly/react-icons/dist/esm/icons/unknown-icon"; import TopologyIcon from "@patternfly/react-icons/dist/esm/icons/topology-icon"; +import { IconWithLabel } from "./IconWithLabel"; +import { ReactElement } from "react-markdown/lib/react-markdown"; export type IconedStatusPreset = | "InheritedReviews" @@ -31,7 +33,9 @@ export type IconedStatusStatusType = | "danger"; type IconedStatusPresetType = { - [key in IconedStatusPreset]: Omit; + [key in IconedStatusPreset]: Omit & { + topologyIcon?: ReactElement; + }; }; export interface IIconedStatusProps { @@ -61,6 +65,7 @@ export const IconedStatus: React.FC = ({ tooltipMessage: t("message.inheritedReviewTooltip", { count: tooltipCount, }), + topologyIcon: , }, InProgressInheritedAssessments: { icon: , @@ -69,6 +74,7 @@ export const IconedStatus: React.FC = ({ tooltipMessage: t("message.inheritedAssessmentTooltip", { count: tooltipCount, }), + topologyIcon: , }, InheritedReviews: { icon: , @@ -77,6 +83,7 @@ export const IconedStatus: React.FC = ({ tooltipMessage: t("message.inheritedReviewTooltip", { count: tooltipCount, }), + topologyIcon: , }, InheritedAssessments: { icon: , @@ -85,6 +92,7 @@ export const IconedStatus: React.FC = ({ tooltipMessage: t("message.inheritedAssessmentTooltip", { count: tooltipCount, }), + topologyIcon: , }, Canceled: { icon: , @@ -129,63 +137,18 @@ export const IconedStatus: React.FC = ({ }, }; const presetProps = preset && presets[preset]; - const IconWithOptionalTooltip: React.FC<{ children: React.ReactElement }> = ({ - children, - }) => - presetProps?.tooltipMessage ? ( - {children} - ) : ( - <>{children} - ); - - const getTooltipContent = () => { - switch (preset) { - case "InheritedReviews": - return t("message.inheritedReviewTooltip", { - count: tooltipCount, - }); - - case "InheritedAssessments": - return t("message.inheritedAssessmentTooltip", { - count: tooltipCount, - }); - case "InProgressInheritedReviews": - return t("message.inheritedReviewTooltip", { - count: tooltipCount, - }); - case "InProgressInheritedAssessments": - return t("message.inheritedAssessmentTooltip", { - count: tooltipCount, - }); - - default: - return ""; - } - }; return ( - - - - - {icon || presetProps?.icon || } - - - - {label || presetProps?.label} - {(preset === "InheritedReviews" || - preset === "InheritedAssessments" || - preset === "InProgressInheritedAssessments" || - preset === "InProgressInheritedReviews") && ( - - - - - - )} - + + {icon || presetProps?.icon || } + + } + label={label || presetProps?.label} + trailingItemTooltipMessage={presetProps?.tooltipMessage} + trailingItem={presetProps?.topologyIcon} + /> ); }; diff --git a/client/src/app/components/Icons/OptionalTooltip.tsx b/client/src/app/components/Icons/OptionalTooltip.tsx new file mode 100644 index 0000000000..5c4ff178c0 --- /dev/null +++ b/client/src/app/components/Icons/OptionalTooltip.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { Tooltip } from "@patternfly/react-core"; + +export const OptionalTooltip: React.FC<{ + tooltipMessage?: string; + children: React.ReactElement; +}> = ({ children, tooltipMessage }) => + tooltipMessage ? ( + {children} + ) : ( + <>{children} + ); diff --git a/client/src/app/components/Icons/index.ts b/client/src/app/components/Icons/index.ts new file mode 100644 index 0000000000..dbd719c6ef --- /dev/null +++ b/client/src/app/components/Icons/index.ts @@ -0,0 +1,3 @@ +export * from "./OptionalTooltip"; +export * from "./IconedStatus"; +export * from "./IconWithLabel"; diff --git a/client/src/app/components/answer-table/answer-table.tsx b/client/src/app/components/answer-table/answer-table.tsx index 8f015fe588..f94aa5e59e 100644 --- a/client/src/app/components/answer-table/answer-table.tsx +++ b/client/src/app/components/answer-table/answer-table.tsx @@ -12,7 +12,7 @@ import { NoDataEmptyState } from "@app/components/NoDataEmptyState"; import { Answer } from "@app/api/models"; import { Label, Text, Tooltip } from "@patternfly/react-core"; import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; -import { IconedStatus } from "@app/components/IconedStatus"; +import { IconedStatus } from "@app/components/Icons"; import { TimesCircleIcon } from "@patternfly/react-icons"; import { WarningTriangleIcon } from "@patternfly/react-icons"; import { List, ListItem } from "@patternfly/react-core"; diff --git a/client/src/app/components/risk-icon/risk-icon.tsx b/client/src/app/components/risk-icon/risk-icon.tsx index d4a7ddf52d..23f5f1562c 100644 --- a/client/src/app/components/risk-icon/risk-icon.tsx +++ b/client/src/app/components/risk-icon/risk-icon.tsx @@ -1,6 +1,6 @@ import React from "react"; import { TimesCircleIcon, WarningTriangleIcon } from "@patternfly/react-icons"; -import { IconedStatus } from "@app/components/IconedStatus"; +import { IconedStatus } from "@app/components/Icons"; interface RiskIconProps { risk: string; diff --git a/client/src/app/components/tests/StatusIcon.test.tsx b/client/src/app/components/tests/StatusIcon.test.tsx index a9f2da90e4..a8edc6dff8 100644 --- a/client/src/app/components/tests/StatusIcon.test.tsx +++ b/client/src/app/components/tests/StatusIcon.test.tsx @@ -1,6 +1,6 @@ import { render } from "@app/test-config/test-utils"; import React from "react"; -import { IconedStatus } from "../IconedStatus"; +import { IconedStatus } from "../Icons"; describe("StatusIcon", () => { it("Renders without crashing", () => { diff --git a/client/src/app/pages/applications/analysis-wizard/__tests__/analysis-wizard.test.tsx b/client/src/app/pages/applications/analysis-wizard/__tests__/analysis-wizard.test.tsx index 94bb00cf6f..a2e1cd4a0d 100644 --- a/client/src/app/pages/applications/analysis-wizard/__tests__/analysis-wizard.test.tsx +++ b/client/src/app/pages/applications/analysis-wizard/__tests__/analysis-wizard.test.tsx @@ -2,11 +2,9 @@ import React from "react"; import "@testing-library/jest-dom"; import { render, screen, waitFor } from "@app/test-config/test-utils"; import { AnalysisWizard } from "../analysis-wizard"; -import { TASKGROUPS } from "@app/api/rest"; -import mock from "@app/test-config/mockInstance"; import userEvent from "@testing-library/user-event"; - -mock.onAny().reply(200, []); +import { server } from "@mocks/server"; +import { rest } from "msw"; const applicationData1 = { id: 1, @@ -53,6 +51,13 @@ const taskgroupData = { }; describe("", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + server.resetHandlers(); + }); + let isAnalyzeModalOpen = true; const setAnalyzeModalOpen = (toggle: boolean) => (isAnalyzeModalOpen = toggle); @@ -157,7 +162,11 @@ describe("", () => { }, ]; - mock.onPost(`${TASKGROUPS}`).reply(200, taskgroupData); + server.use( + rest.get("/hub/taskgroups", (req, res, ctx) => { + return res(ctx.json([taskgroupData])); + }) + ); render( { const { t } = useTranslation(); @@ -950,8 +951,12 @@ export const ApplicationsTable: React.FC = () => { modifier="truncate" {...getTdProps({ columnKey: "tags" })} > - - {application.tags ? application.tags.length : 0} + } + label={ + application.tags ? application.tags.length : 0 + } + /> )} {getColumnVisibility("effort") && ( diff --git a/client/src/app/pages/applications/components/application-analysis-status.tsx b/client/src/app/pages/applications/components/application-analysis-status.tsx index 3734811642..5759c4293d 100644 --- a/client/src/app/pages/applications/components/application-analysis-status.tsx +++ b/client/src/app/pages/applications/components/application-analysis-status.tsx @@ -1,7 +1,7 @@ import React from "react"; import { TaskState } from "@app/api/models"; -import { IconedStatus } from "@app/components/IconedStatus"; +import { IconedStatus } from "@app/components/Icons"; export interface ApplicationAnalysisStatusProps { state: TaskState; diff --git a/client/src/app/pages/applications/components/application-assessment-status/application-assessment-status.tsx b/client/src/app/pages/applications/components/application-assessment-status/application-assessment-status.tsx index 00894e5a4a..2c92c5cbb2 100644 --- a/client/src/app/pages/applications/components/application-assessment-status/application-assessment-status.tsx +++ b/client/src/app/pages/applications/components/application-assessment-status/application-assessment-status.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Application } from "@app/api/models"; -import { IconedStatus, IconedStatusPreset } from "@app/components/IconedStatus"; +import { IconedStatus, IconedStatusPreset } from "@app/components/Icons"; import { Spinner } from "@patternfly/react-core"; import { useAssessmentStatus } from "@app/hooks/useAssessmentStatus"; interface ApplicationAssessmentStatusProps { diff --git a/client/src/app/pages/applications/components/application-form/__tests__/application-form.test.tsx b/client/src/app/pages/applications/components/application-form/__tests__/application-form.test.tsx index b9d078202f..709e77a711 100644 --- a/client/src/app/pages/applications/components/application-form/__tests__/application-form.test.tsx +++ b/client/src/app/pages/applications/components/application-form/__tests__/application-form.test.tsx @@ -6,26 +6,31 @@ import { fireEvent, } from "@app/test-config/test-utils"; -import { BUSINESS_SERVICES } from "@app/api/rest"; -import mock from "@app/test-config/mockInstance"; import userEvent from "@testing-library/user-event"; import "@testing-library/jest-dom"; -import { BusinessService } from "@app/api/models"; import { ApplicationFormModal } from "../application-form-modal"; +import { server } from "@mocks/server"; +import { rest } from "msw"; describe("Component: application-form", () => { const mockChangeValue = jest.fn(); + beforeAll(() => server.listen({ onUnhandledRequest: "warn" })); + afterAll(() => server.close()); + + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + server.resetHandlers(); + }); + server.use( + rest.get("/hub/businessservices", (req, res, ctx) => { + return res(ctx.status(200), ctx.json([{ id: 1, name: "service" }])); + }) + ); it("Validation tests", async () => { - const businessServices: BusinessService[] = [{ id: 1, name: "service" }]; - - mock - .onGet(`${BUSINESS_SERVICES}`) - .reply(200, businessServices) - .onAny() - .reply(200, []); - render( ); 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 index 37b629306d..e8a57002ad 100644 --- 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 @@ -1,6 +1,6 @@ import React from "react"; import { Application } from "@app/api/models"; -import { IconedStatus, IconedStatusPreset } from "@app/components/IconedStatus"; +import { IconedStatus, IconedStatusPreset } from "@app/components/Icons"; import { Spinner } from "@patternfly/react-core"; import { EmptyTextMessage } from "@app/components/EmptyTextMessage"; import { useTranslation } from "react-i18next"; diff --git a/client/src/app/pages/applications/manage-imports-details/manage-imports-details.tsx b/client/src/app/pages/applications/manage-imports-details/manage-imports-details.tsx index e45678f000..d28fe345a5 100644 --- a/client/src/app/pages/applications/manage-imports-details/manage-imports-details.tsx +++ b/client/src/app/pages/applications/manage-imports-details/manage-imports-details.tsx @@ -6,34 +6,41 @@ import { saveAs } from "file-saver"; import { Button, ButtonVariant, + EmptyState, + EmptyStateBody, + EmptyStateIcon, PageSection, + Title, + Toolbar, + ToolbarContent, ToolbarGroup, ToolbarItem, } from "@patternfly/react-core"; -import { cellWidth, ICell, IRow, truncate } from "@patternfly/react-table"; +import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table"; import { ImportSummaryRoute, Paths } from "@app/Paths"; import { getApplicationSummaryCSV } from "@app/api/rest"; -import { ApplicationImport } from "@app/api/models"; import { getAxiosErrorMessage } from "@app/utils/utils"; -import { useLegacyPaginationState } from "@app/hooks/useLegacyPaginationState"; import { useFetchImports, useFetchImportSummaryById, } from "@app/queries/imports"; import { - FilterCategory, + FilterToolbar, FilterType, } from "@app/components/FilterToolbar/FilterToolbar"; -import { useLegacyFilterState } from "@app/hooks/useLegacyFilterState"; -import { useLegacySortState } from "@app/hooks/useLegacySortState"; import { NotificationsContext } from "@app/components/NotificationsContext"; import { PageHeader } from "@app/components/PageHeader"; -import { AppTableWithControls } from "@app/components/AppTableWithControls"; import { AppPlaceholder } from "@app/components/AppPlaceholder"; import { ConditionalRender } from "@app/components/ConditionalRender"; - -const ENTITY_FIELD = "entity"; +import { SimplePagination } from "@app/components/SimplePagination"; +import { + TableHeaderContentWithControls, + ConditionalTableBody, + TableRowContentWithControls, +} from "@app/components/TableControls"; +import { useLocalTableControls } from "@app/hooks/table-controls"; +import { CubesIcon } from "@patternfly/react-icons"; export const ManageImportsDetails: React.FC = () => { // i18 @@ -44,20 +51,6 @@ export const ManageImportsDetails: React.FC = () => { const { pushNotification } = React.useContext(NotificationsContext); - // Table - const columns: ICell[] = [ - { - title: t("terms.application"), - transforms: [cellWidth(30)], - cellTransforms: [truncate], - }, - { - title: t("terms.message"), - transforms: [cellWidth(70)], - cellTransforms: [truncate], - }, - ]; - const { imports, isFetching, fetchError } = useFetchImports( parseInt(importId), false @@ -65,23 +58,6 @@ export const ManageImportsDetails: React.FC = () => { const { importSummary } = useFetchImportSummaryById(importId); - const rows: IRow[] = []; - imports?.forEach((item) => { - rows.push({ - [ENTITY_FIELD]: item, - cells: [ - { - title: item["Application Name"], - }, - { - title: item.errorMessage, - }, - ], - }); - }); - - // - const exportCSV = () => { getApplicationSummaryCSV(importId) .then((response) => { @@ -95,42 +71,52 @@ export const ManageImportsDetails: React.FC = () => { }); }); }; - - const filterCategories: FilterCategory< - ApplicationImport, - "Application Name" - >[] = [ - { - categoryKey: "Application Name", - title: "Application Name", - type: FilterType.search, - placeholderText: "Filter by application name...", - getItemValue: (item) => { - return item["Application Name"] || ""; - }, + const tableControls = useLocalTableControls({ + tableName: "manage-imports-details", + idProperty: "Application Name", + items: imports || [], + columnNames: { + name: t("terms.name"), + message: t("terms.message"), }, - ]; - - const { filterValues, setFilterValues, filteredItems } = useLegacyFilterState( - imports || [], - filterCategories - ); - const handleOnClearAllFilters = () => { - setFilterValues({}); - }; - - const getSortValues = (item: ApplicationImport) => [ - item?.["Application Name"] || "", - "", // Action column - ]; - - const { sortBy, onSort, sortedItems } = useLegacySortState( - filteredItems, - getSortValues - ); + isFilterEnabled: true, + isSortEnabled: true, + isPaginationEnabled: true, + hasActionsColumn: true, + filterCategories: [ + { + categoryKey: "name", + title: "Application Name", + type: FilterType.search, + placeholderText: "Filter by application name...", + getItemValue: (item) => { + return item["Application Name"] || ""; + }, + }, + ], + initialItemsPerPage: 10, + sortableColumns: ["name", "message"], + initialSort: { columnKey: "name", direction: "asc" }, + getSortValues: (item) => ({ + name: item["Application Name"], + message: item.errorMessage, + }), + isLoading: isFetching, + }); - const { currentPageItems, setPageNumber, paginationProps } = - useLegacyPaginationState(sortedItems, 10); + const { + currentPageItems, + numRenderedColumns, + propHelpers: { + toolbarProps, + filterToolbarProps, + paginationProps, + tableProps, + getThProps, + getTrProps, + getTdProps, + }, + } = tableControls; return ( <> @@ -158,20 +144,14 @@ export const ManageImportsDetails: React.FC = () => { when={isFetching && !(imports || fetchError)} then={} > - +
+ + + - - } - /> + + + + + + + + + + + + {t("composed.noDataStateTitle", { + what: t("terms.imports").toLowerCase(), + })} + + + {t("composed.noDataStateBody", { + how: t("terms.create"), + what: t("terms.imports").toLowerCase(), + })} + + + } + numRenderedColumns={numRenderedColumns} + > + + {currentPageItems?.map((appImport, rowIndex) => { + return ( + + + + + + + ); + })} + + +
+ + +
+ {appImport["Application Name"]} + + {appImport.errorMessage} +
+ +
diff --git a/client/src/app/pages/applications/manage-imports/manage-imports.tsx b/client/src/app/pages/applications/manage-imports/manage-imports.tsx index cc20d3d4cf..3bcf7d0a23 100644 --- a/client/src/app/pages/applications/manage-imports/manage-imports.tsx +++ b/client/src/app/pages/applications/manage-imports/manage-imports.tsx @@ -27,7 +27,7 @@ import { AppPlaceholder } from "@app/components/AppPlaceholder"; import { ConditionalRender } from "@app/components/ConditionalRender"; import { ConfirmDialog } from "@app/components/ConfirmDialog"; import { FilterType, FilterToolbar } from "@app/components/FilterToolbar"; -import { IconedStatus } from "@app/components/IconedStatus"; +import { IconedStatus } from "@app/components/Icons"; import { KebabDropdown } from "@app/components/KebabDropdown"; import { NotificationsContext } from "@app/components/NotificationsContext"; import { SimplePagination } from "@app/components/SimplePagination"; diff --git a/client/src/app/pages/archetypes/archetypes-page.tsx b/client/src/app/pages/archetypes/archetypes-page.tsx index 6911b91426..ccbfbc9c60 100644 --- a/client/src/app/pages/archetypes/archetypes-page.tsx +++ b/client/src/app/pages/archetypes/archetypes-page.tsx @@ -71,7 +71,7 @@ import { } from "@app/rbac"; import { checkAccess } from "@app/utils/rbac-utils"; import keycloak from "@app/keycloak"; -import { IconedStatus } from "@app/components/IconedStatus"; +import { IconedStatus } from "@app/components/Icons"; import { useQueryClient } from "@tanstack/react-query"; const Archetypes: React.FC = () => { diff --git a/client/src/app/pages/controls/business-services/business-services.tsx b/client/src/app/pages/controls/business-services/business-services.tsx index 0eb1323a31..c516f6f8ae 100644 --- a/client/src/app/pages/controls/business-services/business-services.tsx +++ b/client/src/app/pages/controls/business-services/business-services.tsx @@ -165,7 +165,7 @@ export const BusinessServices: React.FC = () => { const closeCreateUpdateModal = () => { setCreateUpdateModalState(null); - refetch; + refetch(); }; return ( @@ -263,16 +263,14 @@ export const BusinessServices: React.FC = () => { {businessService.owner?.name} - - - setCreateUpdateModalState(businessService) - } - onDelete={() => deleteRow(businessService)} - /> - + + setCreateUpdateModalState(businessService) + } + onDelete={() => deleteRow(businessService)} + /> diff --git a/client/src/app/pages/controls/job-functions/job-functions.tsx b/client/src/app/pages/controls/job-functions/job-functions.tsx index 9a7d86fe5c..b3b6220610 100644 --- a/client/src/app/pages/controls/job-functions/job-functions.tsx +++ b/client/src/app/pages/controls/job-functions/job-functions.tsx @@ -4,43 +4,39 @@ import { useTranslation } from "react-i18next"; import { Button, ButtonVariant, + EmptyState, + EmptyStateBody, + EmptyStateIcon, Modal, - ToolbarGroup, + Title, + Toolbar, + ToolbarContent, ToolbarItem, } from "@patternfly/react-core"; -import { - cellWidth, - ICell, - IRow, - sortable, - TableText, -} from "@patternfly/react-table"; +import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table"; import { AppPlaceholder } from "@app/components/AppPlaceholder"; import { AppTableActionButtons } from "@app/components/AppTableActionButtons"; -import { AppTableWithControls } from "@app/components/AppTableWithControls"; import { ConditionalRender } from "@app/components/ConditionalRender"; import { ConfirmDialog } from "@app/components/ConfirmDialog"; -import { NoDataEmptyState } from "@app/components/NoDataEmptyState"; import { getAxiosErrorMessage } from "@app/utils/utils"; import { JobFunction } from "@app/api/models"; import { JobFunctionForm } from "./components/job-function-form"; -import { - FilterCategory, - FilterToolbar, - FilterType, -} from "@app/components/FilterToolbar"; -import { useLegacyFilterState } from "@app/hooks/useLegacyFilterState"; -import { useLegacySortState } from "@app/hooks/useLegacySortState"; -import { useLegacyPaginationState } from "@app/hooks/useLegacyPaginationState"; -import { controlsWriteScopes, RBAC, RBAC_TYPE } from "@app/rbac"; +import { FilterToolbar, FilterType } from "@app/components/FilterToolbar"; import { useDeleteJobFunctionMutation, useFetchJobFunctions, } from "@app/queries/jobfunctions"; import { NotificationsContext } from "@app/components/NotificationsContext"; - -const ENTITY_FIELD = "entity"; +import { SimplePagination } from "@app/components/SimplePagination"; +import { + TableHeaderContentWithControls, + ConditionalTableBody, + TableRowContentWithControls, +} from "@app/components/TableControls"; +import { useLocalTableControls } from "@app/hooks/table-controls"; +import { CubesIcon } from "@patternfly/react-icons"; +import { RBAC, RBAC_TYPE, controlsWriteScopes } from "@app/rbac"; export const JobFunctions: React.FC = () => { const { t } = useTranslation(); @@ -59,39 +55,56 @@ export const JobFunctions: React.FC = () => { const { jobFunctions, isFetching, fetchError, refetch } = useFetchJobFunctions(); - const filterCategories: FilterCategory[] = [ - { - categoryKey: "name", - title: t("terms.name"), - type: FilterType.search, - placeholderText: - t("actions.filterBy", { - what: t("terms.name").toLowerCase(), - }) + "...", - getItemValue: (item) => { - return item?.name || ""; - }, + const tableControls = useLocalTableControls({ + tableName: "job-functions-table", + idProperty: "name", + items: jobFunctions || [], + columnNames: { + name: t("terms.name"), }, - ]; - - const { filterValues, setFilterValues, filteredItems } = useLegacyFilterState( - jobFunctions || [], - filterCategories - ); - const getSortValues = (jobFunction: JobFunction) => [ - jobFunction.name || "", - "", // Action column - ]; - - const { sortBy, onSort, sortedItems } = useLegacySortState( - filteredItems, - getSortValues - ); + isFilterEnabled: true, + isSortEnabled: true, + isPaginationEnabled: true, + hasActionsColumn: true, + filterCategories: [ + { + categoryKey: "name", + title: t("terms.name"), + type: FilterType.search, + placeholderText: + t("actions.filterBy", { + what: t("terms.name").toLowerCase(), + }) + "...", + getItemValue: (item) => { + return item?.name || ""; + }, + }, + ], + initialItemsPerPage: 10, + sortableColumns: ["name"], + initialSort: { columnKey: "name", direction: "asc" }, + getSortValues: (item) => ({ + name: item?.name || "", + }), + isLoading: isFetching, + }); - const { currentPageItems, setPageNumber, paginationProps } = - useLegacyPaginationState(sortedItems, 10); + const { + currentPageItems, + numRenderedColumns, + propHelpers: { + toolbarProps, + filterToolbarProps, + paginationToolbarItemProps, + paginationProps, + tableProps, + getThProps, + getTrProps, + getTdProps, + }, + } = tableControls; - const onDeleteJobFunctionSuccess = (response: any) => { + const onDeleteJobFunctionSuccess = () => { pushNotification({ title: t("terms.jobFunctionDeleted"), variant: "success", @@ -110,57 +123,13 @@ export const JobFunctions: React.FC = () => { onDeleteJobFunctionError ); - const columns: ICell[] = [ - { - title: t("terms.name"), - transforms: [sortable, cellWidth(70)], - cellFormatters: [], - }, - { - title: "", - props: { - className: "pf-v5-u-text-align-right", - }, - }, - ]; - - const rows: IRow[] = []; - currentPageItems?.forEach((item) => { - rows.push({ - [ENTITY_FIELD]: item, - cells: [ - { - title: {item.name}, - }, - { - title: ( - setCreateUpdateModalState(item)} - onDelete={() => deleteRow(item)} - /> - ), - }, - ], - }); - }); - - // Rows - const deleteRow = (row: JobFunction) => { setJobFunctionToDelete(row); }; - // Advanced filters - - const handleOnClearAllFilters = () => { - setFilterValues({}); - }; - const closeCreateUpdateModal = () => { setCreateUpdateModalState(null); - refetch; + refetch(); }; return ( @@ -169,59 +138,101 @@ export const JobFunctions: React.FC = () => { when={isFetching && !(jobFunctions || fetchError)} then={} > - - } - toolbarActions={ - - - + + + + + - + {t("actions.createNew")} + + + + - - } - noDataState={ - - } - /> + + + + + + + + + + + + {t("composed.noDataStateTitle", { + what: t("terms.jobFunction").toLowerCase(), + })} + + + {t("composed.noDataStateBody", { + how: t("terms.create"), + what: t("terms.jobFunction").toLowerCase(), + })} + + + } + numRenderedColumns={numRenderedColumns} + > + + {currentPageItems?.map((jobFunction, rowIndex) => { + return ( + + + + setCreateUpdateModalState(jobFunction)} + onDelete={() => deleteRow(jobFunction)} + /> + + + ); + })} + + +
+ +
+ {jobFunction.name} +
+ + { const closeCreateUpdateModal = () => { setCreateUpdateModalState(null); - refetch; + refetch(); }; const tableControls = useLocalTableControls({ diff --git a/client/src/app/pages/controls/stakeholders/stakeholders.tsx b/client/src/app/pages/controls/stakeholders/stakeholders.tsx index b1948f93c1..9011223fdd 100644 --- a/client/src/app/pages/controls/stakeholders/stakeholders.tsx +++ b/client/src/app/pages/controls/stakeholders/stakeholders.tsx @@ -91,7 +91,7 @@ export const Stakeholders: React.FC = () => { const closeCreateUpdateModal = () => { setCreateUpdateModalState(null); - refetch; + refetch(); }; const tableControls = useLocalTableControls({ diff --git a/client/src/app/pages/controls/tags/tags.tsx b/client/src/app/pages/controls/tags/tags.tsx index 2a8e3d5c77..6fc73aa331 100644 --- a/client/src/app/pages/controls/tags/tags.tsx +++ b/client/src/app/pages/controls/tags/tags.tsx @@ -91,7 +91,7 @@ export const Tags: React.FC = () => { const onDeleteTagError = (error: AxiosError) => { if ( error.response?.status === 500 && - error.response?.data.error === "FOREIGN KEY constraint failed" + error.message === "FOREIGN KEY constraint failed" ) { pushNotification({ title: "Cannot delete a used tag", @@ -121,7 +121,7 @@ export const Tags: React.FC = () => { const onDeleteTagCategoryError = (error: AxiosError) => { if ( error.response?.status === 500 && - error.response?.data.error === "FOREIGN KEY constraint failed" + error.message === "FOREIGN KEY constraint failed" ) { pushNotification({ title: "Cannot delete a used tag", @@ -142,12 +142,12 @@ export const Tags: React.FC = () => { const closeTagCategoryModal = () => { setTagCategoryModalState(null); - refetch; + refetch(); }; const closeTagModal = () => { setTagModalState(null); - refetch; + refetch(); }; const { diff --git a/client/src/app/pages/external/jira/components/tracker-status.tsx b/client/src/app/pages/external/jira/components/tracker-status.tsx index ec94cb1153..88c463684e 100644 --- a/client/src/app/pages/external/jira/components/tracker-status.tsx +++ b/client/src/app/pages/external/jira/components/tracker-status.tsx @@ -13,7 +13,7 @@ import { Spinner, } from "@patternfly/react-core"; import ExclamationCircleIcon from "@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon"; -import { IconedStatus } from "@app/components/IconedStatus"; +import { IconedStatus } from "@app/components/Icons"; interface ITrackerStatusProps { name: string; diff --git a/client/src/app/pages/identities/components/identity-form/__tests__/identity-form.test.tsx b/client/src/app/pages/identities/components/identity-form/__tests__/identity-form.test.tsx index 73978806cf..3e316f4b28 100644 --- a/client/src/app/pages/identities/components/identity-form/__tests__/identity-form.test.tsx +++ b/client/src/app/pages/identities/components/identity-form/__tests__/identity-form.test.tsx @@ -6,18 +6,25 @@ import { fireEvent, } from "@app/test-config/test-utils"; -import { IDENTITIES } from "@app/api/rest"; -import mock from "@app/test-config/mockInstance"; - import { IdentityForm } from ".."; import "@testing-library/jest-dom"; +import { server } from "@mocks/server"; +import { rest } from "msw"; -const data: any[] = []; +describe("Component: identity-form", () => { + beforeAll(() => server.listen({ onUnhandledRequest: "bypass" })); -mock.onGet(`${IDENTITIES}`).reply(200, data); + afterEach(() => server.resetHandlers()); + afterAll(() => server.close()); -describe("Component: identity-form", () => { const mockChangeValue = jest.fn(); + const data: any = []; + + server.use( + rest.get("*", (req, res, ctx) => { + return res(ctx.json(data)); + }) + ); it("Display form on initial load", async () => { render(); @@ -176,7 +183,7 @@ describe("Component: identity-form", () => { expect(createButton).toBeDisabled(); }); - it.skip("Identity form validation test - source - key upload", async () => { + it("Identity form validation test - source - key upload", async () => { render(); const identityNameInput = await screen.findByLabelText("Name *"); @@ -231,7 +238,7 @@ describe("Component: identity-form", () => { expect(createButton).toBeEnabled(); }); - it.skip("Identity form validation test - maven", async () => { + it("Identity form validation test - maven", async () => { render(); const identityNameInput = await screen.findByLabelText("Name *"); diff --git a/client/src/app/pages/identities/identities.tsx b/client/src/app/pages/identities/identities.tsx index ec42cfe090..b10f14ca52 100644 --- a/client/src/app/pages/identities/identities.tsx +++ b/client/src/app/pages/identities/identities.tsx @@ -11,27 +11,19 @@ import { Text, Modal, ModalVariant, + EmptyState, + EmptyStateBody, + EmptyStateIcon, + Title, + Toolbar, + ToolbarContent, } from "@patternfly/react-core"; -import { - cellWidth, - expandable, - ICell, - IRow, - sortable, - TableText, -} from "@patternfly/react-table"; +import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table"; import { Identity, ITypeOptions } from "@app/api/models"; -import { useLegacyFilterState } from "@app/hooks/useLegacyFilterState"; -import { useLegacyPaginationState } from "@app/hooks/useLegacyPaginationState"; -import { useLegacySortState } from "@app/hooks/useLegacySortState"; import { AxiosError } from "axios"; import { getAxiosErrorMessage } from "@app/utils/utils"; -import { - FilterCategory, - FilterToolbar, - FilterType, -} from "@app/components/FilterToolbar"; +import { FilterToolbar, FilterType } from "@app/components/FilterToolbar"; import { useDeleteIdentityMutation, useFetchIdentities, @@ -41,16 +33,19 @@ import { NotificationsContext } from "@app/components/NotificationsContext"; import { IdentityForm } from "./components/identity-form"; import { validateXML } from "./components/identity-form/validateXML"; import { useFetchTrackers } from "@app/queries/trackers"; -import { isAuthRequired } from "@app/Constants"; import { AppTableActionButtons } from "@app/components/AppTableActionButtons"; import { ConditionalRender } from "@app/components/ConditionalRender"; import { AppPlaceholder } from "@app/components/AppPlaceholder"; -import { AppTableWithControls } from "@app/components/AppTableWithControls"; -import { NoDataEmptyState } from "@app/components/NoDataEmptyState"; import { ConfirmDialog } from "@app/components/ConfirmDialog"; import { useFetchTargets } from "@app/queries/targets"; - -const ENTITY_FIELD = "entity"; +import { SimplePagination } from "@app/components/SimplePagination"; +import { + TableHeaderContentWithControls, + ConditionalTableBody, + TableRowContentWithControls, +} from "@app/components/TableControls"; +import { useLocalTableControls } from "@app/hooks/table-controls"; +import { CubesIcon } from "@patternfly/react-icons"; export const Identities: React.FC = () => { const { t } = useTranslation(); @@ -108,89 +103,6 @@ export const Identities: React.FC = () => { { key: "basic-auth", value: "Basic Auth (Jira)" }, { key: "bearer", value: "Bearer Token (Jira)" }, ]; - const filterCategories: FilterCategory< - Identity, - "name" | "type" | "createdBy" - >[] = [ - { - categoryKey: "name", - title: "Name", - type: FilterType.search, - placeholderText: "Filter by name...", - getItemValue: (item) => { - return item?.name || ""; - }, - }, - { - categoryKey: "type", - title: "Type", - type: FilterType.select, - placeholderText: "Filter by type...", - selectOptions: typeOptions.map(({ key, value }) => ({ - value: key, - label: value, - })), - getItemValue: (item) => { - return item.kind || ""; - }, - }, - ...(isAuthRequired - ? [ - { - categoryKey: "createdBy", - title: "Created By", - type: FilterType.search, - placeholderText: "Filter by created by User...", - getItemValue: (item: Identity) => { - return item.createUser || ""; - }, - } as const, - ] - : []), - ]; - - const { filterValues, setFilterValues, filteredItems } = useLegacyFilterState( - identities || [], - filterCategories - ); - const getSortValues = (identity: Identity) => [ - identity?.name || "", - "", // description column - identity?.kind || "", - ...((isAuthRequired && identity?.createUser) || ""), - - "", // Action column - ]; - const { sortBy, onSort, sortedItems } = useLegacySortState( - filteredItems, - getSortValues - ); - - const { currentPageItems, setPageNumber, paginationProps } = - useLegacyPaginationState(sortedItems, 10); - - const columns: ICell[] = [ - { - title: "Name", - transforms: [sortable, cellWidth(20)], - cellFormatters: [expandable], - }, - { title: "Description", transforms: [cellWidth(25)] }, - { title: "Type", transforms: [sortable, cellWidth(20)] }, - ...(isAuthRequired - ? [{ title: "Created by", transforms: [sortable, cellWidth(10)] }] - : []), - { - title: "", - props: { - className: "pf-v5-u-text-align-right", - }, - }, - ]; - - const handleOnClearAllFilters = () => { - setFilterValues({}); - }; const getBlockDeleteMessage = (item: Identity) => { if (trackers.some((tracker) => tracker?.identity?.id === item.id)) { @@ -218,65 +130,6 @@ export const Identities: React.FC = () => { } }; - const rows: IRow[] = []; - currentPageItems?.forEach((item: Identity) => { - const typeFormattedString = typeOptions.find( - (type) => type.key === item.kind - ); - rows.push({ - [ENTITY_FIELD]: item, - cells: [ - { - title: {item.name}, - }, - { - title: ( - {item.description} - ), - }, - { - title: ( - - {typeFormattedString?.value} - - ), - }, - ...(isAuthRequired - ? [ - { - title: ( - - {item.createUser} - - ), - }, - ] - : []), - { - title: ( - tracker?.identity?.id === item.id) || - applications?.some( - (app) => app?.identities?.some((id) => id.id === item.id) - ) || - targets?.some( - (target) => target?.ruleset?.identity?.id === item.id - ) - } - tooltipMessage={getBlockDeleteMessage(item)} - onEdit={() => setCreateUpdateModalState(item)} - onDelete={() => { - setIdentityToDelete(item); - setIsConfirmDialogOpen(true); - }} - /> - ), - }, - ], - }); - }); - const dependentApplications = React.useMemo(() => { if (identityToDelete) { const res = applications?.filter( @@ -288,6 +141,89 @@ export const Identities: React.FC = () => { return []; }, [applications, identityToDelete]); + const tableControls = useLocalTableControls({ + tableName: "identities-table", + idProperty: "name", + items: identities, + columnNames: { + name: t("terms.name"), + description: t("terms.description"), + type: t("terms.type"), + + //TODO: Enable conditional rendering for createdBy column + // ...(isAuthRequired && { createdBy: t("terms.createdBy") } + // ? { createdBy: t("terms.createdBy") } + // : {}), + }, + isFilterEnabled: true, + isSortEnabled: true, + isPaginationEnabled: true, + hasActionsColumn: true, + filterCategories: [ + { + categoryKey: "name", + title: "Name", + type: FilterType.search, + placeholderText: "Filter by name...", + getItemValue: (item) => { + return item?.name || ""; + }, + }, + { + categoryKey: "type", + title: "Type", + type: FilterType.select, + placeholderText: "Filter by type...", + selectOptions: typeOptions.map(({ key, value }) => ({ + value: key, + label: value, + })), + getItemValue: (item) => { + return item.kind || ""; + }, + }, + //TODO: Enable conditional rendering for createdBy column + // ...(isAuthRequired + // ? [ + // { + // categoryKey: "createdBy", + // title: "Created By", + // type: FilterType.search, + // placeholderText: "Filter by created by User...", + // getItemValue: (item: Identity) => { + // return item.createUser || ""; + // }, + // } as const, + // ] + // : []), + ], + initialItemsPerPage: 10, + sortableColumns: ["name", "type"], + initialSort: { columnKey: "name", direction: "asc" }, + getSortValues: (item) => ({ + name: item?.name || "", + type: item?.kind || "", + //TODO: Enable conditional rendering for createdBy column + // ...(isAuthRequired && { createdBy: item?.createUser } ? { createdBy: item?.createUser } : {}), + }), + isLoading: isFetching, + }); + + const { + currentPageItems, + numRenderedColumns, + propHelpers: { + toolbarProps, + filterToolbarProps, + paginationToolbarItemProps, + paginationProps, + tableProps, + getThProps, + getTrProps, + getTdProps, + }, + } = tableControls; + return ( <> @@ -300,51 +236,155 @@ export const Identities: React.FC = () => { when={isFetching && !(identities || fetchErrorIdentities)} then={} > - - - +
+ + + + + + + + + + - - } - noDataState={ - - } - paginationProps={paginationProps} - paginationIdPrefix="identities" - toolbarToggle={ - - } - /> + + + + + + + + + + + + {t("composed.noDataStateTitle", { + what: t("terms.credential").toLowerCase(), + })} + + + {t("composed.noDataStateBody", { + how: t("terms.create"), + what: t("terms.credential").toLowerCase(), + })} + + + } + numRenderedColumns={numRenderedColumns} + > + + {currentPageItems?.map((identity, rowIndex) => { + const typeFormattedString = typeOptions.find( + (type) => type.key === identity.kind + ); + return ( + + + + + + {/* + Todo: Enable conditional rendering for createdBy column + */} + + tracker?.identity?.id === identity.id + ) || + applications?.some( + (app) => + app?.identities?.some( + (id) => id.id === identity.id + ) + ) || + targets?.some( + (target) => + target?.ruleset?.identity?.id === identity.id + ) + } + tooltipMessage={getBlockDeleteMessage(identity)} + onEdit={() => setCreateUpdateModalState(identity)} + onDelete={() => { + setIdentityToDelete(identity); + setIsConfirmDialogOpen(true); + }} + /> + + + ); + })} + + +
+ + + + {/* + //TODO: Enable conditional rendering for createdBy column + */} + +
+ {identity.name} + + {identity.description} + + {typeFormattedString?.value} + + {identity.createdBy} +
+ +
{ + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + server.resetHandlers(); + }); + server.use( + rest.get("/hub/identities", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json([ + { id: 0, name: "proxy-cred", kind: "proxy" }, + { id: 1, name: "maven-cred", kind: "maven" }, + { id: 2, name: "source-cred", kind: "source" }, + ]) + ); + }) + ); + it("Display switch statements on initial load", async () => { render(); await screen.findByLabelText("HTTP proxy"); @@ -49,7 +39,7 @@ describe("Component: proxy-form", () => { await screen.findByLabelText("HTTPS proxy"); }); - it.skip("Show HTTP proxy form when switch button clicked", async () => { + it("Show HTTP proxy form when switch button clicked", async () => { render(); const httpProxySwitch = await screen.findByLabelText("HTTP proxy"); @@ -62,7 +52,7 @@ describe("Component: proxy-form", () => { ); }); - it.skip("Show HTTPS proxy form when switch button clicked", async () => { + it("Show HTTPS proxy form when switch button clicked", async () => { render(); const httpsProxySwitch = await screen.findByLabelText("HTTPS proxy"); @@ -75,14 +65,19 @@ describe("Component: proxy-form", () => { ); }); - it.skip("Select http proxy identity", async () => { - const identitiesData: Identity[] = [ - { id: 0, name: "proxy-cred", kind: "proxy" }, - { id: 1, name: "maven-cred", kind: "maven" }, - { id: 2, name: "source-cred", kind: "source" }, - ]; - - mock.onGet(`${IDENTITIES}`).reply(200, identitiesData); + it("Select http proxy identity", async () => { + server.use( + rest.get("/hub/identities", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json([ + { id: 0, name: "proxy-cred", kind: "proxy" }, + { id: 1, name: "maven-cred", kind: "maven" }, + { id: 2, name: "source-cred", kind: "source" }, + ]) + ); + }) + ); render(); const httpProxySwitch = await screen.findByLabelText("HTTP proxy"); @@ -112,14 +107,19 @@ describe("Component: proxy-form", () => { expect(sourceCred).toBeNull(); // it doesn't exist }); - it.skip("Select https proxy identity", async () => { - const identitiesData: Identity[] = [ - { id: 0, name: "proxy-cred", kind: "proxy" }, - { id: 1, name: "maven-cred", kind: "maven" }, - { id: 2, name: "source-cred", kind: "source" }, - ]; - - mock.onGet(`${IDENTITIES}`).reply(200, identitiesData); + it("Select https proxy identity", async () => { + server.use( + rest.get("/hub/identities", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json([ + { id: 0, name: "proxy-cred", kind: "proxy" }, + { id: 1, name: "maven-cred", kind: "maven" }, + { id: 2, name: "source-cred", kind: "source" }, + ]) + ); + }) + ); render(); const httpsProxySwitch = await screen.findByLabelText("HTTPS proxy"); diff --git a/client/src/app/pages/review/components/application-assessment-summary-table/application-assessment-summary-table.tsx b/client/src/app/pages/review/components/application-assessment-summary-table/application-assessment-summary-table.tsx deleted file mode 100644 index 5503be418b..0000000000 --- a/client/src/app/pages/review/components/application-assessment-summary-table/application-assessment-summary-table.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import React, { useMemo } from "react"; -import { useTranslation } from "react-i18next"; -import { - cellWidth, - ICell, - IRow, - sortable, - TableText, -} from "@patternfly/react-table"; - -import { AppTableWithControls } from "@app/components/AppTableWithControls"; -import { Assessment, ITypeOptions, Question, Section } from "@app/api/models"; - -import { useLegacyPaginationState } from "@app/hooks/useLegacyPaginationState"; -import { useLegacySortState } from "@app/hooks/useLegacySortState"; -import { - FilterCategory, - FilterToolbar, - FilterType, -} from "@app/components/FilterToolbar/FilterToolbar"; -import { useLegacyFilterState } from "@app/hooks/useLegacyFilterState"; -import { useFetchQuestionnaires } from "@app/queries/questionnaires"; - -interface ITableItem { - answerValue: string; - //TODO: fix this once the API is updated - // riskValue: Risk; - riskValue: string; - section: Section; - question: Question; -} - -export interface IApplicationAssessmentSummaryTableProps { - assessment: Assessment; -} - -export const ApplicationAssessmentSummaryTable: React.FC< - IApplicationAssessmentSummaryTableProps -> = ({ assessment }) => { - const { t } = useTranslation(); - const { questionnaires } = useFetchQuestionnaires(); - - const matchingQuestionnaire = questionnaires.find( - (questionnaire) => questionnaire.id === assessment?.questionnaire?.id - ); - if (!matchingQuestionnaire) { - return null; - } - - const tableItems: ITableItem[] = useMemo(() => { - return matchingQuestionnaire.sections - .slice(0) - .map((section) => { - const result: ITableItem[] = section.questions.map((question) => { - const checkedOption = question.answers.find( - (q) => q.selected === true - ); - const item: ITableItem = { - answerValue: checkedOption ? checkedOption.text : "", - riskValue: checkedOption ? checkedOption.risk : "unknown", - section, - question, - }; - return item; - }); - return result; - }) - .flatMap((f) => f) - .sort((a, b) => { - if (a.section.order !== b.section.order) { - return a.section.order - b.section.order; - } else { - return a.question.order - b.question.order; - } - }); - }, [assessment]); - const typeOptions: Array = [ - { key: "green", value: "Low" }, - { key: "yellow", value: "Medium" }, - { key: "red", value: "High" }, - { key: "unknown", value: "Unknown" }, - ]; - - const filterCategories: FilterCategory[] = [ - { - categoryKey: "riskValue", - title: "Risk", - type: FilterType.select, - placeholderText: "Filter by name...", - getItemValue: (item) => { - return item.riskValue || ""; - }, - selectOptions: typeOptions.map(({ key, value }) => ({ - value: key, - label: value, - })), - }, - ]; - - const { filterValues, setFilterValues, filteredItems } = useLegacyFilterState( - tableItems || [], - filterCategories - ); - const getSortValues = (tableItem: ITableItem) => [ - tableItem.section.name || "", - tableItem.question.text || "", - tableItem.answerValue || "", - //TODO: fix this once the API is updated - // RISK_LIST[tableItem.riskValue].sortFactor || "", - ]; - - const { sortBy, onSort, sortedItems } = useLegacySortState( - filteredItems, - getSortValues - ); - const handleOnClearAllFilters = () => { - setFilterValues({}); - }; - - const { currentPageItems, setPageNumber, paginationProps } = - useLegacyPaginationState(sortedItems, 10); - - const columns: ICell[] = [ - { - title: t("terms.category"), - transforms: [cellWidth(20)], - cellFormatters: [], - }, - { - title: t("terms.question"), - transforms: [cellWidth(35)], - cellFormatters: [], - }, - { - title: t("terms.answer"), - transforms: [cellWidth(35)], - cellFormatters: [], - }, - { - title: t("terms.risk"), - transforms: [cellWidth(10), sortable], - cellFormatters: [], - }, - ]; - - const rows: IRow[] = []; - currentPageItems.forEach((item) => { - rows.push({ - cells: [ - { - title: ( - {item.section.name} - ), - }, - { - title: ( - {item.question.text} - ), - }, - { - title: ( - {item.answerValue} - ), - }, - { - // title: , - title: "Risk component will go here", - }, - ], - }); - }); - - return ( - - } - /> - ); -}; diff --git a/client/src/app/pages/review/components/application-assessment-summary-table/components/select-risk-filter/index.ts b/client/src/app/pages/review/components/application-assessment-summary-table/components/select-risk-filter/index.ts deleted file mode 100644 index 08f8433fa2..0000000000 --- a/client/src/app/pages/review/components/application-assessment-summary-table/components/select-risk-filter/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { SelectRiskFilter } from "./select-risk-filter"; diff --git a/client/src/app/pages/review/components/application-assessment-summary-table/components/select-risk-filter/select-risk-filter.tsx b/client/src/app/pages/review/components/application-assessment-summary-table/components/select-risk-filter/select-risk-filter.tsx deleted file mode 100644 index 761118b943..0000000000 --- a/client/src/app/pages/review/components/application-assessment-summary-table/components/select-risk-filter/select-risk-filter.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; - -import { ToolbarChip } from "@patternfly/react-core"; -import { SelectVariant } from "@patternfly/react-core/deprecated"; -import FilterIcon from "@patternfly/react-icons/dist/esm/icons/filter-icon"; - -import { OptionWithValue, SimpleSelect } from "@app/components/SimpleSelect"; - -import { RISK_LIST, DEFAULT_SELECT_MAX_HEIGHT } from "@app/Constants"; -import { Risk } from "@app/api/models"; -import { getToolbarChipKey } from "@app/utils/utils"; - -export interface ISelectRiskFilterProps { - value?: ToolbarChip[]; - onChange: (values: ToolbarChip[]) => void; -} - -export const SelectRiskFilter: React.FC = ({ - value = [], - onChange, -}) => { - const { t } = useTranslation(); - - const riskToToolbarChip = (value: Risk): ToolbarChip => { - const risk = RISK_LIST[value]; - const label: string = risk ? t(risk.i18Key) : value; - - return { - key: value, - node: label, - }; - }; - - const riskToOption = (value: Risk): OptionWithValue => { - const risk = RISK_LIST[value]; - const label = risk ? t(risk.i18Key) : value; - - return { - value, - toString: () => label, - compareTo: (selectOption: any) => { - // If "string" we are just filtering - if (typeof selectOption === "string") { - return label.toLowerCase().includes(selectOption.toLowerCase()); - } - // If not "string" we are selecting a checkbox - else { - return ( - selectOption.value && - (selectOption as OptionWithValue).value === value - ); - } - }, - }; - }; - - return ( - } - width={220} - variant={SelectVariant.checkbox} - aria-label="risk" - aria-labelledby="risk" - placeholderText={t("terms.risk")} - maxHeight={DEFAULT_SELECT_MAX_HEIGHT} - value={value - .map((a) => { - const risks: Risk[] = Object.keys(RISK_LIST) as Risk[]; - return risks.find((b) => getToolbarChipKey(a) === b); - }) - .filter((f) => f !== undefined) - .map((f) => riskToOption(f!))} - options={Object.keys(RISK_LIST).map((k) => riskToOption(k as Risk))} - onChange={(option) => { - const optionValue = (option as OptionWithValue).value; - - const elementExists = value.some( - (f) => getToolbarChipKey(f) === optionValue - ); - let newIds: ToolbarChip[]; - if (elementExists) { - newIds = value.filter((f) => getToolbarChipKey(f) !== optionValue); - } else { - newIds = [...value, riskToToolbarChip(optionValue)]; - } - - onChange(newIds); - }} - hasInlineFilter - onClear={() => onChange([])} - /> - ); -}; diff --git a/client/src/app/pages/review/components/application-assessment-summary-table/index.ts b/client/src/app/pages/review/components/application-assessment-summary-table/index.ts deleted file mode 100644 index e92ad6acb4..0000000000 --- a/client/src/app/pages/review/components/application-assessment-summary-table/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ApplicationAssessmentSummaryTable } from "./application-assessment-summary-table"; diff --git a/client/src/app/test-config/mockInstance.ts b/client/src/app/test-config/mockInstance.ts deleted file mode 100644 index f614569e8a..0000000000 --- a/client/src/app/test-config/mockInstance.ts +++ /dev/null @@ -1,4 +0,0 @@ -import MockAdapter from "axios-mock-adapter"; -import axios from "axios"; - -export default new MockAdapter(axios); diff --git a/client/src/app/utils/utils.test.ts b/client/src/app/utils/utils.test.ts index 99c634afa3..1cd13176c9 100644 --- a/client/src/app/utils/utils.test.ts +++ b/client/src/app/utils/utils.test.ts @@ -22,7 +22,6 @@ describe("utils", () => { isAxiosError: true, name: "error", message: errorMsg, - config: {}, toJSON: () => ({}), }; @@ -30,32 +29,6 @@ describe("utils", () => { expect(errorMessage).toBe(errorMsg); }); - it("getAxiosErrorMessage: should pick body message", () => { - const errorMsg = "Internal server error"; - - const mockAxiosError: AxiosError = { - isAxiosError: true, - name: "error", - message: "Network error", - config: {}, - response: { - data: { - errorMessage: errorMsg, - }, - status: 400, - statusText: "", - headers: {}, - config: {}, - }, - toJSON: () => ({}), - }; - - const errorMessage = getAxiosErrorMessage(mockAxiosError); - expect(errorMessage).toBe(errorMsg); - }); - - // getValidatedFromError - it("getValidatedFromError: given value should return 'error'", () => { const error = "Any value"; diff --git a/client/src/app/utils/utils.ts b/client/src/app/utils/utils.ts index 77fbc46901..a9c91d18a1 100644 --- a/client/src/app/utils/utils.ts +++ b/client/src/app/utils/utils.ts @@ -6,19 +6,10 @@ import { Paths } from "@app/Paths"; // Axios error export const getAxiosErrorMessage = (axiosError: AxiosError) => { - if ( - axiosError.response && - axiosError.response.data && - axiosError.response.data.errorMessage - ) { - return axiosError.response.data.errorMessage; - } else if ( - axiosError.response?.data?.error && - typeof axiosError?.response?.data?.error === "string" - ) { - return axiosError?.response?.data?.error; - } else { + if (axiosError.response && axiosError.response.data && axiosError.message) { return axiosError.message; + } else { + return "Network error"; } }; diff --git a/package-lock.json b/package-lock.json index 997e0ab6dd..aa11c07a68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,7 @@ "@react-keycloak/web": "^3.4.0", "@tanstack/react-query": "^4.22.0", "@tanstack/react-query-devtools": "^4.22.0", - "axios": "^0.21.2", + "axios": "^1.6.8", "dayjs": "^1.11.7", "ejs": "^3.1.7", "fast-xml-parser": "^4.0.3", @@ -110,7 +110,6 @@ "@types/react-measure": "^2.0.12", "@types/react-router-dom": "^5.1.7", "@types/tinycolor2": "^1.4.6", - "axios-mock-adapter": "^1.19.0", "browserslist": "^4.19.1", "case-sensitive-paths-webpack-plugin": "^2.4.0", "copy-webpack-plugin": "^12.0.2", @@ -141,6 +140,16 @@ "webpack-merge": "^5.10.0" } }, + "client/node_modules/axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "common": { "name": "@konveyor-ui/common", "version": "0.1.0", @@ -3777,8 +3786,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/attr-accept": { "version": "2.2.2", @@ -3804,23 +3812,11 @@ "version": "0.21.4", "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "peer": true, "dependencies": { "follow-redirects": "^1.14.0" } }, - "node_modules/axios-mock-adapter": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.22.0.tgz", - "integrity": "sha512-dmI0KbkyAhntUR05YY96qg2H6gg0XMl2+qTW0xmYg6Up+BFBAJYRLROMXRdDEL06/Wqwa0TJThAYvFtSFdRCZw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "is-buffer": "^2.0.5" - }, - "peerDependencies": { - "axios": ">= 0.17.0" - } - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -4715,7 +4711,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -6118,7 +6113,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -7698,7 +7692,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -13473,6 +13466,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",