diff --git a/ui/frontend/src/components/BasicLayout/BasicLayout.css b/ui/frontend/src/components/BasicLayout/BasicLayout.css index 2440fdf78..b91f99c87 100644 --- a/ui/frontend/src/components/BasicLayout/BasicLayout.css +++ b/ui/frontend/src/components/BasicLayout/BasicLayout.css @@ -22,6 +22,11 @@ margin-bottom: var(--pf-global--spacer--2xl); } +.basic-layout__alert { + /* like .content-section */ + width: 50vw; +} + .basic-layout-left-center { height: 70vh; } @@ -30,7 +35,7 @@ background-color: var(--pf-global--Color--300) !important; } -.basic-layout-content { +.basic-layout__navigation { min-height: 80vh; } diff --git a/ui/frontend/src/components/BasicLayout/BasicLayout.tsx b/ui/frontend/src/components/BasicLayout/BasicLayout.tsx index 83657b340..8fc49fc8b 100644 --- a/ui/frontend/src/components/BasicLayout/BasicLayout.tsx +++ b/ui/frontend/src/components/BasicLayout/BasicLayout.tsx @@ -7,8 +7,11 @@ import { Divider, Flex, FlexItem, + List, + ListItem, Panel, PanelMain, + Spinner, Stack, StackItem, Text, @@ -23,6 +26,7 @@ import { UIError } from '../types'; import { SaveInProgress } from '../SaveInProgress'; import { onLogout } from '../logout'; import { reloadPage } from '../utils'; +import { useOperatorsReconciling } from '../operators'; import RedHatLogo from './RedHatLogo.svg'; import cloudyCircles from './cloudyCircles.svg'; @@ -37,7 +41,10 @@ export const BasicLayout: React.FC<{ isSaving?: boolean; actions?: React.ReactNode[]; }> = ({ error, warning, isValueChanged, isSaving, onSave, actions = [], children }) => { + const operatorsReconciling = useOperatorsReconciling(); + const isSaveButton = onSave !== undefined; + const isOperatorReconciling = operatorsReconciling && operatorsReconciling.length > 0; return ( @@ -77,7 +84,7 @@ export const BasicLayout: React.FC<{ - + Settings @@ -107,16 +114,50 @@ export const BasicLayout: React.FC<{ {!isSaving && ( - {error?.title && ( - - {error.message} - - )} - {warning?.title && ( - - {warning.message} - - )} + + {error?.title && ( + + {error.message} + + )} + {isOperatorReconciling && isSaveButton && ( + + Saving changes is not possible until operators become ready. They are probably + reconciling after previous changes. + {operatorsReconciling !== undefined && ( + <> +
+ + {operatorsReconciling.map((op) => ( + {op.metadata.name} + ))} + + + )} +
+ )} + {warning?.title && ( + + {warning.message} + + )} +
+ {children} @@ -125,6 +166,12 @@ export const BasicLayout: React.FC<{ {isSaveButton && ( )} @@ -133,7 +180,12 @@ export const BasicLayout: React.FC<{ diff --git a/ui/frontend/src/components/ContentSection/ContentSection.css b/ui/frontend/src/components/ContentSection/ContentSection.css index 93c8447fe..4c0d8d430 100644 --- a/ui/frontend/src/components/ContentSection/ContentSection.css +++ b/ui/frontend/src/components/ContentSection/ContentSection.css @@ -1,5 +1,4 @@ .content-section { - /* width: 768px; */ width: 50vw; background-color: var(--pf-global--BackgroundColor--200); border-radius: 16px; diff --git a/ui/frontend/src/components/DomainPage/DomainCertificates.tsx b/ui/frontend/src/components/DomainPage/DomainCertificates.tsx index 1eecffdbd..31bd8b356 100644 --- a/ui/frontend/src/components/DomainPage/DomainCertificates.tsx +++ b/ui/frontend/src/components/DomainPage/DomainCertificates.tsx @@ -8,7 +8,6 @@ import { PanelMainBody, FlexItem, Flex, - FlexProps, DescriptionList, DescriptionListGroup, DescriptionListTerm, @@ -48,8 +47,6 @@ const getTitle = ( keyValidated: FormGroupProps['validated'], ): React.ReactElement | string => { let title: React.ReactElement | string; - const forDomain = isExpanded ? undefined : <> certificate for {domain}; - if (!domainCert?.['tls.crt'] && !domainCert?.['tls.key']) { title = ( <> diff --git a/ui/frontend/src/components/DomainPage/dataLoad.ts b/ui/frontend/src/components/DomainPage/dataLoad.ts index 0349b6340..92a2b917f 100644 --- a/ui/frontend/src/components/DomainPage/dataLoad.ts +++ b/ui/frontend/src/components/DomainPage/dataLoad.ts @@ -1,4 +1,4 @@ -import { getClusterDomainFromComponentRoutes, Ingress, Service } from '../../copy-backend-common'; +import { getClusterDomainFromComponentRoutes, Ingress } from '../../copy-backend-common'; import { getIngressConfig } from '../../resources/ingress'; import { workaroundUnmarshallObject } from '../../test-utils'; import { setUIErrorType } from '../types'; @@ -21,17 +21,13 @@ export const loadDomainData = async ({ message: 'The cluster is not properly deployed.', }); return; - } - - if (e.code === 401) { + } else if (e.code === 401) { setError({ title: 'Unauthorized', message: 'Redirecting to login page.', }); return; - } - - if (e.code !== 404) { + } else { console.error(e, e.code); setError({ title: 'Failed to contact OpenShift Platform API.', message: e.message }); return; diff --git a/ui/frontend/src/components/Navigation/Navigation.tsx b/ui/frontend/src/components/Navigation/Navigation.tsx index ffdd17a04..ba9ff6407 100644 --- a/ui/frontend/src/components/Navigation/Navigation.tsx +++ b/ui/frontend/src/components/Navigation/Navigation.tsx @@ -8,7 +8,7 @@ import { URI_CREDENTIALS, URI_DOMAIN, URI_INGRESS, - URI_LAYER3, + // URI_LAYER3, URI_SSHKEY, } from './routes'; diff --git a/ui/frontend/src/components/constants.ts b/ui/frontend/src/components/constants.ts index 8c176793d..620a2f69f 100644 --- a/ui/frontend/src/components/constants.ts +++ b/ui/frontend/src/components/constants.ts @@ -5,6 +5,7 @@ export const ADDRESS_POOL_NAMESPACE = 'metallb'; export const DELAY_BEFORE_RECONCILIATION = 10 * 1000; export const DELAY_BEFORE_QUERY_RETRY = 5 * 1000; /* ms */ +export const CLUSTER_OPERATOR_POLLING_INTERVAL = DELAY_BEFORE_QUERY_RETRY; export const MAX_LIVENESS_CHECK_COUNT = 20 * ((60 * 1000) / DELAY_BEFORE_QUERY_RETRY); // max 20 minutes export const KubeadminSecret = { name: 'kubeadmin', namespace: 'kube-system' }; @@ -12,3 +13,9 @@ export const SSH_PRIVATE_KEY_SECRET = { name: 'cluster-ssh-keypair', namespace: 'default' /* !?! */, }; + +export const MONITORED_CLUSTER_OPERATORS = [ + 'kube-apiserver', + 'openshift-apiserver', + 'authentication', +]; diff --git a/ui/frontend/src/components/operators.tsx b/ui/frontend/src/components/operators.tsx new file mode 100644 index 000000000..99a4a1d31 --- /dev/null +++ b/ui/frontend/src/components/operators.tsx @@ -0,0 +1,50 @@ +import React from 'react'; + +import { ClusterOperator, getCondition } from '../copy-backend-common'; +import { getClusterOperators } from '../resources/clusteroperator'; + +import { CLUSTER_OPERATOR_POLLING_INTERVAL, MONITORED_CLUSTER_OPERATORS } from './constants'; +import { delay } from './utils'; + +export const useOperatorsReconciling = (): ClusterOperator[] | undefined => { + const [operatorsReconciling, setOperatorsReconciling] = React.useState(); + const [pollingTimmer, setPollingTimmer] = React.useState(0); + + React.useEffect(() => { + let stopIt = false; + + const doItAsync = async () => { + const operators = await getClusterOperators().promise; + + const filteredOperators = operators.filter( + (op) => + MONITORED_CLUSTER_OPERATORS.includes(op.metadata.name as string) && + (getCondition(op, 'Progressing')?.status === 'True' || + getCondition(op, 'Degraded')?.status === 'True' || + getCondition(op, 'Available')?.status === 'False'), + ); + + if (!stopIt) { + setOperatorsReconciling( + filteredOperators.sort( + (op1, op2) => op1.metadata.name?.localeCompare(op2.metadata.name as string) || -1, + ), + ); + + await delay(CLUSTER_OPERATOR_POLLING_INTERVAL); + + if (!stopIt) { + setPollingTimmer(pollingTimmer + 1); + } + } + }; + + doItAsync(); + + return () => { + stopIt = true; + }; + }, [pollingTimmer]); + + return operatorsReconciling; +};