diff --git a/client/src/app/components/FilterToolbar/FilterToolbar.tsx b/client/src/app/components/FilterToolbar/FilterToolbar.tsx index 8987a63432..1563e9250c 100644 --- a/client/src/app/components/FilterToolbar/FilterToolbar.tsx +++ b/client/src/app/components/FilterToolbar/FilterToolbar.tsx @@ -22,8 +22,12 @@ export enum FilterType { export type FilterValue = string[] | undefined | null; -export interface FilterSelectOptionProps extends SelectOptionProps { - key: string; +export interface FilterSelectOptionProps { + optionProps?: SelectOptionProps; + value: string; + label?: string; + chipLabel?: string; + groupLabel?: string; } export interface IBasicFilterCategory< diff --git a/client/src/app/components/FilterToolbar/MultiselectFilterControl.tsx b/client/src/app/components/FilterToolbar/MultiselectFilterControl.tsx index 1d2297ee7b..eb404d9ad3 100644 --- a/client/src/app/components/FilterToolbar/MultiselectFilterControl.tsx +++ b/client/src/app/components/FilterToolbar/MultiselectFilterControl.tsx @@ -25,8 +25,6 @@ import { TimesIcon } from "@patternfly/react-icons"; import "./select-overrides.css"; -const CHIP_BREAK_DELINEATOR = " / "; - export interface IMultiselectFilterControlProps extends IFilterControlProps { category: IMultiselectFilterCategory; @@ -63,15 +61,8 @@ export const MultiselectFilterControl = ({ (i) => i ) as FilterSelectOptionProps[]); - const getOptionKeyFromOptionValue = (optionValue: string) => - flatOptions.find(({ value }) => value === optionValue)?.key; - - const getOptionValueFromOptionKey = (optionKey: string) => - flatOptions.find(({ key }) => key === optionKey)?.value; - - const getOptionKeyFromChip = (chipDisplayValue: string) => { - return flatOptions.find(({ value }) => value === chipDisplayValue)?.key; - }; + const getOptionFromOptionValue = (optionValue: string) => + flatOptions.find(({ value }) => value === optionValue); const [focusedItemIndex, setFocusedItemIndex] = React.useState( null @@ -82,39 +73,43 @@ export const MultiselectFilterControl = ({ const [inputValue, setInputValue] = React.useState(""); const onFilterClear = (chip: string | ToolbarChip) => { - const displayValue = typeof chip === "string" ? chip : chip.key; - const optionKey = getOptionKeyFromChip(displayValue); + const value = typeof chip === "string" ? chip : chip.key; - if (optionKey) { - const newValue = filterValue?.filter((val) => val !== optionKey) ?? []; + if (value) { + const newValue = filterValue?.filter((val) => val !== value) ?? []; setFilterValue(newValue.length > 0 ? newValue : null); } }; /* - * Note: Chips can be a `ToolbarChip` or a plain `string`. Use a hack to split a - * selected option in 2 parts. Assuming the option is in the format "Group / Item" - * break the text and show a chip with the Item and the Group as a tooltip. + * Note: Create chips only as `ToolbarChip` (no plain string) */ - const chips = filterValue?.map((s, index) => { - const displayValue = getOptionValueFromOptionKey(s); - const chip: string = displayValue?.toString() ?? ""; - const idx = chip.indexOf(CHIP_BREAK_DELINEATOR); - - if (idx > 0) { - const tooltip = chip.substring(0, idx); - const text = chip.substring(idx + CHIP_BREAK_DELINEATOR.length); + const chips = filterValue + ?.map((value, index) => { + const option = getOptionFromOptionValue(value); + if (!option) { + return null; + } + + const { chipLabel, label, groupLabel } = option; + const displayValue: string = chipLabel ?? label ?? value ?? ""; + return { - key: chip, - node: ( - {tooltip}}> -
{text}
+ key: value, + node: groupLabel ? ( + {groupLabel}} + > +
{displayValue}
+ ) : ( + displayValue ), - } as ToolbarChip; - } - return chip; - }); + }; + }) + + .filter(Boolean); const renderSelectOptions = ( filter: (option: FilterSelectOptionProps, groupName?: string) => boolean @@ -124,64 +119,51 @@ export const MultiselectFilterControl = ({ selectOptions as Record ) .sort(([groupA], [groupB]) => groupA.localeCompare(groupB)) - .map(([group, options], index) => { - const groupFiltered = - options?.filter((o) => filter(o, group)) ?? []; - return groupFiltered.length === 0 ? undefined : ( - - {groupFiltered.map((optionProps) => { - const optionKey = getOptionKeyFromOptionValue( - optionProps.value - ); - if (!optionKey) return null; - return ( - - ); - })} - - ); - }) - .filter(Boolean) + .map(([group, options]): [string, FilterSelectOptionProps[]] => [ + group, + options?.filter((o) => filter(o, group)) ?? [], + ]) + .filter(([, groupFiltered]) => groupFiltered?.length) + .map(([group, groupFiltered], index) => ( + + {groupFiltered.map(({ value, label, optionProps }) => ( + + {label ?? value} + + ))} + + )) : flatOptions .filter((o) => filter(o)) - .map((optionProps, index) => { - const optionKey = getOptionKeyFromOptionValue(optionProps.value); - if (!optionKey) return null; - return ( - - {optionProps.value} - - ); - }); + .map(({ label, value, optionProps = {} }, index) => ( + + {label ?? value} + + )); const onSelect = (value: string | undefined) => { if (value && value !== "No results") { - const optionKey = getOptionKeyFromOptionValue(value); - - if (optionKey) { - let newFilterValue: string[]; - - if (filterValue && filterValue.includes(optionKey)) { - newFilterValue = filterValue.filter((item) => item !== optionKey); - } else { - newFilterValue = filterValue - ? [...filterValue, optionKey] - : [optionKey]; - } + let newFilterValue: string[]; - setFilterValue(newFilterValue); + if (filterValue && filterValue.includes(value)) { + newFilterValue = filterValue.filter((item) => item !== value); + } else { + newFilterValue = filterValue ? [...filterValue, value] : [value]; } + + setFilterValue(newFilterValue); } textInputRef.current?.focus(); }; @@ -210,9 +192,9 @@ export const MultiselectFilterControl = ({ } setFocusedItemIndex(indexToFocus); - const focusedItem = selectOptions.filter((option) => !option.isDisabled)[ - indexToFocus - ]; + const focusedItem = selectOptions.filter( + ({ optionProps }) => !optionProps?.isDisabled + )[indexToFocus]; setActiveItem( `select-multi-typeahead-checkbox-${focusedItem.value.replace(" ", "-")}` ); @@ -236,27 +218,25 @@ export const MultiselectFilterControl = ({ if (!newSelectOptions.length) { newSelectOptions = [ { - key: "no-results", - isDisabled: false, - children: `No results found for "${inputValue}"`, - value: "No results", + value: "no-results", + optionProps: { + isDisabled: true, + hasCheckbox: false, + }, + label: `No results found for "${inputValue}"`, }, ]; } - - if (!isFilterDropdownOpen) { - setIsFilterDropdownOpen(true); - } } setSelectOptions(newSelectOptions); setFocusedItemIndex(null); setActiveItem(null); - }, [inputValue]); + }, [inputValue, category.selectOptions]); const onInputKeyDown = (event: React.KeyboardEvent) => { const enabledMenuItems = Array.isArray(selectOptions) - ? selectOptions.filter((option) => !option.isDisabled) + ? selectOptions.filter(({ optionProps }) => !optionProps?.isDisabled) : []; const [firstMenuItem] = enabledMenuItems; const focusedItem = focusedItemIndex @@ -299,6 +279,9 @@ export const MultiselectFilterControl = ({ value: string ) => { setInputValue(value); + if (!isFilterDropdownOpen) { + setIsFilterDropdownOpen(true); + } }; const toggle = (toggleRef: React.Ref) => ( diff --git a/client/src/app/components/FilterToolbar/SelectFilterControl.tsx b/client/src/app/components/FilterToolbar/SelectFilterControl.tsx index 55d077205b..50f0cf85d3 100644 --- a/client/src/app/components/FilterToolbar/SelectFilterControl.tsx +++ b/client/src/app/components/FilterToolbar/SelectFilterControl.tsx @@ -33,20 +33,26 @@ export const SelectFilterControl = ({ >): JSX.Element | null => { const [isFilterDropdownOpen, setIsFilterDropdownOpen] = React.useState(false); - const getOptionKeyFromOptionValue = (optionValue: string) => - category.selectOptions.find(({ value }) => value === optionValue)?.key; + const getOptionFromOptionValue = (optionValue: string) => + category.selectOptions.find(({ value }) => value === optionValue); - const getOptionValueFromOptionKey = (optionKey: string) => - category.selectOptions.find(({ key }) => key === optionKey)?.value; - - const chips = filterValue?.map((key) => { - const displayValue = getOptionValueFromOptionKey(key); - return displayValue ? displayValue : key; - }); + const chips = filterValue + ?.map((value) => { + const option = getOptionFromOptionValue(value); + if (!option) { + return null; + } + const { chipLabel, label } = option; + return { + key: value, + node: chipLabel ?? label ?? value, + }; + }) + .filter(Boolean); const onFilterSelect = (value: string) => { - const optionKey = getOptionKeyFromOptionValue(value); - setFilterValue(optionKey ? [optionKey] : null); + const option = getOptionFromOptionValue(value); + setFilterValue(option ? [value] : null); setIsFilterDropdownOpen(false); }; @@ -59,7 +65,7 @@ export const SelectFilterControl = ({ let displayText = "Any"; if (filterValue && filterValue.length > 0) { const selectedKey = filterValue[0]; - const selectedDisplayValue = getOptionValueFromOptionKey(selectedKey); + const selectedDisplayValue = getOptionFromOptionValue(selectedKey)?.label; displayText = selectedDisplayValue ? selectedDisplayValue : selectedKey; } @@ -103,15 +109,16 @@ export const SelectFilterControl = ({ shouldFocusToggleOnSelect > - {category.selectOptions.map((o, index) => { - const isSelected = filterValue?.includes(o.key); + {category.selectOptions.map(({ label, value, optionProps }) => { + const isSelected = filterValue?.includes(value); return ( - {o.value} + {label ?? value} ); })} 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 7bc41a4318..1adf1e3e0e 100644 --- a/client/src/app/pages/applications/applications-table/applications-table.tsx +++ b/client/src/app/pages/applications/applications-table/applications-table.tsx @@ -400,9 +400,9 @@ export const ApplicationsTable: React.FC = () => { }) + "...", type: FilterType.multiselect, selectOptions: [ - { key: "source", value: "Source" }, - { key: "maven", value: "Maven" }, - { key: "proxy", value: "Proxy" }, + { value: "source", label: "Source" }, + { value: "maven", label: "Maven" }, + { value: "proxy", label: "Proxy" }, ], getItemValue: (item) => { const searchStringArr: string[] = []; @@ -425,8 +425,8 @@ export const ApplicationsTable: React.FC = () => { }) + "...", type: FilterType.select, selectOptions: [ - { key: "git", value: "Git" }, - { key: "subversion", value: "Subversion" }, + { value: "git", label: "Git" }, + { value: "subversion", label: "Subversion" }, ], getItemValue: (item) => item?.repository?.kind || "", }, @@ -439,8 +439,8 @@ export const ApplicationsTable: React.FC = () => { }) + "...", type: FilterType.select, selectOptions: [ - { key: "binary", value: t("terms.artifactAssociated") }, - { key: "none", value: t("terms.artifactNotAssociated") }, + { value: "binary", label: t("terms.artifactAssociated") }, + { value: "none", label: t("terms.artifactNotAssociated") }, ], getItemValue: (item) => { const hasBinary = @@ -459,7 +459,12 @@ export const ApplicationsTable: React.FC = () => { t("actions.filterBy", { what: t("terms.tagName").toLowerCase(), }) + "...", - selectOptions: tagItems.map(({ name }) => ({ key: name, value: name })), + selectOptions: tagItems.map(({ name, tagName, categoryName }) => ({ + value: name, + label: name, + chipLabel: tagName, + groupLabel: categoryName, + })), /** * Create a single string from an Application's Tags that can be used to * match against the `selectOptions`'s values (here on the client side) @@ -485,10 +490,10 @@ export const ApplicationsTable: React.FC = () => { what: t("terms.risk").toLowerCase(), }) + "...", selectOptions: [ - { key: "green", value: "Low" }, - { key: "yellow", value: "Medium" }, - { key: "red", value: "High" }, - { key: "unknown", value: "Unknown" }, + { value: "green", label: "Low" }, + { value: "yellow", label: "Medium" }, + { value: "red", label: "High" }, + { value: "unknown", label: "Unknown" }, ], getItemValue: (item) => item.risk || "", }, diff --git a/client/src/app/pages/dependencies/dependency-apps-table.tsx b/client/src/app/pages/dependencies/dependency-apps-table.tsx index 0efcf836bb..f5706a40d2 100644 --- a/client/src/app/pages/dependencies/dependency-apps-table.tsx +++ b/client/src/app/pages/dependencies/dependency-apps-table.tsx @@ -96,7 +96,12 @@ export const DependencyAppsTable: React.FC = ({ t("actions.filterBy", { what: t("terms.tagName").toLowerCase(), }) + "...", - selectOptions: tagItems.map(({ name }) => ({ key: name, value: name })), + selectOptions: tagItems.map(({ name, tagName, categoryName }) => ({ + value: name, + label: name, + chipLabel: tagName, + groupLabel: categoryName, + })), /** * Convert the selected `selectOptions` to an array of tag ids the server side * filtering will understand. diff --git a/client/src/app/pages/identities/identities.tsx b/client/src/app/pages/identities/identities.tsx index 1e897e8774..a30e1485a8 100644 --- a/client/src/app/pages/identities/identities.tsx +++ b/client/src/app/pages/identities/identities.tsx @@ -126,7 +126,10 @@ export const Identities: React.FC = () => { title: "Type", type: FilterType.select, placeholderText: "Filter by type...", - selectOptions: typeOptions, + selectOptions: typeOptions.map(({ key, value }) => ({ + value: key, + label: value, + })), getItemValue: (item) => { return item.kind || ""; }, diff --git a/client/src/app/pages/issues/helpers.ts b/client/src/app/pages/issues/helpers.ts index 9beb0aad7b..c2a8143de5 100644 --- a/client/src/app/pages/issues/helpers.ts +++ b/client/src/app/pages/issues/helpers.ts @@ -122,7 +122,12 @@ export const useSharedAffectedApplicationFilterCategories = < t("actions.filterBy", { what: t("terms.tagName").toLowerCase(), }) + "...", - selectOptions: tagItems.map(({ name }) => ({ key: name, value: name })), + selectOptions: tagItems.map(({ name, tagName, categoryName }) => ({ + value: name, + label: name, + chipLabel: tagName, + groupLabel: categoryName, + })), /** * Convert the selected `selectOptions` to an array of tag ids the server side * filtering will understand. diff --git a/client/src/app/pages/reports/components/identified-risks-table/identified-risks-table.tsx b/client/src/app/pages/reports/components/identified-risks-table/identified-risks-table.tsx index cc97ee7cd9..8f53568711 100644 --- a/client/src/app/pages/reports/components/identified-risks-table/identified-risks-table.tsx +++ b/client/src/app/pages/reports/components/identified-risks-table/identified-risks-table.tsx @@ -246,10 +246,10 @@ export const IdentifiedRisksTable: React.FC = ({ return riskValue.toString(); }, selectOptions: [ - { key: "3", value: "High" }, - { key: "2", value: "Medium" }, - { key: "1", value: "Low" }, - { key: "0", value: "Unknown" }, + { value: "3", label: "High" }, + { value: "2", label: "Medium" }, + { value: "1", label: "Low" }, + { value: "0", label: "Unknown" }, ], }, ], 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 index 62b18bb3ae..5bff11bbfb 100644 --- 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 @@ -90,7 +90,10 @@ export const ApplicationAssessmentSummaryTable: React.FC< getItemValue: (item) => { return item.riskValue || ""; }, - selectOptions: typeOptions, + selectOptions: typeOptions.map(({ key, value }) => ({ + value: key, + label: value, + })), }, ]; diff --git a/file b/file new file mode 100644 index 0000000000..e69de29bb2