diff --git a/.eslintignore b/.eslintignore index 14e8752f14..29fad0d33e 100755 --- a/.eslintignore +++ b/.eslintignore @@ -54,7 +54,6 @@ src/components/CIPipelineN/ciPipeline.utils.tsx src/components/ClusterNodes/ClusterEvents.tsx src/components/ClusterNodes/ClusterManifest.tsx src/components/ClusterNodes/ClusterNodeEmptyStates.tsx -src/components/ClusterNodes/ClusterOverview.tsx src/components/ClusterNodes/NodeActions/EditTaintsModal.tsx src/components/ClusterNodes/NodeActions/NodeActionsMenu.tsx src/components/ClusterNodes/NodeActions/validationRules.ts @@ -210,7 +209,6 @@ src/components/ciPipeline/ciPipeline.service.ts src/components/ciPipeline/validationRules.ts src/components/cluster/Cluster.tsx src/components/cluster/ClusterComponentModal.tsx -src/components/cluster/ClusterForm.tsx src/components/cluster/ClusterInfoStepsModal.tsx src/components/cluster/ClusterInstallStatus.tsx src/components/cluster/UseNameListDropdown.tsx diff --git a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/constants.tsx b/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/constants.tsx index 327f0fe409..4ef8110e5e 100644 --- a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/constants.tsx +++ b/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/constants.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EnterpriseTag, OverrideMergeStrategyType, SelectPickerOptionType } from '@devtron-labs/devtron-fe-common-lib' +import { Icon, OverrideMergeStrategyType, SelectPickerOptionType } from '@devtron-labs/devtron-fe-common-lib' import { importComponentFromFELibrary } from '@Components/common' @@ -37,7 +37,11 @@ export const MERGE_STRATEGY_OPTIONS: SelectPickerOptionType[] = [ label: (
Patch - {!isFELibAvailable && } + {!isFELibAvailable && ( +
+ +
+ )}
), description: 'Override values for specific keys', diff --git a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/ClusterEnvironmentDrawer.tsx b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/ClusterEnvironmentDrawer.tsx index 04d9c0feff..3880876e55 100644 --- a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/ClusterEnvironmentDrawer.tsx +++ b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/ClusterEnvironmentDrawer.tsx @@ -15,6 +15,7 @@ */ import { useEffect, useState } from 'react' +import { generatePath } from 'react-router-dom' import { Button, @@ -45,6 +46,7 @@ import { ADD_ENVIRONMENT_FORM_LOCAL_STORAGE_KEY } from '@Components/cluster/cons import { importComponentFromFELibrary } from '@Components/common' import { URLS } from '@Config/routes' +import { CreateClusterTypeEnum } from '../CreateCluster/types' import { EnvironmentDeleteComponent } from '../EnvironmentDeleteComponent' import { clusterEnvironmentDrawerFormValidationSchema } from './schema' import { ClusterEnvironmentDrawerFormProps, ClusterEnvironmentDrawerProps, ClusterNamespacesDTO } from './types' @@ -67,6 +69,7 @@ export const ClusterEnvironmentDrawer = ({ reload, hideClusterDrawer, isVirtual, + clusterName, }: ClusterEnvironmentDrawerProps) => { // STATES // Manages the loading state for create and update actions @@ -259,7 +262,9 @@ export const ClusterEnvironmentDrawer = ({ + + + + + {openDeleteClusterModal && ( + + )} ) } diff --git a/src/components/ResourceBrowser/ResourceList/NodeActionsMenu.tsx b/src/components/ResourceBrowser/ResourceList/NodeActionsMenu.tsx index 29689d6749..11f3a85bbf 100644 --- a/src/components/ResourceBrowser/ResourceList/NodeActionsMenu.tsx +++ b/src/components/ResourceBrowser/ResourceList/NodeActionsMenu.tsx @@ -19,11 +19,11 @@ import { useHistory, useLocation, useRouteMatch } from 'react-router-dom' import { noop, PopupMenu } from '@devtron-labs/devtron-fe-common-lib' +import { ReactComponent as MenuDots } from '@Icons/ic-dot.svg' import { ReactComponent as UncordonIcon } from '@Icons/ic-play-outline.svg' import { TaintType } from '@Components/ClusterNodes/types' import { AppDetailsTabs } from '@Components/v2/appDetails/appDetails.store' -import { ReactComponent as MenuDots } from '../../../assets/icons/appstatus/ic-menu-dots.svg' import { ReactComponent as DrainIcon } from '../../../assets/icons/ic-clean-brush.svg' import { ReactComponent as CordonIcon } from '../../../assets/icons/ic-cordon.svg' import { ReactComponent as DeleteIcon } from '../../../assets/icons/ic-delete-interactive.svg' @@ -158,7 +158,7 @@ const NodeActionsMenu = ({ nodeData, getNodeListData, addTab, handleClearBulkSel <> - +
diff --git a/src/components/ResourceBrowser/ResourceList/ResourceBrowserActionMenu.tsx b/src/components/ResourceBrowser/ResourceList/ResourceBrowserActionMenu.tsx index 8c17f03ed2..4c52ea98ba 100644 --- a/src/components/ResourceBrowser/ResourceList/ResourceBrowserActionMenu.tsx +++ b/src/components/ResourceBrowser/ResourceList/ResourceBrowserActionMenu.tsx @@ -28,7 +28,8 @@ import { useMainContext, } from '@devtron-labs/devtron-fe-common-lib' -import { ReactComponent as MenuDots } from '../../../assets/icons/appstatus/ic-menu-dots.svg' +import { ReactComponent as MenuDots } from '@Icons/ic-dot.svg' + import { ReactComponent as CalendarIcon } from '../../../assets/icons/ic-calendar.svg' import { ReactComponent as DeleteIcon } from '../../../assets/icons/ic-delete-interactive.svg' import { ReactComponent as ManifestIcon } from '../../../assets/icons/ic-file-code.svg' @@ -108,7 +109,7 @@ const ResourceBrowserActionMenu: React.FC = ({ <> - +
diff --git a/src/components/ResourceBrowser/ResourceList/ResourceList.tsx b/src/components/ResourceBrowser/ResourceList/ResourceList.tsx index 8fe4fdab65..97ae3a0e9b 100644 --- a/src/components/ResourceBrowser/ResourceList/ResourceList.tsx +++ b/src/components/ResourceBrowser/ResourceList/ResourceList.tsx @@ -20,14 +20,12 @@ import { useHistory, useLocation, useParams, useRouteMatch } from 'react-router- import { ALL_NAMESPACE_OPTION, ApiResourceGroupType, - BreadCrumb, DevtronProgressing, DynamicTabType, ErrorScreenManager, getResourceGroupListRaw, InitTabType, noop, - PageHeader, useAsync, useBreadcrumb, useEffectAfterMount, @@ -45,10 +43,9 @@ import { DynamicTabsProps, DynamicTabsVariantType, UpdateTabUrlParamsType } from import { URLS } from '../../../config' import { DEFAULT_CLUSTER_ID } from '../../cluster/cluster.type' -import { getClusterListMin } from '../../ClusterNodes/clusterNodes.service' import ClusterOverview from '../../ClusterNodes/ClusterOverview' import NodeDetails from '../../ClusterNodes/NodeDetails' -import { convertToOptionsList, importComponentFromFELibrary, sortObjectArrayAlphabetically } from '../../common' +import { importComponentFromFELibrary } from '../../common' import { DynamicTabs, useTabs } from '../../common/DynamicTabs' import { AppDetailsTabs } from '../../v2/appDetails/appDetails.store' import NodeDetailComponent from '../../v2/appDetails/k8Resource/nodeDetail/NodeDetail.component' @@ -60,15 +57,22 @@ import { UPGRADE_CLUSTER_CONSTANTS, } from '../Constants' import { renderCreateResourceButton } from '../PageHeader.buttons' +import { getClusterListing } from '../ResourceBrowser.service' import { ClusterOptionType, K8SResourceListType, URLParams } from '../Types' -import { getTabsBasedOnRole } from '../Utils' +import { getClusterChangeRedirectionUrl, getTabsBasedOnRole } from '../Utils' import AdminTerminal from './AdminTerminal' import ClusterSelector from './ClusterSelector' import ClusterUpgradeCompatibilityInfo from './ClusterUpgradeCompatibilityInfo' import K8SResourceTabComponent from './K8SResourceTabComponent' import { renderRefreshBar } from './ResourceList.component' +import ResourcePageHeader from './ResourcePageHeader' import { ResourceListUrlFiltersType } from './types' -import { getFirstResourceFromKindResourceMap, getUpgradeCompatibilityTippyConfig, parseSearchParams } from './utils' +import { + getClusterOptions, + getFirstResourceFromKindResourceMap, + getUpgradeCompatibilityTippyConfig, + parseSearchParams, +} from './utils' const EventsAIResponseWidget = importComponentFromFELibrary('EventsAIResponseWidget', null, 'function') const MonitoringDashboard = importComponentFromFELibrary('MonitoringDashboard', null, 'function') @@ -105,22 +109,9 @@ const ResourceList = () => { [clusterId], ) - const [loading, clusterListData, error] = useAsync(() => getClusterListMin()) + const [loading, clusterList, error] = useAsync(() => getClusterListing(true)) - const clusterList = clusterListData?.result || null - - const clusterOptions = useMemo( - () => - clusterList && - (convertToOptionsList( - sortObjectArrayAlphabetically(clusterList, 'name').filter(({ isVirtualCluster }) => !isVirtualCluster), - 'name', - 'id', - 'nodeErrors', - 'isProd', - ) as ClusterOptionType[]), - [clusterList], - ) + const clusterOptions: ClusterOptionType[] = useMemo(() => getClusterOptions(clusterList), [clusterList]) /* NOTE: this is being used as dependency in useEffect down the tree */ const selectedCluster = useMemo( @@ -128,8 +119,8 @@ const ResourceList = () => { clusterOptions?.find((cluster) => String(cluster.value) === clusterId) || { label: '', value: clusterId, - errorInConnecting: '', isProd: false, + isInstallationCluster: false, }, [clusterId, clusterOptions], ) @@ -299,9 +290,7 @@ const ResourceList = () => { return } - const path = `${URLS.RESOURCE_BROWSER}/${selected.value}/${ - ALL_NAMESPACE_OPTION.value - }/${SIDEBAR_KEYS.nodeGVK.Kind.toLowerCase()}/${K8S_EMPTY_GROUP}` + const path = getClusterChangeRedirectionUrl(selected.isInstallationCluster, selected.value) replace({ pathname: path, @@ -364,8 +353,6 @@ const ResourceList = () => { } } - const renderBreadcrumbs = () => - const updateTerminalTabUrl = (queryParams: string) => { const terminalTab = getTabById(ResourceBrowserTabsId.terminal) if (!terminalTab || terminalTab.name !== AppDetailsTabs.terminal) { @@ -587,11 +574,9 @@ const ResourceList = () => { return (
- {renderMainBody()}
diff --git a/src/components/ResourceBrowser/ResourceList/ResourcePageHeader.tsx b/src/components/ResourceBrowser/ResourceList/ResourcePageHeader.tsx new file mode 100644 index 0000000000..ffb1d4154e --- /dev/null +++ b/src/components/ResourceBrowser/ResourceList/ResourcePageHeader.tsx @@ -0,0 +1,18 @@ +import { BreadCrumb, noop, PageHeader } from '@devtron-labs/devtron-fe-common-lib' + +import { ResourcePageHeaderProps } from './types' + +const ResourcePageHeader = ({ breadcrumbs, renderPageHeaderActionButtons }: ResourcePageHeaderProps) => { + const renderBreadcrumbs = () => + + return ( + + ) +} + +export default ResourcePageHeader diff --git a/src/components/ResourceBrowser/ResourceList/types.ts b/src/components/ResourceBrowser/ResourceList/types.ts index 604c2e0d07..718e9893c6 100644 --- a/src/components/ResourceBrowser/ResourceList/types.ts +++ b/src/components/ResourceBrowser/ResourceList/types.ts @@ -21,6 +21,7 @@ import { K8sResourceDetailType, RBBulkOperationType, ServerErrors, + useBreadcrumb, } from '@devtron-labs/devtron-fe-common-lib' import { ClusterListType } from '@Components/ClusterNodes/types' @@ -82,3 +83,8 @@ export interface ResourceListUrlFiltersType { } export type BulkOperationsModalState = RBBulkOperationType | 'closed' + +export interface ResourcePageHeaderProps { + breadcrumbs: ReturnType['breadcrumbs'] + renderPageHeaderActionButtons?: () => JSX.Element +} diff --git a/src/components/ResourceBrowser/ResourceList/utils.tsx b/src/components/ResourceBrowser/ResourceList/utils.tsx index 3779fd0506..473e00eeab 100644 --- a/src/components/ResourceBrowser/ResourceList/utils.tsx +++ b/src/components/ResourceBrowser/ResourceList/utils.tsx @@ -14,7 +14,9 @@ * limitations under the License. */ -import { logExceptionToSentry, noop } from '@devtron-labs/devtron-fe-common-lib' +import { ClusterDetail, logExceptionToSentry, noop } from '@devtron-labs/devtron-fe-common-lib' + +import { sortObjectArrayAlphabetically } from '@Components/common' import { LOCAL_STORAGE_EXISTS, @@ -22,7 +24,7 @@ import { OPTIONAL_NODE_LIST_HEADERS, TARGET_K8S_VERSION_SEARCH_KEY, } from '../Constants' -import { K8SResourceListType } from '../Types' +import { ClusterOptionType, K8SResourceListType } from '../Types' import { ResourceListUrlFiltersType } from './types' export const parseSearchParams = (searchParams: URLSearchParams) => ({ @@ -88,3 +90,16 @@ export const getFirstResourceFromKindResourceMap = ( (resourceGroup) => resourceGroup.gvk.Kind?.toLowerCase() === kind?.toLowerCase(), ) } + +export const getClusterOptions = (clusterList: ClusterDetail[]): ClusterOptionType[] => + clusterList + ? sortObjectArrayAlphabetically(clusterList, 'name') + .filter(({ isVirtualCluster }) => !isVirtualCluster) + .map(({ name, id, nodeErrors, isProd, installationId }) => ({ + label: name, + value: String(id ?? installationId), + description: nodeErrors, + isProd, + isInstallationCluster: !!installationId, + })) + : [] diff --git a/src/components/ResourceBrowser/Types.ts b/src/components/ResourceBrowser/Types.ts index 969d89f6c4..c464f87213 100644 --- a/src/components/ResourceBrowser/Types.ts +++ b/src/components/ResourceBrowser/Types.ts @@ -89,8 +89,8 @@ export interface SidebarType { } export interface ClusterOptionType extends OptionType { - errorInConnecting: string isProd: boolean + isInstallationCluster: boolean } export interface ResourceFilterOptionsProps extends Pick { @@ -222,6 +222,7 @@ export interface ClusterSelectorType { onChange: ({ label, value }) => void clusterList: ClusterOptionType[] clusterId: string + isInstallationStatusView?: boolean } export interface CreateResourceButtonType { diff --git a/src/components/ResourceBrowser/Utils.tsx b/src/components/ResourceBrowser/Utils.tsx index d2e71b311b..445dc108b1 100644 --- a/src/components/ResourceBrowser/Utils.tsx +++ b/src/components/ResourceBrowser/Utils.tsx @@ -20,6 +20,7 @@ import moment from 'moment' import queryString from 'query-string' import { + ALL_NAMESPACE_OPTION, ApiResourceGroupType, DATE_TIME_FORMAT_STRING, GVKType, @@ -408,3 +409,10 @@ export const parseNodeList = (response: ResponseType): Response }), }, }) + +export const getClusterChangeRedirectionUrl = (isInstallationCluster: boolean, clusterId: string) => + isInstallationCluster + ? `${URLS.RESOURCE_BROWSER}/installation-cluster/${clusterId}` + : `${URLS.RESOURCE_BROWSER}/${clusterId}/${ + ALL_NAMESPACE_OPTION.value + }/${SIDEBAR_KEYS.nodeGVK.Kind.toLowerCase()}/${K8S_EMPTY_GROUP}` diff --git a/src/components/cdPipeline/MigrateToDevtron/MigrateToDevtronValidationFactory.tsx b/src/components/cdPipeline/MigrateToDevtron/MigrateToDevtronValidationFactory.tsx index a4edf2d00a..dd27d4469f 100644 --- a/src/components/cdPipeline/MigrateToDevtron/MigrateToDevtronValidationFactory.tsx +++ b/src/components/cdPipeline/MigrateToDevtron/MigrateToDevtronValidationFactory.tsx @@ -15,7 +15,7 @@ */ import { ComponentProps } from 'react' -import { Link } from 'react-router-dom' +import { generatePath, Link } from 'react-router-dom' import { Button, @@ -41,6 +41,7 @@ import { ADD_ENVIRONMENT_FORM_LOCAL_STORAGE_KEY, } from '@Components/cluster/constants' import { URLS } from '@Config/routes' +import { CreateClusterTypeEnum } from '@Pages/GlobalConfigurations/ClustersAndEnvironments/CreateCluster/types' import { MigrationSourceValidationReasonType } from '../cdPipeline.types' import { GENERIC_SECTION_ERROR_STATE_COMMON_PROPS, TARGET_ENVIRONMENT_INFO_LIST } from './constants' @@ -249,7 +250,9 @@ const MigrateToDevtronValidationFactory = ({ component: ButtonComponentType.link, onClick: handleAddClusterClick, linkProps: { - to: URLS.GLOBAL_CONFIG_CREATE_CLUSTER, + to: generatePath(URLS.GLOBAL_CONFIG_CREATE_CLUSTER, { + type: CreateClusterTypeEnum.CONNECT_CLUSTER, + }), target: '_blank', }, }} diff --git a/src/components/charts/ChartGroupUpdate.tsx b/src/components/charts/ChartGroupUpdate.tsx index cbf0cfb867..8a1383f254 100644 --- a/src/components/charts/ChartGroupUpdate.tsx +++ b/src/components/charts/ChartGroupUpdate.tsx @@ -16,7 +16,16 @@ import { useState, useEffect, useRef, useMemo } from 'react' import { useParams, useRouteMatch, useHistory, useLocation, Prompt } from 'react-router-dom' -import { showError, Progressing, BreadCrumb, useBreadcrumb, PageHeader, DetectBottom, ToastManager, ToastVariantType } from '@devtron-labs/devtron-fe-common-lib' +import { + showError, + Progressing, + BreadCrumb, + useBreadcrumb, + PageHeader, + DetectBottom, + ToastManager, + ToastVariantType, +} from '@devtron-labs/devtron-fe-common-lib' import ChartSelect from './util/ChartSelect' import { ChartGroupEntry, Chart, ChartListType } from './charts.types' import MultiChartSummary from './MultiChartSummary' @@ -66,6 +75,7 @@ export default function ChartGroupUpdate({}) { const [chartListLoading, setChartListLoading] = useState(true) const chartList: Chart[] = Array.from(state.availableCharts.values()) const [chartLists, setChartLists] = useState([]) + const [chartCategoryIds, setChartCategoryIds] = useState([]) const [isGrid, setIsGrid] = useState(true) const { breadcrumbs } = useBreadcrumb( @@ -109,10 +119,10 @@ export default function ChartGroupUpdate({}) { await updateChartGroupEntries(requestBody) await reloadState() updateChartGroupEntriesFromResponse() - ToastManager.showToast({ - variant: ToastVariantType.success, - description: 'Successfully saved', - }) + ToastManager.showToast({ + variant: ToastVariantType.success, + description: 'Successfully saved', + }) } catch (err) { showError(err) } finally { @@ -186,6 +196,7 @@ export default function ChartGroupUpdate({}) { const allRegistryIds: string = searchParams.get(QueryParams.RegistryId) const deprecated: string = searchParams.get(QueryParams.IncludeDeprecated) const appStoreName: string = searchParams.get(QueryParams.AppStoreName) + const chartCategoryCsv: string = searchParams.get(QueryParams.ChartCategoryId) let chartRepoIdArray = [] let ociRegistryArray = [] if (allChartRepoIds) { @@ -215,6 +226,8 @@ export default function ChartGroupUpdate({}) { } if (deprecated) { setIncludeDeprecated(parseInt(deprecated)) + } else { + setIncludeDeprecated(0) } if (appStoreName) { setSearchApplied(true) @@ -223,6 +236,14 @@ export default function ChartGroupUpdate({}) { setSearchApplied(false) setAppStoreName('') } + if (chartCategoryCsv) { + const idsArray = chartCategoryCsv.split(',') + if (idsArray) { + setChartCategoryIds(idsArray) + } + } else { + setChartCategoryIds([]) + } } async function callApplyFilterOnCharts(resetPage?: boolean) { @@ -291,6 +312,8 @@ export default function ChartGroupUpdate({}) { selectedChartRepo={selectedChartRepo} isGrid={isGrid} setIsGrid={setIsGrid} + chartCategoryIds={chartCategoryIds} + setChartCategoryIds={setChartCategoryIds} /> ) : null} {chartListLoading ? ( @@ -309,9 +332,7 @@ export default function ChartGroupUpdate({}) { discardValuesYamlChanges={discardValuesYamlChanges} /> ) : !chartList.length ? ( - + ) : (
{ const match = useRouteMatch() const history = useHistory() @@ -53,12 +57,15 @@ const ChartHeaderFilter = ({ history.push(`${match.url.split('/chart-store')[0]}${URLS.GLOBAL_CONFIG_CHART}`) } + // Should be replaced with useURLFilters function handleFilterChanges(selected, key): void { const searchParams = new URLSearchParams(location.search) const app = searchParams.get(QueryParams.AppStoreName) const deprecate = searchParams.get(QueryParams.IncludeDeprecated) const chartRepoId = searchParams.get(QueryParams.ChartRepoId) const registryId = searchParams.get(QueryParams.RegistryId) + const chartCategoryIdsCsv = searchParams.get(QueryParams.ChartCategoryId) + let isOCIRegistry if (key === CHART_KEYS.CHART_REPO) { const paramsChartRepoIds = selected @@ -86,6 +93,9 @@ const ChartHeaderFilter = ({ if (deprecate) { qsr = `${qsr}&${QueryParams.IncludeDeprecated}=${deprecate}` } + if (chartCategoryIdsCsv) { + qsr = `${qsr}&${QueryParams.ChartCategoryId}=${chartCategoryIdsCsv}` + } history.push(`${url}?${qsr}`) } else { let qs = `${QueryParams.ChartRepoId}=${paramsChartRepoIds}` @@ -98,12 +108,16 @@ const ChartHeaderFilter = ({ if (deprecate) { qs = `${qs}&${QueryParams.IncludeDeprecated}=${deprecate}` } + if (chartCategoryIdsCsv) { + qs = `${qs}&${QueryParams.ChartCategoryId}=${chartCategoryIdsCsv}` + } history.push(`${url}?${qs}`) } } - if (key === CHART_KEYS.DEPRECATED) { - let qs = `${QueryParams.IncludeDeprecated}=${selected}` + if (key === CHART_KEYS.CHART_CATEGORY) { + const chartCategoryCsv = selected.join(',') + let qs = `${QueryParams.ChartCategoryId}=${chartCategoryCsv}` if (app) { qs = `${qs}&${QueryParams.AppStoreName}=${app}` } @@ -113,13 +127,16 @@ const ChartHeaderFilter = ({ if (registryId) { qs = `${qs}&${QueryParams.RegistryId}=${registryId}` } + if (deprecate) { + qs = `${qs}&${QueryParams.IncludeDeprecated}=${deprecate}` + } history.push(`${url}?${qs}`) } - if (key === CHART_KEYS.SEARCH) { - let qs = `${QueryParams.AppStoreName}=${selected}` - if (deprecate) { - qs = `${qs}&${QueryParams.IncludeDeprecated}=${deprecate}` + if (key === CHART_KEYS.DEPRECATED) { + let qs = `${QueryParams.IncludeDeprecated}=${selected}` + if (app) { + qs = `${qs}&${QueryParams.AppStoreName}=${app}` } if (chartRepoId) { qs = `${qs}&${QueryParams.ChartRepoId}=${chartRepoId}` @@ -127,11 +144,14 @@ const ChartHeaderFilter = ({ if (registryId) { qs = `${qs}&${QueryParams.RegistryId}=${registryId}` } + if (chartCategoryIdsCsv) { + qs = `${qs}&${QueryParams.ChartCategoryId}=${chartCategoryIdsCsv}` + } history.push(`${url}?${qs}`) } - if (key === CHART_KEYS.CLEAR) { - let qs: string = '' + if (key === CHART_KEYS.SEARCH) { + let qs = `${QueryParams.AppStoreName}=${selected}` if (deprecate) { qs = `${qs}&${QueryParams.IncludeDeprecated}=${deprecate}` } @@ -141,6 +161,9 @@ const ChartHeaderFilter = ({ if (registryId) { qs = `${qs}&${QueryParams.RegistryId}=${registryId}` } + if (chartCategoryIdsCsv) { + qs = `${qs}&${QueryParams.ChartCategoryId}=${chartCategoryIdsCsv}` + } history.push(`${url}?${qs}`) } } @@ -162,6 +185,11 @@ const ChartHeaderFilter = ({ handleFilterChanges(searchKey, CHART_KEYS.SEARCH) } + const handleUpdateCategoryFilter = (selectedCategories: string[]) => { + setChartCategoryIds(selectedCategories) + handleFilterChanges(selectedCategories, CHART_KEYS.CHART_CATEGORY) + } + return (
@@ -171,35 +199,29 @@ const ChartHeaderFilter = ({ handleEnter={handleSearchEnter} inputProps={{ placeholder: 'Search charts', - autoFocus: true + autoFocus: true, }} dataTestId="chart-store-search-box" />
-
- VIEW AS -
-
-
- - Grid -
-
- - List +
+ VIEW AS +
+ +

@@ -215,6 +237,15 @@ const ChartHeaderFilter = ({ >
Show deprecated charts
+ {ChartCategoryFilters && ( + <> +
+ + + )}
void + chartCategoryIds: string[] + setChartCategoryIds: Dispatch> } export interface DeleteInstalledChartParamsType { diff --git a/src/components/charts/charts.util.tsx b/src/components/charts/charts.util.tsx index be27a9d554..13c5ed7c35 100644 --- a/src/components/charts/charts.util.tsx +++ b/src/components/charts/charts.util.tsx @@ -117,6 +117,7 @@ export const QueryParams = { AppStoreName: 'appStoreName', RegistryId: 'registryId', SearchKey: 'searchKey', + ChartCategoryId: 'chartCategoryId', } export const PaginationParams = { diff --git a/src/components/charts/constants.ts b/src/components/charts/constants.ts index bd38f5063c..8c38cb6012 100644 --- a/src/components/charts/constants.ts +++ b/src/components/charts/constants.ts @@ -34,7 +34,7 @@ export const APP_NAME_TAKEN = 'App name already taken' export enum CHART_KEYS { CHART_REPO = 'chart-repo', + CHART_CATEGORY = 'chart-category', DEPRECATED = 'deprecated', SEARCH = 'search', - CLEAR = 'clear', } diff --git a/src/components/charts/discoverChartDetail/ChartDeploymentList.tsx b/src/components/charts/discoverChartDetail/ChartDeploymentList.tsx index ec0015b3c3..eabd1b7331 100644 --- a/src/components/charts/discoverChartDetail/ChartDeploymentList.tsx +++ b/src/components/charts/discoverChartDetail/ChartDeploymentList.tsx @@ -36,7 +36,7 @@ import { Td } from '../../common' import { Routes, URLS, ViewType, SERVER_MODE, DELETE_ACTION } from '../../../config' import { deleteInstalledChart } from '../charts.service' import AppNotDeployedIcon from '../../../assets/img/app-not-configured.png' -import dots from '../../../assets/icons/appstatus/ic-menu-dots.svg' +import { ReactComponent as Dots } from '@Icons/ic-dot.svg' import trash from '../../../assets/icons/ic-delete.svg' import { getAppId } from '../../v2/appDetails/k8Resource/nodeDetail/nodeDetail.api' import ClusterNotReachableDialog from '../../common/ClusterNotReachableDialog/ClusterNotReachableDialog' @@ -243,8 +243,8 @@ export const DeploymentRow = ({ - - + +
setConfirmation(true)}> diff --git a/src/components/charts/list/DiscoverCharts.tsx b/src/components/charts/list/DiscoverCharts.tsx index 8c1eae5504..5adfb66f15 100644 --- a/src/components/charts/list/DiscoverCharts.tsx +++ b/src/components/charts/list/DiscoverCharts.tsx @@ -26,6 +26,9 @@ import { FeatureTitleWithInfo, ToastVariantType, ToastManager, + Button, + ComponentSizeType, + ButtonVariantType, } from '@devtron-labs/devtron-fe-common-lib' import { Switch, Route, NavLink, useHistory, useLocation, useRouteMatch, Prompt } from 'react-router-dom' import Tippy from '@tippyjs/react' @@ -111,6 +114,7 @@ const DiscoverChartList = ({ isSuperAdmin }: { isSuperAdmin: boolean }) => { const [appStoreName, setAppStoreName] = useState('') const [searchApplied, setSearchApplied] = useState(false) const [includeDeprecated, setIncludeDeprecated] = useState(0) + const [chartCategoryIds, setChartCategoryIds] = useState([]) const projectsMap = mapByKey(state.projects, 'id') const chartList: Chart[] = Array.from(state.availableCharts.values()) const isLeavingPageNotAllowed = useRef(false) @@ -125,7 +129,7 @@ const DiscoverChartList = ({ isSuperAdmin }: { isSuperAdmin: boolean }) => { const [isLoading, setIsLoading] = useState(false) const [filteredChartList, setFilteredChartList] = useState([]) - const noChartAvailable: boolean = chartList.length > 0 || searchApplied || selectedChartRepo.length > 0 + const noChartAvailable: boolean = chartList.length > 0 || searchApplied || selectedChartRepo.length > 0 || !!chartCategoryIds isLeavingPageNotAllowed.current = !state.charts.reduce((acc: boolean, chart: ChartGroupEntry) => { return (acc = acc && chart.originalValuesYaml === chart.valuesYaml) }, true) @@ -262,12 +266,14 @@ const DiscoverChartList = ({ isSuperAdmin }: { isSuperAdmin: boolean }) => { toggleDeployModal(false) } + // should be removed and use useUrlFilters instead function initialiseFromQueryParams(chartRepoList): void { const searchParams = new URLSearchParams(location.search) const allChartRepoIds: string = searchParams.get(QueryParams.ChartRepoId) const allRegistryIds: string = searchParams.get(QueryParams.RegistryId) const deprecated: string = searchParams.get(QueryParams.IncludeDeprecated) const appStoreName: string = searchParams.get(QueryParams.AppStoreName) + const chartCategoryCsv: string = searchParams.get(QueryParams.ChartCategoryId) let chartRepoIdArray = [] let ociRegistryArray = [] if (allChartRepoIds) { @@ -297,6 +303,8 @@ const DiscoverChartList = ({ isSuperAdmin }: { isSuperAdmin: boolean }) => { } if (deprecated) { setIncludeDeprecated(parseInt(deprecated)) + } else { + setIncludeDeprecated(0) } if (appStoreName) { setSearchApplied(true) @@ -305,6 +313,14 @@ const DiscoverChartList = ({ isSuperAdmin }: { isSuperAdmin: boolean }) => { setSearchApplied(false) setAppStoreName('') } + if (chartCategoryCsv) { + const idsArray = chartCategoryCsv.split(',') + if (idsArray) { + setChartCategoryIds(idsArray) + } + } else { + setChartCategoryIds([]) + } } async function callApplyFilterOnCharts(resetPage?: boolean) { @@ -369,24 +385,25 @@ const DiscoverChartList = ({ isSuperAdmin }: { isSuperAdmin: boolean }) => { / )} - +
{state.charts.length === 0 ? ( <> - Chart Store + Chart Store {isSuperAdmin && ( - + size={ComponentSizeType.xs} + variant={ButtonVariantType.secondary} + /> )} ) : ( 'Deploy multiple charts' )} - +
{showSourcePopoUp && ( @@ -429,9 +446,7 @@ const DiscoverChartList = ({ isSuperAdmin }: { isSuperAdmin: boolean }) => { return ( <> -
0 ? 'summary-show' : ''} chart-store-header`} - > +
0 ? 'summary-show' : ''}`}> 0} wrap={(children) =>
{children}
}>
@@ -451,6 +466,8 @@ const DiscoverChartList = ({ isSuperAdmin }: { isSuperAdmin: boolean }) => { selectedChartRepo={selectedChartRepo} isGrid={isGrid} setIsGrid={setIsGrid} + chartCategoryIds={chartCategoryIds} + setChartCategoryIds={setChartCategoryIds} /> )} {state.loading || chartListLoading ? ( @@ -495,7 +512,8 @@ const DiscoverChartList = ({ isSuperAdmin }: { isSuperAdmin: boolean }) => {
{serverMode == SERVER_MODE.FULL && !searchApplied && - selectedChartRepo.length === 0 && ( + !selectedChartRepo.length && + !chartCategoryIds.length && ( Pick = +) => Pick = importComponentFromFELibrary('getSSHConfig', noop, 'function') +const VirtualClusterForm = importComponentFromFELibrary('VirtualClusterForm', null, 'function') class ClusterList extends Component { timerRef @@ -73,21 +72,14 @@ class ClusterList extends Component { view: ViewType.LOADING, clusters: [], clusterEnvMap: {}, - isTlsConnection: false, appCreationType: AppCreationType.Blank, - isKubeConfigFile: false, browseFile: false, - isClusterDetails: false, showEditCluster: false, isConnectedViaProxy: false, isConnectedViaSSHTunnel: false, } this.initialise = this.initialise.bind(this) - this.toggleCheckTlsConnection = this.toggleCheckTlsConnection.bind(this) - this.setTlsConnectionFalse = this.setTlsConnectionFalse.bind(this) - this.toggleKubeConfigFile = this.toggleKubeConfigFile.bind(this) this.toggleBrowseFile = this.toggleBrowseFile.bind(this) - this.toggleClusterDetails = this.toggleClusterDetails.bind(this) this.toggleShowEditCluster = this.toggleShowEditCluster.bind(this) } @@ -187,26 +179,10 @@ class ClusterList extends Component { clearInterval(this.timerRef) } - toggleCheckTlsConnection() { - this.setState({ isTlsConnection: !this.state.isTlsConnection }) - } - - setTlsConnectionFalse() { - this.setState({ isTlsConnection: false }) - } - - toggleClusterDetails(updateClusterDetails: boolean) { - this.setState({ isClusterDetails: updateClusterDetails }) - } - toggleShowEditCluster() { this.setState({ showEditCluster: !this.state.showEditCluster }) } - toggleKubeConfigFile(updateKubeConfigFile: boolean) { - this.setState({ isKubeConfigFile: updateKubeConfigFile }) - } - toggleBrowseFile() { this.setState({ browseFile: !this.state.browseFile }) } @@ -250,12 +226,14 @@ class ClusterList extends Component {
{this.state.clusters.map( @@ -267,41 +245,16 @@ class ClusterList extends Component { key={cluster.id || Math.random().toString(36).substr(2, 5)} showEditCluster={this.state.showEditCluster} toggleShowAddCluster={this.toggleShowEditCluster} - toggleCheckTlsConnection={this.toggleCheckTlsConnection} - setTlsConnectionFalse={this.setTlsConnectionFalse} - isTlsConnection={this.state.isTlsConnection} + isTlsConnection={cluster.isTlsConnection} prometheus_url={cluster.prometheus_url} /> ), )} - - - + { return ( { export default withRouter(ClusterList) const Cluster = ({ - id, id: clusterId, cluster_name, insecureSkipTlsVerify, - defaultClusterComponent, - agentInstallationStage, server_url, - active, - config: defaultConfig, environments, reload, prometheus_url, proxyUrl, toConnectWithSSHTunnel, sshTunnelConfig, - isTlsConnection, - toggleShowAddCluster, - toggleCheckTlsConnection, - setTlsConnectionFalse, isVirtualCluster, isProd, }) => { const [editMode, toggleEditMode] = useState(false) const [environment, setEnvironment] = useState(null) - const [config, setConfig] = useState(defaultConfig) const [prometheusAuth, setPrometheusAuth] = useState(undefined) const [showWindow, setShowWindow] = useState(false) const [confirmation, setConfirmation] = useState(false) - const [prometheusToggleEnabled] = useState(!!prometheus_url) - - const [prometheusAuthenticationType] = useState({ - type: prometheusAuth?.userName ? AuthenticationType.BASIC : AuthenticationType.ANONYMOUS, - }) - const authenticationType = prometheusAuth?.userName ? AuthenticationType.BASIC : AuthenticationType.ANONYMOUS const drawerRef = useRef(null) @@ -380,189 +318,28 @@ const Cluster = ({ identifier: `cluster-list__${cluster_name}`, }) - const isDefaultCluster = (): boolean => { - return id == 1 + const handleModalClose = () => { + toggleEditMode(false) } - const { state } = useForm( - { - cluster_name: { value: cluster_name, error: '' }, - url: { value: server_url, error: '' }, - userName: { value: prometheusAuth?.userName, error: '' }, - password: { value: prometheusAuth?.password, error: '' }, - prometheusTlsClientKey: { value: prometheusAuth?.tlsClientKey, error: '' }, - prometheusTlsClientCert: { value: prometheusAuth?.tlsClientCert, error: '' }, - proxyUrl: { value: proxyUrl, error: '' }, - sshUsername: { value: sshTunnelConfig?.user, error: '' }, - sshPassword: { value: sshTunnelConfig?.password, error: '' }, - sshAuthKey: { value: sshTunnelConfig?.authKey, error: '' }, - sshServerAddress: { value: sshTunnelConfig?.sshServerAddress, error: '' }, - isConnectedViaProxy: !!proxyUrl?.length, - isConnectedViaSSHTunnel: toConnectWithSSHTunnel, - tlsClientKey: { value: config.tls_key, error: '' }, - tlsClientCert: { value: config.cert_data, error: '' }, - certificateAuthorityData: { value: config.cert_auth_data, error: '' }, - token: { value: config?.bearer_token ? config.bearer_token : '', error: '' }, - endpoint: { value: prometheus_url || '', error: '' }, - authType: { value: authenticationType, error: '' }, - }, - { - cluster_name: { - required: true, - validators: [ - { error: 'Name is required', regex: /^.*$/ }, - { error: "Use only lowercase alphanumeric characters, '-', '_' or '.'", regex: /^[a-z0-9-\.\_]+$/ }, - { error: "Cannot start/end with '-', '_' or '.'", regex: /^(?![-._]).*[^-._]$/ }, - { error: 'Minimum 3 and Maximum 63 characters required', regex: /^.{3,63}$/ }, - ], - }, - url: { - required: true, - validator: { error: 'URL is required', regex: /^.*$/ }, - }, - proxyUrl: { - required: false, - validator: { error: 'Incorrect Url', regex: /^.*$/ }, - }, - toConnectWithSSHTunnel: { - required: false, - }, - sshUsername: { - required: false, - validator: { - error: 'Username or User Identifier is required. Username cannot contain spaces or special characters other than _ and -', - regex: /^[A-Za-z0-9_-]+$/, - }, - }, - sshPassword: { - required: false, - validator: { error: 'password is required', regex: /^(?!\s*$).+/ }, - }, - sshAuthKey: { - required: false, - validator: { error: 'ssh private key is required', regex: /^(?!\s*$).+/ }, - }, - sshServerAddress: { - required: false, - validator: { error: 'URL is required', regex: /^.*$/ }, - }, - isConnectedViaProxy: { - required: false, - }, - authType: { - required: false, - validator: { error: 'Authentication Type is required', regex: /^(?!\s*$).+/ }, - }, - userName: { - required: !!(prometheusToggleEnabled && prometheusAuthenticationType.type === AuthenticationType.BASIC), - validator: { error: 'username is required', regex: /^(?!\s*$).+/ }, - }, - password: { - required: !!(prometheusToggleEnabled && prometheusAuthenticationType.type === AuthenticationType.BASIC), - validator: { error: 'password is required', regex: /^(?!\s*$).+/ }, - }, - tlsClientKey: { - required: false, - validator: { error: 'TLS Key is required', regex: /^(?!\s*$).+/ }, - }, - tlsClientCert: { - required: false, - validator: { error: 'TLS Certificate is required', regex: /^(?!\s*$).+/ }, - }, - prometheusTlsClientKey: { - required: false, - }, - prometheusTlsClientCert: { - required: false, - }, - certificateAuthorityData: { - required: false, - validator: { error: 'Certificate authority data is required', regex: /^(?!\s*$).+/ }, - }, - token: - isDefaultCluster() || id - ? {} - : { - required: true, - validator: { error: 'token is required', regex: /[^]+/ }, - }, - endpoint: { - required: !!prometheusToggleEnabled, - validator: { error: 'endpoint is required', regex: /^.*$/ }, - }, - }, - onValidation, - ) - - const history = useHistory() const newEnvs = useMemo(() => { return clusterId ? [{ id: null }].concat(environments || []) : environments || [] }, [environments]) - async function handleEdit(e) { + async function handleEdit() { try { const { result } = await getCluster(clusterId) setPrometheusAuth(result.prometheusAuth) - setConfig({ ...result.config, ...(clusterId != 1 ? { bearer_token: DEFAULT_SECRET_PLACEHOLDER } : null) }) toggleEditMode((t) => !t) } catch (err) { showError(err) } } - const hideClusterDrawer = (e) => { + const hideClusterDrawer = () => { setShowWindow(false) } - async function onValidation() { - const payload = getClusterPayload() - const urlValue = state.url.value?.trim() ?? '' - if (urlValue.endsWith('/')) { - payload['server_url'] = urlValue.slice(0, -1) - } else { - payload['server_url'] = urlValue - } - const proxyUrlValue = state.proxyUrl.value?.trim() ?? '' - if (proxyUrlValue.endsWith('/')) { - payload.remoteConnectionConfig.proxyConfig['proxyUrl'] = proxyUrlValue.slice(0, -1) - } - if (state.authType.value === AuthenticationType.BASIC && prometheusToggleEnabled) { - const isValid = state.userName?.value && state.password?.value - if (!isValid) { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Please add both username and password', - }) - } else { - payload.prometheusAuth['userName'] = state.userName.value || '' - payload.prometheusAuth['password'] = state.password.value || '' - } - } - } - - const getClusterPayload = () => { - return { - id, - cluster_name: state.cluster_name.value, - config: { - bearer_token: - state.token.value && state.token.value !== DEFAULT_SECRET_PLACEHOLDER ? state.token.value : '', - }, - active, - prometheus_url: prometheusToggleEnabled ? state.endpoint.value : '', - prometheusAuth: { - userName: prometheusToggleEnabled ? state.userName.value : '', - password: prometheusToggleEnabled ? state.password.value : '', - tlsClientKey: prometheusToggleEnabled ? state.tlsClientKey.value : '', - tlsClientCert: prometheusToggleEnabled ? state.tlsClientCert.value : '', - }, - remoteConnectionConfig: getRemoteConnectionConfig(state), - insecureSkipTlsVerify: !isTlsConnection, - } - } - - const envName: string = getEnvName(defaultClusterComponent, agentInstallationStage) - const renderNoEnvironmentTab = () => { return (
@@ -586,9 +363,6 @@ const Cluster = ({ if (!clusterId) { toggleEditMode((t) => !t) } - if (isTlsConnection === insecureSkipTlsVerify) { - toggleCheckTlsConnection() - } } const clusterIcon = () => { @@ -635,9 +409,14 @@ const Cluster = ({ clusterId ? 'cluster-list--update' : 'cluster-list--create collapsed-list' }`} > - + {!clusterId && ( @@ -774,42 +553,40 @@ const Cluster = ({ ) : ( clusterId && renderNoEnvironmentTab() )} - {editMode && ( - -
- -
-
- )} + {editMode && + (!isVirtualCluster ? ( + +
+ +
+
+ ) : ( + + ))} + {showWindow && ( { - return ( -
-
- -
- Warning: Prometheus configuration will be removed and - you won’t be able to see metrics for applications deployed in this cluster. -
+const PrometheusWarningInfo = () => ( +
+
+ +
+ Warning: Prometheus configuration will be removed and you + won’t be able to see metrics for applications deployed in this cluster.
- ) -} - -const PrometheusRequiredFieldInfo = () => { - return ( -
-
- -
- Fill all the required fields OR turn off the above switch to skip configuring prometheus. -
+
+) + +const PrometheusRequiredFieldInfo = () => ( +
+
+ +
+ Fill all the required fields OR turn off the above switch to skip configuring prometheus.
- ) -} +
+) -export default function ClusterForm({ +const ClusterForm = ({ id = null, - cluster_name, - server_url, - active, - config, + clusterName, + serverUrl, toggleEditMode = noop, - reload, - prometheus_url, + reload = noop, + prometheusUrl = '', prometheusAuth, - defaultClusterComponent, - proxyUrl, - sshUsername, - sshPassword, - sshAuthKey, - sshServerAddress, - isConnectedViaProxy, - isConnectedViaSSHTunnel, - isTlsConnection, - toggleCheckTlsConnection, - setTlsConnectionFalse, + proxyUrl = '', + sshUsername = '', + sshPassword = '', + sshAuthKey = '', + sshServerAddress = '', + isConnectedViaSSHTunnel = false, handleCloseCreateClusterForm = noop, - toggleKubeConfigFile, - isKubeConfigFile, - isClusterDetails, - toggleClusterDetails, - isVirtualCluster, isProd = false, -}: ClusterFormProps) { - const [prometheusToggleEnabled, setPrometheusToggleEnabled] = useState(!!prometheus_url) + FooterComponent, + handleModalClose = noop, + isTlsConnection: initialIsTlsConnection = false, +}: ClusterFormProps & Partial) => { + const [prometheusToggleEnabled, setPrometheusToggleEnabled] = useState(!!prometheusUrl) const [prometheusAuthenticationType, setPrometheusAuthenticationType] = useState({ type: prometheusAuth?.userName ? AuthenticationType.BASIC : AuthenticationType.ANONYMOUS, }) + const [isTlsConnection, setIsTlsConnection] = useState(initialIsTlsConnection) + const [isKubeConfigFile, toggleKubeConfigFile] = useState(false) + const [isClusterDetails, toggleClusterDetails] = useState(false) const authenTicationType = prometheusAuth?.userName ? AuthenticationType.BASIC : AuthenticationType.ANONYMOUS - const isDefaultCluster = (): boolean => { - return id == 1 + const isConnectedViaProxy = !!proxyUrl + + const toggleCheckTlsConnection = () => { + setIsTlsConnection((prev) => !prev) + } + + const setTlsConnectionFalse = () => { + setIsTlsConnection(false) } + + const isDefaultCluster = id === 1 const [confirmation, setConfirmation] = useState(false) const inputFileRef = useRef(null) const [uploadState, setUploadState] = useState(UPLOAD_STATE.UPLOAD) @@ -164,8 +158,6 @@ export default function ClusterForm({ const [selectedUserNameOptions, setSelectedUserNameOptions] = useState>({}) const [isClusterSelected, setClusterSeleceted] = useState>({}) const [selectAll, setSelectAll] = useState(false) - const [getClusterVar, setGetClusterState] = useState(false) - const [isVirtual, setIsVirtual] = useState(isVirtualCluster) const [isConnectedViaProxyTemp, setIsConnectedViaProxyTemp] = useState(isConnectedViaProxy) const [isConnectedViaSSHTunnelTemp, setIsConnectedViaSSHTunnelTemp] = useState(isConnectedViaSSHTunnel) @@ -184,151 +176,42 @@ export default function ClusterForm({ !window._env_.K8S_CLIENT, ) - const _remoteConnectionMethod = isConnectedViaProxyTemp - ? RemoteConnectionType.Proxy - : isConnectedViaSSHTunnelTemp - ? RemoteConnectionType.SSHTunnel - : RemoteConnectionType.Direct - const [remoteConnectionMethod, setRemoteConnectionMethod] = useState(_remoteConnectionMethod) - const initialSSHAuthenticationType = - sshPassword && sshAuthKey - ? SSHAuthenticationType.Password_And_SSH_Private_Key - : sshAuthKey - ? SSHAuthenticationType.SSH_Private_Key - : SSHAuthenticationType.Password - const [SSHConnectionType, setSSHConnectionType] = useState(initialSSHAuthenticationType) + const getRemoteConnectionConfigType = () => { + if (isConnectedViaProxyTemp) { + return RemoteConnectionType.Proxy + } - const { state, handleOnChange, handleOnSubmit } = useForm( - { - cluster_name: { value: cluster_name, error: '' }, - url: { value: !id ? getServerURLFromLocalStorage(server_url) : server_url, error: '' }, - userName: { value: prometheusAuth?.userName, error: '' }, - password: { value: prometheusAuth?.password, error: '' }, - prometheusTlsClientKey: { value: prometheusAuth?.tlsClientKey, error: '' }, - prometheusTlsClientCert: { value: prometheusAuth?.tlsClientCert, error: '' }, - proxyUrl: { value: proxyUrl, error: '' }, - sshUsername: { value: sshUsername, error: '' }, - sshPassword: { value: sshPassword, error: '' }, - sshAuthKey: { value: sshAuthKey, error: '' }, - sshServerAddress: { value: sshServerAddress, error: '' }, - tlsClientKey: { value: config?.tls_key, error: '' }, - tlsClientCert: { value: config?.cert_data, error: '' }, - certificateAuthorityData: { value: config?.cert_auth_data, error: '' }, - token: { value: config?.bearer_token ? config.bearer_token : '', error: '' }, - endpoint: { value: prometheus_url || '', error: '' }, - authType: { value: authenTicationType, error: '' }, - isProd: { value: isProd.toString(), error: '' }, - }, - { - cluster_name: { - required: true, - validators: [ - { error: 'Name is required', regex: /^.*$/ }, - { error: "Use only lowercase alphanumeric characters, '-', '_' or '.'", regex: /^[a-z0-9-\.\_]+$/ }, - { error: "Cannot start/end with '-', '_' or '.'", regex: /^(?![-._]).*[^-._]$/ }, - { error: 'Minimum 3 and Maximum 63 characters required', regex: /^.{3,63}$/ }, - ], - }, - url: { - required: true, - validator: { error: 'URL is required', regex: /^.*$/ }, - }, - authType: { - required: false, - validator: { error: 'Authentication Type is required', regex: /^(?!\s*$).+/ }, - }, - userName: { - required: !!(prometheusToggleEnabled && prometheusAuthenticationType.type === AuthenticationType.BASIC), - validator: { error: 'username is required', regex: /^(?!\s*$).+/ }, - }, - password: { - required: !!(prometheusToggleEnabled && prometheusAuthenticationType.type === AuthenticationType.BASIC), - validator: { error: 'password is required', regex: /^(?!\s*$).+/ }, - }, - prometheusTlsClientKey: { - required: false, - }, - prometheusTlsClientCert: { - required: false, - }, - proxyUrl: { - required: RemoteConnectionRadio && proxyUrl, - validator: { - error: 'Please provide a valid URL. URL must start with http:// or https://', - regex: /^(http(s)?:\/\/)[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/, - }, - }, - sshUsername: { - required: RemoteConnectionRadio && remoteConnectionMethod === RemoteConnectionType.SSHTunnel, - validator: { - error: 'Username or User Identifier is required. Username cannot contain spaces or special characters other than _ and -', - regex: /^[A-Za-z0-9_-]+$/, - }, - }, - sshPassword: { - required: - RemoteConnectionRadio && - remoteConnectionMethod === RemoteConnectionType.SSHTunnel && - (SSHConnectionType === SSHAuthenticationType.Password || - SSHConnectionType === SSHAuthenticationType.Password_And_SSH_Private_Key), - validator: { error: 'password is required', regex: /^(?!\s*$).+/ }, - }, - sshAuthKey: { - required: - RemoteConnectionRadio && - remoteConnectionMethod === RemoteConnectionType.SSHTunnel && - (SSHConnectionType === SSHAuthenticationType.SSH_Private_Key || - SSHConnectionType === SSHAuthenticationType.Password_And_SSH_Private_Key), - validator: { error: 'private key is required', regex: /^(?!\s*$).+/ }, - }, - sshServerAddress: { - required: RemoteConnectionRadio && remoteConnectionMethod === RemoteConnectionType.SSHTunnel, - validator: - remoteConnectionMethod === RemoteConnectionType.SSHTunnel - ? { - error: 'Please provide a valid URL. URL must start with http:// or https://', - regex: /^(http(s)?:\/\/)[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/, - } - : { error: '', regex: /^(?!\s*$).+/ }, - }, - tlsClientKey: { - required: id ? false : isTlsConnection, - }, - tlsClientCert: { - required: id ? false : isTlsConnection, - }, - certificateAuthorityData: { - required: id ? false : isTlsConnection, - }, - token: - isDefaultCluster() || id - ? {} - : { - required: true, - validator: { error: 'token is required', regex: /[^]+/ }, - }, - endpoint: { - required: !!prometheusToggleEnabled, - validator: { error: 'endpoint is required', regex: /^.*$/ }, - }, - }, - onValidation, - ) + if (isConnectedViaSSHTunnelTemp) { + return RemoteConnectionType.SSHTunnel + } - const isGrafanaModuleInstalled = grafanaModuleStatus?.result?.status === ModuleStatus.INSTALLED + return RemoteConnectionType.Direct + } + + const resolveSSHAuthType = () => { + if (sshPassword && sshAuthKey) { + return SSHAuthenticationType.Password_And_SSH_Private_Key + } + + if (sshAuthKey) { + return SSHAuthenticationType.SSH_Private_Key + } - const toggleGetCluster = () => { - setGetClusterState(!getClusterVar) + return SSHAuthenticationType.Password } + const [remoteConnectionMethod, setRemoteConnectionMethod] = useState(getRemoteConnectionConfigType) + const [SSHConnectionType, setSSHConnectionType] = useState(resolveSSHAuthType) + + const isGrafanaModuleInstalled = grafanaModuleStatus?.result?.status === ModuleStatus.INSTALLED + const handleEditConfigClick = () => { - toggleGetCluster() - toggleKubeConfigFile(true) + setDataList([]) } const getSaveClusterPayload = (dataLists: DataListType[]) => { const saveClusterPayload: SaveClusterPayloadType[] = [] - for (const _dataList of dataLists) { + dataLists.forEach((_dataList) => { if (isClusterSelected[_dataList.cluster_name]) { const _clusterDetails: SaveClusterPayloadType = { id: _dataList.id, @@ -348,34 +231,34 @@ export default function ClusterForm({ } saveClusterPayload.push(_clusterDetails) } - } + }) return saveClusterPayload } - async function saveClustersDetails() { + const saveClustersDetails = async () => { try { const payload = getSaveClusterPayload(dataList) await saveClusters(payload).then((response) => { - const _clusterList = response.result.map((_clusterSaveDetails, index) => { + const _clusterList = response.result.map((_clusterSaveDetails) => { let status let message if ( - _clusterSaveDetails['errorInConnecting'].length === 0 && - _clusterSaveDetails['clusterUpdated'] === false + _clusterSaveDetails.errorInConnecting.length === 0 && + _clusterSaveDetails.clusterUpdated === false ) { status = 'Added' message = 'Cluster Added' - } else if (_clusterSaveDetails['clusterUpdated'] === true) { + } else if (_clusterSaveDetails.clusterUpdated === true) { status = 'Updated' message = 'Cluster Updated' } else { status = 'Failed' - message = _clusterSaveDetails['errorInConnecting'] + message = _clusterSaveDetails.errorInConnecting } return { - clusterName: _clusterSaveDetails['cluster_name'], + clusterName: _clusterSaveDetails.cluster_name, status, message, } @@ -389,59 +272,61 @@ export default function ClusterForm({ } } - function YAMLtoJSON(saveYamlData) { + const YAMLtoJSON = (yamlString: string) => { try { - const obj = YAML.parse(saveYamlData) + const obj = YAML.parse(yamlString) const jsonStr = JSON.stringify(obj) return jsonStr - } catch (error) {} + } catch { + noop() + return '' + } } - function isCheckboxDisabled() { + const isCheckboxDisabled = () => { const clusters = Object.values(selectedUserNameOptions) if (clusters.length === 0) { return true } - return clusters.every((cluster) => { - return cluster.errorInConnecting !== 'cluster-already-exists' && cluster.errorInConnecting.length > 0 - }) + return clusters.every( + (cluster) => cluster.errorInConnecting !== 'cluster-already-exists' && cluster.errorInConnecting.length > 0, + ) } - async function validateClusterDetail() { + const validateClusterDetail = async () => { try { const payload = { config: YAMLtoJSON(saveYamlData) } await validateCluster(payload).then((response) => { const defaultUserNameSelections: Record = {} const _clusterSelections: Record = {} setDataList([ - ...Object.values(response.result).map((_cluster) => { - const _userInfoList = [...Object.values(_cluster['userInfos'] as UserDetails[])] - defaultUserNameSelections[_cluster['cluster_name']] = { + ...Object.values(response.result).map((_cluster) => { + const _userInfoList = [...Object.values(_cluster.userInfos as UserDetails[])] + defaultUserNameSelections[_cluster.cluster_name] = { label: _userInfoList[0].userName, value: _userInfoList[0].userName, errorInConnecting: _userInfoList[0].errorInConnecting, config: _userInfoList[0].config, } - _clusterSelections[_cluster['cluster_name']] = false + _clusterSelections[_cluster.cluster_name] = false return { - cluster_name: _cluster['cluster_name'], + cluster_name: _cluster.cluster_name, userInfos: _userInfoList, - server_url: _cluster['server_url'], - active: _cluster['active'], - defaultClusterComponent: _cluster['defaultClusterComponent'], - insecureSkipTlsVerify: _cluster['insecureSkipTlsVerify'], - id: _cluster['id'], - remoteConnectionConfig: _cluster['remoteConnectionConfig'], + server_url: _cluster.server_url, + active: _cluster.active, + defaultClusterComponent: _cluster.defaultClusterComponent, + insecureSkipTlsVerify: _cluster.insecureSkipTlsVerify, + id: _cluster.id, + remoteConnectionConfig: _cluster.remoteConnectionConfig, } }), ]) setSelectedUserNameOptions(defaultUserNameSelections) setClusterSeleceted(_clusterSelections) setLoadingState(false) - toggleGetCluster() setValidationError(false) }) } catch (err: any) { @@ -464,47 +349,49 @@ export default function ClusterForm({ } } - const getClusterPayload = () => { - return { - id, - insecureSkipTlsVerify: !isTlsConnection, - cluster_name: state.cluster_name.value, - config: { - bearer_token: - state.token.value && state.token.value !== DEFAULT_SECRET_PLACEHOLDER - ? state.token.value.trim() - : '', - tls_key: state.tlsClientKey.value, - cert_data: state.tlsClientCert.value, - cert_auth_data: state.certificateAuthorityData.value, - }, - isProd: state.isProd.value === 'true', - active, - remoteConnectionConfig: getRemoteConnectionConfig(state, remoteConnectionMethod, SSHConnectionType), - prometheus_url: prometheusToggleEnabled ? state.endpoint.value : '', - prometheusAuth: { - userName: - prometheusToggleEnabled && state.authType.value === AuthenticationType.BASIC - ? state.userName.value - : '', - password: - prometheusToggleEnabled && state.authType.value === AuthenticationType.BASIC - ? state.password.value - : '', - tlsClientKey: prometheusToggleEnabled ? state.prometheusTlsClientKey.value : '', - tlsClientCert: prometheusToggleEnabled ? state.prometheusTlsClientCert.value : '', - isAnonymous: state.authType.value === AuthenticationType.ANONYMOUS, - }, - } + const setRemoteConnectionFalse = () => { + setIsConnectedViaProxyTemp(false) + setIsConnectedViaSSHTunnelTemp(false) } - async function onValidation() { - const payload = getClusterPayload() + const getClusterPayload = (state) => ({ + id, + insecureSkipTlsVerify: !isTlsConnection, + cluster_name: state.cluster_name.value, + config: { + bearer_token: + state.token.value && state.token.value !== DEFAULT_SECRET_PLACEHOLDER ? state.token.value.trim() : '', + tls_key: state.tlsClientKey.value, + cert_data: state.tlsClientCert.value, + cert_auth_data: state.certificateAuthorityData.value, + }, + isProd: state.isProd.value === 'true', + active: true, + remoteConnectionConfig: getRemoteConnectionConfig(state, remoteConnectionMethod, SSHConnectionType), + prometheus_url: prometheusToggleEnabled ? state.endpoint.value : '', + prometheusAuth: { + userName: + prometheusToggleEnabled && state.authType.value === AuthenticationType.BASIC + ? state.userName.value + : '', + password: + prometheusToggleEnabled && state.authType.value === AuthenticationType.BASIC + ? state.password.value + : '', + tlsClientKey: prometheusToggleEnabled ? state.prometheusTlsClientKey.value : '', + tlsClientCert: prometheusToggleEnabled ? state.prometheusTlsClientCert.value : '', + isAnonymous: state.authType.value === AuthenticationType.ANONYMOUS, + }, + server_url: '', + }) + + const onValidation = async (state) => { + const payload = getClusterPayload(state) const urlValue = state.url.value?.trim() ?? '' if (urlValue.endsWith('/')) { - payload['server_url'] = urlValue.slice(0, -1) + payload.server_url = urlValue.slice(0, -1) } else { - payload['server_url'] = urlValue + payload.server_url = urlValue } if (remoteConnectionMethod === RemoteConnectionType.Proxy) { let proxyUrlValue = state.proxyUrl?.value?.trim() ?? '' @@ -525,16 +412,16 @@ export default function ClusterForm({ }) return } - payload.prometheusAuth['userName'] = state.userName.value || '' - payload.prometheusAuth['password'] = state.password.value || '' - payload.prometheusAuth['tlsClientKey'] = state.prometheusTlsClientKey.value || '' - payload.prometheusAuth['tlsClientCert'] = state.prometheusTlsClientCert.value || '' + payload.prometheusAuth.userName = state.userName.value || '' + payload.prometheusAuth.password = state.password.value || '' + payload.prometheusAuth.tlsClientKey = state.prometheusTlsClientKey.value || '' + payload.prometheusAuth.tlsClientCert = state.prometheusTlsClientCert.value || '' } if (isTlsConnection) { if (state.tlsClientKey.value || state.tlsClientCert.value || state.certificateAuthorityData.value) { - payload.config['tls_key'] = state.tlsClientKey.value || '' - payload.config['cert_data'] = state.tlsClientCert.value || '' - payload.config['cert_auth_data'] = state.certificateAuthorityData.value || '' + payload.config.tls_key = state.tlsClientKey.value || '' + payload.config.cert_data = state.tlsClientCert.value || '' + payload.config.cert_auth_data = state.certificateAuthorityData.value || '' } } @@ -558,20 +445,138 @@ export default function ClusterForm({ } } + const { state, handleOnChange, handleOnSubmit } = useForm( + { + cluster_name: { value: clusterName, error: '' }, + url: { value: !id ? getServerURLFromLocalStorage(serverUrl) : serverUrl, error: '' }, + userName: { value: prometheusAuth?.userName, error: '' }, + password: { value: prometheusAuth?.password, error: '' }, + prometheusTlsClientKey: { value: prometheusAuth?.tlsClientKey, error: '' }, + prometheusTlsClientCert: { value: prometheusAuth?.tlsClientCert, error: '' }, + proxyUrl: { value: proxyUrl, error: '' }, + sshUsername: { value: sshUsername, error: '' }, + sshPassword: { value: sshPassword, error: '' }, + sshAuthKey: { value: sshAuthKey, error: '' }, + sshServerAddress: { value: sshServerAddress, error: '' }, + tlsClientKey: { value: undefined, error: '' }, + tlsClientCert: { value: undefined, error: '' }, + certificateAuthorityData: { value: undefined, error: '' }, + token: { value: '', error: '' }, + endpoint: { value: prometheusUrl || '', error: '' }, + authType: { value: authenTicationType, error: '' }, + isProd: { value: isProd.toString(), error: '' }, + }, + { + cluster_name: { + required: true, + validators: [ + { error: 'Name is required', regex: /^.*$/ }, + { error: "Use only lowercase alphanumeric characters, '-', '_' or '.'", regex: /^[a-z0-9-._]+$/ }, + { error: "Cannot start/end with '-', '_' or '.'", regex: /^(?![-._]).*[^-._]$/ }, + { error: 'Minimum 3 and Maximum 63 characters required', regex: /^.{3,63}$/ }, + ], + }, + url: { + required: true, + validator: { error: 'URL is required', regex: /^.*$/ }, + }, + authType: { + required: false, + validator: { error: 'Authentication Type is required', regex: /^(?!\s*$).+/ }, + }, + userName: { + required: !!(prometheusToggleEnabled && prometheusAuthenticationType.type === AuthenticationType.BASIC), + validator: { error: 'username is required', regex: /^(?!\s*$).+/ }, + }, + password: { + required: !!(prometheusToggleEnabled && prometheusAuthenticationType.type === AuthenticationType.BASIC), + validator: { error: 'password is required', regex: /^(?!\s*$).+/ }, + }, + prometheusTlsClientKey: { + required: false, + }, + prometheusTlsClientCert: { + required: false, + }, + proxyUrl: { + required: RemoteConnectionRadio && proxyUrl, + validator: { + error: 'Please provide a valid URL. URL must start with http:// or https://', + regex: /^(http(s)?:\/\/)[\w.-]+(?:\.[\w.-]+)+[\w\-._~:/?#[\]@!$&'()*+,;=.]+$/, + }, + }, + sshUsername: { + required: RemoteConnectionRadio && remoteConnectionMethod === RemoteConnectionType.SSHTunnel, + validator: { + error: 'Username or User Identifier is required. Username cannot contain spaces or special characters other than _ and -', + regex: /^[A-Za-z0-9_-]+$/, + }, + }, + sshPassword: { + required: + RemoteConnectionRadio && + remoteConnectionMethod === RemoteConnectionType.SSHTunnel && + (SSHConnectionType === SSHAuthenticationType.Password || + SSHConnectionType === SSHAuthenticationType.Password_And_SSH_Private_Key), + validator: { error: 'password is required', regex: /^(?!\s*$).+/ }, + }, + sshAuthKey: { + required: + RemoteConnectionRadio && + remoteConnectionMethod === RemoteConnectionType.SSHTunnel && + (SSHConnectionType === SSHAuthenticationType.SSH_Private_Key || + SSHConnectionType === SSHAuthenticationType.Password_And_SSH_Private_Key), + validator: { error: 'private key is required', regex: /^(?!\s*$).+/ }, + }, + sshServerAddress: { + required: RemoteConnectionRadio && remoteConnectionMethod === RemoteConnectionType.SSHTunnel, + validator: + remoteConnectionMethod === RemoteConnectionType.SSHTunnel + ? { + error: 'Please provide a valid URL. URL must start with http:// or https://', + regex: /^(http(s)?:\/\/)[\w.-]+(?:\.[\w.-]+)+[\w\-._~:/?#[\]@!$&'()*+,;=.]+$/, + } + : { error: '', regex: /^(?!\s*$).+/ }, + }, + tlsClientKey: { + required: id ? false : isTlsConnection, + }, + tlsClientCert: { + required: id ? false : isTlsConnection, + }, + certificateAuthorityData: { + required: id ? false : isTlsConnection, + }, + token: + isDefaultCluster || id + ? {} + : { + required: true, + validator: { error: 'token is required', regex: /[^]+/ }, + }, + endpoint: { + required: !!prometheusToggleEnabled, + validator: { error: 'endpoint is required', regex: /^.*$/ }, + }, + }, + // eslint-disable-next-line @typescript-eslint/no-use-before-define + onValidation, + ) + const setPrometheusToggle = () => { setPrometheusToggleEnabled(!prometheusToggleEnabled) } const OnPrometheusAuthTypeChange = (e) => { handleOnChange(e) - if (state.authType.value == AuthenticationType.BASIC) { + if (state.authType.value === AuthenticationType.BASIC) { setPrometheusAuthenticationType({ type: AuthenticationType.ANONYMOUS }) } else { setPrometheusAuthenticationType({ type: AuthenticationType.BASIC }) } } - const ClusterInfoComponent = () => { + const renderClusterInfo = () => { const k8sClusters = Object.values(CLUSTER_COMMAND) return ( <> @@ -601,18 +606,16 @@ export default function ClusterForm({ ) } - const clusterLabel = () => { - return ( -
- Server URL & Bearer token{isDefaultCluster() ? '' : '*'} - - - - How to find for - -
- ) - } + const clusterLabel = () => ( +
+ Server URL & Bearer token{isDefaultCluster ? '' : '*'} + + + + How to find for + {renderClusterInfo()} +
+ ) const onFileChange = (e): void => { setUploadState(UPLOAD_STATE.UPLOADING) @@ -621,7 +624,9 @@ export default function ClusterForm({ reader.onload = () => { try { setSaveYamlData(reader.result.toString()) - } catch (e) {} + } catch { + noop() + } } reader.readAsText(file) setUploadState(UPLOAD_STATE.SUCCESS) @@ -641,9 +646,6 @@ export default function ClusterForm({ if (isKubeConfigFile) { toggleKubeConfigFile(!isKubeConfigFile) } - if (getClusterVar) { - toggleGetCluster() - } if (isClusterDetails) { toggleClusterDetails(!isClusterDetails) } @@ -671,11 +673,34 @@ export default function ClusterForm({ setSSHConnectionType(authType) } - const setRemoteConnectionFalse = () => { - setIsConnectedViaProxyTemp(false) - setIsConnectedViaSSHTunnelTemp(false) + const clusterTitle = () => { + if (!id) { + return 'Add Cluster' + } + return 'Edit Cluster' } + const renderHeader = () => ( +
+

+ {clusterTitle()} +

+ +
+ ) + const renderUrlAndBearerToken = () => { let proxyConfig let sshConfig @@ -701,13 +726,29 @@ export default function ClusterForm({ } } const passedRemoteConnectionMethod = { value: remoteConnectionMethod, error: '' } + + const getTokenText = () => { + if (!id) { + return state.token.value + } + + return id === DEFAULT_CLUSTER_ID ? '' : DEFAULT_SECRET_PLACEHOLDER + } + + const getGrafanaModuleSectionClassName = () => { + if (prometheusToggleEnabled) { + return 'mb-20' + } + return prometheusUrl ? 'mb-20' : 'mb-40' + } + return ( <>
@@ -730,15 +771,7 @@ export default function ClusterForm({ {id !== DEFAULT_CLUSTER_ID && (