diff --git a/locales/en/plugin__odf-console.json b/locales/en/plugin__odf-console.json index 6272b3090..70253b511 100644 --- a/locales/en/plugin__odf-console.json +++ b/locales/en/plugin__odf-console.json @@ -290,7 +290,6 @@ "Review and assign": "Review and assign", "In use: {{targetClusters}}": "In use: {{targetClusters}}", "Used: {{targetClusters}}": "Used: {{targetClusters}}", - "View documentation": "View documentation", "StorageSystems": "StorageSystems", "StorageSystem details": "StorageSystem details", "Edit BlockPool": "Edit BlockPool", @@ -455,6 +454,12 @@ "This card shows the requested capacity for different Kubernetes resources. The figures shown represent the usable storage, meaning that data replication is not taken into consideration.": "This card shows the requested capacity for different Kubernetes resources. The figures shown represent the usable storage, meaning that data replication is not taken into consideration.", "Internal": "Internal", "Raw capacity is the absolute total disk space available to the array subsystem.": "Raw capacity is the absolute total disk space available to the array subsystem.", + "Cluster ready for Regional-DR setup.": "Cluster ready for Regional-DR setup.", + "Setting up disaster recovery": "Setting up disaster recovery", + "Cluster OSDs are being migrated": "Cluster OSDs are being migrated", + "{{ percentageComplete }}% completed ({{ migratedDevices }}/{{ totalOsd }} remaining)": "{{ percentageComplete }}% completed ({{ migratedDevices }}/{{ totalOsd }} remaining)", + "Could not migrate cluster OSDs.": "Could not migrate cluster OSDs.", + "Check documentation": "Check documentation", "Troubleshoot": "Troubleshoot", "Active health checks": "Active health checks", "Progressing": "Progressing", @@ -1145,6 +1150,7 @@ "No conditions found": "No conditions found", "Copied": "Copied", "Copy to clipboard": "Copy to clipboard", + "View documentation": "View documentation", "Oh no! Something went wrong.": "Oh no! Something went wrong.", "Copied to clipboard": "Copied to clipboard", "Drag to reorder": "Drag to reorder", diff --git a/packages/mco/components/modals/app-failover-relocate/error-messages.tsx b/packages/mco/components/modals/app-failover-relocate/error-messages.tsx index 39032df75..c44cbd9db 100644 --- a/packages/mco/components/modals/app-failover-relocate/error-messages.tsx +++ b/packages/mco/components/modals/app-failover-relocate/error-messages.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; +import { DOC_LINKS } from '@odf/mco/constants/doc'; +import { ViewDocumentation } from '@odf/shared/utils/doc-utils'; import { TFunction } from 'i18next'; import { Trans } from 'react-i18next'; import { AlertVariant } from '@patternfly/react-core'; -import { ViewDocumentation, DOC_LINKS } from '../../../utils'; export enum ErrorMessageType { // Priority wise error messages diff --git a/packages/mco/components/modals/app-failover-relocate/subscriptions/error-messages.tsx b/packages/mco/components/modals/app-failover-relocate/subscriptions/error-messages.tsx index e9f4c868a..b31c04f63 100644 --- a/packages/mco/components/modals/app-failover-relocate/subscriptions/error-messages.tsx +++ b/packages/mco/components/modals/app-failover-relocate/subscriptions/error-messages.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; +import { ViewDocumentation } from '@odf/shared/utils/doc-utils'; import { TFunction } from 'i18next'; import { Trans } from 'react-i18next'; import { AlertVariant } from '@patternfly/react-core'; -import { ViewDocumentation, DOC_LINKS } from '../../../../utils/doc-utils'; +import { DOC_LINKS } from '../../../../constants/doc'; export enum ErrorMessageType { // Priority wise error messages diff --git a/packages/mco/constants/doc.tsx b/packages/mco/constants/doc.tsx new file mode 100644 index 000000000..089c874ab --- /dev/null +++ b/packages/mco/constants/doc.tsx @@ -0,0 +1,13 @@ +import { ODF_DOC_BASE_PATH, ODF_DOC_VERSION } from '@odf/shared/constants/doc'; + +export const ACM_DOC_VERSION = '2.9'; +export const ACM_DOC_HOME = `https://access.redhat.com/documentation/en-us/red_hat_advanced_cluster_management_for_kubernetes/${ACM_DOC_VERSION}`; +export const ACM_DOC_BASE_PATH = `${ACM_DOC_HOME}/html-single`; + +export const DOC_LINKS = { + APPLY_POLICY: `${ODF_DOC_BASE_PATH}/configuring_openshift_data_foundation_disaster_recovery_for_openshift_workloads/index#apply-drpolicy-to-sample-application_manage-dr`, + MDR_FAILOVER: `${ODF_DOC_BASE_PATH}/configuring_openshift_data_foundation_disaster_recovery_for_openshift_workloads/index#application-failover-between-managed-clusters_manage-dr`, + MDR_RELOCATE: `${ODF_DOC_BASE_PATH}/configuring_openshift_data_foundation_disaster_recovery_for_openshift_workloads/index#relocating-application-between-managed-clusters_manage-dr`, + DR_RELEASE_NOTES: `${ODF_DOC_BASE_PATH}/${ODF_DOC_VERSION}_release_notes/index#disaster_recovery`, + ACM_OFFLINE_CLUSTER: `${ACM_DOC_BASE_PATH}/troubleshooting/index#troubleshooting-an-offline-cluster`, +}; diff --git a/packages/mco/utils/doc-utils.tsx b/packages/mco/utils/doc-utils.tsx deleted file mode 100644 index d0239ce1a..000000000 --- a/packages/mco/utils/doc-utils.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import * as React from 'react'; -import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; -import { Text, TextVariants } from '@patternfly/react-core'; -import { ExternalLinkAltIcon } from '@patternfly/react-icons'; - -export const ODF_DOC_VERSION = '4.13'; -export const ODF_DOC_HOME = `https://access.redhat.com/documentation/en-us/red_hat_openshift_data_foundation/${ODF_DOC_VERSION}`; -export const ODF_DOC_BASE_PATH = `${ODF_DOC_HOME}/html-single`; - -export const ACM_DOC_VERSION = '2.7'; -export const ACM_DOC_HOME = `https://access.redhat.com/documentation/en-us/red_hat_advanced_cluster_management_for_kubernetes/${ACM_DOC_VERSION}`; -export const ACM_DOC_BASE_PATH = `${ACM_DOC_HOME}/html-single`; - -export const DOC_LINKS = { - APPLY_POLICY: `${ODF_DOC_BASE_PATH}/configuring_openshift_data_foundation_disaster_recovery_for_openshift_workloads/index#apply-drpolicy-to-sample-application_manage-dr`, - MDR_FAILOVER: `${ODF_DOC_BASE_PATH}/configuring_openshift_data_foundation_disaster_recovery_for_openshift_workloads/index#application-failover-between-managed-clusters_manage-dr`, - MDR_RELOCATE: `${ODF_DOC_BASE_PATH}/configuring_openshift_data_foundation_disaster_recovery_for_openshift_workloads/index#relocating-application-between-managed-clusters_manage-dr`, - DR_RELEASE_NOTES: `${ODF_DOC_BASE_PATH}/${ODF_DOC_VERSION}_release_notes/index#disaster_recovery`, - ACM_OFFLINE_CLUSTER: `${ACM_DOC_BASE_PATH}/troubleshooting/index#troubleshooting-an-offline-cluster`, -}; - -export const ViewDocumentation: React.FC = ({ - doclink, - text, -}) => { - const { t } = useCustomTranslation(); - return ( - - {text || t('View documentation')} - - ); -}; - -type ViewDocumentationProps = { - doclink: string; - text?: string; -}; diff --git a/packages/mco/utils/index.ts b/packages/mco/utils/index.ts index e28c6ef95..f569188fe 100644 --- a/packages/mco/utils/index.ts +++ b/packages/mco/utils/index.ts @@ -1,3 +1,2 @@ export * from './disaster-recovery'; export * from './common'; -export * from './doc-utils'; diff --git a/packages/ocs/dashboards/persistent-internal/status-card/osd-migration/osd-migration-status.spec.tsx b/packages/ocs/dashboards/persistent-internal/status-card/osd-migration/osd-migration-status.spec.tsx new file mode 100644 index 000000000..edc65ebfb --- /dev/null +++ b/packages/ocs/dashboards/persistent-internal/status-card/osd-migration/osd-migration-status.spec.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { BLUESTORE, BLUESTORE_RDR } from '@odf/core/constants'; +import { cleanup, render, screen, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; +import { MemoryRouter } from 'react-router-dom'; +import { getOSDMigrationStatus } from '../../../../utils/osd-migration'; +import { OSDMigration } from './osd-migration-status'; + +jest.mock('@odf/shared/status/icons', () => ({ + RedExclamationCircleIcon: 'div', +})); + +jest.mock('@openshift-console/dynamic-plugin-sdk-internal', () => ({ + HealthBody: 'div', + HealthItem: ({ title }) =>
{title}
, + ViewDocumentation: ({ text, doclink }) => {text}, + HealthState: { + OK: 'OK', + }, +})); + +jest.mock('../../../../utils/osd-migration'); +afterEach(cleanup); + +describe('OSDMigrationStatus', () => { + test('renders the component with COMPLETED status', async () => { + const cephData = { + status: { + storage: { + osd: { + storeType: { + [BLUESTORE_RDR]: 5, + }, + }, + }, + }, + }; + + getOSDMigrationStatus.mockReturnValue('Completed'); + + render( + + + + ); + + await waitFor(() => { + expect( + screen.getByText('Cluster ready for Regional-DR setup.') + ).toBeInTheDocument(); + }); + }); + + test('renders the component with PENDING status', async () => { + const cephData = { + status: { + storage: { + osd: { + storeType: { + [BLUESTORE]: 10, + [BLUESTORE_RDR]: 5, + }, + }, + }, + }, + }; + + getOSDMigrationStatus.mockReturnValue('In Progress'); + + render( + + + + ); + + await waitFor(() => { + expect( + screen.getByText('Cluster OSDs are being migrated') + ).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/ocs/dashboards/persistent-internal/status-card/osd-migration/osd-migration-status.tsx b/packages/ocs/dashboards/persistent-internal/status-card/osd-migration/osd-migration-status.tsx new file mode 100644 index 000000000..9559f7fa0 --- /dev/null +++ b/packages/ocs/dashboards/persistent-internal/status-card/osd-migration/osd-migration-status.tsx @@ -0,0 +1,149 @@ +import * as React from 'react'; +import { + OSDMigrationStatus, + BLUESTORE, + BLUESTORE_RDR, +} from '@odf/core/constants'; +import { ODF_DR_DOC_HOME } from '@odf/shared/constants/doc'; +import { RedExclamationCircleIcon } from '@odf/shared/status/icons'; +import { CephClusterKind } from '@odf/shared/types'; +import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; +import { ViewDocumentation } from '@odf/shared/utils/doc-utils'; +import { HealthState } from '@openshift-console/dynamic-plugin-sdk'; +import { + HealthBody, + HealthItem, +} from '@openshift-console/dynamic-plugin-sdk-internal'; +import { Button, Divider, Flex, FlexItem } from '@patternfly/react-core'; +import { CloseIcon, InProgressIcon } from '@patternfly/react-icons'; +import { getOSDMigrationStatus } from '../../../../utils/osd-migration'; + +const calculateOSDMigration = ( + cephData: CephClusterKind +): [number, number, number] => { + const migratedDevices = + cephData?.status?.storage?.osd?.storeType?.[BLUESTORE_RDR] || 0; + const totalOsd = + (cephData?.status?.storage?.osd?.storeType?.[BLUESTORE] || 0) + + migratedDevices; + const percentageComplete = + totalOsd !== 0 ? Math.round((migratedDevices / totalOsd) * 100) : 0; + + return [migratedDevices, totalOsd, percentageComplete]; +}; + +export const OSDMigration: React.FC = ({ + cephData, +}) => { + const { t } = useCustomTranslation(); + const [isMigrationStatusVisible, setIsMigrationStatusVisible] = + React.useState(true); + const [migratedDevices, totalOsd, percentageComplete] = + calculateOSDMigration(cephData); + const migrationStatus: string = getOSDMigrationStatus(cephData); + + const handleClose = () => { + setIsMigrationStatusVisible(false); + }; + + React.useEffect(() => { + // If it's the initial mount and the status is COMPLETED, hide the migration status + if ( + !isMigrationStatusVisible && + migrationStatus === OSDMigrationStatus.COMPLETED + ) { + return; + } + // For updates, trigger visibility + setIsMigrationStatusVisible(true); + }, [isMigrationStatusVisible, migrationStatus]); + + return ( + <> + {migrationStatus !== OSDMigrationStatus.PENDING && + isMigrationStatusVisible && } + + + {migrationStatus === OSDMigrationStatus.COMPLETED && + isMigrationStatusVisible && ( + <> + + + + + + + + + + + )} + + {migrationStatus === OSDMigrationStatus.IN_PROGRESS && ( + <> + + } + state={HealthState.OK} + title={t('Cluster OSDs are being migrated')} + /> + + + {t( + '{{ percentageComplete }}% completed ({{ migratedDevices }}/{{ totalOsd }} remaining)', + { + percentageComplete, + migratedDevices, + totalOsd, + } + )} + + + )} + + {migrationStatus === OSDMigrationStatus.FAILED && ( + <> + + + } + title={t('Could not migrate cluster OSDs.')} + /> + + + + + + {t( + '{{ percentageComplete }}% completed ({{ migratedDevices }}/{{ totalOsd }} remaining)', + { + percentageComplete, + migratedDevices, + totalOsd, + } + )} + + + )} + + + + ); +}; + +type OSDMigrationStatusProps = { + cephData?: CephClusterKind; +}; diff --git a/packages/ocs/dashboards/persistent-internal/status-card/status-card.tsx b/packages/ocs/dashboards/persistent-internal/status-card/status-card.tsx index b066f8c32..6813f9156 100644 --- a/packages/ocs/dashboards/persistent-internal/status-card/status-card.tsx +++ b/packages/ocs/dashboards/persistent-internal/status-card/status-card.tsx @@ -33,6 +33,7 @@ import { } from '@patternfly/react-core'; import { CephClusterModel } from '../../../models'; import { DATA_RESILIENCY_QUERY, StorageDashboardQuery } from '../../../queries'; +import { OSDMigration } from './osd-migration/osd-migration-status'; import { getCephHealthState, getDataResiliencyState } from './utils'; import { whitelistedHealthChecksRef } from './whitelisted-health-checks'; import './healthchecks.scss'; @@ -183,6 +184,7 @@ export const StatusCard: React.FC = () => { + ); diff --git a/packages/ocs/utils/osd-migration.ts b/packages/ocs/utils/osd-migration.ts new file mode 100644 index 000000000..ba315ff28 --- /dev/null +++ b/packages/ocs/utils/osd-migration.ts @@ -0,0 +1,39 @@ +import { + BLUESTORE, + BLUESTORE_RDR, + OSDMigrationStatus, +} from '@odf/core/constants'; +import { CephClusterKind } from '@odf/shared/types'; + +export function getCephStoreType(ceph: CephClusterKind) { + return ceph?.status?.storage?.osd?.storeType; +} + +export const getBluestoreCount = (ceph: CephClusterKind): number => { + return getCephStoreType(ceph)?.[BLUESTORE] || 0; +}; + +export const getBluestoreRdrCount = (ceph: CephClusterKind): number => { + return getCephStoreType(ceph)?.[BLUESTORE_RDR] || 0; +}; + +export const getOSDMigrationStatus = (ceph: CephClusterKind) => { + if (!!ceph) { + const bluestoreCount = getBluestoreCount(ceph); + const bluestoreRdrCount = getBluestoreRdrCount(ceph); + + if (bluestoreCount > 0) { + if (bluestoreRdrCount > 0) { + return OSDMigrationStatus.IN_PROGRESS; + } else { + return OSDMigrationStatus.PENDING; + } + } else if (bluestoreRdrCount > 0) { + return OSDMigrationStatus.COMPLETED; + } + } else { + return OSDMigrationStatus.FAILED; + } // TODO Add condition for migration failure + + return ''; +}; diff --git a/packages/odf/constants/dataProtection.ts b/packages/odf/constants/dataProtection.ts index ae531d4cb..f920ab0ab 100644 --- a/packages/odf/constants/dataProtection.ts +++ b/packages/odf/constants/dataProtection.ts @@ -1,2 +1,12 @@ export const DISASTER_RECOVERY_TARGET_ANNOTATION = 'ocs.openshift.io/clusterIsDisasterRecoveryTarget'; + +export enum OSDMigrationStatus { + IN_PROGRESS = 'In Progress', + PENDING = 'Pending', + COMPLETED = 'Completed', + FAILED = 'Failed', +} + +export const BLUESTORE_RDR = 'bluestore-rdr'; +export const BLUESTORE = 'bluestore'; diff --git a/packages/shared/src/constants/doc.ts b/packages/shared/src/constants/doc.ts new file mode 100644 index 000000000..a82007f6d --- /dev/null +++ b/packages/shared/src/constants/doc.ts @@ -0,0 +1,4 @@ +export const ODF_DOC_VERSION = '4.15'; +export const ODF_DOC_HOME = `https://access.redhat.com/documentation/en-us/red_hat_openshift_data_foundation/${ODF_DOC_VERSION}`; +export const ODF_DOC_BASE_PATH = `${ODF_DOC_HOME}/html-single`; +export const ODF_DR_DOC_HOME = `${ODF_DOC_BASE_PATH}/configuring_openshift_data_foundation_disaster_recovery_for_openshift_workloads/index#apply-drpolicy-to-sample-application_manage-dr`; diff --git a/packages/shared/src/status/icons.tsx b/packages/shared/src/status/icons.tsx index 972f78796..809f62c9e 100644 --- a/packages/shared/src/status/icons.tsx +++ b/packages/shared/src/status/icons.tsx @@ -15,6 +15,7 @@ import { SyncAltIcon, ResourcesAlmostFullIcon, ResourcesFullIcon, + TimesIcon, } from '@patternfly/react-icons'; export type ColoredIconProps = { @@ -128,3 +129,7 @@ export const BlueArrowCircleUpIcon: React.FC = ({ title={title} /> ); + +export const CloseIcon: React.FC = ({ className, title }) => ( + +); diff --git a/packages/shared/src/types/storage.ts b/packages/shared/src/types/storage.ts index 4bc1f2f36..b014d2074 100644 --- a/packages/shared/src/types/storage.ts +++ b/packages/shared/src/types/storage.ts @@ -104,7 +104,13 @@ type CephDeviceClass = { export type CephClusterKind = K8sResourceCommon & { status?: { - storage: { + storage?: { + osd?: { + storeType?: { + bluestore?: number; + 'bluestore-rdr'?: number; + }; + }; deviceClasses: CephDeviceClass[]; }; ceph?: { diff --git a/packages/shared/src/utils/doc-utils.tsx b/packages/shared/src/utils/doc-utils.tsx new file mode 100644 index 000000000..40b09eaff --- /dev/null +++ b/packages/shared/src/utils/doc-utils.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook'; +import { Text, TextVariants } from '@patternfly/react-core'; +import { ExternalLinkAltIcon } from '@patternfly/react-icons'; + +export const ViewDocumentation: React.FC = ({ + doclink, + text, + padding = '15px 10px', +}) => { + const { t } = useCustomTranslation(); + return ( + + {text || t('View documentation')} + + ); +}; + +type ViewDocumentationProps = { + doclink: string; + text?: string; + padding?: string; +};