diff --git a/src/SfxWeb/cypress/e2e/cluster.cy.js b/src/SfxWeb/cypress/e2e/cluster.cy.js index e433d766f..0ef1407fe 100644 --- a/src/SfxWeb/cypress/e2e/cluster.cy.js +++ b/src/SfxWeb/cypress/e2e/cluster.cy.js @@ -21,6 +21,10 @@ context('Cluster page', () => { }) describe("essentials", () => { + beforeEach(() => { + cy.intercept(apiUrl('/Partitions/00000000-0000-0000-0000-000000000001?*'), + {fixture: 'cluster-page/fm/notInQuorumLoss.json'}).as("getFMQuorumLossStatus") + }) it('load essentials', () => { cy.visit('') @@ -71,6 +75,19 @@ context('Cluster page', () => { }) + + it('displays FM quorum loss warning when in quorum loss status', () => { + cy.visit(''); + cy.wait('@getFMQuorumLossStatus') + cy.get('[data-cy="fmql-warning"]').should('not.exist'); + cy.intercept(apiUrl('/Partitions/00000000-0000-0000-0000-000000000001?*'), + {fixture: 'cluster-page/fm/inQuorumLoss.json'}).as("getFMQuorumLossStatus") + cy.reload(); + cy.wait('@getFMQuorumLossStatus') + cy.get('[data-cy="fmql-warning"]').should('exist').should('be.visible'); + }); + + it('xss', () => { addDefaultFixtures(xssPrefix); diff --git a/src/SfxWeb/cypress/fixtures/cluster-page/fm/inQuorumLoss.json b/src/SfxWeb/cypress/fixtures/cluster-page/fm/inQuorumLoss.json new file mode 100644 index 000000000..f5de85d61 --- /dev/null +++ b/src/SfxWeb/cypress/fixtures/cluster-page/fm/inQuorumLoss.json @@ -0,0 +1,4 @@ +{ + "ServiceKind": "Stateful", + "PartitionStatus": "InQuorumLoss" +} \ No newline at end of file diff --git a/src/SfxWeb/cypress/fixtures/cluster-page/fm/notInQuorumLoss.json b/src/SfxWeb/cypress/fixtures/cluster-page/fm/notInQuorumLoss.json new file mode 100644 index 000000000..12fa02e26 --- /dev/null +++ b/src/SfxWeb/cypress/fixtures/cluster-page/fm/notInQuorumLoss.json @@ -0,0 +1,4 @@ +{ + "ServiceKind": "Stateful", + "PartitionStatus": "OK" +} \ No newline at end of file diff --git a/src/SfxWeb/src/app/Common/Constants.ts b/src/SfxWeb/src/app/Common/Constants.ts index fbe8d6013..42f4e1245 100644 --- a/src/SfxWeb/src/app/Common/Constants.ts +++ b/src/SfxWeb/src/app/Common/Constants.ts @@ -224,6 +224,11 @@ export class TelemetryEventNames { } +export class SystemServicePartitionIds { + public static FailoverManagerId = '00000000-0000-0000-0000-000000000001'; +} + + export class RepairTaskMessages { public static longExecutingMessage = "This update can prevent other updates from going through. Please reach out to the Azure Compute teams (“Compute Manager/Blackbird”) to figure out why the updates are not completing."; public static longExecutingId = "longExecuting"; diff --git a/src/SfxWeb/src/app/services/rest-client.service.ts b/src/SfxWeb/src/app/services/rest-client.service.ts index 468fb0d4a..632a18a53 100644 --- a/src/SfxWeb/src/app/services/rest-client.service.ts +++ b/src/SfxWeb/src/app/services/rest-client.service.ts @@ -12,7 +12,7 @@ import { IRawCollection, IRawClusterManifest, IRawClusterHealth, IRawClusterUpgr IRawApplication, IRawService, IRawCreateServiceDescription, IRawCreateServiceFromTemplateDescription, IRawUpdateServiceDescription, IRawServiceDescription, IRawServiceHealth, IRawApplicationUpgradeProgress, IRawCreateComposeDeploymentDescription, IRawPartition, IRawPartitionHealth, IRawPartitionLoadInformation, IRawReplicaOnPartition, IRawReplicaHealth, IRawImageStoreContent, IRawStoreFolderSize, IRawClusterVersion, IRawList, IRawAadMetadata, IRawStorage, IRawRepairTask, - IRawServiceNameInfo, IRawApplicationNameInfo, IRawBackupEntity, IRawInfrastructureJob, IRawInfraRepairTask, IRawRoleInstanceImpact } from '../Models/RawDataTypes'; + IRawServiceNameInfo, IRawApplicationNameInfo, IRawBackupEntity, IRawInfrastructureJob, IRawInfraRepairTask, IRawRoleInstanceImpact} from '../Models/RawDataTypes'; import { mergeMap, map, catchError, finalize, skip } from 'rxjs/operators'; import { Application } from '../Models/DataModels/Application'; import { Service } from '../Models/DataModels/Service'; diff --git a/src/SfxWeb/src/app/views/cluster/essentials/essentials.component.html b/src/SfxWeb/src/app/views/cluster/essentials/essentials.component.html index 4ce6990e3..0d4ea132e 100644 --- a/src/SfxWeb/src/app/views/cluster/essentials/essentials.component.html +++ b/src/SfxWeb/src/app/views/cluster/essentials/essentials.component.html @@ -40,6 +40,10 @@

Throttled Infrastructure Jobs

+
+ +
+
diff --git a/src/SfxWeb/src/app/views/cluster/essentials/essentials.component.ts b/src/SfxWeb/src/app/views/cluster/essentials/essentials.component.ts index 07e15320c..7b47a8b27 100644 --- a/src/SfxWeb/src/app/views/cluster/essentials/essentials.component.ts +++ b/src/SfxWeb/src/app/views/cluster/essentials/essentials.component.ts @@ -5,6 +5,7 @@ import { HealthStateFilterFlags } from 'src/app/Models/HealthChunkRawDataTypes'; import { SystemApplication } from 'src/app/Models/DataModels/Application'; import { Observable, forkJoin, of } from 'rxjs'; import { IResponseMessageHandler } from 'src/app/Common/ResponseMessageHandlers'; +import { SystemServicePartitionIds } from 'src/app/Common/Constants'; import { BaseControllerDirective } from 'src/app/ViewModels/BaseController'; import { NodeCollection } from 'src/app/Models/DataModels/collections/NodeCollection'; import { ListSettings } from 'src/app/Models/ListSettings'; @@ -16,6 +17,8 @@ import { HealthUtils, HealthStatisticsEntityKind } from 'src/app/Utils/healthUti import { RepairTaskCollection } from 'src/app/Models/DataModels/collections/RepairTaskCollection'; import { IEssentialListItem } from 'src/app/modules/charts/essential-health-tile/essential-health-tile.component'; import { InfrastructureCollection } from 'src/app/Models/DataModels/collections/infrastructureCollection'; +import { RestClientService } from 'src/app/services/rest-client.service'; +import { IRawPartition } from 'src/app/Models/RawDataTypes'; @Component({ selector: 'app-essentials', @@ -41,12 +44,15 @@ export class EssentialsComponent extends BaseControllerDirective { replicasDashboard: IDashboardViewModel; upgradesDashboard: IDashboardViewModel; upgradeAppsCount = 0; + fmQuorumLossStatus : boolean; + fmQuorumLossWarning : string; essentialItems: IEssentialListItem[] = []; constructor(public data: DataService, public injector: Injector, public settings: SettingsService, + public RestClient: RestClientService, private routes: RoutesService) { super(injector); } @@ -61,6 +67,9 @@ export class EssentialsComponent extends BaseControllerDirective { this.infraCollection = this.data.infrastructureCollection; this.infraSettings = this.settings.getNewOrExistingInfrastructureSettings(); + this.fmQuorumLossWarning = `The Failover Manager service is in quorum loss state, which can cause disruptions in the cluster. Operations in PowerShell will likely to fail. + Cluster will not be loaded and the availability state of Application/System/Nodes may not be reflected in sfx. Failover Manager partition info + can still be queried by using Service Fabric client API. Please refer to this link: https://learn.microsoft.com/en-us/rest/api/servicefabric/sfclient-api-getpartitioninfo` } refresh(messageHandler?: IResponseMessageHandler): Observable { @@ -91,6 +100,10 @@ export class EssentialsComponent extends BaseControllerDirective { this.nodes.refresh(messageHandler).pipe(map(() => {this.updateItemInEssentials(); })), this.systemApp.refresh(messageHandler).pipe(catchError(err => of(null))), this.clusterUpgradeProgress.refresh(messageHandler), + this.RestClient.getPartitionById(SystemServicePartitionIds.FailoverManagerId, messageHandler).pipe(map((partition) => { + this.fmQuorumLossStatus = partition.PartitionStatus === 'InQuorumLoss'; + })), + this.data.getClusterManifest().pipe(map((manifest) => { if (manifest.isRepairManagerEnabled) { return this.repairtaskCollection.refresh(messageHandler);