diff --git a/.env b/.env index 4fa188f12b..bd64be921a 100644 --- a/.env +++ b/.env @@ -76,3 +76,4 @@ FEATURE_INFRA_PROVISION_INFO_BLOCK_HIDE=false FEATURE_GROUPED_APP_LIST_FILTERS_ENABLE=false FEATURE_FLUX_DEPLOYMENTS_ENABLE=false FEATURE_LINK_EXTERNAL_FLUX_ENABLE=false +FEATURE_CANARY_ROLLOUT_PROGRESS_ENABLE=true diff --git a/package.json b/package.json index 360acfcf26..baf3139d3e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "homepage": "/dashboard", "dependencies": { - "@devtron-labs/devtron-fe-common-lib": "1.18.0-pre-0", + "@devtron-labs/devtron-fe-common-lib": "1.18.1-pre-0", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@rjsf/core": "^5.13.3", "@rjsf/utils": "^5.13.3", diff --git a/src/Pages/GlobalConfigurations/Authorization/Shared/components/AppPermissions/AppOrJobSelector.tsx b/src/Pages/GlobalConfigurations/Authorization/Shared/components/AppPermissions/AppOrJobSelector.tsx index b2f0d0bef9..5cf2a5552b 100644 --- a/src/Pages/GlobalConfigurations/Authorization/Shared/components/AppPermissions/AppOrJobSelector.tsx +++ b/src/Pages/GlobalConfigurations/Authorization/Shared/components/AppPermissions/AppOrJobSelector.tsx @@ -70,7 +70,7 @@ const AppOrJobSelector = ({ _permission.entityName.filter((option) => option.value !== SELECT_ALL_VALUE).map((app) => app.label) ?? [] const { appIdWorkflowNamesMapping } = await getUserAccessAllWorkflows({ - appIds: jobNames, + appNames: jobNames, options: { abortControllerRef }, }) const workflowOptions = getWorkflowOptions(appIdWorkflowNamesMapping) diff --git a/src/Pages/GlobalConfigurations/Authorization/Shared/components/AppPermissions/AppPermissions.component.tsx b/src/Pages/GlobalConfigurations/Authorization/Shared/components/AppPermissions/AppPermissions.component.tsx index ca5d056129..f01b2b7fa7 100644 --- a/src/Pages/GlobalConfigurations/Authorization/Shared/components/AppPermissions/AppPermissions.component.tsx +++ b/src/Pages/GlobalConfigurations/Authorization/Shared/components/AppPermissions/AppPermissions.component.tsx @@ -345,7 +345,7 @@ const AppPermissions = () => { async function setAllWorkflows(jobOptions) { const jobNames = jobOptions.filter((job) => job.value !== SELECT_ALL_VALUE).map((job) => job.label) try { - const result = await getUserAccessAllWorkflows(jobNames) + const result = await getUserAccessAllWorkflows({ appNames: jobNames }) const workflowOptions = getWorkflowOptions(result?.appIdWorkflowNamesMapping) return [ diff --git a/src/Pages/GlobalConfigurations/Authorization/authorization.service.ts b/src/Pages/GlobalConfigurations/Authorization/authorization.service.ts index 92fac36bb5..e970117e35 100644 --- a/src/Pages/GlobalConfigurations/Authorization/authorization.service.ts +++ b/src/Pages/GlobalConfigurations/Authorization/authorization.service.ts @@ -312,10 +312,10 @@ export const getUserAccessClusterList = () => payload: { entity: EntityTypes.CLUSTER }, }) -export const getUserAccessAllWorkflows = ({ appIds, options }: GetUserAccessAllWorkflowsParams) => +export const getUserAccessAllWorkflows = ({ appNames, options }: GetUserAccessAllWorkflowsParams) => getUserResourceOptions({ - kind: UserAccessResourceKind.JOBS, - payload: { entity: EntityTypes.JOB, appIds }, + kind: UserAccessResourceKind.WORKFLOW, + payload: { entity: EntityTypes.JOB, appNames }, options, }) diff --git a/src/Pages/GlobalConfigurations/Authorization/types.ts b/src/Pages/GlobalConfigurations/Authorization/types.ts index 730a16421b..70c3c5ff39 100644 --- a/src/Pages/GlobalConfigurations/Authorization/types.ts +++ b/src/Pages/GlobalConfigurations/Authorization/types.ts @@ -347,6 +347,7 @@ export interface GetUserPermissionResourcesPayload accessType?: ACCESS_TYPE_MAP.DEVTRON_APPS | ACCESS_TYPE_MAP.HELM_APPS teamIds?: number[] appIds?: string[] + appNames?: string[] } export interface GetUserResourceOptionsProps { @@ -356,5 +357,5 @@ export interface GetUserResourceOptionsProps { } export interface GetUserAccessAllWorkflowsParams - extends Pick, + extends Pick, Pick {} diff --git a/src/Pages/Shared/ConfigMapSecret/ConfigMapSecretContainer.tsx b/src/Pages/Shared/ConfigMapSecret/ConfigMapSecretContainer.tsx index de803fadca..fc4d4687ea 100644 --- a/src/Pages/Shared/ConfigMapSecret/ConfigMapSecretContainer.tsx +++ b/src/Pages/Shared/ConfigMapSecret/ConfigMapSecretContainer.tsx @@ -328,15 +328,6 @@ export const ConfigMapSecretContainer = ({ } }, [configMapSecretResLoading, configMapSecretRes]) - // CONFIGMAP/SECRET DELETED - const configHasBeenDeleted = useMemo( - () => - !configMapSecretResLoading && configMapSecretRes - ? !configMapSecretData && !inheritedConfigMapSecretData && !draftData - : null, - [configMapSecretResLoading, configMapSecretRes], - ) - // CONFIGMAP/SECRET ERROR const configMapSecretResErr = useMemo( () => @@ -348,6 +339,18 @@ export const ConfigMapSecretContainer = ({ [configMapSecretResLoading, configMapSecretRes], ) + // CONFIGMAP/SECRET DELETED + const configHasBeenDeleted = useMemo( + () => + !configMapSecretResLoading && + configMapSecretRes && + !configMapSecretResErr && + !configMapSecretData && + !inheritedConfigMapSecretData && + !draftData, + [configMapSecretResLoading, configMapSecretRes, configMapSecretResErr], + ) + // ASYNC CALL - CONFIGMAP/SECRET RESOLVED DATA const [resolvedScopeVariablesResLoading, resolvedScopeVariablesRes, reloadResolvedScopeVariablesResErr] = useAsync( () => diff --git a/src/components/ResourceBrowser/Constants.ts b/src/components/ResourceBrowser/Constants.ts index 3c9677b482..d7dd3f3e16 100644 --- a/src/components/ResourceBrowser/Constants.ts +++ b/src/components/ResourceBrowser/Constants.ts @@ -325,29 +325,13 @@ export const NODE_LIST_HEADERS_TO_KEY_MAP: Record<(typeof NODE_LIST_HEADERS)[num unschedulable: 'unschedulable', } as const -export const NODE_SEARCH_KEY_OPTIONS = [ - { value: NODE_SEARCH_KEYS.NAME, label: 'Name' }, - { value: NODE_SEARCH_KEYS.LABEL, label: 'Label' }, - { value: NODE_SEARCH_KEYS.NODE_GROUP, label: 'Node group' }, -] as const - -export const DEFAULT_NODE_K8S_VERSION = { - label: 'K8s version: Any', - value: 'K8s version: Any', -} - -export const NODE_SEARCH_KEY_PLACEHOLDER: Record = { - [NODE_SEARCH_KEYS.NAME]: 'Search by node name Eg. ip-172-31-2-152.us-east-2.compute.internal', - [NODE_SEARCH_KEYS.LABEL]: 'Search by key=value Eg. environment=production, tier=frontend', - [NODE_SEARCH_KEYS.NODE_GROUP]: 'Search by node group name Eg. mainnode', -} +export const NODE_LIST_HEADER_KEYS_TO_SEARCH = Object.values(NODE_LIST_HEADERS_TO_KEY_MAP) export const NODE_SEARCH_KEYS_TO_OBJECT_KEYS: Record< NODE_SEARCH_KEYS, (typeof NODE_LIST_HEADERS_TO_KEY_MAP)[keyof typeof NODE_LIST_HEADERS_TO_KEY_MAP] > = { [NODE_SEARCH_KEYS.LABEL]: 'labels', - [NODE_SEARCH_KEYS.NAME]: 'name', [NODE_SEARCH_KEYS.NODE_GROUP]: 'nodeGroup', } diff --git a/src/components/ResourceBrowser/ResourceBrowser.scss b/src/components/ResourceBrowser/ResourceBrowser.scss index 34d03103fa..9c4bf4a1b9 100644 --- a/src/components/ResourceBrowser/ResourceBrowser.scss +++ b/src/components/ResourceBrowser/ResourceBrowser.scss @@ -66,12 +66,14 @@ .resource-list-container { .node-listing-search-container { display: grid; - grid-template-columns: auto 160px 1px 180px; + grid-template-columns: 1fr auto 1px 180px; column-gap: 8px; - &__shortcut-key { - right: 10px; - top: 6px; + .grouped-filter-select-picker { + width: 350px; + > .dc__mxw-250 { + max-width: 350px; + } } } @@ -92,6 +94,7 @@ .cluster-terminal-hidden { visibility: hidden; height: 0; + .terminal-action-strip { display: none; } diff --git a/src/components/ResourceBrowser/ResourceList/K8SResourceList.tsx b/src/components/ResourceBrowser/ResourceList/K8SResourceList.tsx index 1c0a245fb6..d1bf791f92 100644 --- a/src/components/ResourceBrowser/ResourceList/K8SResourceList.tsx +++ b/src/components/ResourceBrowser/ResourceList/K8SResourceList.tsx @@ -44,7 +44,7 @@ import { updateManifestResourceHelmApps, } from '@Components/v2/appDetails/k8Resource/nodeDetail/nodeDetail.api' -import { NODE_LIST_HEADERS_TO_KEY_MAP } from '../Constants' +import { NODE_LIST_HEADER_KEYS_TO_SEARCH, NODE_LIST_HEADERS_TO_KEY_MAP } from '../Constants' import { getResourceData } from '../ResourceBrowser.service' import { K8SResourceListType } from '../Types' import K8sResourceListTableCellComponent from './K8sResourceListTableCellComponent' @@ -88,6 +88,7 @@ const K8SResourceListViewWrapper = ({ updateSearchParams, eventType = 'warning', filteredRows, + rows, ...restProps }: K8SResourceListViewWrapperProps) => (
@@ -97,6 +98,9 @@ const K8SResourceListViewWrapper = ({ setVisibleColumns={setVisibleColumns} allColumns={allColumns} searchParams={restProps} + rows={rows} + searchKey={searchKey} + handleSearch={handleSearch} /> ) : ( { + let nodeFilters = true + if (isNodeListing) { - return isItemASearchMatchForNodeListing(row.data, filterData) + nodeFilters = isItemASearchMatchForNodeListing(row.data, filterData) } const isSearchMatch = @@ -227,6 +233,7 @@ export const K8SResourceList = ({ Object.entries(row.data).some( ([key, value]) => key !== 'id' && + (!isNodeListing || NODE_LIST_HEADER_KEYS_TO_SEARCH.includes(key)) && value !== null && value !== undefined && String(value).toLowerCase().includes(filterData.searchKey.toLowerCase()), @@ -239,7 +246,7 @@ export const K8SResourceList = ({ ) } - return isSearchMatch + return isSearchMatch && nodeFilters } const getDefaultSortKey = () => { diff --git a/src/components/ResourceBrowser/ResourceList/NodeListSearchFilter.tsx b/src/components/ResourceBrowser/ResourceList/NodeListSearchFilter.tsx index 48c9faa5a8..8b8d8a229f 100644 --- a/src/components/ResourceBrowser/ResourceList/NodeListSearchFilter.tsx +++ b/src/components/ResourceBrowser/ResourceList/NodeListSearchFilter.tsx @@ -14,316 +14,256 @@ * limitations under the License. */ -import { ChangeEvent, KeyboardEvent, RefCallback, useEffect, useMemo, useRef, useState } from 'react' +import { KeyboardEvent, useEffect, useMemo, useRef } from 'react' import { useHistory, useLocation, useParams } from 'react-router-dom' import { parse as parseQueryString, ParsedQuery, stringify as stringifyQueryString } from 'query-string' -import { OptionType, SelectPicker, useAsync, useRegisterShortcut } from '@devtron-labs/devtron-fe-common-lib' +import { + FilterChips, + GroupedFilterSelectPicker, + SearchBar, + useAsync, + useRegisterShortcut, +} from '@devtron-labs/devtron-fe-common-lib' -import { ReactComponent as ICClear } from '@Icons/ic-error.svg' -import { ReactComponent as ICSearch } from '@Icons/ic-search.svg' import { getClusterCapacity } from '@Components/ClusterNodes/clusterNodes.service' -import { ShortcutKeyBadge } from '../../common/formFields/Widgets/Widgets' -import { - DEFAULT_NODE_K8S_VERSION, - NODE_K8S_VERSION_FILTER_KEY, - NODE_SEARCH_KEY_OPTIONS, - NODE_SEARCH_KEY_PLACEHOLDER, -} from '../Constants' +import { NODE_K8S_VERSION_FILTER_KEY } from '../Constants' import { ClusterDetailBaseParams, NODE_SEARCH_KEYS, NodeListSearchFilterType } from '../Types' import ColumnSelector from './ColumnSelector' +import { + NODE_LIST_SEARCH_FILTER_OPTIONS, + NODE_SEARCH_KEY_PLACEHOLDER, + NODE_SEARCH_KEY_TO_LABEL_PREFIX_MAP, +} from './constants' +import { NodeSearchListOptionType } from './types' +import { getNodeSearchKeysOptionsList } from './utils' const NodeListSearchFilter = ({ visibleColumns, setVisibleColumns, searchParams, allColumns, + rows, + searchKey, + handleSearch, }: NodeListSearchFilterType) => { + // HOOKS const { clusterId } = useParams() + const { search } = useLocation() + const { push } = useHistory() - const selectedSearchTextType: NODE_SEARCH_KEYS | '' = Object.values(NODE_SEARCH_KEYS).reduce((type, key) => { - if (searchParams[key]) { - return key - } - - return type - }, '') - - const selectedK8sNodeVersion = searchParams[NODE_K8S_VERSION_FILTER_KEY] ?? '' + // REFS + const searchInputRef = useRef(null) - const [isSearchKeySelectorOpen, setIsSearchKeySelectorOpen] = useState(false) - const [searchTextType, setSearchTextType] = useState(selectedSearchTextType) - const [searchInputText, setSearchInputText] = useState( - selectedSearchTextType ? searchParams[selectedSearchTextType] : '', - ) + const { registerShortcut, unregisterShortcut } = useRegisterShortcut() - const searchInputRef = useRef(null) + useEffect(() => { + const handleSearchFocus = () => { + searchInputRef.current?.focus() + } - const handleSearchInputMount: RefCallback = (node) => { - if (node) { - searchInputRef.current = node + if (registerShortcut) { + registerShortcut({ keys: ['/'], callback: handleSearchFocus }) + } - node.focus() + return () => { + unregisterShortcut(['/']) } - } + }, []) - const location = useLocation() - const { push } = useHistory() + // CONSTANTS + const isNodeSearchFilterApplied = + searchParams[NODE_SEARCH_KEYS.LABEL] || + searchParams[NODE_SEARCH_KEYS.NODE_GROUP] || + searchParams[NODE_K8S_VERSION_FILTER_KEY] + // ASYNC CALLS const [nodeK8sVersionsLoading, nodeK8sVersionOptions, nodeK8sVersionsError, refetchNodeK8sVersions] = useAsync(async () => { const { result: { nodeK8sVersions: versions }, } = await getClusterCapacity(clusterId) - return [ - DEFAULT_NODE_K8S_VERSION, - ...(versions?.map((version) => ({ - label: `K8s version: ${version}`, - value: version, - })) || []), - ] + return (versions || []).map((version) => ({ + label: version, + value: version, + })) }, [clusterId]) - const selectedK8sVersionOption = useMemo( - () => - nodeK8sVersionOptions?.find((option) => option.value === selectedK8sNodeVersion) ?? - DEFAULT_NODE_K8S_VERSION, - [nodeK8sVersionOptions, selectedK8sNodeVersion], - ) + // CONFIGS + const { nodeGroups, labels } = useMemo(() => getNodeSearchKeysOptionsList(rows), [JSON.stringify(rows)]) - const handleFocusInput = () => { - setIsSearchKeySelectorOpen(true) - searchInputRef.current?.focus() - } - - const handleBlurInput = () => { - setIsSearchKeySelectorOpen(false) - searchInputRef.current?.blur() - } - - const { registerShortcut, unregisterShortcut } = useRegisterShortcut() + const appliedFilters = useMemo(() => { + const nodeGroupMap = new Set(((searchParams[NODE_SEARCH_KEYS.NODE_GROUP] as string) || '').split(',')) + const labelMap = new Set(((searchParams[NODE_SEARCH_KEYS.LABEL] as string) || '').split(',')) + const k8sNodeVersionMap = new Set(((searchParams[NODE_K8S_VERSION_FILTER_KEY] as string) || '').split(',')) - useEffect(() => { - if (registerShortcut) { - registerShortcut({ keys: ['/'], callback: handleFocusInput }) - registerShortcut({ keys: ['Escape'], callback: handleBlurInput }) + return { + [NODE_SEARCH_KEYS.NODE_GROUP]: nodeGroups.filter(({ value }) => nodeGroupMap.has(value)), + [NODE_SEARCH_KEYS.LABEL]: labels.filter(({ value }) => labelMap.has(value)), + [NODE_K8S_VERSION_FILTER_KEY]: (nodeK8sVersionOptions || []).filter(({ value }) => + k8sNodeVersionMap.has(value), + ), } + }, [searchParams]) - return (): void => { - unregisterShortcut(['/']) - unregisterShortcut(['Escape']) - } - }, []) - + // HANDLERS const handleQueryParamsUpdate = (callback: (queryObject: ParsedQuery) => ParsedQuery) => { if (!callback) { return } - const queryObject = parseQueryString(location.search) - + const queryObject = parseQueryString(search) const finalQueryString = stringifyQueryString(callback(queryObject)) push(`?${finalQueryString}`) } - const handleQueryParamsSearch = (searchString: string) => { - handleQueryParamsUpdate((queryObject) => { - const finalQueryObject = structuredClone(queryObject) + const handleSearchInputKeyUp = (e: KeyboardEvent) => { + if (e.key === 'Escape' || e.key === 'Esc') { + searchInputRef.current?.blur() + } + } - Object.values(NODE_SEARCH_KEYS).forEach((key) => { - if (key === searchTextType) { - finalQueryObject[key] = searchString + const handleSearchFilterChange = + (nodeSearchKey: NODE_SEARCH_KEYS | typeof NODE_K8S_VERSION_FILTER_KEY) => + (filtersToApply: NodeSearchListOptionType[]) => { + handleQueryParamsUpdate((queryObject) => { + const updatedQueryObject = structuredClone(queryObject) + + if (filtersToApply.length) { + updatedQueryObject[nodeSearchKey] = filtersToApply.map(({ value }) => value).join(',') } else { - delete finalQueryObject[key] + delete updatedQueryObject[nodeSearchKey] } - }) - return finalQueryObject - }) - } - - const handleSearchTextChange = (event: ChangeEvent): void => { - setSearchInputText(event.target.value) - } - - const handleClearTextFilters = (): void => { - setSearchInputText('') + return updatedQueryObject + }) + } - setSearchTextType('') + const getOptionValue = ({ value, label, identifier }: NodeSearchListOptionType) => `${identifier}/${value}/${label}` + const handleRemoveFilter = (filterConfig: Partial>) => { handleQueryParamsUpdate((queryObject) => { - const finalQueryObject = structuredClone(queryObject) + const updatedQueryObject = structuredClone(queryObject) - Object.values(NODE_SEARCH_KEYS).forEach((key) => { - delete finalQueryObject[key] + Object.keys(filterConfig).forEach((filterKey) => { + if (filterConfig[filterKey].length) { + updatedQueryObject[filterKey] = filterConfig[filterKey].join(',') + } else { + delete updatedQueryObject[filterKey] + } }) - return finalQueryObject + return updatedQueryObject }) } - const handleKeyDownOnSearchInput = (event: KeyboardEvent): void => { - const { key } = event - - if (key === 'Enter') { - handleQueryParamsSearch(searchInputText) - - setIsSearchKeySelectorOpen(false) - } - - if (key === 'Backspace' && searchInputText.length === 0 && searchTextType) { - setSearchTextType('') - - setIsSearchKeySelectorOpen(false) - } - - if (key === 'Escape') { - event.currentTarget.blur() - } - } - - const handleToggleIsSearchKeySelectorOpen = () => { - setIsSearchKeySelectorOpen((prev) => !prev) - } - - const getSelectSearchKeyTypeHandler = (key: NODE_SEARCH_KEYS) => () => { - setSearchTextType(key) - setSearchInputText('') - setIsSearchKeySelectorOpen(false) - } - - const handleApplyNodeK8sVersion = (option: OptionType) => { + const handleClearFilters = () => { handleQueryParamsUpdate((queryObject) => { - const finalQueryObject = structuredClone(queryObject) + const updatedQueryObject = structuredClone(queryObject) - if (option.value === DEFAULT_NODE_K8S_VERSION.value) { - delete finalQueryObject[NODE_K8S_VERSION_FILTER_KEY] - } else { - finalQueryObject[NODE_K8S_VERSION_FILTER_KEY] = option.value - } + Object.values(NODE_SEARCH_KEYS).forEach((keyValue) => { + delete updatedQueryObject[keyValue] + }) - return finalQueryObject - }) - } + delete updatedQueryObject[NODE_K8S_VERSION_FILTER_KEY] - const handleOpenSearchKeySelectorMenu = () => { - setIsSearchKeySelectorOpen(true) + return updatedQueryObject + }) } - const renderTextFilter = (): JSX.Element => { - const placeholderText = NODE_SEARCH_KEY_PLACEHOLDER[searchTextType] - - return ( -
- - - {isSearchKeySelectorOpen && ( - <> - - ))} -
- )} - - )} - - {(searchTextType || searchInputText) && ( - - )} -
- ) - } + const getFormattedFilterLabel = (filterKey: NODE_SEARCH_KEYS) => NODE_SEARCH_KEY_TO_LABEL_PREFIX_MAP[filterKey] return ( -
- {renderTextFilter()} - - - -
+ <> +
+ - {allColumns.length ? ( - + filterSelectPickerPropsMap={{ + [NODE_SEARCH_KEYS.NODE_GROUP]: { + inputId: 'node-search-filter-node-groups', + placeholder: NODE_SEARCH_KEY_PLACEHOLDER[NODE_SEARCH_KEYS.NODE_GROUP], + options: [{ label: 'Node Groups', options: nodeGroups }], + getOptionValue, + appliedFilterOptions: appliedFilters[NODE_SEARCH_KEYS.NODE_GROUP], + handleApplyFilter: handleSearchFilterChange(NODE_SEARCH_KEYS.NODE_GROUP), + isDisabled: false, + isLoading: false, + }, + [NODE_SEARCH_KEYS.LABEL]: { + inputId: 'node-search-filter-labels', + placeholder: NODE_SEARCH_KEY_PLACEHOLDER[NODE_SEARCH_KEYS.LABEL], + options: [{ label: 'Labels', options: labels }], + getOptionValue, + appliedFilterOptions: appliedFilters[NODE_SEARCH_KEYS.LABEL], + handleApplyFilter: handleSearchFilterChange(NODE_SEARCH_KEYS.LABEL), + isDisabled: false, + isLoading: false, + }, + [NODE_K8S_VERSION_FILTER_KEY]: { + inputId: 'k8s-version-select', + placeholder: NODE_SEARCH_KEY_PLACEHOLDER[NODE_K8S_VERSION_FILTER_KEY], + options: [{ label: 'K8s version', options: nodeK8sVersionOptions || [] }], + getOptionValue, + appliedFilterOptions: appliedFilters[NODE_K8S_VERSION_FILTER_KEY], + handleApplyFilter: handleSearchFilterChange(NODE_K8S_VERSION_FILTER_KEY), + isDisabled: false, + isLoading: nodeK8sVersionsLoading, + optionListError: nodeK8sVersionsError, + reloadOptionList: refetchNodeK8sVersions, + }, }} + id="node-list-search-filter" + options={NODE_LIST_SEARCH_FILTER_OPTIONS} + isFilterApplied={isNodeSearchFilterApplied} + width={150} /> - ) : ( -
- )} -
+ +
+ + {allColumns.length ? ( + + ) : ( +
+ )} +
+ >> + className="px-20 py-16" + filterConfig={{ + [NODE_SEARCH_KEYS.NODE_GROUP]: appliedFilters[NODE_SEARCH_KEYS.NODE_GROUP].map( + ({ value }) => value, + ), + [NODE_SEARCH_KEYS.LABEL]: appliedFilters[NODE_SEARCH_KEYS.LABEL].map(({ value }) => value), + [NODE_K8S_VERSION_FILTER_KEY]: appliedFilters[NODE_K8S_VERSION_FILTER_KEY].map( + ({ value }) => value, + ), + }} + onRemoveFilter={handleRemoveFilter} + clearFilters={handleClearFilters} + getFormattedLabel={getFormattedFilterLabel} + /> + ) } diff --git a/src/components/ResourceBrowser/ResourceList/ResourceFilterOptions.tsx b/src/components/ResourceBrowser/ResourceList/ResourceFilterOptions.tsx index 9c79115f79..446c126949 100644 --- a/src/components/ResourceBrowser/ResourceList/ResourceFilterOptions.tsx +++ b/src/components/ResourceBrowser/ResourceList/ResourceFilterOptions.tsx @@ -22,7 +22,6 @@ import { ALL_NAMESPACE_OPTION, Checkbox, CHECKBOX_VALUE, - ComponentSizeType, GVK_FILTER_API_VERSION_QUERY_PARAM_KEY, GVK_FILTER_KIND_QUERY_PARAM_KEY, GVKOptionValueType, @@ -205,12 +204,11 @@ const ResourceFilterOptions = ({ <> {typeof renderRefreshBar === 'function' && renderRefreshBar()}
-
+
{isEventListing && ( [] => [ { id: NodeActionMenuOptionIdEnum.terminal, @@ -35,3 +42,30 @@ export const getNodeActions = (unschedulable: boolean): ActionMenuItemType['options'] = [ + { + items: [ + { id: NODE_SEARCH_KEYS.LABEL, label: 'Label' }, + { id: NODE_SEARCH_KEYS.NODE_GROUP, label: 'Node Groups' }, + { id: NODE_K8S_VERSION_FILTER_KEY, label: 'K8s Version' }, + ], + }, +] + +export const NODE_SEARCH_KEY_TO_LABEL_PREFIX_MAP: Record< + NODE_SEARCH_KEYS | typeof NODE_K8S_VERSION_FILTER_KEY, + string +> = { + [NODE_SEARCH_KEYS.NODE_GROUP]: 'Node group', + [NODE_SEARCH_KEYS.LABEL]: 'Label', + [NODE_K8S_VERSION_FILTER_KEY]: 'K8s version', +} + +export const NODE_SEARCH_KEY_PLACEHOLDER: Record = { + [NODE_SEARCH_KEYS.LABEL]: 'Search by key=value Eg. environment=production, tier=frontend', + [NODE_SEARCH_KEYS.NODE_GROUP]: 'Search by node group name Eg. mainnode', + [NODE_K8S_VERSION_FILTER_KEY]: 'Select K8s version', +} diff --git a/src/components/ResourceBrowser/ResourceList/types.ts b/src/components/ResourceBrowser/ResourceList/types.ts index 16e1b77da5..0ec17bb661 100644 --- a/src/components/ResourceBrowser/ResourceList/types.ts +++ b/src/components/ResourceBrowser/ResourceList/types.ts @@ -20,6 +20,7 @@ import { FiltersTypeEnum, K8sResourceDetailDataType, ResponseType, + SelectPickerOptionType, ServerErrors, TableCellComponentProps, TableProps, @@ -175,6 +176,10 @@ export interface ResourceRecommenderTableViewWrapperProps } > {} +export interface NodeSearchListOptionType extends SelectPickerOptionType { + identifier: NODE_SEARCH_KEYS +} + export interface ResourceListProps { selectedCluster: ClusterOptionType k8SObjectMapRaw: ResponseType diff --git a/src/components/ResourceBrowser/ResourceList/utils.tsx b/src/components/ResourceBrowser/ResourceList/utils.tsx index e60349bce7..624e3eb9a7 100644 --- a/src/components/ResourceBrowser/ResourceList/utils.tsx +++ b/src/components/ResourceBrowser/ResourceList/utils.tsx @@ -53,11 +53,12 @@ import { ClusterOptionType, K8SResourceListType, NODE_SEARCH_KEYS, + NodeListSearchFilterType, RBResourceSidebarDataAttributeType, ShowAIButtonConfig, } from '../Types' import { convertResourceGroupListToK8sObjectList } from '../Utils' -import { K8sResourceListFilterType, ResourceListUrlFiltersType } from './types' +import { K8sResourceListFilterType, NodeSearchListOptionType, ResourceListUrlFiltersType } from './types' const getFilterOptionsFromSearchParams = importComponentFromFELibrary( 'getFilterOptionsFromSearchParams', @@ -264,7 +265,7 @@ export const dynamicSort = (property: string) => (valueA: unknown, valueB: unkno export const isItemASearchMatchForNodeListing = (item: Record, searchParams: Record) => { const isK8sVersionFilterAppliedAndMatchFound = !searchParams[NODE_K8S_VERSION_FILTER_KEY] || - item[NODE_K8S_VERSION_FILTER_KEY] === searchParams[NODE_K8S_VERSION_FILTER_KEY] + searchParams[NODE_K8S_VERSION_FILTER_KEY].includes(item[NODE_K8S_VERSION_FILTER_KEY]) if (!isK8sVersionFilterAppliedAndMatchFound) { return false @@ -509,3 +510,42 @@ export const getRBSidebarTreeViewNodes = (list: ReturnType { + const { labels, nodeGroups } = (rows || []).reduce<{ + labels: Map + nodeGroups: Map + }>( + (acc, curr) => { + ;(curr.data.labels as { key: string; value: string }[]).forEach(({ key, value }) => { + if (!acc.labels.has(`${key}/${value}`)) { + acc.labels.set(`${key}/${value}`, { + label: `${key}=${value}`, + value: `${key}=${value}`, + identifier: NODE_SEARCH_KEYS.LABEL, + }) + } + }) + + if (!acc.nodeGroups.has(curr.data.nodeGroup as string)) { + acc.nodeGroups.set(curr.data.nodeGroup as string, { + label: curr.data.nodeGroup, + value: curr.data.nodeGroup as string, + identifier: NODE_SEARCH_KEYS.NODE_GROUP, + }) + } + + return acc + }, + { labels: new Map(), nodeGroups: new Map() }, + ) + + return { + labels: Array.from(labels) + .map(([, value]) => value) + .sort((a, b) => stringComparatorBySortOrder(a.label as string, b.label as string)), + nodeGroups: Array.from(nodeGroups) + .map(([, value]) => value) + .sort((a, b) => stringComparatorBySortOrder(a.label as string, b.label as string)), + } +} diff --git a/src/components/ResourceBrowser/Types.ts b/src/components/ResourceBrowser/Types.ts index 6122ebb377..8e0dc057df 100644 --- a/src/components/ResourceBrowser/Types.ts +++ b/src/components/ResourceBrowser/Types.ts @@ -258,14 +258,13 @@ export interface NodeRowDetail { export interface NodeListSearchFilterType extends Pick< - TableViewWrapperProps, - 'visibleColumns' | 'setVisibleColumns' | 'allColumns' + TableViewWrapperProps, + 'visibleColumns' | 'setVisibleColumns' | 'allColumns' | 'rows' | 'handleSearch' | 'searchKey' > { searchParams: Record } export enum NODE_SEARCH_KEYS { - NAME = 'name', LABEL = 'label', NODE_GROUP = 'nodeGroup', } diff --git a/src/index.tsx b/src/index.tsx index 0e28f9d617..0cefb7d0a3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -181,6 +181,7 @@ if (!window || !window._env_) { FEATURE_GROUPED_APP_LIST_FILTERS_ENABLE: true, FEATURE_FLUX_DEPLOYMENTS_ENABLE: false, FEATURE_LINK_EXTERNAL_FLUX_ENABLE: false, + FEATURE_CANARY_ROLLOUT_PROGRESS_ENABLE: true, } } diff --git a/yarn.lock b/yarn.lock index 6c09813c4d..1a7eb51295 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1722,9 +1722,9 @@ __metadata: languageName: node linkType: hard -"@devtron-labs/devtron-fe-common-lib@npm:1.18.0-pre-0": - version: 1.18.0-pre-0 - resolution: "@devtron-labs/devtron-fe-common-lib@npm:1.18.0-pre-0" +"@devtron-labs/devtron-fe-common-lib@npm:1.18.1-pre-0": + version: 1.18.1-pre-0 + resolution: "@devtron-labs/devtron-fe-common-lib@npm:1.18.1-pre-0" dependencies: "@codemirror/autocomplete": "npm:6.18.6" "@codemirror/lang-json": "npm:6.0.1" @@ -1774,7 +1774,7 @@ __metadata: react-select: 5.8.0 rxjs: ^7.8.1 yaml: ^2.4.1 - checksum: 10c0/1bcecf97315a63222d7063d181ec751fb7a8970e0de585e71d0725acaa2d0d60e264311404bd1c333ae2623fc9607ad965e47a26948156a7e4942b77bb1c1303 + checksum: 10c0/de563e385f44c1ed357fad7c2ce01989857a7acd3164958b7817e0a64fbb61648e4bff4aee207c3de2c18027b6364ab8f67a962420ddfd7aeebf813500dd985d languageName: node linkType: hard @@ -5721,7 +5721,7 @@ __metadata: version: 0.0.0-use.local resolution: "dashboard@workspace:." dependencies: - "@devtron-labs/devtron-fe-common-lib": "npm:1.18.0-pre-0" + "@devtron-labs/devtron-fe-common-lib": "npm:1.18.1-pre-0" "@esbuild-plugins/node-globals-polyfill": "npm:0.2.3" "@playwright/test": "npm:^1.32.1" "@rjsf/core": "npm:^5.13.3"