diff --git a/app/config/Module.scala b/app/config/Module.scala index ce88c807..32cdbe4d 100644 --- a/app/config/Module.scala +++ b/app/config/Module.scala @@ -42,6 +42,7 @@ class Module extends play.api.inject.Module { bindz[AddAnApiCheckContextActionProvider].to(classOf[AddAnApiCheckContextActionProviderImpl]).eagerly(), bindz(classOf[IdentifierAction]).to(classOf[AuthenticatedIdentifierAction]).eagerly(), bindz(classOf[OptionalIdentifierAction]).to(classOf[OptionallyAuthenticatedIdentifierAction]), + bindz[StatusActionProvider].to(classOf[StatusActionProviderImpl]).eagerly(), bindz(classOf[Clock]).toInstance(Clock.systemDefaultZone.withZone(ZoneOffset.UTC)), bindz[Encrypter & Decrypter].toProvider[CryptoProvider], bindz[Domains].to(classOf[DomainsImpl]).eagerly(), diff --git a/app/connectors/ApplicationsConnector.scala b/app/connectors/ApplicationsConnector.scala index 73849012..41c3fa42 100644 --- a/app/connectors/ApplicationsConnector.scala +++ b/app/connectors/ApplicationsConnector.scala @@ -27,6 +27,7 @@ import models.deployment.* import models.exception.{ApplicationCredentialLimitException, ApplicationsException, TeamNameNotUniqueException} import models.requests.{AddApiRequest, ChangeTeamNameRequest, TeamMemberRequest} import models.stats.ApisInProductionStatistic +import models.status.ServiceStatuses import models.team.{NewTeam, Team} import models.user.UserContactDetails import play.api.Logging @@ -598,4 +599,11 @@ class ApplicationsConnector @Inject()( .setHeader(AUTHORIZATION -> clientAuthToken) .execute[Seq[EgressGateway]] } + + def status()(implicit hc: HeaderCarrier): Future[ServiceStatuses] = { + httpClient.get(url"$applicationsBaseUrl/api-hub-applications/status") + .setHeader(ACCEPT -> JSON) + .setHeader(AUTHORIZATION -> clientAuthToken) + .execute[ServiceStatuses] + } } diff --git a/app/controllers/IndexController.scala b/app/controllers/IndexController.scala index 36138a42..e2d57cde 100644 --- a/app/controllers/IndexController.scala +++ b/app/controllers/IndexController.scala @@ -16,10 +16,10 @@ package controllers -import controllers.actions.IdentifierAction +import controllers.actions.{IdentifierAction, StatusActionProvider} import play.api.Logging import play.api.i18n.I18nSupport -import play.api.mvc._ +import play.api.mvc.* import services.ApiHubService import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendBaseController import views.html.IndexView @@ -30,11 +30,12 @@ import scala.concurrent.ExecutionContext class IndexController @Inject()( val controllerComponents: MessagesControllerComponents, identify: IdentifierAction, + status: StatusActionProvider, view: IndexView, apiHubService: ApiHubService )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport with Logging { - def onPageLoad: Action[AnyContent] = identify.async { implicit request => + def onPageLoad: Action[AnyContent] = (identify andThen status()).async { implicit request => val maxApplicationsToShow = 5 val maxTeamsToShow = 5 @@ -46,7 +47,8 @@ class IndexController @Inject()( userApps.size, userTeams.sortBy(_.created).reverse.take(maxTeamsToShow), userTeams.size, - Some(request.user) + Some(request.user), + request.serviceStatuses )) } diff --git a/app/controllers/actions/StatusAction.scala b/app/controllers/actions/StatusAction.scala new file mode 100644 index 00000000..87b716a2 --- /dev/null +++ b/app/controllers/actions/StatusAction.scala @@ -0,0 +1,53 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * 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. + */ + +package controllers.actions + +import com.google.inject.Inject +import connectors.ApplicationsConnector +import handlers.ErrorHandler +import models.requests.{ApiRequest, IdentifierRequest, StatusRequest} +import models.user.{LdapUser, StrideUser, UserModel, UserType} +import play.api.Logging +import play.api.mvc.Results.* +import play.api.mvc.* +import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendHeaderCarrierProvider + +import scala.concurrent.{ExecutionContext, Future} + +trait StatusAction extends ActionRefiner[IdentifierRequest, StatusRequest] + +trait StatusActionProvider { + def apply()(implicit ec: ExecutionContext): StatusAction +} + +class StatusActionProviderImpl @Inject()( + applicationsConnector: ApplicationsConnector, + errorHandler: ErrorHandler +)(implicit val executionContext: ExecutionContext) extends StatusActionProvider { + + def apply()(implicit ec: ExecutionContext): StatusAction = + new StatusAction with FrontendHeaderCarrierProvider { + override protected def refine[A](identifierRequest: IdentifierRequest[A]): Future[Either[Result, StatusRequest[A]]] = + implicit val request: Request[?] = identifierRequest + applicationsConnector.status().map(statuses => + Right(StatusRequest(identifierRequest, identifierRequest.user, statuses)) + ) + + override protected def executionContext: ExecutionContext = ec + } + +} diff --git a/app/models/requests/StatusRequest.scala b/app/models/requests/StatusRequest.scala new file mode 100644 index 00000000..e0a3d081 --- /dev/null +++ b/app/models/requests/StatusRequest.scala @@ -0,0 +1,23 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * 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. + */ + +package models.requests + +import models.status.ServiceStatuses +import models.user.UserModel +import play.api.mvc.WrappedRequest + +case class StatusRequest[A](identifierRequest: IdentifierRequest[A], user: UserModel, serviceStatuses: ServiceStatuses) extends WrappedRequest[A](identifierRequest) \ No newline at end of file diff --git a/app/models/status/ServiceStatus.scala b/app/models/status/ServiceStatus.scala new file mode 100644 index 00000000..9421cb8d --- /dev/null +++ b/app/models/status/ServiceStatus.scala @@ -0,0 +1,31 @@ +/* + * Copyright 2024 HM Revenue & Customs + * + * 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. + */ + +package models.status + +import models.application.EnvironmentName +import play.api.libs.json.{Format, Json} + +case class ServiceStatus(isDown: Boolean, service: String, environmentName: EnvironmentName) +object ServiceStatus: + given statusFormat: Format[ServiceStatus] = Json.format[ServiceStatus] + +case class ServiceStatuses(statuses: Seq[ServiceStatus]) { + lazy val hasServiceDown: Boolean = statuses.exists(_.isDown) +} +object ServiceStatuses: + given statusesFormat: Format[ServiceStatuses] = Json.format[ServiceStatuses] + diff --git a/app/views/IndexView.scala.html b/app/views/IndexView.scala.html index 44a5da4f..dbe903d3 100644 --- a/app/views/IndexView.scala.html +++ b/app/views/IndexView.scala.html @@ -15,6 +15,7 @@ *@ @import models.application.Application +@import models.status.ServiceStatuses @import models.user.UserModel @import views.ViewUtils @import models.team.Team @@ -26,14 +27,15 @@ govukTable: GovukTable ) -@(applications: Seq[Application], totalApplicationCount: Int, teams: Seq[Team], totalTeamCount: Int, user: Option[UserModel])(implicit request: Request[?], messages: Messages) +@(applications: Seq[Application], totalApplicationCount: Int, teams: Seq[Team], totalTeamCount: Int, user: Option[UserModel], statuses: ServiceStatuses)(implicit request: Request[?], messages: Messages) @layout( pageTitle = titleNoForm(messages("dashboard.title")), showBackLink = true, fullWidth = true, user = user, - activeLink = Some("dashboard") + activeLink = Some("dashboard"), + statuses = Some(statuses) ) {
diff --git a/app/views/templates/Layout.scala.html b/app/views/templates/Layout.scala.html index 01ede4c5..e8c536bf 100644 --- a/app/views/templates/Layout.scala.html +++ b/app/views/templates/Layout.scala.html @@ -16,6 +16,7 @@ @import components.FullWidthMainContent @import config.FrontendAppConfig +@import models.status.ServiceStatuses @import models.user.{StrideUser, UserModel} @import templates.ApiHubHeader @import uk.gov.hmrc.hmrcfrontend.views.config.StandardAlphaBanner @@ -37,7 +38,8 @@ twoThirdsMainContent: TwoThirdsMainContent, fullWidthMainContentLayout: FullWidthMainContent, header: ApiHubHeader, - hmrcInternalHead: HmrcInternalHead + hmrcInternalHead: HmrcInternalHead, + govukPhaseBanner: GovukPhaseBanner ) @( @@ -49,7 +51,8 @@ fullWidth: Boolean = false, customScriptsBlock: Option[Html] = None, customStyles: Option[Html] = None, - activeLink: Option[String] = None + activeLink: Option[String] = None, + statuses: Option[ServiceStatuses] = None, )(contentBlock: Html)(implicit request: RequestHeader, messages: Messages) @head = { @@ -103,6 +106,26 @@ @messages("site.feedback") + @for(serviceStatuses <- statuses) { + @govukPhaseBanner(PhaseBanner( + tag = Some(Tag( + content = Text( + if (serviceStatuses.hasServiceDown) then + "Ongoing issues" + else + "Good service" + ), + classes = s"${if (serviceStatuses.hasServiceDown) then "govuk-tag--red" else ""}" + )), + content = Text( + if (serviceStatuses.hasServiceDown) then + "We are experiencing some issues." + else + "All services are healthy." + ) + )) + } + @contentBlock
diff --git a/test/controllers/IndexControllerSpec.scala b/test/controllers/IndexControllerSpec.scala index 7eedbae2..9761acf5 100644 --- a/test/controllers/IndexControllerSpec.scala +++ b/test/controllers/IndexControllerSpec.scala @@ -21,6 +21,7 @@ import controllers.IndexControllerSpec.buildFixture import controllers.actions.FakeUser import generators.TeamGenerator import models.application.{Application, Creator, TeamMember} +import models.status.ServiceStatuses import org.mockito.ArgumentMatchers.{any, eq => eqTo} import org.mockito.Mockito.when import org.scalatestplus.mockito.MockitoSugar @@ -66,7 +67,7 @@ class IndexControllerSpec extends SpecBase with MockitoSugar with TeamGenerator val sortedApplications = applications.sortBy(_.created).reverse val sortedTeams = teams.sortBy(_.created).reverse - contentAsString(result) mustEqual view(sortedApplications, applications.size, sortedTeams, teams.size, Some(FakeUser))(request, messages(fixture.application)).toString + contentAsString(result) mustEqual view(sortedApplications, applications.size, sortedTeams, teams.size, Some(FakeUser), ServiceStatuses(Seq.empty))(request, messages(fixture.application)).toString contentAsString(result) must validateAsHtml } } @@ -93,7 +94,7 @@ class IndexControllerSpec extends SpecBase with MockitoSugar with TeamGenerator status(result) mustEqual OK - contentAsString(result) mustEqual view(applications, 0, teams, 0, Some(FakeUser))(request, messages(fixture.application)).toString + contentAsString(result) mustEqual view(applications, 0, teams, 0, Some(FakeUser), ServiceStatuses(Seq.empty))(request, messages(fixture.application)).toString contentAsString(result) must validateAsHtml } }