diff --git a/.github/workflows/ci-global.yml b/.github/workflows/ci-global.yml index f6d8d6d618..3497b0acc9 100644 --- a/.github/workflows/ci-global.yml +++ b/.github/workflows/ci-global.yml @@ -6,6 +6,10 @@ on: - "main" pull_request: + paths-ignore: + - "docs/**" + - "hack/**" + - "*.md" branches: - "main" diff --git a/.github/workflows/ci-image-build.yml b/.github/workflows/ci-image-build.yml index 6199750d93..6a165b27df 100644 --- a/.github/workflows/ci-image-build.yml +++ b/.github/workflows/ci-image-build.yml @@ -2,6 +2,10 @@ name: CI (test image build for a PR with build related changes) on: pull_request: + paths-ignore: + - "docs/**" + - "hack/**" + - "*.md" branches: - "main" - "release-*" diff --git a/.github/workflows/ci-repo.yml b/.github/workflows/ci-repo.yml index 9920661914..3089ef5174 100644 --- a/.github/workflows/ci-repo.yml +++ b/.github/workflows/ci-repo.yml @@ -20,10 +20,12 @@ concurrency: cancel-in-progress: true jobs: - unit-test-lookup-image: + unit-test-lookup: runs-on: ubuntu-latest outputs: builder-image: ${{ steps.grepBuilder.outputs.builder }} + should-test: ${{ steps.check-changes.outputs.should-test }} + steps: - uses: actions/checkout@v4 @@ -31,15 +33,64 @@ jobs: id: grepBuilder run: | builder=$(grep 'as builder' Dockerfile | sed -e 's/^FROM \(.*\) as builder$/\1/') - echo "Builder image: \`$builder\`" >> "$GITHUB_STEP_SUMMARY" echo "builder=$builder" >> "$GITHUB_OUTPUT" + - name: Did docs and hacks change? + id: docs-and-hacks + uses: tj-actions/changed-files@v44 + with: + files: | + docs/** + hack/** + *.md + + - name: Check if only docs and hacks changes have been made in a PR + id: check-changes + env: + IS_PR: ${{ !!github.event.pull_request }} + ONLY_DOCS: ${{ steps.docs-and-hacks.outputs.only_modified }} + run: | + SHOULD_TEST=$( + if [[ $IS_PR == true ]] && [[ $ONLY_DOCS == true ]]; then + echo "false" + else + echo "true" + fi + ) + + echo "is-pr=$IS_PR" >> "$GITHUB_OUTPUT" + echo "changes_only_docs=${ONLY_DOCS:-false}" >> "$GITHUB_OUTPUT" + echo "should-test=$SHOULD_TEST" >> "$GITHUB_OUTPUT" + + - name: Summarize findings + env: + ONLY_DOCS: ${{ steps.docs-and-hacks.outputs.only_modified }} + MODIFIED_FILES: ${{ steps.docs-and-hacks.outputs.all_modified_files }} + run: | + cat >> "$GITHUB_STEP_SUMMARY" <> "$GITHUB_STEP_SUMMARY" + for file in ${MODIFIED_FILES}; do + echo " - \`$file\`" >> "$GITHUB_STEP_SUMMARY" + done + fi + unit-test: runs-on: ubuntu-latest - needs: unit-test-lookup-image + needs: unit-test-lookup + if: ${{ needs.unit-test-lookup.outputs.should-test == 'true' }} # Use the same container as the Dockerfile's "FROM * as builder" - container: ${{ needs.unit-test-lookup-image.outputs.builder-image }} + container: ${{ needs.unit-test-lookup.outputs.builder-image }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/image-build.yaml b/.github/workflows/image-build.yaml index 7bcbcc5fff..22b3ab25be 100644 --- a/.github/workflows/image-build.yaml +++ b/.github/workflows/image-build.yaml @@ -17,8 +17,8 @@ jobs: image-build: uses: konveyor/release-tools/.github/workflows/build-push-images.yaml@main with: - registry: "quay.io/konveyor" - image_name: "tackle2-ui" + registry: ${{ vars.IMAGE_BUILD_REGISTRY || 'quay.io/konveyor' }} + image_name: ${{ vars.IMAGE_BUILD_IMAGE_NAME || 'tackle2-ui' }} containerfile: "./Dockerfile" # keep the architectures in sync with `ci-image-build.yml` diff --git a/client/src/app/components/FilterToolbar/SelectFilterControl.tsx b/client/src/app/components/FilterToolbar/SelectFilterControl.tsx index e33eb5083f..b1b6bf4183 100644 --- a/client/src/app/components/FilterToolbar/SelectFilterControl.tsx +++ b/client/src/app/components/FilterToolbar/SelectFilterControl.tsx @@ -5,6 +5,7 @@ import { Select, SelectList, SelectOption, + ToolbarChip, ToolbarFilter, } from "@patternfly/react-core"; import { IFilterControlProps } from "./FilterControl"; @@ -56,8 +57,9 @@ export const SelectFilterControl = ({ setIsFilterDropdownOpen(false); }; - const onFilterClear = (chip: string) => { - const newValue = filterValue?.filter((val) => val !== chip); + const onFilterClear = (chip: string | ToolbarChip) => { + const chipValue = typeof chip === "string" ? chip : chip.key; + const newValue = filterValue?.filter((val) => val !== chipValue); setFilterValue(newValue?.length ? newValue : null); }; @@ -90,7 +92,7 @@ export const SelectFilterControl = ({ onFilterClear(chip as string)} + deleteChip={(_, chip) => onFilterClear(chip)} categoryName={category.title} showToolbarItem={showToolbarItem} > diff --git a/client/src/app/components/InfiniteScroller/InfiniteScroller.tsx b/client/src/app/components/InfiniteScroller/InfiniteScroller.tsx index 9e4ddfbc69..eb990f39a9 100644 --- a/client/src/app/components/InfiniteScroller/InfiniteScroller.tsx +++ b/client/src/app/components/InfiniteScroller/InfiniteScroller.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useEffect, useRef } from "react"; +import React, { ReactNode, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useVisibilityTracker } from "./useVisibilityTracker"; import "./InfiniteScroller.css"; @@ -12,6 +12,7 @@ export interface InfiniteScrollerProps { hasMore: boolean; // number of items currently displayed/known itemCount: number; + pageSize: number; } export const InfiniteScroller = ({ @@ -19,40 +20,38 @@ export const InfiniteScroller = ({ fetchMore, hasMore, itemCount, + pageSize, }: InfiniteScrollerProps) => { const { t } = useTranslation(); - // Track how many items were known at time of triggering the fetch. - // This allows to detect edge case when second(or more) fetchMore() is triggered before - // IntersectionObserver is able to detect out-of-view event. - // Initializing with zero ensures that the effect will be triggered immediately - // (parent is expected to display empty state until some items are available). - const itemCountRef = useRef(0); + const [readyForFetch, setReadyForFetch] = useState(false); const { visible: isSentinelVisible, nodeRef: sentinelRef } = useVisibilityTracker({ enable: hasMore, }); + useEffect(() => { + // enable or clear the flag depending on the sentinel visibility + setReadyForFetch(!!isSentinelVisible); + }, [isSentinelVisible]); - useEffect( - () => { - if ( - isSentinelVisible && - itemCountRef.current !== itemCount && - fetchMore() // fetch may be blocked if background refresh is in progress (or other manual fetch) - ) { - // fetchMore call was triggered (it may fail but will be subject to React Query retry policy) - itemCountRef.current = itemCount; - } - }, + useEffect(() => { + if (readyForFetch) { + // clear the flag if fetch request is accepted + setReadyForFetch(!fetchMore()); + } // reference to fetchMore() changes based on query state and ensures that the effect is triggered in the right moment // i.e. after fetch triggered by the previous fetchMore() call finished - [isSentinelVisible, fetchMore, itemCount] - ); + }, [fetchMore, readyForFetch]); return (
{children} {hasMore && ( -
+
{t("message.loadingTripleDot")}
)} diff --git a/client/src/app/components/InfiniteScroller/useVisibilityTracker.tsx b/client/src/app/components/InfiniteScroller/useVisibilityTracker.tsx index c6f8135b24..c30b42d7d0 100644 --- a/client/src/app/components/InfiniteScroller/useVisibilityTracker.tsx +++ b/client/src/app/components/InfiniteScroller/useVisibilityTracker.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState, useCallback } from "react"; export function useVisibilityTracker({ enable }: { enable: boolean }) { const nodeRef = useRef(null); - const [visible, setVisible] = useState(false); + const [visible, setVisible] = useState(false); const node = nodeRef.current; // state is set from IntersectionObserver callbacks which may not align with React lifecycle @@ -22,14 +22,19 @@ export function useVisibilityTracker({ enable }: { enable: boolean }) { }, []); useEffect(() => { + if (enable && !node) { + // use falsy value different than initial value - state change will trigger render() + // otherwise we need to wait for the next render() to read node ref + setVisibleSafe(undefined); + return undefined; + } + if (!enable || !node) { return undefined; } // Observer with default options - the whole view port used. // Note that if root element is used then it needs to be the ancestor of the target. - // In case of infinite scroller the target is always within the (scrollable!)parent - // even if the node is technically hidden from the user. // https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#root const observer = new IntersectionObserver( (entries: IntersectionObserverEntry[]) => diff --git a/client/src/app/components/task-manager/TaskManagerDrawer.tsx b/client/src/app/components/task-manager/TaskManagerDrawer.tsx index 4a1faf69fd..10824bc16e 100644 --- a/client/src/app/components/task-manager/TaskManagerDrawer.tsx +++ b/client/src/app/components/task-manager/TaskManagerDrawer.tsx @@ -66,7 +66,8 @@ interface TaskManagerDrawerProps { export const TaskManagerDrawer: React.FC = forwardRef( (_props, ref) => { const { isExpanded, setIsExpanded, queuedCount } = useTaskManagerContext(); - const { tasks, hasNextPage, fetchNextPage } = useTaskManagerData(); + const { tasks, hasNextPage, fetchNextPage, pageSize } = + useTaskManagerData(); const [expandedItems, setExpandedItems] = useState([]); const [taskWithExpandedActions, setTaskWithExpandedAction] = useState< @@ -106,6 +107,7 @@ export const TaskManagerDrawer: React.FC = forwardRef( fetchMore={fetchNextPage} hasMore={hasNextPage} itemCount={tasks?.length ?? 0} + pageSize={pageSize} > {tasks.map((task) => ( @@ -282,6 +284,7 @@ const useTaskManagerData = () => { [data] ); + // note that the callback will change when query fetching state changes const fetchMore = useCallback(() => { // forced fetch is not allowed when background fetch or other forced fetch is in progress if (!isFetching && !isFetchingNextPage) { @@ -297,6 +300,6 @@ const useTaskManagerData = () => { isFetching, hasNextPage, fetchNextPage: fetchMore, - isReadyToFetch: !isFetching && !isFetchingNextPage, + pageSize: PAGE_SIZE, }; }; diff --git a/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts b/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts index f0e3f3626f..54988da75d 100644 --- a/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts +++ b/client/src/app/hooks/table-controls/active-item/useActiveItemState.ts @@ -1,5 +1,5 @@ import { parseMaybeNumericString } from "@app/utils/utils"; -import { IFeaturePersistenceArgs } from "../types"; +import { IFeaturePersistenceArgs, isPersistenceProvider } from "../types"; import { usePersistentState } from "@app/hooks/usePersistentState"; /** @@ -76,7 +76,13 @@ export const useActiveItemState = < persistTo, key: "activeItem", } - : { persistTo }), + : isPersistenceProvider(persistTo) + ? { + persistTo: "provider", + serialize: persistTo.write, + deserialize: () => persistTo.read() as string | number | null, + } + : { persistTo: "state" }), }); return { activeItemId, setActiveItemId }; }; diff --git a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts index ac3a873bbf..848e62d12b 100644 --- a/client/src/app/hooks/table-controls/expansion/useExpansionState.ts +++ b/client/src/app/hooks/table-controls/expansion/useExpansionState.ts @@ -1,6 +1,6 @@ import { usePersistentState } from "@app/hooks/usePersistentState"; import { objectKeys } from "@app/utils/utils"; -import { IFeaturePersistenceArgs } from "../types"; +import { IFeaturePersistenceArgs, isPersistenceProvider } from "../types"; import { DiscriminatedArgs } from "@app/utils/type-utils"; /** @@ -93,7 +93,9 @@ export const useExpansionState = < ? { persistTo, keys: ["expandedCells"], - serialize: (expandedCellsObj) => { + serialize: ( + expandedCellsObj: Partial> + ) => { if (!expandedCellsObj || objectKeys(expandedCellsObj).length === 0) return { expandedCells: null }; return { expandedCells: JSON.stringify(expandedCellsObj) }; @@ -111,7 +113,13 @@ export const useExpansionState = < persistTo, key: "expandedCells", } - : { persistTo }), + : isPersistenceProvider(persistTo) + ? { + persistTo: "provider", + serialize: persistTo.write, + deserialize: () => persistTo.read() as TExpandedCells, + } + : { persistTo: "state" }), }); return { expandedCells, setExpandedCells }; }; diff --git a/client/src/app/hooks/table-controls/filtering/useFilterState.ts b/client/src/app/hooks/table-controls/filtering/useFilterState.ts index 2de43e1f7c..e52a43c28f 100644 --- a/client/src/app/hooks/table-controls/filtering/useFilterState.ts +++ b/client/src/app/hooks/table-controls/filtering/useFilterState.ts @@ -1,5 +1,5 @@ import { FilterCategory, IFilterValues } from "@app/components/FilterToolbar"; -import { IFeaturePersistenceArgs } from "../types"; +import { IFeaturePersistenceArgs, isPersistenceProvider } from "../types"; import { usePersistentState } from "@app/hooks/usePersistentState"; import { serializeFilterUrlParams } from "./helpers"; import { deserializeFilterUrlParams } from "./helpers"; @@ -90,7 +90,6 @@ export const useFilterState = < "filters" >({ isEnabled: !!isFilterEnabled, - defaultValue: initialFilterValues, persistenceKeyPrefix, // Note: For the discriminated union here to work without TypeScript getting confused // (e.g. require the urlParams-specific options when persistTo === "urlParams"), @@ -99,12 +98,21 @@ export const useFilterState = < ? { persistTo, keys: ["filters"], + defaultValue: initialFilterValues, serialize: serializeFilterUrlParams, deserialize: deserializeFilterUrlParams, } : persistTo === "localStorage" || persistTo === "sessionStorage" - ? { persistTo, key: "filters" } - : { persistTo }), + ? { persistTo, key: "filters", defaultValue: initialFilterValues } + : isPersistenceProvider(persistTo) + ? { + persistTo: "provider", + serialize: persistTo.write, + deserialize: () => + persistTo.read() as IFilterValues, + defaultValue: isFilterEnabled ? args?.initialFilterValues ?? {} : {}, + } + : { persistTo: "state", defaultValue: initialFilterValues }), }); return { filterValues, setFilterValues }; }; diff --git a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts index 6fd87c84b1..61f8b6e981 100644 --- a/client/src/app/hooks/table-controls/pagination/usePaginationState.ts +++ b/client/src/app/hooks/table-controls/pagination/usePaginationState.ts @@ -1,5 +1,5 @@ import { usePersistentState } from "@app/hooks/usePersistentState"; -import { IFeaturePersistenceArgs } from "../types"; +import { IFeaturePersistenceArgs, isPersistenceProvider } from "../types"; import { DiscriminatedArgs } from "@app/utils/type-utils"; /** @@ -94,7 +94,7 @@ export const usePaginationState = < ? { persistTo, keys: ["pageNumber", "itemsPerPage"], - serialize: (state) => { + serialize: (state: Partial) => { const { pageNumber, itemsPerPage } = state || {}; return { pageNumber: pageNumber ? String(pageNumber) : undefined, @@ -116,7 +116,13 @@ export const usePaginationState = < persistTo, key: "pagination", } - : { persistTo }), + : isPersistenceProvider(persistTo) + ? { + persistTo: "provider", + serialize: persistTo.write, + deserialize: () => persistTo.read() as IActivePagination, + } + : { persistTo: "state" }), }); const { pageNumber, itemsPerPage } = paginationState || defaultValue; const setPageNumber = (num: number) => diff --git a/client/src/app/hooks/table-controls/sorting/useSortState.ts b/client/src/app/hooks/table-controls/sorting/useSortState.ts index fc583142e9..91d2840a1b 100644 --- a/client/src/app/hooks/table-controls/sorting/useSortState.ts +++ b/client/src/app/hooks/table-controls/sorting/useSortState.ts @@ -1,5 +1,5 @@ import { DiscriminatedArgs } from "@app/utils/type-utils"; -import { IFeaturePersistenceArgs } from ".."; +import { IFeaturePersistenceArgs, isPersistenceProvider } from ".."; import { usePersistentState } from "@app/hooks/usePersistentState"; /** @@ -96,7 +96,9 @@ export const useSortState = < ? { persistTo, keys: ["sortColumn", "sortDirection"], - serialize: (activeSort) => ({ + serialize: ( + activeSort: Partial | null> + ) => ({ sortColumn: activeSort?.columnKey || null, sortDirection: activeSort?.direction || null, }), @@ -113,7 +115,14 @@ export const useSortState = < persistTo, key: "sort", } - : { persistTo }), + : isPersistenceProvider(persistTo) + ? { + persistTo: "provider", + serialize: persistTo.write, + deserialize: () => + persistTo.read() as IActiveSort | null, + } + : { persistTo: "state" }), }); return { activeSort, setActiveSort }; }; diff --git a/client/src/app/hooks/table-controls/types.ts b/client/src/app/hooks/table-controls/types.ts index d2293c2348..38b8e00b8a 100644 --- a/client/src/app/hooks/table-controls/types.ts +++ b/client/src/app/hooks/table-controls/types.ts @@ -64,6 +64,17 @@ export type TableFeature = | "activeItem" | "columns"; +export interface PersistenceProvider { + write: (value: T) => void; + read: () => T; +} + +export const isPersistenceProvider = ( + persistTo?: PersistTarget | PersistenceProvider +): persistTo is PersistenceProvider => + !!(persistTo as PersistenceProvider)?.write && + !!(persistTo as PersistenceProvider)?.read; + /** * Identifier for where to persist state for a single table feature or for all table features. * - "state" (default) - Plain React state. Resets on component unmount or page reload. @@ -106,7 +117,7 @@ export type IFeaturePersistenceArgs< /** * Where to persist state for this feature. */ - persistTo?: PersistTarget; + persistTo?: PersistTarget | PersistenceProvider; }; export interface ColumnSetting { @@ -131,7 +142,9 @@ export type ITablePersistenceArgs< */ persistTo?: | PersistTarget - | Partial>; + | Partial< + Record> + >; }; /** diff --git a/client/src/app/hooks/table-controls/useTableControlState.ts b/client/src/app/hooks/table-controls/useTableControlState.ts index 7e9d1ca651..85b36543e2 100644 --- a/client/src/app/hooks/table-controls/useTableControlState.ts +++ b/client/src/app/hooks/table-controls/useTableControlState.ts @@ -1,7 +1,8 @@ import { + IFeaturePersistenceArgs, ITableControlState, + ITablePersistenceArgs, IUseTableControlStateArgs, - PersistTarget, TableFeature, } from "./types"; import { useFilterState } from "./filtering"; @@ -11,6 +12,21 @@ import { useActiveItemState } from "./active-item"; import { useExpansionState } from "./expansion"; import { useColumnState } from "./column/useColumnState"; +const getPersistTo = ({ + feature, + persistTo, +}: { + feature: TableFeature; + persistTo: ITablePersistenceArgs["persistTo"]; +}): { + persistTo: IFeaturePersistenceArgs["persistTo"]; +} => ({ + persistTo: + !persistTo || typeof persistTo === "string" + ? persistTo + : persistTo[feature], +}); + /** * Provides the "source of truth" state for all table features. * - State can be persisted in one or more configurable storage targets, either the same for the entire table or different targets per feature. @@ -41,31 +57,29 @@ export const useTableControlState = < TFilterCategoryKey, TPersistenceKeyPrefix > => { - const getPersistTo = (feature: TableFeature): PersistTarget | undefined => - !args.persistTo || typeof args.persistTo === "string" - ? args.persistTo - : args.persistTo[feature] || args.persistTo.default; - const filterState = useFilterState< TItem, TFilterCategoryKey, TPersistenceKeyPrefix - >({ ...args, persistTo: getPersistTo("filter") }); + >({ + ...args, + ...getPersistTo({ feature: "filter", persistTo: args.persistTo }), + }); const sortState = useSortState({ ...args, - persistTo: getPersistTo("sort"), + ...getPersistTo({ feature: "sort", persistTo: args.persistTo }), }); const paginationState = usePaginationState({ ...args, - persistTo: getPersistTo("pagination"), + ...getPersistTo({ persistTo: args.persistTo, feature: "pagination" }), }); const expansionState = useExpansionState({ ...args, - persistTo: getPersistTo("expansion"), + ...getPersistTo({ persistTo: args.persistTo, feature: "expansion" }), }); const activeItemState = useActiveItemState({ ...args, - persistTo: getPersistTo("activeItem"), + ...getPersistTo({ persistTo: args.persistTo, feature: "activeItem" }), }); const { columnNames, tableName, initialColumns } = args; diff --git a/client/src/app/hooks/usePersistentState.ts b/client/src/app/hooks/usePersistentState.ts index ccd50c452a..5dd6045fac 100644 --- a/client/src/app/hooks/usePersistentState.ts +++ b/client/src/app/hooks/usePersistentState.ts @@ -9,7 +9,15 @@ import { DisallowCharacters } from "@app/utils/type-utils"; type PersistToStateOptions = { persistTo?: "state" }; -type PersistToUrlParamsOptions< +type PersistToProvider = { + persistTo: "provider"; + defaultValue: TValue; + isEnabled?: boolean; + serialize: (params: TValue) => void; + deserialize: () => TValue; +}; + +export type PersistToUrlParamsOptions< TValue, TPersistenceKeyPrefix extends string, TURLParamKey extends string, @@ -33,6 +41,7 @@ export type UsePersistentStateOptions< | PersistToStateOptions | PersistToUrlParamsOptions | PersistToStorageOptions + | PersistToProvider ); export const usePersistentState = < @@ -92,7 +101,37 @@ export const usePersistentState = < ? { ...options, key: prefixKey(options.key) } : { ...options, isEnabled: false, key: "" } ), + provider: usePersistenceProvider( + isPersistenceProviderOptions(options) + ? options + : { + serialize: () => {}, + deserialize: () => defaultValue, + defaultValue, + isEnabled: false, + persistTo: "provider", + } + ), }; const [value, setValue] = persistence[persistTo || "state"]; return isEnabled ? [value, setValue] : [defaultValue, () => {}]; }; + +const usePersistenceProvider = ({ + serialize, + deserialize, + defaultValue, +}: PersistToProvider): [TValue, (val: TValue) => void] => { + // use default value if nulish value was deserialized + return [deserialize() ?? defaultValue, serialize]; +}; + +export const isPersistenceProviderOptions = < + TValue, + TPersistenceKeyPrefix extends string, + TURLParamKey extends string, +>( + o: Partial< + UsePersistentStateOptions + > +): o is PersistToProvider => o.persistTo === "provider"; diff --git a/client/src/app/pages/applications/analysis-wizard/analysis-wizard.tsx b/client/src/app/pages/applications/analysis-wizard/analysis-wizard.tsx index a432b85be9..05541b89b1 100644 --- a/client/src/app/pages/applications/analysis-wizard/analysis-wizard.tsx +++ b/client/src/app/pages/applications/analysis-wizard/analysis-wizard.tsx @@ -166,6 +166,8 @@ export const AnalysisWizard: React.FC = ({ mode: "source-code-deps", formLabels: [], selectedTargets: [], + // defaults will be passed as initialFilterValues to the table hook + targetFilters: undefined, selectedSourceLabels: [], withKnownLibs: "app", includedPackages: [], diff --git a/client/src/app/pages/applications/analysis-wizard/schema.ts b/client/src/app/pages/applications/analysis-wizard/schema.ts index 770dbff5a2..a1294e7f23 100644 --- a/client/src/app/pages/applications/analysis-wizard/schema.ts +++ b/client/src/app/pages/applications/analysis-wizard/schema.ts @@ -57,12 +57,14 @@ const useModeStepSchema = ({ export interface TargetsStepValues { formLabels: TargetLabel[]; selectedTargets: Target[]; + targetFilters?: Record; } const useTargetsStepSchema = (): yup.SchemaOf => { return yup.object({ formLabels: yup.array(), selectedTargets: yup.array(), + targetFilters: yup.object(), }); }; diff --git a/client/src/app/pages/applications/analysis-wizard/set-targets.tsx b/client/src/app/pages/applications/analysis-wizard/set-targets.tsx index 75d5730647..3b255d375e 100644 --- a/client/src/app/pages/applications/analysis-wizard/set-targets.tsx +++ b/client/src/app/pages/applications/analysis-wizard/set-targets.tsx @@ -101,7 +101,7 @@ interface SetTargetsInternalProps { isLoading: boolean; isError: boolean; languageProviders: string[]; - initialFilters: string[]; + applicationProviders: string[]; } const SetTargetsInternal: React.FC = ({ @@ -109,7 +109,7 @@ const SetTargetsInternal: React.FC = ({ isLoading, isError, languageProviders, - initialFilters = [], + applicationProviders = [], }) => { const { t } = useTranslation(); @@ -177,13 +177,23 @@ const SetTargetsInternal: React.FC = ({ tableName: "target-cards", items: targets, idProperty: "name", - initialFilterValues: { name: initialFilters }, + initialFilterValues: { name: applicationProviders }, columnNames: { name: "name", }, isFilterEnabled: true, isPaginationEnabled: false, isLoading, + persistTo: { + filter: { + write(value) { + setValue("targetFilters", value as Record); + }, + read() { + return getValues().targetFilters; + }, + }, + }, filterCategories: [ { selectOptions: languageProviders?.map((language) => ({ @@ -281,7 +291,7 @@ export const SetTargets: React.FC = ({ applications }) => { return ( { const { t } = useTranslation(); @@ -284,7 +284,7 @@ export const StakeholderGroups: React.FC = () => { > {stakeholderGroup.stakeholders?.length} - setCreateUpdateModalState(stakeholderGroup) } diff --git a/common/src/proxies.ts b/common/src/proxies.ts index 94246c544b..a70039342c 100644 --- a/common/src/proxies.ts +++ b/common/src/proxies.ts @@ -60,4 +60,35 @@ export const proxyMap: Record = { } }, }, + + "/kai": { + target: KONVEYOR_ENV.TACKLE_HUB_URL || "http://localhost:9002", + logLevel: process.env.DEBUG ? "debug" : "info", + + changeOrigin: true, + pathRewrite: { + "^/kai": "/services/kai", + }, + + onProxyReq: (proxyReq, req, _res) => { + // Add the Bearer token to the request if it is not already present, AND if + // the token is part of the request as a cookie + if (req.cookies?.keycloak_cookie && !req.headers["authorization"]) { + proxyReq.setHeader( + "Authorization", + `Bearer ${req.cookies.keycloak_cookie}` + ); + } + }, + onProxyRes: (proxyRes, req, res) => { + const includesJsonHeaders = + req.headers.accept?.includes("application/json"); + if ( + (!includesJsonHeaders && proxyRes.statusCode === 401) || + (!includesJsonHeaders && proxyRes.statusMessage === "Unauthorized") + ) { + res.redirect("/"); + } + }, + }, }; diff --git a/package-lock.json b/package-lock.json index b25f9513ff..9eb5e33fe7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4314,9 +4314,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001600", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz", - "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==", + "version": "1.0.30001660", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz", + "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==", "dev": true, "funding": [ {