diff --git a/src/Assets/IconV2/ic-container-registry.svg b/src/Assets/IconV2/ic-container-registry.svg new file mode 100644 index 000000000..95c9543a8 --- /dev/null +++ b/src/Assets/IconV2/ic-container-registry.svg @@ -0,0 +1,19 @@ + + + + + diff --git a/src/Assets/IconV2/ic-container.svg b/src/Assets/IconV2/ic-container.svg index 95c9543a8..1dc10d7df 100644 --- a/src/Assets/IconV2/ic-container.svg +++ b/src/Assets/IconV2/ic-container.svg @@ -1,19 +1,3 @@ - - - - + + diff --git a/src/Assets/IconV2/ic-gavel.svg b/src/Assets/IconV2/ic-gavel.svg new file mode 100644 index 000000000..e2f32f692 --- /dev/null +++ b/src/Assets/IconV2/ic-gavel.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-minus.svg b/src/Assets/IconV2/ic-minus.svg index c3e3d3831..3f0a5f895 100644 --- a/src/Assets/IconV2/ic-minus.svg +++ b/src/Assets/IconV2/ic-minus.svg @@ -1,3 +1,3 @@ - \ No newline at end of file + diff --git a/src/Assets/IconV2/ic-speedometer.svg b/src/Assets/IconV2/ic-speedometer.svg new file mode 100644 index 000000000..b93c8c1f5 --- /dev/null +++ b/src/Assets/IconV2/ic-speedometer.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Assets/IconV2/ic-two-cubes.svg b/src/Assets/IconV2/ic-two-cubes.svg new file mode 100644 index 000000000..15ca41392 --- /dev/null +++ b/src/Assets/IconV2/ic-two-cubes.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index a5a013b75..c74a92f82 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -86,6 +86,7 @@ export const URLS = { // NOTE: using appId since we are re-using AppConfig component GLOBAL_CONFIG_TEMPLATES_DEVTRON_APP_DETAIL: `${GLOBAL_CONFIG_TEMPLATES_DEVTRON_APP}/detail/:appId`, LICENSE_AUTH: '/license-auth', + GLOBAL_CONFIG_EDIT_CLUSTER: '/global-config/cluster-env/edit/:clusterId', } as const export const ROUTES = { @@ -381,6 +382,7 @@ export const API_STATUS_CODES = { NOT_FOUND: 404, REQUEST_TIMEOUT: 408, CONFLICT: 409, + PRE_CONDITION_FAILED: 412, EXPECTATION_FAILED: 417, UNPROCESSABLE_ENTITY: 422, LOCKED: 423, diff --git a/src/Common/RJSF/rjsfForm.scss b/src/Common/RJSF/rjsfForm.scss index 2ada15fe7..3a158ed33 100644 --- a/src/Common/RJSF/rjsfForm.scss +++ b/src/Common/RJSF/rjsfForm.scss @@ -26,6 +26,10 @@ input.form__input { padding: 6px 8px; + + &[readonly] { + opacity: 0.5; + } } } diff --git a/src/Pages/ResourceBrowser/ResourceBrowser.Types.ts b/src/Pages/ResourceBrowser/ResourceBrowser.Types.ts index 237bb62a8..baad07dc7 100644 --- a/src/Pages/ResourceBrowser/ResourceBrowser.Types.ts +++ b/src/Pages/ResourceBrowser/ResourceBrowser.Types.ts @@ -14,8 +14,11 @@ * limitations under the License. */ -import { RefObject } from 'react' +import { Dispatch, RefObject, SetStateAction } from 'react' +import { GroupBase } from 'react-select' +import { ServerErrors } from '@Common/ServerError' +import { SelectPickerOptionType } from '@Shared/Components' import { Nodes, NodeType } from '@Shared/types' export interface GVKType { @@ -59,8 +62,54 @@ export interface K8sResourceListPayloadType { k8sRequest: ResourceListPayloadK8sRequestType } +export enum ResourceRecommenderHeaderType { + NAME = 'name', + NAMESPACE = 'namespace', + KIND = 'kind', + API_VERSION = 'apiVersion', + CONTAINER_NAME = 'containerName', + CPU_REQUEST = 'cpuRequest', + CPU_LIMIT = 'cpuLimit', + MEMORY_REQUEST = 'memoryRequest', + MEMORY_LIMIT = 'memoryLimit', +} + +export type ResourceRecommenderHeaderWithStringValue = Extract< + ResourceRecommenderHeaderType, + | ResourceRecommenderHeaderType.NAME + | ResourceRecommenderHeaderType.NAMESPACE + | ResourceRecommenderHeaderType.KIND + | ResourceRecommenderHeaderType.API_VERSION + | ResourceRecommenderHeaderType.CONTAINER_NAME +> + +export type ResourceRecommenderHeaderWithRecommendation = Extract< + ResourceRecommenderHeaderType, + | ResourceRecommenderHeaderType.CPU_REQUEST + | ResourceRecommenderHeaderType.CPU_LIMIT + | ResourceRecommenderHeaderType.MEMORY_REQUEST + | ResourceRecommenderHeaderType.MEMORY_LIMIT +> + export type K8sResourceDetailDataType = { [key: string]: string | number | object | boolean + additionalMetadata?: Partial< + Record< + ResourceRecommenderHeaderWithRecommendation, + { + // In case there is not limit or request set, it will be null + current: { + value: string | 'none' + } | null + // In case cron is yet to run + recommended: { + value: string | 'none' + } | null + // In case any of current or recommended is null, delta will be null + delta: number | null + } + > + > } export interface K8sResourceDetailType { @@ -69,6 +118,7 @@ export interface K8sResourceDetailType { } export interface BulkSelectionActionWidgetProps { + isResourceRecommendationView: boolean count: number handleOpenBulkDeleteModal: () => void handleClearBulkSelection: () => void @@ -76,12 +126,13 @@ export interface BulkSelectionActionWidgetProps { handleOpenUncordonNodeModal: () => void handleOpenDrainNodeModal: () => void handleOpenRestartWorkloadModal: () => void + handleOpenApplyResourceRecommendationModal: () => void parentRef: RefObject showBulkRestartOption: boolean showNodeListingOptions: boolean } -export type RBBulkOperationType = 'restart' | 'delete' | 'cordon' | 'uncordon' | 'drain' +export type RBBulkOperationType = 'restart' | 'delete' | 'cordon' | 'uncordon' | 'drain' | 'applyResourceRecommendation' export interface CreateResourceRequestBodyType { appId: string @@ -95,6 +146,7 @@ export interface CreateResourceRequestBodyType { export interface ResourceManifestDTO { manifestResponse: { manifest: Record + recommendedManifest?: Record } secretViewAccess: boolean } @@ -146,3 +198,21 @@ export interface NodeActionRequest { version: string kind: string } + +export interface GVKOptionValueType { + kind: string + apiVersion: string +} + +export interface GetResourceRecommenderResourceListPropsType { + resourceList: K8sResourceDetailType + reloadResourceListData: () => void + setShowAbsoluteValuesInResourceRecommender: Dispatch> + showAbsoluteValuesInResourceRecommender: boolean + gvkOptions: GroupBase>[] + areGVKOptionsLoading: boolean + reloadGVKOptions: () => void + gvkOptionsError: ServerErrors + isResourceListLoading: boolean + resourceListError: ServerErrors +} diff --git a/src/Pages/ResourceBrowser/constants.tsx b/src/Pages/ResourceBrowser/constants.tsx index 263605a5d..598939977 100644 --- a/src/Pages/ResourceBrowser/constants.tsx +++ b/src/Pages/ResourceBrowser/constants.tsx @@ -111,3 +111,6 @@ export const NODE_DRAIN_OPTIONS_CHECKBOX_CONFIG: { label: DRAIN_NODE_MODAL_MESSAGING.IgnoreDaemonSets.heading, }, ] as const + +export const GVK_FILTER_KIND_QUERY_PARAM_KEY = 'gvkFilterKind' +export const GVK_FILTER_API_VERSION_QUERY_PARAM_KEY = 'gvkFilterApiVersion' diff --git a/src/Pages/ResourceBrowser/service.ts b/src/Pages/ResourceBrowser/service.ts index 9c2318238..588f5d8a7 100644 --- a/src/Pages/ResourceBrowser/service.ts +++ b/src/Pages/ResourceBrowser/service.ts @@ -39,7 +39,9 @@ export const getK8sResourceList = ( export const createNewResource = ( resourceListPayload: CreateResourcePayload, -): Promise> => post(ROUTES.K8S_RESOURCE_CREATE, resourceListPayload) + abortControllerRef?: APIOptions['abortControllerRef'], +): Promise> => + post(ROUTES.K8S_RESOURCE_CREATE, resourceListPayload, { abortControllerRef }) export const deleteResource = ( resourceListPayload: ResourceListPayloadType, diff --git a/src/Shared/Components/Icon/Icon.tsx b/src/Shared/Components/Icon/Icon.tsx index 56cd6901e..1095c977d 100644 --- a/src/Shared/Components/Icon/Icon.tsx +++ b/src/Shared/Components/Icon/Icon.tsx @@ -45,6 +45,7 @@ import { ReactComponent as ICCluster } from '@IconsV2/ic-cluster.svg' import { ReactComponent as ICClusterIsolated } from '@IconsV2/ic-cluster-isolated.svg' import { ReactComponent as ICCode } from '@IconsV2/ic-code.svg' import { ReactComponent as ICContainer } from '@IconsV2/ic-container.svg' +import { ReactComponent as ICContainerRegistry } from '@IconsV2/ic-container-registry.svg' import { ReactComponent as ICCookr } from '@IconsV2/ic-cookr.svg' import { ReactComponent as ICCopy } from '@IconsV2/ic-copy.svg' import { ReactComponent as ICCpu } from '@IconsV2/ic-cpu.svg' @@ -84,6 +85,7 @@ import { ReactComponent as ICFilterApplied } from '@IconsV2/ic-filter-applied.sv import { ReactComponent as ICFlask } from '@IconsV2/ic-flask.svg' import { ReactComponent as ICFolderColor } from '@IconsV2/ic-folder-color.svg' import { ReactComponent as ICFolderUser } from '@IconsV2/ic-folder-user.svg' +import { ReactComponent as ICGavel } from '@IconsV2/ic-gavel.svg' import { ReactComponent as ICGear } from '@IconsV2/ic-gear.svg' import { ReactComponent as ICGift } from '@IconsV2/ic-gift.svg' import { ReactComponent as ICGiftGradient } from '@IconsV2/ic-gift-gradient.svg' @@ -160,6 +162,7 @@ import { ReactComponent as ICSortDescending } from '@IconsV2/ic-sort-descending. import { ReactComponent as ICSortable } from '@IconsV2/ic-sortable.svg' import { ReactComponent as ICSparkleAiColor } from '@IconsV2/ic-sparkle-ai-color.svg' import { ReactComponent as ICSparkleColor } from '@IconsV2/ic-sparkle-color.svg' +import { ReactComponent as ICSpeedometer } from '@IconsV2/ic-speedometer.svg' import { ReactComponent as ICSpinny } from '@IconsV2/ic-spinny.svg' import { ReactComponent as ICSprayCan } from '@IconsV2/ic-spray-can.svg' import { ReactComponent as ICStack } from '@IconsV2/ic-stack.svg' @@ -186,6 +189,7 @@ import { ReactComponent as ICTimeoutDash } from '@IconsV2/ic-timeout-dash.svg' import { ReactComponent as ICTimer } from '@IconsV2/ic-timer.svg' import { ReactComponent as ICTrafficSignal } from '@IconsV2/ic-traffic-signal.svg' import { ReactComponent as ICTravclan } from '@IconsV2/ic-travclan.svg' +import { ReactComponent as ICTwoCubes } from '@IconsV2/ic-two-cubes.svg' import { ReactComponent as ICUbuntu } from '@IconsV2/ic-ubuntu.svg' import { ReactComponent as ICUnknown } from '@IconsV2/ic-unknown.svg' import { ReactComponent as ICUserCircle } from '@IconsV2/ic-user-circle.svg' @@ -245,6 +249,7 @@ export const iconMap = { 'ic-cluster-isolated': ICClusterIsolated, 'ic-cluster': ICCluster, 'ic-code': ICCode, + 'ic-container-registry': ICContainerRegistry, 'ic-container': ICContainer, 'ic-cookr': ICCookr, 'ic-copy': ICCopy, @@ -285,6 +290,7 @@ export const iconMap = { 'ic-flask': ICFlask, 'ic-folder-color': ICFolderColor, 'ic-folder-user': ICFolderUser, + 'ic-gavel': ICGavel, 'ic-gear': ICGear, 'ic-gift-gradient': ICGiftGradient, 'ic-gift': ICGift, @@ -361,6 +367,7 @@ export const iconMap = { 'ic-sortable': ICSortable, 'ic-sparkle-ai-color': ICSparkleAiColor, 'ic-sparkle-color': ICSparkleColor, + 'ic-speedometer': ICSpeedometer, 'ic-spinny': ICSpinny, 'ic-spray-can': ICSprayCan, 'ic-stack': ICStack, @@ -387,6 +394,7 @@ export const iconMap = { 'ic-timer': ICTimer, 'ic-traffic-signal': ICTrafficSignal, 'ic-travclan': ICTravclan, + 'ic-two-cubes': ICTwoCubes, 'ic-ubuntu': ICUbuntu, 'ic-unknown': ICUnknown, 'ic-user-circle': ICUserCircle, diff --git a/src/Shared/Components/InfoBlock/InfoBlock.component.tsx b/src/Shared/Components/InfoBlock/InfoBlock.component.tsx index 0f5309242..194cd67a1 100644 --- a/src/Shared/Components/InfoBlock/InfoBlock.component.tsx +++ b/src/Shared/Components/InfoBlock/InfoBlock.component.tsx @@ -19,8 +19,9 @@ import { deriveBorderRadiusAndBorderClassFromConfig } from '@Shared/Helpers' import { Button } from '../Button' import { - CONTAINER_SIZE_TO_BUTTON_SIZE, CONTAINER_SIZE_TO_CLASS_MAP, + CONTAINER_SIZE_TO_ICON_BUTTON_SIZE, + CONTAINER_SIZE_TO_TEXT_BUTTON_SIZE, SIZE_TO_ICON_CLASS_MAP, VARIANT_TO_ICON_MAP, } from './constants' @@ -40,6 +41,9 @@ const InfoBlock = ({ const baseContainerClass = `${CONTAINER_SIZE_TO_CLASS_MAP[size]} ${VARIANT_TO_BG_MAP[variant]} ${VARIANT_TO_BORDER_MAP[variant]} ${deriveBorderRadiusAndBorderClassFromConfig({ borderConfig, borderRadiusConfig })} w-100 py-8 br-4 bw-1` const iconClass = `dc__no-shrink flex dc__fill-available-space ${SIZE_TO_ICON_CLASS_MAP[size]}` const icon = customIcon ?? VARIANT_TO_ICON_MAP[variant] + const buttonSize = buttonProps?.icon + ? CONTAINER_SIZE_TO_ICON_BUTTON_SIZE[size] + : CONTAINER_SIZE_TO_TEXT_BUTTON_SIZE[size] const renderIcon = () => {icon} @@ -99,7 +103,7 @@ const InfoBlock = ({ {renderContent()} - {buttonProps &&