From a72347017482a28058afad431e2357dfcd69f62a Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Fri, 25 Oct 2024 17:52:16 +0530 Subject: [PATCH 01/10] feat: add ToggleManifestConfigurationMode --- package.json | 2 +- .../v2/appDetails/appDetails.type.ts | 3 +- .../nodeDetail/NodeDetail.component.tsx | 140 +++++++++++------- .../NodeDetailTabs/Manifest.component.tsx | 39 ++++- yarn.lock | 8 +- 5 files changed, 132 insertions(+), 60 deletions(-) diff --git a/package.json b/package.json index a8c1f6f4eb..7a46a02cb2 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "homepage": "/dashboard", "dependencies": { - "@devtron-labs/devtron-fe-common-lib": "0.6.0-patch-1", + "@devtron-labs/devtron-fe-common-lib": "0.6.0-patch-1-beta-1", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@rjsf/core": "^5.13.3", "@rjsf/utils": "^5.13.3", diff --git a/src/components/v2/appDetails/appDetails.type.ts b/src/components/v2/appDetails/appDetails.type.ts index 23919ac814..ee3b51117e 100644 --- a/src/components/v2/appDetails/appDetails.type.ts +++ b/src/components/v2/appDetails/appDetails.type.ts @@ -470,6 +470,8 @@ export interface ManifestViewRefType { manifest: string activeManifestEditorData: string modifiedManifest: string + guiSchema: Record + unableToParseManifest: boolean } id: string } @@ -481,7 +483,6 @@ export enum ManifestCodeEditorMode { CANCEL = 'cancel', } - export interface ManifestActionPropsType extends ResourceInfoActionPropsType { hideManagedFields: boolean toggleManagedFields: (managedFieldsExist: boolean) => void diff --git a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx index 56ec29c78d..0fd14de91b 100644 --- a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx +++ b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx @@ -22,6 +22,7 @@ import { CHECKBOX_VALUE, OptionType, DeploymentAppTypes, + ConfigurationType, } from '@devtron-labs/devtron-fe-common-lib' import { ReactComponent as ICArrowsLeftRight } from '@Icons/ic-arrows-left-right.svg' import { ReactComponent as ICPencil } from '@Icons/ic-pencil.svg' @@ -56,6 +57,13 @@ import { ReactComponent as DeleteIcon } from '../../../../../assets/icons/ic-del import { CLUSTER_NODE_ACTIONS_LABELS } from '../../../../ClusterNodes/constants' import DeleteResourcePopup from '../../../../ResourceBrowser/ResourceList/DeleteResourcePopup' import { EDITOR_VIEW } from '@Config/constants' +import { importComponentFromFELibrary } from '@Components/common' + +const ToggleManifestConfigurationMode = importComponentFromFELibrary( + 'ToggleManifestConfigurationMode', + false, + 'function', +) const NodeDetailComponent = ({ loadingResources, @@ -140,8 +148,14 @@ const NodeDetailComponent = ({ const selectedContainerValue = isResourceBrowserView ? selectedResource?.name : podMetaData?.name const _selectedContainer = selectedContainer.get(selectedContainerValue) || containers?.[0]?.name || '' - const [selectedContainerName, setSelectedContainerName] = useState(({label: _selectedContainer, value: _selectedContainer})) + const [selectedContainerName, setSelectedContainerName] = useState({ + label: _selectedContainer, + value: _selectedContainer, + }) const [hideDeleteButton, setHideDeleteButton] = useState(false) + const [manifestConfigurationType, setManifestConfigurationType] = useState( + ConfigurationType.YAML, + ) // States uplifted from Manifest Component const manifestViewRef = useRef({ @@ -152,6 +166,8 @@ const NodeDetailComponent = ({ manifest: '', activeManifestEditorData: '', modifiedManifest: '', + guiSchema: {}, + unableToParseManifest: false, }, id: '', }) @@ -323,7 +339,7 @@ const NodeDetailComponent = ({ } const switchSelectedContainer = (containerName: string) => { - setSelectedContainerName({label: containerName, value: containerName}) + setSelectedContainerName({ label: containerName, value: containerName }) setSelectedContainer(selectedContainer.set(selectedContainerValue, containerName)) } @@ -366,68 +382,88 @@ const NodeDetailComponent = ({ ) } - const renderManifestTabHeader = () => ( - <> - {(isExternalApp || - isResourceBrowserView || - (appDetails.deploymentAppType === DeploymentAppTypes.GITOPS && - appDetails.deploymentAppDeleteRequest)) && - manifestCodeEditorMode && - !showManifestCompareView && - !isResourceMissing && ( - <> -
- {manifestCodeEditorMode === ManifestCodeEditorMode.EDIT ? ( -
+ const handleToggleManifestConfigurationMode = () => { + setManifestConfigurationType((prev) => + prev === ConfigurationType.YAML ? ConfigurationType.GUI : ConfigurationType.YAML, + ) + } + + const renderManifestTabHeader = () => { + const componentKey = getComponentKeyFromParams() + + return ( + <> + {(isExternalApp || + isResourceBrowserView || + (appDetails.deploymentAppType === DeploymentAppTypes.GITOPS && + appDetails.deploymentAppDeleteRequest)) && + manifestCodeEditorMode && + !showManifestCompareView && + !isResourceMissing && ( + <> +
+ {manifestCodeEditorMode === ManifestCodeEditorMode.EDIT ? ( +
+ {ToggleManifestConfigurationMode && ( + + )} + + + +
+ ) : ( - -
- ) : ( + )} + + )} + {manifestCodeEditorMode === ManifestCodeEditorMode.READ && + !showManifestCompareView && + (showDesiredAndCompareManifest || isResourceMissing) && ( + <> +
- )} - - )} - {manifestCodeEditorMode === ManifestCodeEditorMode.READ && - !showManifestCompareView && - (showDesiredAndCompareManifest || isResourceMissing) && ( - <> -
- - - )} - - ) + + )} + + ) + } return ( <> diff --git a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx index abe5089f3f..47ae856b91 100644 --- a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx +++ b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx @@ -42,7 +42,13 @@ import { } from '../nodeDetail.api' import IndexStore from '../../../index.store' import MessageUI, { MsgUIType } from '../../../../common/message.ui' -import { AppType, ManifestActionPropsType, ManifestCodeEditorMode, NodeType } from '../../../appDetails.type' +import { + AppType, + ManifestActionPropsType, + ManifestCodeEditorMode, + ManifestViewRefType, + NodeType, +} from '../../../appDetails.type' import { appendRefetchDataToUrl } from '../../../../../util/URLUtil' import { EA_MANIFEST_SECRET_EDIT_MODE_INFO_TEXT, @@ -99,6 +105,9 @@ const ManifestComponent = ({ const [showDecodedData, setShowDecodedData] = useState(false) const [secretViewAccess, setSecretViewAccess] = useState(false) + const [guiSchema, setGUISchema] = useState({}) + const [unableToParseManifest, setUnableToParseManifest] = useState(false) + const { isSuperAdmin } = useMainContext() // to show the cluster meta data at the bottom // Cancel is an intermediate state wherein edit is true const isEditMode = @@ -111,6 +120,8 @@ const ManifestComponent = ({ setDesiredManifest(manifestViewRef.current.data.desiredManifest) setManifest(manifestViewRef.current.data.manifest) setModifiedManifest(manifestViewRef.current.data.modifiedManifest) + setGUISchema(manifestViewRef.current.data.guiSchema) + setUnableToParseManifest(manifestViewRef.current.data.unableToParseManifest) if (showManifestCompareView) { setActiveManifestEditorData(manifestViewRef.current.data.manifest) @@ -128,11 +139,27 @@ const ManifestComponent = ({ manifest, activeManifestEditorData, modifiedManifest, + guiSchema, + unableToParseManifest, }, /* NOTE: id is unlikely to change but still kept as dep */ id, } - }, [error, secretViewAccess, desiredManifest, activeManifestEditorData, manifest, modifiedManifest, id]) + }, [ + error, + secretViewAccess, + desiredManifest, + activeManifestEditorData, + manifest, + modifiedManifest, + id, + guiSchema, + unableToParseManifest, + ]) + + const handleInitializeGUISchema = async () => { + setGUISchema({}) + } useEffect(() => { selectedTab(NodeDetailTab.MANIFEST, url) @@ -177,8 +204,10 @@ const ManifestComponent = ({ setLoading(false) setManifestCodeEditorMode(ManifestCodeEditorMode.READ) } else { + // TODO: Move to util and add gui call as well setLoading(true) + handleInitializeGUISchema() try { Promise.all([ !_isResourceMissing && @@ -279,6 +308,12 @@ const ManifestComponent = ({ const handleEditorValueChange = (codeEditorData: string) => { if (!showManifestCompareView && isEditMode) { setModifiedManifest(codeEditorData) + + try { + YAML.parse(codeEditorData) + } catch (err) { + setUnableToParseManifest(true) + } } } diff --git a/yarn.lock b/yarn.lock index b6456ca2ec..8cb1b8bd5e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1061,10 +1061,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@devtron-labs/devtron-fe-common-lib@0.6.0-patch-1": - version "0.6.0-patch-1" - resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-0.6.0-patch-1.tgz#f8f87d7f5f080e9199d70869be46e51bf9b03457" - integrity sha512-12tcyovuN1tr4ZwR2LEDLcednMUaqa3fHVUmNqKPkv3434+e2OIEAw5yezRU24XVsPVkWZcIFl3+MsnK2lIO0A== +"@devtron-labs/devtron-fe-common-lib@0.6.0-patch-1-beta-1": + version "0.6.0-patch-1-beta-1" + resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-0.6.0-patch-1-beta-1.tgz#9d11280d7cb37af476f93006e85b0c52b7eeef31" + integrity sha512-RfzEPqQe0qzZWfMUdO7oDlf3qBxMysgSJCsJpJRTEavffB1AAfO6amgERg4IJnR4lSeAd5T1x6Is2o9kb8G46A== dependencies: "@types/react-dates" "^21.8.6" ansi_up "^5.2.1" From 35b43d0ab73d8e8a59c0638d11867dac3fd7227c Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Fri, 25 Oct 2024 19:13:51 +0530 Subject: [PATCH 02/10] chore: migrate getResourceList to common lib --- .../K8sObjectPermissions/K8sListItemCard.tsx | 4 +-- src/components/ClusterNodes/types.ts | 6 ++-- .../ResourceBrowser.service.tsx | 10 +----- .../ResourceList/K8SResourceList.tsx | 13 +++++--- src/components/ResourceBrowser/Types.ts | 32 ++++++------------- src/components/ResourceBrowser/Utils.tsx | 22 +++++++------ src/config/constants.ts | 1 - 7 files changed, 37 insertions(+), 51 deletions(-) diff --git a/src/Pages/GlobalConfigurations/Authorization/Shared/components/K8sObjectPermissions/K8sListItemCard.tsx b/src/Pages/GlobalConfigurations/Authorization/Shared/components/K8sObjectPermissions/K8sListItemCard.tsx index 24328e4532..a94728ab54 100644 --- a/src/Pages/GlobalConfigurations/Authorization/Shared/components/K8sObjectPermissions/K8sListItemCard.tsx +++ b/src/Pages/GlobalConfigurations/Authorization/Shared/components/K8sObjectPermissions/K8sListItemCard.tsx @@ -28,6 +28,7 @@ import { OptionType, LoadingIndicator, GVKType, + getK8sResourceList, EntityTypes, } from '@devtron-labs/devtron-fe-common-lib' import CreatableSelect from 'react-select/creatable' @@ -42,7 +43,6 @@ import { import { getClusterList, getResourceGroupList, - getResourceList, namespaceListByClusterId, } from '../../../../../../components/ResourceBrowser/ResourceBrowser.service' import { K8SObjectType, ResourceListPayloadType } from '../../../../../../components/ResourceBrowser/Types' @@ -152,7 +152,7 @@ const K8sListItemCard = ({ }, }, } - const { result } = await getResourceList(resourceListPayload) + const { result } = await getK8sResourceList(resourceListPayload) if (result) { const _data = result.data?.map((ele) => ({ label: ele.name, value: ele.name })).sort(sortOptionsByLabel) ?? [] diff --git a/src/components/ClusterNodes/types.ts b/src/components/ClusterNodes/types.ts index 141d1bc0d2..c6e146f9d9 100644 --- a/src/components/ClusterNodes/types.ts +++ b/src/components/ClusterNodes/types.ts @@ -16,11 +16,11 @@ import React from 'react' import { MultiValue } from 'react-select' -import { ResponseType, ApiResourceGroupType } from '@devtron-labs/devtron-fe-common-lib' +import { ResponseType, ApiResourceGroupType, K8sResourceDetailDataType } from '@devtron-labs/devtron-fe-common-lib' import { LabelTag, OptionType } from '../app/types' import { CLUSTER_PAGE_TAB, NODE_SEARCH_TEXT } from './constants' import { EditModeType } from '../v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/terminal/constants' -import { ClusterOptionType, ResourceDetailDataType } from '../ResourceBrowser/Types' +import { ClusterOptionType } from '../ResourceBrowser/Types' import { useTabs } from '../common/DynamicTabs' export enum ERROR_TYPE { @@ -133,7 +133,7 @@ export interface NodeListResponse extends ResponseType { result?: NodeRowDetail[] } -export interface PodType extends ResourceDetailDataType { +export interface PodType extends K8sResourceDetailDataType { name: string namespace: string cpu: ResourceDetail diff --git a/src/components/ResourceBrowser/ResourceBrowser.service.tsx b/src/components/ResourceBrowser/ResourceBrowser.service.tsx index 75b338f55c..1de5eece3f 100644 --- a/src/components/ResourceBrowser/ResourceBrowser.service.tsx +++ b/src/components/ResourceBrowser/ResourceBrowser.service.tsx @@ -17,7 +17,7 @@ import { ApiResourceType, get, post, ResponseType, ApiResourceGroupType } from '@devtron-labs/devtron-fe-common-lib' import { Routes } from '../../config' import { ClusterListResponse } from '../../services/service.types' -import { CreateResourcePayload, CreateResourceResponse, ResourceListPayloadType, ResourceListResponse } from './Types' +import { CreateResourcePayload, CreateResourceResponse, ResourceListPayloadType } from './Types' import { ALL_NAMESPACE_OPTION } from './Constants' export const getClusterList = (): Promise => get(Routes.CLUSTER_LIST_PERMISSION) @@ -25,14 +25,6 @@ export const getClusterList = (): Promise => get(Routes.CLU export const namespaceListByClusterId = (clusterId: string): Promise => get(`${Routes.CLUSTER_NAMESPACE}/${clusterId}`) -export const getResourceList = ( - resourceListPayload: ResourceListPayloadType, - signal?: AbortSignal, -): Promise => - post(Routes.K8S_RESOURCE_LIST, resourceListPayload, { - signal, - }) - export const getResourceGroupList = (clusterId: string, signal?: AbortSignal): Promise> => get(`${Routes.API_RESOURCE}/${clusterId}`, { signal, diff --git a/src/components/ResourceBrowser/ResourceList/K8SResourceList.tsx b/src/components/ResourceBrowser/ResourceList/K8SResourceList.tsx index c6290a4e7c..37d69bd39a 100644 --- a/src/components/ResourceBrowser/ResourceList/K8SResourceList.tsx +++ b/src/components/ResourceBrowser/ResourceList/K8SResourceList.tsx @@ -33,6 +33,9 @@ import { useStateFilters, ClipboardButton, Tooltip, + getK8sResourceList, + K8sResourceDetailDataType, + K8sResourceDetailType, } from '@devtron-labs/devtron-fe-common-lib' import WebWorker from '../../app/WebWorker' import searchWorker from '../../../config/searchWorker' @@ -48,8 +51,8 @@ import { SEARCH_QUERY_PARAM_KEY, DEFAULT_K8SLIST_PAGE_SIZE, } from '../Constants' -import { getResourceList, getResourceListPayload } from '../ResourceBrowser.service' -import { K8SResourceListType, ResourceDetailDataType, ResourceDetailType, URLParams } from '../Types' +import { getResourceListPayload } from '../ResourceBrowser.service' +import { K8SResourceListType, URLParams } from '../Types' import ResourceListEmptyState from './ResourceListEmptyState' import { EventList } from './EventList' import ResourceFilterOptions from './ResourceFilterOptions' @@ -92,7 +95,7 @@ export const K8SResourceList = ({ const [selectedNamespace, setSelectedNamespace] = useState(ALL_NAMESPACE_OPTION) const [resourceListOffset, setResourceListOffset] = useState(0) const [pageSize, setPageSize] = useState(DEFAULT_K8SLIST_PAGE_SIZE) - const [filteredResourceList, setFilteredResourceList] = useState(null) + const [filteredResourceList, setFilteredResourceList] = useState(null) // REFS const resourceListRef = useRef(null) @@ -111,7 +114,7 @@ export const K8SResourceList = ({ () => abortPreviousRequests( () => - getResourceList( + getK8sResourceList( getResourceListPayload( clusterId, selectedNamespace.value.toLowerCase(), @@ -281,7 +284,7 @@ export const K8SResourceList = ({ const gridTemplateColumns = `350px repeat(${(resourceList?.headers.length ?? 1) - 1}, 180px)` - const renderResourceRow = (resourceData: ResourceDetailDataType): JSX.Element => ( + const renderResourceRow = (resourceData: K8sResourceDetailDataType): JSX.Element => (
> @@ -148,20 +137,19 @@ export interface K8SResourceListType extends ResourceFilterOptionsProps { export interface ResourceBrowserActionMenuType { clusterId: string - resourceData: ResourceDetailDataType + resourceData: K8sResourceDetailDataType selectedResource: ApiResourceGroupType handleResourceClick: (e: React.MouseEvent) => void removeTabByIdentifier?: ReturnType['removeTabByIdentifier'] getResourceListData?: () => Promise } -export interface DeleteResourcePopupType { - clusterId: string - resourceData: ResourceDetailDataType - selectedResource: ApiResourceGroupType +export interface DeleteResourcePopupType + extends Pick< + ResourceBrowserActionMenuType, + 'clusterId' | 'resourceData' | 'selectedResource' | 'getResourceListData' | 'removeTabByIdentifier' + > { toggleDeleteDialog: () => void - removeTabByIdentifier?: ReturnType['removeTabByIdentifier'] - getResourceListData?: () => Promise } export interface ResourceListEmptyStateType { @@ -174,7 +162,7 @@ export interface ResourceListEmptyStateType { export interface EventListType { listRef: React.MutableRefObject - filteredData: ResourceDetailType['data'] + filteredData: K8sResourceDetailType['data'] handleResourceClick: (e: React.MouseEvent) => void paginatedView: boolean syncError: boolean diff --git a/src/components/ResourceBrowser/Utils.tsx b/src/components/ResourceBrowser/Utils.tsx index 58479762d5..04734b239c 100644 --- a/src/components/ResourceBrowser/Utils.tsx +++ b/src/components/ResourceBrowser/Utils.tsx @@ -17,7 +17,12 @@ import React from 'react' import queryString from 'query-string' import { useLocation } from 'react-router-dom' -import { ApiResourceGroupType, DATE_TIME_FORMAT_STRING, GVKType } from '@devtron-labs/devtron-fe-common-lib' +import { + ApiResourceGroupType, + DATE_TIME_FORMAT_STRING, + GVKType, + K8sResourceDetailDataType, +} from '@devtron-labs/devtron-fe-common-lib' import moment from 'moment' import { URLS, LAST_SEEN } from '../../config' import { eventAgeComparator, processK8SObjects } from '../common' @@ -30,7 +35,6 @@ import { K8SObjectType, K8sObjectOptionType, FIXED_TABS_INDICES, - ResourceDetailDataType, } from './Types' import { InitTabType } from '../common/DynamicTabs/Types' import TerminalIcon from '../../assets/icons/ic-terminal-fill.svg' @@ -97,12 +101,12 @@ export const getK8SObjectMapAfterGroupHeadingClick = ( return _k8SObjectMap } -export const sortEventListData = (eventList: ResourceDetailDataType[]): ResourceDetailDataType[] => { +export const sortEventListData = (eventList: K8sResourceDetailDataType[]): K8sResourceDetailDataType[] => { if (!eventList?.length) { return [] } - const warningEvents: ResourceDetailDataType[] = [] - const otherEvents: ResourceDetailDataType[] = [] + const warningEvents: K8sResourceDetailDataType[] = [] + const otherEvents: K8sResourceDetailDataType[] = [] eventList.forEach((event) => { if (event.type === 'Warning') { warningEvents.push(event) @@ -111,12 +115,12 @@ export const sortEventListData = (eventList: ResourceDetailDataType[]): Resource } }) return [ - ...warningEvents.sort(eventAgeComparator(LAST_SEEN)), - ...otherEvents.sort(eventAgeComparator(LAST_SEEN)), + ...warningEvents.sort(eventAgeComparator(LAST_SEEN)), + ...otherEvents.sort(eventAgeComparator(LAST_SEEN)), ] } -export const removeDefaultForStorageClass = (storageList: ResourceDetailDataType[]): ResourceDetailDataType[] => +export const removeDefaultForStorageClass = (storageList: K8sResourceDetailDataType[]): K8sResourceDetailDataType[] => storageList.map((storage) => (storage.name as string).includes('(default)') ? { @@ -341,7 +345,7 @@ export const getResourceFromK8SObjectMap = (map: ApiResourceGroupType[], nodeTyp export const getRenderNodeButton = ( - resourceData: ResourceDetailDataType, + resourceData: K8sResourceDetailDataType, columnName: string, handleNodeClick: (e: React.MouseEvent) => void, ) => diff --git a/src/config/constants.ts b/src/config/constants.ts index d7810d56dd..b01a5dd161 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -234,7 +234,6 @@ export const Routes = { POD_EVENTS: 'pod/events', UPDATE_HELM_APP_META_INFO: 'app-store/deployment/application/update/project', API_RESOURCE: 'k8s/api-resources', - K8S_RESOURCE_LIST: 'k8s/resource/list', K8S_RESOURCE_CREATE: 'k8s/resources/apply', CLUSTER_LIST_PERMISSION: 'cluster/auth-list', ENVIRONMENT_APPS: 'env/app-grouping', From 190c87db3a995b828129b56d3519750d24607359 Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Mon, 28 Oct 2024 15:13:15 +0530 Subject: [PATCH 03/10] feat: handle gui view in manifest mode --- .../v2/appDetails/appDetails.type.ts | 5 +- .../nodeDetail/NodeDetail.component.tsx | 140 +++++----- .../NodeDetailTabs/Manifest.component.tsx | 245 ++++++++++++------ .../k8Resource/nodeDetail/nodeDetail.api.ts | 83 ++++-- .../k8Resource/nodeDetail/nodeDetail.type.ts | 11 +- .../k8Resource/nodeDetail/nodeDetail.util.ts | 13 +- src/css/base.scss | 22 ++ 7 files changed, 350 insertions(+), 169 deletions(-) diff --git a/src/components/v2/appDetails/appDetails.type.ts b/src/components/v2/appDetails/appDetails.type.ts index ee3b51117e..207af8ff73 100644 --- a/src/components/v2/appDetails/appDetails.type.ts +++ b/src/components/v2/appDetails/appDetails.type.ts @@ -21,6 +21,7 @@ import { Node as CommonNode, iNode as CommoniNode, ApiResourceGroupType, + ConfigurationType, } from '@devtron-labs/devtron-fe-common-lib' import { ExternalLink, OptionTypeWithIcon } from '../../externalLinks/ExternalLinks.type' import { iLink } from '../utils/tabUtils/link.type' @@ -471,7 +472,6 @@ export interface ManifestViewRefType { activeManifestEditorData: string modifiedManifest: string guiSchema: Record - unableToParseManifest: boolean } id: string } @@ -492,6 +492,9 @@ export interface ManifestActionPropsType extends ResourceInfoActionPropsType { setShowManifestCompareView: Dispatch> manifestCodeEditorMode: ManifestCodeEditorMode setManifestCodeEditorMode: Dispatch> + handleSwitchToYAMLMode: () => void + manifestFormConfigurationType: ConfigurationType + handleUpdateUnableToParseManifest: (value: boolean) => void } export interface NodeTreeDetailTabProps { diff --git a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx index 0fd14de91b..5ec83693ca 100644 --- a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx +++ b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx @@ -35,6 +35,7 @@ import SummaryComponent from './NodeDetailTabs/Summary.component' import { NodeDetailTab, ParamsType } from './nodeDetail.type' import { AppType, + ManifestActionPropsType, ManifestCodeEditorMode, ManifestViewRefType, NodeDetailPropsType, @@ -153,9 +154,10 @@ const NodeDetailComponent = ({ value: _selectedContainer, }) const [hideDeleteButton, setHideDeleteButton] = useState(false) - const [manifestConfigurationType, setManifestConfigurationType] = useState( + const [manifestFormConfigurationType, setManifestFormConfigurationType] = useState( ConfigurationType.YAML, ) + const [unableToParseManifest, setUnableToParseManifest] = useState(false) // States uplifted from Manifest Component const manifestViewRef = useRef({ @@ -167,7 +169,6 @@ const NodeDetailComponent = ({ activeManifestEditorData: '', modifiedManifest: '', guiSchema: {}, - unableToParseManifest: false, }, id: '', }) @@ -383,87 +384,91 @@ const NodeDetailComponent = ({ } const handleToggleManifestConfigurationMode = () => { - setManifestConfigurationType((prev) => + setManifestFormConfigurationType((prev) => prev === ConfigurationType.YAML ? ConfigurationType.GUI : ConfigurationType.YAML, ) } - const renderManifestTabHeader = () => { - const componentKey = getComponentKeyFromParams() + const handleSwitchToYAMLMode = () => { + setManifestFormConfigurationType(ConfigurationType.YAML) + } + + const handleUpdateUnableToParseManifest: ManifestActionPropsType['handleUpdateUnableToParseManifest'] = ( + value: boolean, + ) => { + setUnableToParseManifest(value) + } + + const renderManifestTabHeader = () => ( + <> + {(isExternalApp || + isResourceBrowserView || + (appDetails.deploymentAppType === DeploymentAppTypes.GITOPS && + appDetails.deploymentAppDeleteRequest)) && + manifestCodeEditorMode && + !showManifestCompareView && + !isResourceMissing && ( + <> +
+ {manifestCodeEditorMode === ManifestCodeEditorMode.EDIT ? ( +
+ {ToggleManifestConfigurationMode && ( + + )} - return ( - <> - {(isExternalApp || - isResourceBrowserView || - (appDetails.deploymentAppType === DeploymentAppTypes.GITOPS && - appDetails.deploymentAppDeleteRequest)) && - manifestCodeEditorMode && - !showManifestCompareView && - !isResourceMissing && ( - <> -
- {manifestCodeEditorMode === ManifestCodeEditorMode.EDIT ? ( -
- {ToggleManifestConfigurationMode && ( - - )} - - - -
- ) : ( - )} - - )} - {manifestCodeEditorMode === ManifestCodeEditorMode.READ && - !showManifestCompareView && - (showDesiredAndCompareManifest || isResourceMissing) && ( - <> -
+ +
+ ) : ( - - )} - - ) - } + )} + + )} + {manifestCodeEditorMode === ManifestCodeEditorMode.READ && + !showManifestCompareView && + (showDesiredAndCompareManifest || isResourceMissing) && ( + <> +
+ + + )} + + ) return ( <> @@ -560,6 +565,9 @@ const NodeDetailComponent = ({ setShowManifestCompareView={setShowManifestCompareView} manifestCodeEditorMode={manifestCodeEditorMode} setManifestCodeEditorMode={setManifestCodeEditorMode} + handleSwitchToYAMLMode={handleSwitchToYAMLMode} + manifestFormConfigurationType={manifestFormConfigurationType} + handleUpdateUnableToParseManifest={handleUpdateUnableToParseManifest} /> diff --git a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx index 47ae856b91..7604a08d85 100644 --- a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx +++ b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx @@ -30,14 +30,21 @@ import { ToastManager, ToastVariantType, TOAST_ACCESS_DENIED, + FormProps, + ConfigurationType, + YAMLStringify, + InfoColourBar, } from '@devtron-labs/devtron-fe-common-lib' import Tippy from '@tippyjs/react' import { ReactComponent as ICClose } from '@Icons/ic-close.svg' +import { ReactComponent as ICErrorExclamation } from '@Icons/ic-error-exclamation.svg' +import { ReactComponent as ICInfoFilled } from '@Icons/ic-info-filled.svg' import { NodeDetailTab } from '../nodeDetail.type' import { createResource, getDesiredManifestResource, getManifestResource, + getResourceRequestPayload, updateManifestResourceHelmApps, } from '../nodeDetail.api' import IndexStore from '../../../index.store' @@ -60,6 +67,10 @@ import { SAVE_DATA_VALIDATION_ERROR_MSG, } from '../../../../values/chartValuesDiff/ChartValuesView.constants' import { getDecodedEncodedSecretManifestData, getTrimmedManifestData } from '../nodeDetail.util' +import { importComponentFromFELibrary } from '@Components/common' + +const getManifestGUISchema = importComponentFromFELibrary('getManifestGUISchema', null, 'function') +const ManifestGUIView = importComponentFromFELibrary('ManifestGUIView', null, 'function') const ManifestComponent = ({ selectedTab, @@ -74,6 +85,9 @@ const ManifestComponent = ({ setShowManifestCompareView, manifestCodeEditorMode, setManifestCodeEditorMode, + manifestFormConfigurationType, + handleSwitchToYAMLMode, + handleUpdateUnableToParseManifest, }: ManifestActionPropsType) => { const location = useLocation() const history = useHistory() @@ -106,7 +120,6 @@ const ManifestComponent = ({ const [secretViewAccess, setSecretViewAccess] = useState(false) const [guiSchema, setGUISchema] = useState({}) - const [unableToParseManifest, setUnableToParseManifest] = useState(false) const { isSuperAdmin } = useMainContext() // to show the cluster meta data at the bottom // Cancel is an intermediate state wherein edit is true @@ -121,7 +134,6 @@ const ManifestComponent = ({ setManifest(manifestViewRef.current.data.manifest) setModifiedManifest(manifestViewRef.current.data.modifiedManifest) setGUISchema(manifestViewRef.current.data.guiSchema) - setUnableToParseManifest(manifestViewRef.current.data.unableToParseManifest) if (showManifestCompareView) { setActiveManifestEditorData(manifestViewRef.current.data.manifest) @@ -130,6 +142,8 @@ const ManifestComponent = ({ } } + const isReadOnlyView = showManifestCompareView || !isEditMode + useEffectAfterMount(() => { manifestViewRef.current = { data: { @@ -140,25 +154,32 @@ const ManifestComponent = ({ activeManifestEditorData, modifiedManifest, guiSchema, - unableToParseManifest, }, /* NOTE: id is unlikely to change but still kept as dep */ id, } - }, [ - error, - secretViewAccess, - desiredManifest, - activeManifestEditorData, - manifest, - modifiedManifest, - id, - guiSchema, - unableToParseManifest, - ]) - - const handleInitializeGUISchema = async () => { - setGUISchema({}) + }, [error, secretViewAccess, desiredManifest, activeManifestEditorData, manifest, modifiedManifest, id, guiSchema]) + + const handleInitializeGUISchema = async (abortSignal: AbortSignal) => { + if (!getManifestGUISchema) { + return + } + + const resourceRequestPayload = getResourceRequestPayload({ + appDetails, + nodeName: params.podName, + nodeType: params.nodeType, + isResourceBrowserView, + selectedResource, + }) + + const guiSchemaResponse = await getManifestGUISchema({ + clusterId: resourceRequestPayload.clusterId, + gvk: resourceRequestPayload.k8sRequest.resourceIdentifier.groupVersionKind, + signal: abortSignal, + }) + + setGUISchema(guiSchemaResponse) } useEffect(() => { @@ -204,10 +225,7 @@ const ManifestComponent = ({ setLoading(false) setManifestCodeEditorMode(ManifestCodeEditorMode.READ) } else { - // TODO: Move to util and add gui call as well setLoading(true) - - handleInitializeGUISchema() try { Promise.all([ !_isResourceMissing && @@ -221,6 +239,7 @@ const ManifestComponent = ({ ), _showDesiredAndCompareManifest && getDesiredManifestResource(appDetails, params.podName, params.nodeType, abortController.signal), + handleInitializeGUISchema(abortController.signal), ]) .then((response) => { setSecretViewAccess(response[0]?.result?.secretViewAccess || false) @@ -272,7 +291,7 @@ const ManifestComponent = ({ } if (isEditMode) { try { - const jsonManifestData = YAML.parse(activeManifestEditorData) + const jsonManifestData = YAML.parse(modifiedManifest) if (jsonManifestData?.metadata?.managedFields) { setTrimedManifestEditorData(getTrimmedManifestData(jsonManifestData, true) as string) } @@ -308,15 +327,22 @@ const ManifestComponent = ({ const handleEditorValueChange = (codeEditorData: string) => { if (!showManifestCompareView && isEditMode) { setModifiedManifest(codeEditorData) + // Question: Should we directly set this in case of errored string? + setTrimedManifestEditorData(codeEditorData) try { YAML.parse(codeEditorData) - } catch (err) { - setUnableToParseManifest(true) + handleUpdateUnableToParseManifest(false) + } catch { + handleUpdateUnableToParseManifest(true) } } } + const handleGUIViewValueChange: FormProps['onChange'] = (data) => { + handleEditorValueChange(YAMLStringify(data.formData)) + } + const handleEditLiveManifest = () => { toggleManagedFields(false) setActiveManifestEditorData(modifiedManifest) @@ -447,7 +473,12 @@ const ManifestComponent = ({ const handleDesiredManifestClose = () => setShowManifestCompareView(false) const renderShowDecodedValueCheckbox = () => { - const jsonManifestData = YAML.parse(trimedManifestEditorData) + let jsonManifestData + try { + jsonManifestData = YAML.parse(trimedManifestEditorData) + } catch { + return null + } if (jsonManifestData?.kind === 'Secret' && !isEditMode && secretViewAccess) { return ( { + if (!showInfoText) { + return null + } + + const message = + isEditMode && !showManifestCompareView + ? EA_MANIFEST_SECRET_EDIT_MODE_INFO_TEXT + : EA_MANIFEST_SECRET_INFO_TEXT + + if (isCodeEditorView) { + return ( + + {renderShowDecodedValueCheckbox()} + + ) + } + + return ( + + ) + } + + const renderErrorBar = (isCodeEditorView: boolean = false) => { + if (showManifestCompareView || !errorText) { + return null + } + + if (isCodeEditorView) { + return + } + + return ( + + ) + } + + const renderContent = () => { + if (!isReadOnlyView && manifestFormConfigurationType === ConfigurationType.GUI) { + return ( + <> + {renderEditorInfo()} + {renderErrorBar()} + + + ) + } + + return ( + + } + focus={isEditMode} + > + {renderEditorInfo(true)} + + {showManifestCompareView && ( + +
+
+ Desired manifest + +
+
Live manifest
+
+
+ )} + + {renderErrorBar(true)} +
+ ) + } + return isDeleted ? (
) : ( - - } - focus={isEditMode} - > - {showInfoText && ( - - {renderShowDecodedValueCheckbox()} - - )} - {showManifestCompareView && ( - -
-
- Desired manifest - -
-
Live manifest
-
-
- )} - {!showManifestCompareView && errorText && } -
+ renderContent() )}
)} diff --git a/src/components/v2/appDetails/k8Resource/nodeDetail/nodeDetail.api.ts b/src/components/v2/appDetails/k8Resource/nodeDetail/nodeDetail.api.ts index 920867e6cf..3aa9662ddd 100644 --- a/src/components/v2/appDetails/k8Resource/nodeDetail/nodeDetail.api.ts +++ b/src/components/v2/appDetails/k8Resource/nodeDetail/nodeDetail.api.ts @@ -17,7 +17,12 @@ import { DeploymentAppTypes, post, put, trash, Host, HandleDownloadProps } from '@devtron-labs/devtron-fe-common-lib' import { CUSTOM_LOGS_FILTER, Routes } from '../../../../../config' import { AppDetails, AppType, SelectedResourceType } from '../../appDetails.type' -import { AppDetailsAppIdentifierProps, EphemeralContainerProps, ParamsType } from './nodeDetail.type' +import { + AppDetailsAppIdentifierProps, + EphemeralContainerProps, + GetResourceRequestPayloadParamsType, + ParamsType, +} from './nodeDetail.type' import { getDeploymentType, getK8sResourcePayloadAppType } from './nodeDetail.util' import { FluxCDTemplateType } from '@Components/app/list-new/AppListType' @@ -49,9 +54,14 @@ export const getManifestResource = ( selectedResource?: SelectedResourceType, signal?: AbortSignal, ) => { - const requestData = isResourceBrowserView - ? createResourceRequestBody(selectedResource) - : createBody(ad, podName, nodeType) + const requestData = getResourceRequestPayload({ + appDetails: ad, + nodeName: podName, + nodeType, + isResourceBrowserView, + selectedResource, + }) + return post(Routes.MANIFEST, requestData, { signal }) } @@ -154,6 +164,20 @@ export function createBody(appDetails: AppDetails, nodeName: string, nodeType: s return requestBody } +// TODO: Need to thoroughly review this code util and its replacements +export const getResourceRequestPayload = ({ + appDetails, + nodeName, + nodeType, + isResourceBrowserView, + selectedResource, + updatedManifest, +}: GetResourceRequestPayloadParamsType) => { + return isResourceBrowserView + ? createResourceRequestBody(selectedResource, updatedManifest) + : createBody(appDetails, nodeName, nodeType, updatedManifest) +} + export const updateManifestResourceHelmApps = ( ad: AppDetails, nodeName: string, @@ -162,24 +186,36 @@ export const updateManifestResourceHelmApps = ( isResourceBrowserView?: boolean, selectedResource?: SelectedResourceType, ) => { - const requestData = isResourceBrowserView - ? createResourceRequestBody(selectedResource, updatedManifest) - : createBody(ad, nodeName, nodeType, updatedManifest) - return put(Routes.MANIFEST, requestData) + return put( + Routes.MANIFEST, + getResourceRequestPayload({ + appDetails: ad, + nodeName, + nodeType, + isResourceBrowserView, + selectedResource, + updatedManifest, + }), + ) } -function getEventHelmApps( +const getEventHelmApps = ( ad: AppDetails, nodeName: string, nodeType: string, isResourceBrowserView?: boolean, selectedResource?: SelectedResourceType, -) { - const requestData = isResourceBrowserView - ? createResourceRequestBody(selectedResource) - : createBody(ad, nodeName, nodeType) - return post(Routes.EVENTS, requestData) -} +) => + post( + Routes.EVENTS, + getResourceRequestPayload({ + appDetails: ad, + nodeName, + nodeType, + selectedResource, + isResourceBrowserView, + }), + ) const getFilterWithValue = (type: string, value: string, unit?: string) => { switch (type) { @@ -297,12 +333,17 @@ export const createResource = ( nodeType: string, isResourceBrowserView?: boolean, selectedResource?: SelectedResourceType, -) => { - const requestData = isResourceBrowserView - ? createResourceRequestBody(selectedResource) - : createBody(ad, podName, nodeType) - return post(Routes.CREATE_RESOURCE, requestData) -} +) => + post( + Routes.CREATE_RESOURCE, + getResourceRequestPayload({ + appDetails: ad, + nodeName: podName, + nodeType, + isResourceBrowserView, + selectedResource, + }), + ) const getEphemeralURL = (isResourceBrowserView: boolean, params: ParamsType, appType: string, appIds: string) => { let url: string = Routes.EPHEMERAL_CONTAINERS diff --git a/src/components/v2/appDetails/k8Resource/nodeDetail/nodeDetail.type.ts b/src/components/v2/appDetails/k8Resource/nodeDetail/nodeDetail.type.ts index 6b4dfbb2e2..243b8b2ec6 100644 --- a/src/components/v2/appDetails/k8Resource/nodeDetail/nodeDetail.type.ts +++ b/src/components/v2/appDetails/k8Resource/nodeDetail/nodeDetail.type.ts @@ -16,7 +16,7 @@ import React from 'react' import { OptionType } from '@devtron-labs/devtron-fe-common-lib' -import { Options, OptionsBase } from '../../appDetails.type' +import { AppDetails, Options, OptionsBase, SelectedResourceType } from '../../appDetails.type' import { CUSTOM_LOGS_FILTER, MANIFEST_KEY_FIELDS } from '../../../../../config' import { CustomLogFilterOptionsType, SelectedCustomLogFilterType } from './NodeDetailTabs/node.type' @@ -139,3 +139,12 @@ export interface EphemeralContainerProps { isResourceBrowserView: boolean params: ParamsType } + +export interface GetResourceRequestPayloadParamsType { + appDetails: AppDetails + nodeName: string + nodeType: string + isResourceBrowserView?: boolean + selectedResource?: SelectedResourceType + updatedManifest?: string +} diff --git a/src/components/v2/appDetails/k8Resource/nodeDetail/nodeDetail.util.ts b/src/components/v2/appDetails/k8Resource/nodeDetail/nodeDetail.util.ts index 5c7884ecb3..111fe19575 100644 --- a/src/components/v2/appDetails/k8Resource/nodeDetail/nodeDetail.util.ts +++ b/src/components/v2/appDetails/k8Resource/nodeDetail/nodeDetail.util.ts @@ -15,7 +15,12 @@ */ import { Moment } from 'moment' -import { decode, DeploymentAppTypes, K8sResourcePayloadAppType } from '@devtron-labs/devtron-fe-common-lib' +import { + decode, + DeploymentAppTypes, + K8sResourcePayloadAppType, + YAMLStringify, +} from '@devtron-labs/devtron-fe-common-lib' import { AppType, EnvType, @@ -472,10 +477,10 @@ export const getTrimmedManifestData = ( const { [MANIFEST_KEY_FIELDS.MANAGED_FIELDS]: _, ...metadata } = manifestData[MANIFEST_KEY_FIELDS.METADATA] const trimmedManifestData = { ...manifestData, [MANIFEST_KEY_FIELDS.METADATA]: metadata } - return returnAsString ? JSON.stringify(trimmedManifestData) : trimmedManifestData + return returnAsString ? YAMLStringify(trimmedManifestData) : trimmedManifestData } - return returnAsString ? JSON.stringify(manifestData) : manifestData + return returnAsString ? YAMLStringify(manifestData) : manifestData } export const getK8sResourcePayloadAppType = (appType: string): K8sResourcePayloadAppType => { @@ -499,7 +504,7 @@ export const getDecodedEncodedSecretManifestData = ( ...manifestData, [MANIFEST_KEY_FIELDS.DATA]: decode(manifestData[MANIFEST_KEY_FIELDS.DATA], isEncoded), } - return returnAsString ? JSON.stringify(encodedData) : manifestData + return returnAsString ? YAMLStringify(encodedData) : manifestData } export const getDeploymentType = (deploymentAppType: DeploymentAppTypes): K8sResourcePayloadDeploymentType => { diff --git a/src/css/base.scss b/src/css/base.scss index 4b0a8bcdda..93f00db3c5 100644 --- a/src/css/base.scss +++ b/src/css/base.scss @@ -4988,6 +4988,28 @@ textarea::placeholder { background-color: var(--Y100); } +.code-editor__error { + background-color: #fde7e7; + color: #862020; + font-size: 12px; + font-weight: 400; + line-height: 1.33; + padding: 8px 16px; + border-bottom: 1px solid #d6dbdf; +} + +.code-editor__information { + font-size: 12px; + font-weight: 400; + line-height: 1.33; + letter-spacing: normal; + color: var(--N900); + height: auto; + padding: 8px 16px; + border-bottom: 1px solid #d6dbdf; + background-color: var(--B100); +} + .security-policy--whitelist, .security-policy--whitelisted { color: var(--G500); From 86a7a00d79b817184d8cb70ed68c22b2015296d7 Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Tue, 29 Oct 2024 12:56:27 +0530 Subject: [PATCH 04/10] fix: put default value as null for ToggleManifestConfigurationMode --- .../appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx index 5ec83693ca..dcc0a64596 100644 --- a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx +++ b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx @@ -62,7 +62,7 @@ import { importComponentFromFELibrary } from '@Components/common' const ToggleManifestConfigurationMode = importComponentFromFELibrary( 'ToggleManifestConfigurationMode', - false, + null, 'function', ) From d227fd638f553dfba0f7604c2ab964b15943a055 Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Tue, 5 Nov 2024 16:37:59 +0530 Subject: [PATCH 05/10] feat: Add error handling for GUI view in manifest mode --- .../v2/appDetails/appDetails.type.ts | 2 ++ .../nodeDetail/NodeDetail.component.tsx | 19 ++++++++++++++++--- .../NodeDetailTabs/Manifest.component.tsx | 2 ++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/components/v2/appDetails/appDetails.type.ts b/src/components/v2/appDetails/appDetails.type.ts index 207af8ff73..680e2af09b 100644 --- a/src/components/v2/appDetails/appDetails.type.ts +++ b/src/components/v2/appDetails/appDetails.type.ts @@ -22,6 +22,7 @@ import { iNode as CommoniNode, ApiResourceGroupType, ConfigurationType, + FormProps, } from '@devtron-labs/devtron-fe-common-lib' import { ExternalLink, OptionTypeWithIcon } from '../../externalLinks/ExternalLinks.type' import { iLink } from '../utils/tabUtils/link.type' @@ -495,6 +496,7 @@ export interface ManifestActionPropsType extends ResourceInfoActionPropsType { handleSwitchToYAMLMode: () => void manifestFormConfigurationType: ConfigurationType handleUpdateUnableToParseManifest: (value: boolean) => void + handleManifestGUIErrors: FormProps['onError'] } export interface NodeTreeDetailTabProps { diff --git a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx index dcc0a64596..5c58e09754 100644 --- a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx +++ b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx @@ -23,6 +23,7 @@ import { OptionType, DeploymentAppTypes, ConfigurationType, + FormProps, } from '@devtron-labs/devtron-fe-common-lib' import { ReactComponent as ICArrowsLeftRight } from '@Icons/ic-arrows-left-right.svg' import { ReactComponent as ICPencil } from '@Icons/ic-pencil.svg' @@ -157,6 +158,7 @@ const NodeDetailComponent = ({ const [manifestFormConfigurationType, setManifestFormConfigurationType] = useState( ConfigurationType.YAML, ) + const [manifestErrors, setManifestErrors] = useState[0]>([]) const [unableToParseManifest, setUnableToParseManifest] = useState(false) // States uplifted from Manifest Component @@ -278,6 +280,10 @@ const NodeDetailComponent = ({ } } + const handleManifestGUIError: ManifestActionPropsType['handleManifestGUIErrors'] = (errors = []) => { + setManifestErrors(errors) + } + const handleSelectedTab = (_tabName: string, _url: string) => { setSelectedTabName(_tabName) updateTabUrl?.(_url) @@ -323,6 +329,8 @@ const NodeDetailComponent = ({ ) >= 0 )) + const doesManifestGUIContainsError = manifestErrors.length > 0 + // Assign extracted containers to selected resource before passing further if (selectedResource) { selectedResource.containers = resourceContainers @@ -354,7 +362,10 @@ const NodeDetailComponent = ({ const handleManifestApplyChanges = () => setManifestCodeEditorMode(ManifestCodeEditorMode.APPLY_CHANGES) - const handleManifestCancel = () => setManifestCodeEditorMode(ManifestCodeEditorMode.CANCEL) + const handleManifestCancel = () => { + handleManifestGUIError([]) + setManifestCodeEditorMode(ManifestCodeEditorMode.CANCEL) + } const handleManifestEdit = () => setManifestCodeEditorMode(ManifestCodeEditorMode.EDIT) @@ -416,14 +427,15 @@ const NodeDetailComponent = ({ )}
) : (
@@ -652,7 +652,7 @@ const ManifestComponent = ({ /> )} {!error && ( -
+
{isResourceMissing && !loading && !showManifestCompareView ? ( Date: Wed, 6 Nov 2024 18:03:38 +0530 Subject: [PATCH 08/10] feat: add manifestGUIFormRef to support external validation --- .../v2/appDetails/appDetails.type.ts | 1 + .../nodeDetail/NodeDetail.component.tsx | 20 ++++++++++++++++++- .../NodeDetailTabs/Manifest.component.tsx | 2 ++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/components/v2/appDetails/appDetails.type.ts b/src/components/v2/appDetails/appDetails.type.ts index 680e2af09b..17bcfc28ad 100644 --- a/src/components/v2/appDetails/appDetails.type.ts +++ b/src/components/v2/appDetails/appDetails.type.ts @@ -497,6 +497,7 @@ export interface ManifestActionPropsType extends ResourceInfoActionPropsType { manifestFormConfigurationType: ConfigurationType handleUpdateUnableToParseManifest: (value: boolean) => void handleManifestGUIErrors: FormProps['onError'] + manifestGUIFormRef: FormProps['ref'] } export interface NodeTreeDetailTabProps { diff --git a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx index 5c58e09754..6d4bdb03b9 100644 --- a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx +++ b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx @@ -24,6 +24,8 @@ import { DeploymentAppTypes, ConfigurationType, FormProps, + ToastManager, + ToastVariantType, } from '@devtron-labs/devtron-fe-common-lib' import { ReactComponent as ICArrowsLeftRight } from '@Icons/ic-arrows-left-right.svg' import { ReactComponent as ICPencil } from '@Icons/ic-pencil.svg' @@ -175,6 +177,8 @@ const NodeDetailComponent = ({ id: '', }) + const manifestGUIFormRef: FormProps['ref'] = useRef(null) + useEffect(() => setManagedFields((prev) => prev && selectedTabName === NodeDetailTab.MANIFEST), [selectedTabName]) useEffect(() => { @@ -360,7 +364,20 @@ const NodeDetailComponent = ({ return Object.values(params).join('/') } - const handleManifestApplyChanges = () => setManifestCodeEditorMode(ManifestCodeEditorMode.APPLY_CHANGES) + const handleManifestApplyChanges = () => { + const isFormValid = !manifestGUIFormRef.current?.validateForm || manifestGUIFormRef.current.validateForm() + + if (!isFormValid) { + ToastManager.showToast({ + variant: ToastVariantType.error, + description: 'Validation failed for some input fields, please rectify and apply changes again.', + }) + + return + } + + setManifestCodeEditorMode(ManifestCodeEditorMode.APPLY_CHANGES) + } const handleManifestCancel = () => { handleManifestGUIError([]) @@ -581,6 +598,7 @@ const NodeDetailComponent = ({ manifestFormConfigurationType={manifestFormConfigurationType} handleUpdateUnableToParseManifest={handleUpdateUnableToParseManifest} handleManifestGUIErrors={handleManifestGUIError} + manifestGUIFormRef={manifestGUIFormRef} /> diff --git a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx index 2833a5f969..8550d76c3e 100644 --- a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx +++ b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx @@ -89,6 +89,7 @@ const ManifestComponent = ({ handleSwitchToYAMLMode, handleUpdateUnableToParseManifest, handleManifestGUIErrors, + manifestGUIFormRef, }: ManifestActionPropsType) => { const location = useLocation() const history = useHistory() @@ -578,6 +579,7 @@ const ManifestComponent = ({ // For uniformity have called method but as of now in this case it will always be trimedManifestEditorData manifestYAMLString={trimedManifestEditorData} handleSwitchToYAMLMode={handleSwitchToYAMLMode} + manifestGUIFormRef={manifestGUIFormRef} /> ) From edb04637763a5299a8c564d1048fb3923cb1fb89 Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Wed, 6 Nov 2024 19:05:04 +0530 Subject: [PATCH 09/10] fix: hide gui view in external apps --- src/components/v2/appDetails/appDetails.type.ts | 2 +- .../appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx | 3 ++- .../nodeDetail/NodeDetailTabs/Manifest.component.tsx | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/v2/appDetails/appDetails.type.ts b/src/components/v2/appDetails/appDetails.type.ts index 17bcfc28ad..13b68bec1e 100644 --- a/src/components/v2/appDetails/appDetails.type.ts +++ b/src/components/v2/appDetails/appDetails.type.ts @@ -484,7 +484,7 @@ export enum ManifestCodeEditorMode { CANCEL = 'cancel', } -export interface ManifestActionPropsType extends ResourceInfoActionPropsType { +export interface ManifestActionPropsType extends ResourceInfoActionPropsType, Pick { hideManagedFields: boolean toggleManagedFields: (managedFieldsExist: boolean) => void manifestViewRef: MutableRefObject diff --git a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx index 6d4bdb03b9..6efedbdfe6 100644 --- a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx +++ b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetail.component.tsx @@ -440,7 +440,7 @@ const NodeDetailComponent = ({
{manifestCodeEditorMode === ManifestCodeEditorMode.EDIT ? (
- {ToggleManifestConfigurationMode && ( + {ToggleManifestConfigurationMode && !isExternalApp && ( diff --git a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx index 8550d76c3e..082424d5ff 100644 --- a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx +++ b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx @@ -90,6 +90,7 @@ const ManifestComponent = ({ handleUpdateUnableToParseManifest, handleManifestGUIErrors, manifestGUIFormRef, + isExternalApp, }: ManifestActionPropsType) => { const location = useLocation() const history = useHistory() @@ -163,7 +164,7 @@ const ManifestComponent = ({ }, [error, secretViewAccess, desiredManifest, activeManifestEditorData, manifest, modifiedManifest, id, guiSchema]) const handleInitializeGUISchema = async (abortSignal: AbortSignal) => { - if (!getManifestGUISchema) { + if (!getManifestGUISchema || !isExternalApp) { return } From 84a8dcb3cc3ddf7c14b8bcc137a0220a4b13145d Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Wed, 6 Nov 2024 19:34:03 +0530 Subject: [PATCH 10/10] fix: handleInitializeGUISchema to return null if isExternalApp --- .../k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx index 082424d5ff..c01a049863 100644 --- a/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx +++ b/src/components/v2/appDetails/k8Resource/nodeDetail/NodeDetailTabs/Manifest.component.tsx @@ -164,7 +164,7 @@ const ManifestComponent = ({ }, [error, secretViewAccess, desiredManifest, activeManifestEditorData, manifest, modifiedManifest, id, guiSchema]) const handleInitializeGUISchema = async (abortSignal: AbortSignal) => { - if (!getManifestGUISchema || !isExternalApp) { + if (!getManifestGUISchema || isExternalApp) { return }