diff --git a/.env b/.env index bd64be921a..e489db8e84 100644 --- a/.env +++ b/.env @@ -60,7 +60,6 @@ FEATURE_SWAP_TRAFFIC_ENABLE=false FEATURE_RB_SYNC_CLUSTER_ENABLE=true FEATURE_BULK_RESTART_WORKLOADS_FROM_RB=deployment,rollout,daemonset,statefulset FEATURE_DEFAULT_MERGE_STRATEGY= -FEATURE_CLUSTER_MAP_ENABLE=true FEATURE_DEFAULT_LANDING_RB_ENABLE=false FEATURE_ACTION_AUDIOS_ENABLE=true FEATURE_APPLICATION_TEMPLATES_ENABLE=true diff --git a/package.json b/package.json index baf3139d3e..166e2e394f 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "homepage": "/dashboard", "dependencies": { - "@devtron-labs/devtron-fe-common-lib": "1.18.1-pre-0", + "@devtron-labs/devtron-fe-common-lib": "1.18.1-pre-1", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@rjsf/core": "^5.13.3", "@rjsf/utils": "^5.13.3", diff --git a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DTChartSelector.tsx b/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DTChartSelector.tsx index 4897b58351..16e6115b0f 100644 --- a/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DTChartSelector.tsx +++ b/src/Pages/Applications/DevtronApps/Details/AppConfigurations/MainContent/DeploymentTemplate/DTChartSelector.tsx @@ -120,7 +120,7 @@ const ChartSelectorDropdown = ({ /> - + <> {customCharts.length > 0 && (
{ - const [view, setView] = useState(ViewType.LOADING) - const [clusters, setClusters] = useState([]) - - const history = useHistory() - - const initialize = () => { - Promise.all([getClusterList(), window._env_.K8S_CLIENT ? { result: undefined } : getEnvironmentList()]) - .then(([clusterRes, envResponse]) => { - const environments = envResponse.result || [] - const clusterEnvironmentMap = environments.reduce((agg, curr) => { - const newAgg = { ...agg } - newAgg[curr.cluster_id] = newAgg[curr.cluster_id] || [] - newAgg[curr.cluster_id].push(curr) - return newAgg - }, {}) - - let clustersList = clusterRes.result || [] - clustersList = clustersList.map((cluster) => ({ - ...cluster, - environments: clusterEnvironmentMap[cluster.id] || [], - category: getSelectParsedCategory(cluster.category), - })) - - clustersList = clustersList.sort((a, b) => sortCallback('cluster_name', a, b)) - - setClusters(clustersList) - setView(ViewType.FORM) - }) - .catch((error) => { - showError(error) - setView(ViewType.ERROR) - }) - } - - const handleRedirectToClusterList = () => { - history.push(URLS.GLOBAL_CONFIG_CLUSTER) - } - - useEffect(() => { - if (isSuperAdmin) { - initialize() - } - }, [isSuperAdmin]) - - if (!isSuperAdmin) { - return ( -
- -
- ) - } - - if (view === ViewType.LOADING) return - if (view === ViewType.ERROR) return - - const moduleBasedTitle = `Clusters${window._env_.K8S_CLIENT ? '' : ' and Environments'}` - - return ( -
-
- `Manage your organization’s ${moduleBasedTitle.toLowerCase()}.`} - docLink="GLOBAL_CONFIG_CLUSTER" - showInfoIconTippy - additionalContainerClasses="mb-20" - /> -
- {ManageCategoryButton && } -
-
- - {clusters.map((cluster) => ( - - ))} - - {ManageCategories && ( - - - - )} - - - - - - { - const { clusterId } = props.match.params - const cluster: ClusterMetadataTypes = clusters.find((c) => c.id === +clusterId) - - if (!cluster || !cluster.isVirtualCluster) { - return ( - -
- {!cluster ? ( - - ) : ( - - )} -
-
- ) - } - - return ( - - ) - }} - /> - - {PodSpreadModal && ( - { - const { clusterName } = props.match.params - const foundCluster: ClusterMetadataTypes | { id?: number } = - clusters.find((c) => c.cluster_name === clusterName) || {} - const { id: clusterId } = foundCluster - - return - }} - /> - )} - - {HibernationRulesModal && ( - { - const { clusterName } = props.match.params - const foundCluster: ClusterMetadataTypes | { id?: number } = - clusters.find((c) => c.cluster_name === clusterName) || {} - const { id: clusterId } = foundCluster - - return - }} - /> - )} - - { - const { clusterName } = props.match.params - const foundCluster: ClusterMetadataTypes | { isVirtualCluster?: boolean; id?: null } = - clusters.find((c) => c.cluster_name === clusterName) || {} - const { isVirtualCluster, id: clusterId } = foundCluster - - return ( - - ) - }} - /> -
- ) -} - -export default ClusterComponents diff --git a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/ClusterEnvironmentDrawer.tsx b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/ClusterEnvironmentDrawer.tsx index c9ac9d8fb5..f44a46afa4 100644 --- a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/ClusterEnvironmentDrawer.tsx +++ b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/ClusterEnvironmentDrawer.tsx @@ -14,44 +14,48 @@ * limitations under the License. */ -import { useEffect, useState } from 'react' -import { generatePath } from 'react-router-dom' +import { useEffect, useMemo, useState } from 'react' import { API_STATUS_CODES, Button, - ButtonComponentType, ButtonStyleType, ButtonVariantType, ComponentSizeType, CustomInput, Drawer, - GenericEmptyState, + getSelectPickerOptionByValue, + Icon, + ModalSidebarPanel, noop, + SelectPicker, + SelectPickerOptionType, ServerErrors, showError, stopPropagation, TagType, ToastManager, ToastVariantType, - Tooltip, + useAsync, useForm, UseFormSubmitHandler, } from '@devtron-labs/devtron-fe-common-lib' -import { ReactComponent as ICAdd } from '@Icons/ic-add.svg' -import { ReactComponent as Close } from '@Icons/ic-close.svg' import { ReactComponent as Trash } from '@Icons/ic-delete-interactive.svg' import { importComponentFromFELibrary } from '@Components/common' -import { URLS } from '@Config/routes' +import { getClusterListing } from '@Components/ResourceBrowser/ResourceBrowser.service' import { getNamespaceFromLocalStorage } from '@Pages/GlobalConfigurations/ClustersAndEnvironments/cluster.util' import { ADD_ENVIRONMENT_FORM_LOCAL_STORAGE_KEY } from '@Pages/GlobalConfigurations/ClustersAndEnvironments/constants' -import { deleteEnvironment, saveEnvironment, updateEnvironment } from '../cluster.service' -import { CreateClusterTypeEnum } from '../CreateCluster/types' +import { + deleteEnvironment, + getClusterList as getClusterDetails, + saveEnvironment, + updateEnvironment, +} from '../cluster.service' import { EnvironmentDeleteComponent } from '../EnvironmentDeleteComponent' import { clusterEnvironmentDrawerFormValidationSchema } from './schema' -import { ClusterEnvironmentDrawerFormProps, ClusterEnvironmentDrawerProps, ClusterNamespacesDTO } from './types' +import { ClusterNamespacesDTO, EnvDrawerProps, EnvironmentFormType } from './types' import { getClusterEnvironmentUpdatePayload, getClusterNamespaceByName, getNamespaceLabels } from './utils' const virtualClusterSaveUpdateApi = importComponentFromFELibrary('virtualClusterSaveUpdateApi', null, 'function') @@ -61,41 +65,82 @@ const AssignCategorySelect = importComponentFromFELibrary('AssignCategorySelect' const getVirtualClusterSaveUpdate = (_id) => virtualClusterSaveUpdateApi?.(_id) +const INITIAL_NAMESPACES = { + isFetching: false, + data: null, + error: null, +} + +const INITIAL_NAMESPACE_LABELS = { + labels: null, + resourceVersion: null, +} + export const ClusterEnvironmentDrawer = ({ - environmentName, + envId, + envName, namespace, - id, clusterId, + clusterName, isProduction, description, reload, hideClusterDrawer, - isVirtual, - clusterName, + isVirtualCluster, category, -}: ClusterEnvironmentDrawerProps) => { +}: EnvDrawerProps) => { // STATES // Manages the loading state for create and update actions const [crudLoading, setCrudLoading] = useState(false) // Controls the visibility of the delete confirmation dialog const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false) // Stores namespace labels and resourceVersion fetched from the cluster - const [namespaceLabels, setNamespaceLabels] = useState<{ labels: TagType[]; resourceVersion: string }>({ - labels: null, - resourceVersion: null, - }) + const [namespaceLabels, setNamespaceLabels] = useState<{ labels: TagType[]; resourceVersion: string }>( + INITIAL_NAMESPACE_LABELS, + ) // Stores the response from the clusterNamespaces API, including fetching state, data, and any error messages const [clusterNamespaces, setClusterNamespaces] = useState<{ isFetching: boolean data: ClusterNamespacesDTO[] error: ServerErrors - }>({ - isFetching: false, - data: null, - error: null, + }>(INITIAL_NAMESPACES) + + // Need different state since validations change on basis of this state + const [isSelectedClusterVirtual, setIsSelectedClusterVirtual] = useState(isVirtualCluster ?? false) + + const [clusterListLoading, clusterListResult, clusterListError, reloadClusterList] = useAsync( + () => getClusterListing(true), + [], + !envId, // No need of cluster list in case of edit env + ) + + // FORM METHODS + const { data, errors, register, handleSubmit, trigger, reset } = useForm({ + initialValues: { + clusterId: clusterId ?? null, + envName: envName ?? '', + namespace: envId ? namespace : getNamespaceFromLocalStorage(''), + isProduction: !!isProduction, + category: category ?? null, + description: description ?? '', + }, + validations: clusterEnvironmentDrawerFormValidationSchema({ isNamespaceMandatory: !isSelectedClusterVirtual }), }) - const addEnvironmentHeaderText = `Add Environment in '${clusterName}'` + const [, clusterDetails] = useAsync( + () => getClusterDetails([data.clusterId]), + [data.clusterId], + !envId && !!data.clusterId, + ) + + useEffect(() => { + if (clusterDetails) { + setIsSelectedClusterVirtual(clusterDetails[0].isVirtualCluster) + setClusterNamespaces(INITIAL_NAMESPACES) + setNamespaceLabels(INITIAL_NAMESPACE_LABELS) + reset(data, { keepErrors: false }) + } + }, [clusterDetails]) /** * Fetches the list of namespaces from the cluster and updates the state accordingly. \ @@ -116,7 +161,7 @@ export const ClusterEnvironmentDrawer = ({ try { // Fetch namespaces from the cluster - const { result } = await getClusterNamespaces(+clusterId) + const { result } = await getClusterNamespaces(data.clusterId) // Update clusterNamespaces state with fetched data setClusterNamespaces({ @@ -147,20 +192,6 @@ export const ClusterEnvironmentDrawer = ({ } } - const parsedNamespace = namespace ?? '' - - // FORM METHODS - const { data, errors, register, handleSubmit, trigger } = useForm({ - initialValues: { - environmentName: environmentName ?? '', - namespace: !id ? getNamespaceFromLocalStorage(parsedNamespace) : parsedNamespace, - isProduction: !!isProduction, - category, - description: description ?? '', - }, - validations: clusterEnvironmentDrawerFormValidationSchema({ isNamespaceMandatory: !isVirtual }), - }) - useEffect( () => () => { if (localStorage.getItem(ADD_ENVIRONMENT_FORM_LOCAL_STORAGE_KEY)) { @@ -171,30 +202,29 @@ export const ClusterEnvironmentDrawer = ({ ) const onValidation = - (clusterNamespacesData = clusterNamespaces.data): UseFormSubmitHandler => + (clusterNamespacesData = clusterNamespaces.data): UseFormSubmitHandler => async (formData) => { const payload = getClusterEnvironmentUpdatePayload({ data: formData, - clusterId: +clusterId, - id, + envId, namespaceLabels: namespaceLabels.labels, resourceVersion: namespaceLabels.resourceVersion, - isVirtual, + isVirtualCluster: isSelectedClusterVirtual, }) let api - if (isVirtual) { - api = getVirtualClusterSaveUpdate(id) + if (isSelectedClusterVirtual) { + api = getVirtualClusterSaveUpdate(envId) } else { - api = id ? updateEnvironment : saveEnvironment + api = envId ? updateEnvironment : saveEnvironment } try { setCrudLoading(true) - await api(payload, id) + await api(payload, envId) ToastManager.showToast({ variant: ToastVariantType.success, - description: `Successfully ${id ? 'updated' : 'saved'}`, + description: `Successfully ${envId ? 'updated' : 'saved'}`, }) reload() hideClusterDrawer() @@ -217,7 +247,7 @@ export const ClusterEnvironmentDrawer = ({ } } - const withLabelEditValidation: UseFormSubmitHandler = async () => { + const withLabelEditValidation: UseFormSubmitHandler = async () => { setCrudLoading(true) try { const response = await fetchClusterNamespaces(data.namespace, false) @@ -254,81 +284,103 @@ export const ClusterEnvironmentDrawer = ({ const onDelete = async () => { const payload = getClusterEnvironmentUpdatePayload({ data, - clusterId, - id, - isVirtual, + isVirtualCluster: isSelectedClusterVirtual, + envId, }) await deleteEnvironment(payload) redirectToListAfterReload() } - const renderCreateClusterButton = () => ( -
- - ) - } + +
+ {envId && ( +
+ + + ) return ( -
+
- {/* NOTE: only in case of add environment, can we have truncation */} - -

- {id ? 'Edit Environment' : addEnvironmentHeaderText} -

-
- + showAriaLabelInTippy={false} + size={ComponentSizeType.xs} + style={ButtonStyleType.negativeGrey} + variant={ButtonVariantType.borderLess} + />
{renderContent()} {showDeleteConfirmation && ( )} -
+
) } diff --git a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/schema.ts b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/schema.ts index 8e04e78798..c69c2e4783 100644 --- a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/schema.ts +++ b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/schema.ts @@ -16,14 +16,18 @@ import { UseFormValidations } from '@devtron-labs/devtron-fe-common-lib' -import { ClusterEnvironmentDrawerFormProps } from './types' +import { EnvironmentFormType } from './types' export const clusterEnvironmentDrawerFormValidationSchema = ({ isNamespaceMandatory, }: { isNamespaceMandatory: boolean -}): UseFormValidations => ({ - environmentName: { +}): UseFormValidations => ({ + clusterId: { + required: true, + pattern: [{ message: 'Please select a cluster', value: /^(?!0$)\d+$/ }], + }, + envName: { required: true, pattern: [ { message: 'Environment name is required', value: /^.*$/ }, @@ -32,15 +36,19 @@ export const clusterEnvironmentDrawerFormValidationSchema = ({ { message: 'Minimum 1 and Maximum 16 characters required', value: /^.{1,16}$/ }, ], }, - namespace: { - required: isNamespaceMandatory, - pattern: [ - { message: 'Namespace is required', value: /^.*$/ }, - { message: "Use only lowercase alphanumeric characters or '-'", value: /^[a-z0-9-]+$/ }, - { message: "Cannot start/end with '-'", value: /^(?![-]).*[^-]$/ }, - { message: 'Maximum 63 characters required', value: /^.{1,63}$/ }, - ], - }, + ...(isNamespaceMandatory + ? { + namespace: { + required: true, + pattern: [ + { message: 'Namespace is required', value: /^.*$/ }, + { message: "Use only lowercase alphanumeric characters or '-'", value: /^[a-z0-9-]+$/ }, + { message: "Cannot start/end with '-'", value: /^(?![-]).*[^-]$/ }, + { message: 'Maximum 63 characters required', value: /^.{1,63}$/ }, + ], + }, + } + : {}), isProduction: { required: true, pattern: { message: 'token is required', value: /[^]+/ }, diff --git a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/types.ts b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/types.ts index 8f9c5b9059..fd92ec11ea 100644 --- a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/types.ts +++ b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/types.ts @@ -14,36 +14,40 @@ * limitations under the License. */ -import { DeleteConfirmationModalProps, SelectPickerOptionType, TagType } from '@devtron-labs/devtron-fe-common-lib' +import { + DeleteConfirmationModalProps, + Never, + SelectPickerOptionType, + TagType, +} from '@devtron-labs/devtron-fe-common-lib' -export interface ClusterEnvironmentDrawerFormProps { - environmentName: string +export interface EnvDetails { + envId: number + envName: string namespace: string isProduction: boolean description: string + isVirtualCluster: boolean category: SelectPickerOptionType } -export interface ClusterEnvironmentDrawerProps extends ClusterEnvironmentDrawerFormProps { - id: string +export type EnvDrawerProps = { reload: () => void; hideClusterDrawer: () => void } & ( + | ({ drawerType: 'addEnv'; clusterId?: number; clusterName?: never } & Never>) + | ({ drawerType: 'editEnv'; clusterId: number; clusterName: string } & EnvDetails) +) + +export type EnvironmentFormType = Omit & { clusterId: number - reload: () => void - hideClusterDrawer: () => void - isVirtual: boolean - clusterName: string } -export type GetClusterEnvironmentUpdatePayloadType = Pick< - ClusterEnvironmentDrawerProps, - 'clusterId' | 'id' | 'isVirtual' -> & - Partial> & { - data: ClusterEnvironmentDrawerFormProps - namespaceLabels?: TagType[] - selectedCategory?: SelectPickerOptionType - } - -export interface ClusterNamespacesLabel { +export type GetClusterEnvironmentUpdatePayloadType = Partial> & { + data: EnvironmentFormType + envId: number + namespaceLabels?: TagType[] + isVirtualCluster: boolean +} + +interface ClusterNamespacesLabel { key: string value: string } diff --git a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/utils.ts b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/utils.ts index f8d389945d..633c092c33 100644 --- a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/utils.ts +++ b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer/utils.ts @@ -21,27 +21,27 @@ import { ClusterNamespacesDTO, GetClusterEnvironmentUpdatePayloadType } from './ const getCategoryPayload = importComponentFromFELibrary('getCategoryPayload', null, 'function') export const getClusterEnvironmentUpdatePayload = ({ - id, data, - clusterId, + envId, namespaceLabels, resourceVersion, - isVirtual = false, + isVirtualCluster, }: GetClusterEnvironmentUpdatePayloadType) => - isVirtual + isVirtualCluster ? { - id, - environment_name: data.environmentName, + id: envId, + environment_name: data.envName, namespace: data.namespace || '', IsVirtualEnvironment: true, - cluster_id: +clusterId, + cluster_id: data.clusterId, description: data.description || '', ...(getCategoryPayload ? getCategoryPayload(data.category) : null), + isProd: data.isProduction, } : { - id, - environment_name: data.environmentName, - cluster_id: +clusterId, + id: envId, + environment_name: data.envName, + cluster_id: data.clusterId, namespace: data.namespace || '', active: true, default: data.isProduction, diff --git a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentList.tsx b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentList.tsx deleted file mode 100644 index 2e6be61add..0000000000 --- a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentList.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import { useState } from 'react' - -import { - Button, - ButtonStyleType, - ButtonVariantType, - ComponentSizeType, - Icon, - sortCallback, -} from '@devtron-labs/devtron-fe-common-lib' - -import { ReactComponent as Trash } from '@Icons/ic-delete-interactive.svg' -import { importComponentFromFELibrary } from '@Components/common' -import { InteractiveCellText } from '@Components/common/helpers/InteractiveCellText/InteractiveCellText' -import { ClusterEnvironmentDrawer } from '@Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterEnvironmentDrawer' -import { EnvironmentDeleteComponent } from '@Pages/GlobalConfigurations/ClustersAndEnvironments/EnvironmentDeleteComponent' - -import { deleteEnvironment } from './cluster.service' -import { ClusterEnvironmentListProps } from './cluster.type' -import { getSelectParsedCategory } from './cluster.util' -import { CONFIGURATION_TYPES } from './constants' - -const ManageCategoryButton = importComponentFromFELibrary('ManageCategoryButton', null, 'function') - -export const ClusterEnvironmentList = ({ - clusterId, - reload, - isVirtualCluster, - newEnvs, - clusterName, -}: ClusterEnvironmentListProps) => { - const [environment, setEnvironment] = useState(null) - const [confirmation, setConfirmation] = useState(false) - const [showWindow, setShowWindow] = useState(false) - - const hasCategory = !!ManageCategoryButton - - const baseTableClassName = `dc__grid dc__column-gap-12 cluster-env-list_table${hasCategory ? '--with-category' : ''} dc__align-item-center lh-20 px-16` - - const showWindowModal = () => setShowWindow(true) - - const hideClusterDrawer = () => setShowWindow(false) - - const showConfirmationModal = () => setConfirmation(true) - - const hideConfirmationModal = () => setConfirmation(false) - - const onDelete = async () => { - const deletePayload = { - id: environment.id, - environment_name: environment.environmentName, - cluster_id: +environment.clusterId, - prometheus_endpoint: environment.prometheusEndpoint, - namespace: environment.namespace || '', - active: true, - default: environment.isProduction, - description: environment.description || '', - } - await deleteEnvironment(deletePayload) - reload() - } - - const renderActionButton = (environmentName) => ( -
-
-
-
- ) - - const renderEnvironmentList = () => - newEnvs - .sort((a, b) => sortCallback('environment_name', a, b)) - .map( - ({ - id, - environment_name: environmentName, - prometheus_url: prometheusEndpoint, - namespace, - default: isProduction, - description, - category, - }) => - environmentName && ( -
- setEnvironment({ - id, - environmentName, - prometheusEndpoint, - clusterId, - namespace, - category: getSelectParsedCategory(category), - isProduction, - description, - }) - } - > -
- -
- -
- {environmentName} - - {isProduction && ( -
- Prod -
- )} -
-
{namespace}
- {hasCategory && ( -
- {category?.name ? ( - - - - ) : ( - '-' - )} -
- )} - -
- {description || '-'} -
- {renderActionButton(environmentName)} -
- ), - ) - - return ( -
-
-
-
{CONFIGURATION_TYPES.ENVIRONMENT}
-
{CONFIGURATION_TYPES.NAMESPACE}
- {hasCategory &&
{CONFIGURATION_TYPES.CATEGORY}
} -
{CONFIGURATION_TYPES.DESCRIPTION}
-
-
- {renderEnvironmentList()} - - {confirmation && ( - - )} - - {showWindow && ( - - )} -
- ) -} diff --git a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterForm.tsx b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterForm.tsx index 5de188255b..0e1348dd2e 100644 --- a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterForm.tsx +++ b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterForm.tsx @@ -20,13 +20,11 @@ import YAML from 'yaml' import { Button, - ButtonComponentType, ButtonStyleType, ButtonVariantType, Checkbox, CHECKBOX_VALUE, CodeEditor, - ComponentSizeType, CustomInput, DEFAULT_SECRET_PLACEHOLDER, DTSwitch, @@ -620,27 +618,6 @@ const ClusterForm = ({ inputFileRef.current.click() } - const handleCloseButton = () => { - if (id) { - setRemoteConnectionFalse() - setTlsConnectionFalse() - hideEditModal() - return - } - if (isKubeConfigFile) { - toggleKubeConfigFile(!isKubeConfigFile) - } - if (isClusterDetails) { - toggleClusterDetails(!isClusterDetails) - } - setRemoteConnectionFalse() - setTlsConnectionFalse() - handleCloseCreateClusterForm() - - setLoadingState(false) - reload() - } - const changeRemoteConnectionType = (connectionType) => { setRemoteConnectionMethod(connectionType) if (connectionType === RemoteConnectionType.Proxy) { @@ -657,34 +634,6 @@ const ClusterForm = ({ setSSHConnectionType(authType) } - const clusterTitle = () => { - if (!id) { - return 'Add Cluster' - } - return 'Edit Cluster' - } - - const renderHeader = () => ( -
-

- {clusterTitle()} -

- -
- ) - const renderUrlAndBearerToken = () => { let proxyConfig let sshConfig @@ -1073,7 +1022,7 @@ const ClusterForm = ({ data-testid="close_after_cluster_list_display" className="cta h-36 lh-36" type="button" - onClick={handleCloseButton} + onClick={hideEditModal} style={{ marginLeft: 'auto' }} > Close @@ -1384,7 +1333,7 @@ const ClusterForm = ({ data-testid="cancel_kubeconfig_button" className="cta cancel h-36 lh-36" type="button" - onClick={handleCloseButton} + onClick={hideEditModal} > Cancel @@ -1443,7 +1392,7 @@ const ClusterForm = ({ style={ButtonStyleType.neutral} disabled={loader} dataTestId="cancel_button" - onClick={handleCloseButton} + onClick={hideEditModal} />
+
+ ) + default: + return null + } +} + +export const AddEnvironment = ({ + reloadEnvironments, + handleClose, +}: { + reloadEnvironments: () => void + handleClose +}) => { + const { clusterId } = useParams<{ clusterId?: string }>() + + return ( + + ) +} + +export const EditCluster = ({ clusterList, reloadClusterList, handleClose }: EditDeleteClusterProps) => { + const { clusterId } = useParams<{ clusterId: string }>() + const cluster = clusterList.find((c) => c.clusterId === +clusterId) + + if (!cluster || !cluster.isVirtualCluster) { + return ( + +
+ {!cluster ? ( + + ) : ( + + )} +
+
+ ) + } + return ( + + ) +} + +export const DeleteCluster = ({ clusterList, reloadClusterList, handleClose }: EditDeleteClusterProps) => { + const { clusterId } = useParams<{ clusterId: string }>() + const cluster = clusterList.find((c) => c.clusterId === +clusterId) + + if (!cluster) { + handleClose() + } + + return ( + + ) +} + +export const ClusterEnvLoader = () => ( + <> + {Array.from({ length: 3 }).map((_, idx) => ( +
+ {Array.from({ length: 5 }).map((_val, index) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} +
+ ))} + +) diff --git a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterList.tsx b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterList.tsx index 1bc347a341..536f75ac9a 100644 --- a/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterList.tsx +++ b/src/Pages/GlobalConfigurations/ClustersAndEnvironments/ClusterList.tsx @@ -1,138 +1,490 @@ -import { generatePath, useHistory } from 'react-router-dom' +/* + * Copyright (c) 2024. Devtron Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useMemo, useState } from 'react' +import { generatePath, Route, useHistory, useLocation } from 'react-router-dom' import { + ActionMenu, + ActionMenuItemType, Button, ButtonComponentType, ButtonStyleType, ButtonVariantType, + ClusterMap, ComponentSizeType, - getClassNameForStickyHeaderWithShadow, + ErrorScreenManager, + ErrorScreenNotAuthorized, + FiltersTypeEnum, + GenericEmptyState, + GenericFilterEmptyState, + getSelectPickerOptionByValue, Icon, + noop, + numberComparatorBySortOrder, + OptionType, + PaginationEnum, + SearchBar, + SegmentedControl, + SelectPicker, + SelectPickerOptionType, + stringComparatorBySortOrder, + Table, + TableColumnType, URLS as COMMON_URLS, - useStickyEvent, + useAsync, + useMainContext, + useUrlFilters, } from '@devtron-labs/devtron-fe-common-lib' +import NoClusterImg from '@Images/no-cluster-empty-state.png' import { importComponentFromFELibrary } from '@Components/common' import { URLS } from '@Config/routes' +import CreateCluster from '@Pages/GlobalConfigurations/ClustersAndEnvironments/CreateCluster/CreateCluster.component' +import { CreateClusterTypeEnum } from '@Pages/GlobalConfigurations/ClustersAndEnvironments/CreateCluster/types' + +import { getClusterList, getEnvironmentList } from './cluster.service' +import { + ClusterEnvFilterType, + ClusterEnvTabs, + ClusterListFields, + ClusterRowData, + ClusterTableProps, + Environment, + EnvListSortableKeys, +} from './cluster.type' +import { parseClusterEnvSearchParams } from './cluster.util' +import { + AddEnvironment, + ClusterEnvLoader, + ClusterListCellComponent, + DeleteCluster, + EditCluster, +} from './ClusterList.components' +import { ALL_CLUSTER_VALUE } from './constants' +import EnvironmentList from './EnvironmentList' + +const ManageCategories = importComponentFromFELibrary('ManageCategories', null, 'function') +const ManageCategoryButton = importComponentFromFELibrary('ManageCategoryButton', null, 'function') +const PodSpreadModal = importComponentFromFELibrary('PodSpreadModal', null, 'function') +const HibernationRulesModal = importComponentFromFELibrary('HibernationRulesModal', null, 'function') + +const ClusterList = () => { + const { isSuperAdmin } = useMainContext() + const isK8sClient = window._env_.K8S_CLIENT + + const { push } = useHistory() + const { search } = useLocation() -import { List } from '../../../components/globalConfigurations/GlobalConfiguration' -import { ClusterListProps } from './cluster.type' -import { renderNoEnvironmentTab } from './cluster.util' -import { ClusterEnvironmentList } from './ClusterEnvironmentList' - -const EditClusterPopup = importComponentFromFELibrary('EditClusterPopup', null, 'function') - -export const ClusterList = ({ - isVirtualCluster, - environments, - reload, - clusterName, - isProd, - serverURL, - clusterId, - category, -}: ClusterListProps) => { - const history = useHistory() - - const { stickyElementRef, isStuck: isHeaderStuck } = useStickyEvent({ - containerSelector: '.global-configuration__component-wrapper', - identifier: `cluster-list__${clusterName}`, + const { + searchKey, + sortBy, + sortOrder, + selectedTab, + clusterId: filterClusterId, + updateSearchParams, + handleSearch, + handleSorting, + } = useUrlFilters({ + parseSearchParams: parseClusterEnvSearchParams, + initialSortKey: EnvListSortableKeys.ENV_NAME, }) - const handleEdit = async () => { - history.push(generatePath(COMMON_URLS.GLOBAL_CONFIG_EDIT_CLUSTER, { clusterId })) + const [clusterListLoading, clusterListResult, clusterListError, reloadClusterList] = useAsync( + getClusterList, + [], + isSuperAdmin, + ) + + const [envListLoading, envListResult, envListError, reloadEnvironments] = useAsync( + getEnvironmentList, + [], + isSuperAdmin && !isK8sClient, + ) + + const [showUnmappedEnvs, setShowUnmappedEnvs] = useState(false) + + const clusterIdVsEnvMap: Record = useMemo( + () => + (envListResult ?? []).reduce>((agg, curr) => { + const { clusterId } = curr + if (!agg[clusterId]) { + // eslint-disable-next-line no-param-reassign + agg[clusterId] = [] + } + agg[clusterId].push(curr) + return agg + }, {}), + [envListResult], + ) + + const clusterFilterOptions = useMemo( + () => [ + { label: 'All Clusters', value: ALL_CLUSTER_VALUE }, + ...(clusterListResult ?? []).map(({ clusterName, clusterId }) => ({ + label: clusterName, + value: `${clusterId}`, + })), + ], + [clusterListResult], + ) + + const filteredClusterList = useMemo( + () => (clusterListResult ?? []).filter(({ clusterName }) => clusterName.toLowerCase().includes(searchKey)), + [clusterListResult, searchKey], + ) + + const { + tableColumns, + tableRows, + }: { + tableColumns: ClusterTableProps['columns'] + tableRows: ClusterTableProps['rows'] + } = useMemo( + () => ({ + tableColumns: [ + { + field: ClusterListFields.ICON, + label: '', + size: { fixed: 24 }, + CellComponent: ClusterListCellComponent, + }, + { + field: ClusterListFields.CLUSTER_NAME, + label: 'CLUSTER', + size: { fixed: 200 }, + isSortable: true, + comparator: stringComparatorBySortOrder, + CellComponent: ClusterListCellComponent, + }, + ...(isK8sClient + ? [] + : [ + { + field: ClusterListFields.ENV_COUNT, + label: 'ENVIRONMENTS', + size: { fixed: 150 }, + isSortable: true, + comparator: numberComparatorBySortOrder, + CellComponent: ClusterListCellComponent, + } as TableColumnType, + ]), + { + field: ClusterListFields.CLUSTER_TYPE, + label: 'TYPE', + size: { fixed: 100 }, + isSortable: true, + comparator: stringComparatorBySortOrder, + CellComponent: ClusterListCellComponent, + }, + ...(ManageCategories + ? [ + { + field: ClusterListFields.CLUSTER_CATEGORY, + label: 'CATEGORY', + size: { fixed: 150 }, + isSortable: true, + comparator: stringComparatorBySortOrder, + CellComponent: ClusterListCellComponent, + } as TableColumnType, + ] + : []), + { + field: ClusterListFields.SERVER_URL, + label: 'SERVER URL', + size: null, + CellComponent: ClusterListCellComponent, + }, + { + field: ClusterListFields.ACTIONS, + label: '', + size: { fixed: 90 }, + CellComponent: ClusterListCellComponent, + }, + ], + tableRows: filteredClusterList.map( + ({ clusterId, clusterName, isProd, category, serverUrl, isVirtualCluster, status }) => { + const envCount = clusterIdVsEnvMap[clusterId]?.length + return { + id: `${clusterName}-${clusterId}`, + data: { + clusterId, + clusterName, + clusterType: isProd ? 'Production' : 'Non Production', + serverUrl, + envCount: envCount ?? 0, + clusterCategory: (category?.label as string) ?? '', + isVirtualCluster, + status, + }, + } + }, + ), + }), + [filteredClusterList, clusterIdVsEnvMap], + ) + + const isEnvironmentsView = selectedTab === ClusterEnvTabs.ENVIRONMENTS + const isClusterEnvListLoading = clusterListLoading || envListLoading + + // Early return for non super admin users + if (!isK8sClient && !isSuperAdmin) { + return } - const handleOpenPodSpreadModal = () => { - history.push(`${URLS.GLOBAL_CONFIG_CLUSTER}/${clusterName}/${URLS.POD_SPREAD}`) + const handleToggleShowNamespaces = () => { + setShowUnmappedEnvs((prev) => !prev) } - const handleOpenHibernationRulesModal = () => { - history.push(`${URLS.GLOBAL_CONFIG_CLUSTER}/${clusterName}/${URLS.HIBERNATION_RULES}`) + const handleActionMenuClick = (item: ActionMenuItemType) => { + switch (item.id) { + case 'show-unmapped-namespace': + handleToggleShowNamespaces() + break + default: + break + } } - const renderEditButton = () => { - if (!clusterName) { - return null + const handleChangeTab = (selectedSegment: OptionType) => { + updateSearchParams({ selectedTab: selectedSegment.value, clusterId: null }) + if (searchKey) { + handleSearch('') + } + if (showUnmappedEnvs) { + setShowUnmappedEnvs(false) } + } + + const handleChangeClusterFilter = (selectedOption: SelectPickerOptionType) => { + updateSearchParams({ clusterId: selectedOption.value === ALL_CLUSTER_VALUE ? null : selectedOption.value }) + } + + const handleRedirectToClusterList = () => { + push({ pathname: URLS.GLOBAL_CONFIG_CLUSTER, search }) + } - if (EditClusterPopup && !isVirtualCluster) { + const renderList = () => { + if (isClusterEnvListLoading) { + return + } + + if (clusterListError || envListError) { + return + } + + if (isEnvironmentsView) { return ( - ) } + if (searchKey && !filteredClusterList.length) { + return handleSearch('')} /> + } + return ( - diff --git a/src/assets/img/warning-medium.svg b/src/assets/img/warning-medium.svg deleted file mode 100644 index 655b45c379..0000000000 --- a/src/assets/img/warning-medium.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/src/components/ApplicationGroup/Details/EnvironmentOverview/HibernateModal.tsx b/src/components/ApplicationGroup/Details/EnvironmentOverview/HibernateModal.tsx index 820ba60d42..f37fdb6ce3 100644 --- a/src/components/ApplicationGroup/Details/EnvironmentOverview/HibernateModal.tsx +++ b/src/components/ApplicationGroup/Details/EnvironmentOverview/HibernateModal.tsx @@ -110,8 +110,8 @@ export const HibernateModal = ({
{isDeploymentWindowLoading ? ( -
- +
+
) : ( <> @@ -130,25 +130,25 @@ export const HibernateModal = ({ {renderHibernateModalBody()}
-
- - - -
)} +
+ + + +
) diff --git a/src/components/ApplicationGroup/Details/EnvironmentOverview/RestartWorkloadModal.tsx b/src/components/ApplicationGroup/Details/EnvironmentOverview/RestartWorkloadModal.tsx index 3ca5cbe113..ee903458fd 100644 --- a/src/components/ApplicationGroup/Details/EnvironmentOverview/RestartWorkloadModal.tsx +++ b/src/components/ApplicationGroup/Details/EnvironmentOverview/RestartWorkloadModal.tsx @@ -20,12 +20,16 @@ import { Prompt, useHistory, useLocation } from 'react-router-dom' import { ApiQueuingWithBatch, Button, + ButtonStyleType, + ButtonVariantType, Checkbox, CHECKBOX_VALUE, + ComponentSizeType, DEFAULT_ROUTE_PROMPT_MESSAGE, Drawer, ErrorScreenManager, GenericEmptyState, + Icon, InfoBlock, MODAL_TYPE, stopPropagation, @@ -36,7 +40,6 @@ import { import { ReactComponent as Retry } from '../../../../assets/icons/ic-arrow-clockwise.svg' import { ReactComponent as DropdownIcon } from '../../../../assets/icons/ic-arrow-left.svg' import { ReactComponent as RotateIcon } from '../../../../assets/icons/ic-arrows_clockwise.svg' -import { ReactComponent as Close } from '../../../../assets/icons/ic-close.svg' import { ReactComponent as MechanicalIcon } from '../../../../assets/img/ic-mechanical-operation.svg' import { importComponentFromFELibrary } from '../../../common' import { @@ -205,7 +208,16 @@ export const RestartWorkloadModal = ({ const renderHeaderSection = (): JSX.Element => (
{` Restart workloads on '${envName}'`}
- +
) diff --git a/src/components/ClusterNodes/ClusterList/ClusterSelectionBody.tsx b/src/components/ClusterNodes/ClusterList/ClusterSelectionBody.tsx index 84044f04cb..9bbdaa4e3e 100644 --- a/src/components/ClusterNodes/ClusterList/ClusterSelectionBody.tsx +++ b/src/components/ClusterNodes/ClusterList/ClusterSelectionBody.tsx @@ -21,6 +21,7 @@ import { BulkSelectionIdentifiersType, ClusterDetail, ClusterFiltersType, + ClusterMap, GenericEmptyState, useBulkSelection, useUrlFilters, @@ -38,7 +39,6 @@ import { ClusterSelectionBodyTypes } from './types' import '../clusterNodes.scss' -const ClusterMap = importComponentFromFELibrary('ClusterMap', null, 'function') const ClusterBulkSelectionActionWidget = importComponentFromFELibrary( 'ClusterBulkSelectionActionWidget', null, @@ -94,14 +94,7 @@ const ClusterSelectionBody: React.FC = ({ const renderClusterList = () => (
- {ClusterMap && window._env_.FEATURE_CLUSTER_MAP_ENABLE && ( - - )} + {!filteredList?.length ? (
diff --git a/src/components/ResourceBrowser/ResourceList/K8sResourceListTableCellComponent.tsx b/src/components/ResourceBrowser/ResourceList/K8sResourceListTableCellComponent.tsx index b398a31ab8..d09ec60910 100644 --- a/src/components/ResourceBrowser/ResourceList/K8sResourceListTableCellComponent.tsx +++ b/src/components/ResourceBrowser/ResourceList/K8sResourceListTableCellComponent.tsx @@ -47,7 +47,6 @@ const K8sResourceListTableCellComponent = ({ addTab, isEventListing, lowercaseKindToResourceGroupMap, - clusterName, }: K8sResourceListTableCellComponentProps) => { const { push } = useHistory() const { clusterId } = useParams() @@ -350,17 +349,10 @@ const K8sResourceListTableCellComponent = ({ {showCreateEnvironmentDrawer && ( )} diff --git a/src/components/app/details/appDetails/IssuesListingModal.tsx b/src/components/app/details/appDetails/IssuesListingModal.tsx index d094a662e3..5f6c12157a 100644 --- a/src/components/app/details/appDetails/IssuesListingModal.tsx +++ b/src/components/app/details/appDetails/IssuesListingModal.tsx @@ -16,9 +16,15 @@ import { useEffect, useRef } from 'react' -import { Drawer } from '@devtron-labs/devtron-fe-common-lib' +import { + Button, + ButtonStyleType, + ButtonVariantType, + ComponentSizeType, + Drawer, + Icon, +} from '@devtron-labs/devtron-fe-common-lib' -import { ReactComponent as Close } from '../../../../assets/icons/ic-close.svg' import { ReactComponent as Error } from '../../../../assets/icons/ic-warning.svg' import { IssuesListingModalType } from './appDetails.type' @@ -62,9 +68,16 @@ const IssuesListingModal = ({ errorsList, closeIssuesListingModal }: IssuesListi {getErrorCountText()}
- - - +