Skip to content

Commit

Permalink
BDOG-3332 Add team and digital filter to shutter overview
Browse files Browse the repository at this point in the history
  • Loading branch information
colin-lamed committed Jan 9, 2025
1 parent dfbd4c9 commit 489051f
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package uk.gov.hmrc.cataloguefrontend.shuttering
import javax.inject.{Inject, Singleton}
import play.api.libs.json.{Json, JsValue, JsNull, JsString, Reads, Writes}
import play.api.libs.ws.writeableOf_JsValue
import uk.gov.hmrc.cataloguefrontend.model.{Environment, ServiceName}
import uk.gov.hmrc.cataloguefrontend.model.{DigitalService, Environment, ServiceName, TeamName}
import uk.gov.hmrc.cataloguefrontend.shuttering.ShutterConnector.ShutterEventsFilter
import uk.gov.hmrc.http.{HeaderCarrier, HttpReads, StringContextOps, UpstreamErrorResponse}
import uk.gov.hmrc.http.client.HttpClientV2
Expand All @@ -45,15 +45,17 @@ class ShutterConnector @Inject() (
* Retrieves the current shutter states for all services in given environment
*/
def shutterStates(
st : ShutterType,
env : Environment,
serviceName: Option[ServiceName] = None
st : ShutterType,
env : Environment,
teamName : Option[TeamName],
digitalService: Option[DigitalService],
serviceName : Option[ServiceName]
)(using
HeaderCarrier
): Future[Seq[ShutterState]] =
given Reads[ShutterState] = ShutterState.reads
httpClientV2
.get(url"$baseUrl/shutter-api/${env.asString}/${st.asString}/states?serviceName=${serviceName.map(_.asString)}")
.get(url"$baseUrl/shutter-api/${env.asString}/${st.asString}/states?serviceName=${serviceName.map(_.asString)}&teamName=${teamName.map(_.asString)}&digitalService=${digitalService.map(_.asString)}")
.execute[Seq[ShutterState]]

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,61 +17,96 @@
package uk.gov.hmrc.cataloguefrontend.shuttering

import cats.implicits._

import javax.inject.{Inject, Singleton}
import play.api.Logger
import play.api.data.{Form, Forms}
import play.api.mvc.{Action, AnyContent, MessagesControllerComponents, MessagesRequest, RequestHeader}
import uk.gov.hmrc.cataloguefrontend.auth.CatalogueAuthBuilders
import uk.gov.hmrc.cataloguefrontend.config.CatalogueConfig
import uk.gov.hmrc.cataloguefrontend.model.{Environment, ServiceName}
import uk.gov.hmrc.cataloguefrontend.connector.TeamsAndRepositoriesConnector
import uk.gov.hmrc.cataloguefrontend.model.{DigitalService, Environment, ServiceName, TeamName}
import uk.gov.hmrc.cataloguefrontend.shuttering.ShutterType.Rate
import uk.gov.hmrc.cataloguefrontend.shuttering.view.html.{FrontendRouteWarningsPage, ShutterOverviewPage}
import uk.gov.hmrc.internalauth.client.{FrontendAuthComponents, IAAction, Predicate, Resource, Retrieval}
import uk.gov.hmrc.play.bootstrap.frontend.controller.FrontendController

import javax.inject.{Inject, Singleton}
import scala.concurrent.ExecutionContext
import scala.util.control.NonFatal

@Singleton
class ShutterOverviewController @Inject() (
override val mcc : MessagesControllerComponents,
shutterOverviewPage : ShutterOverviewPage,
frontendRouteWarningPage : FrontendRouteWarningsPage,
shutterService : ShutterService,
catalogueConfig : CatalogueConfig,
override val auth : FrontendAuthComponents
override val mcc : MessagesControllerComponents,
shutterOverviewPage : ShutterOverviewPage,
frontendRouteWarningPage : FrontendRouteWarningsPage,
shutterService : ShutterService,
teamsAndRepositoriesConnector: TeamsAndRepositoriesConnector,
catalogueConfig : CatalogueConfig,
override val auth : FrontendAuthComponents
)(using
override val ec: ExecutionContext
) extends FrontendController(mcc)
with CatalogueAuthBuilders:

private val logger = Logger(getClass)

def allStates(shutterType: ShutterType): Action[AnyContent] =
def allStates(
shutterType : ShutterType,
teamName : Option[TeamName],
digitalService: Option[DigitalService]
): Action[AnyContent] =
allStatesForEnv(
shutterType = shutterType,
env = Environment.Production
shutterType = shutterType,
env = Environment.Production,
teamName = teamName,
digitalService = digitalService
)

def allStatesForEnv(shutterType: ShutterType, env: Environment): Action[AnyContent] =
def allStatesForEnv(
shutterType : ShutterType,
env : Environment,
teamName : Option[TeamName],
digitalService: Option[DigitalService]
): Action[AnyContent] =
BasicAuthAction.async: request =>
given MessagesRequest[AnyContent] = request
for
teams <- teamsAndRepositoriesConnector.allTeams().map(_.map(_.name))
digitalServices <- teamsAndRepositoriesConnector.allDigitalServices()
envAndCurrentStates <- Environment.values.toSeq.traverse: env =>
shutterService
.findCurrentStates(shutterType, env)
.recover:
case NonFatal(ex) =>
logger.error(s"Could not retrieve currentState: ${ex.getMessage}", ex)
Seq.empty
.findCurrentStates(
shutterType,
env,
teamName.filter(_.asString.nonEmpty),
digitalService.filter(_.asString.nonEmpty)
)
.map(ws => (env, ws))
hasGlobalPerm <- auth
.verify:
Retrieval.hasPredicate(Predicate.Permission(Resource.from("shutter-api", "mdtp"), IAAction("SHUTTER")))
.map(_.exists(_ == true))
killSwitchLink = if hasGlobalPerm && shutterType != Rate then Some(catalogueConfig.killSwitchLink(shutterType.asString)) else None
page = shutterOverviewPage(envAndCurrentStates.toMap, shutterType, env, killSwitchLink)
yield Ok(page)
yield Ok(shutterOverviewPage(
form.bindFromRequest(),
envAndCurrentStates.toMap,
shutterType,
env,
teamName,
digitalService,
killSwitchLink,
teams,
digitalServices
))

case class Filter(
team : Option[TeamName],
digitalService: Option[DigitalService]
)

lazy val form: Form[Filter] =
Form(
Forms.mapping(
"teamName" -> Forms.optional(Forms.of[TeamName]),
"digitalService" -> Forms.optional(Forms.of[DigitalService])
)(Filter.apply)(f => Some(Tuple.fromProductTyped(f)))
)


def frontendRouteWarnings(env: Environment, serviceName: ServiceName): Action[AnyContent] =
BasicAuthAction.async: request =>
Expand All @@ -80,10 +115,6 @@ class ShutterOverviewController @Inject() (
envsAndWarnings <- Environment.values.toSeq.traverse: env =>
shutterService
.frontendRouteWarnings(env, serviceName)
.recover:
case NonFatal(ex) =>
logger.error(s"Could not retrieve frontend route warnings for service '${serviceName.asString}' in env: '${env.asString}': ${ex.getMessage}", ex)
Seq.empty
.map(ws => (env, ws))
page = frontendRouteWarningPage(envsAndWarnings.toMap, env, serviceName)
yield Ok(page)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import cats.implicits.*
import javax.inject.{Inject, Singleton}
import uk.gov.hmrc.cataloguefrontend.connector.{GitHubProxyConnector, RouteConfigurationConnector}
import uk.gov.hmrc.cataloguefrontend.connector.RouteConfigurationConnector.RouteType
import uk.gov.hmrc.cataloguefrontend.model.{Environment, ServiceName}
import uk.gov.hmrc.cataloguefrontend.model.{DigitalService, Environment, ServiceName, TeamName}
import uk.gov.hmrc.internalauth.client.AuthenticatedRequest
import uk.gov.hmrc.http.HeaderCarrier

Expand All @@ -46,7 +46,7 @@ class ShutterService @Inject() (
)(using
HeaderCarrier
): Future[Seq[ShutterState]] =
shutterConnector.shutterStates(st, env, serviceName)
shutterConnector.shutterStates(st, env, teamName = None, digitalService = None, serviceName)

def updateShutterStatus(
serviceName: ServiceName,
Expand Down Expand Up @@ -88,13 +88,15 @@ class ShutterService @Inject() (
shutterConnector.frontendRouteWarnings(env, serviceName)

def findCurrentStates(
st : ShutterType,
env: Environment
st : ShutterType,
env : Environment,
teamName : Option[TeamName],
digitalService: Option[DigitalService]
)(using
HeaderCarrier
): Future[Seq[(ShutterState, Option[ShutterStateChangeEvent])]] =
for
states <- shutterConnector.shutterStates(st, env)
states <- shutterConnector.shutterStates(st, env, teamName, digitalService, serviceName = None)
events <- shutterConnector.latestShutterEvents(st, env)
yield
states
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,26 @@

@import uk.gov.hmrc.cataloguefrontend.shuttering.{routes, ShutterCause, ShutterStateChangeEvent, ShutterState, ShutterStatusValue, ShutterType}
@import uk.gov.hmrc.cataloguefrontend.util.DateHelper._
@import views.html.helper.{FieldConstructor, select}

@this()

@(shutterStates : Map[Environment, Seq[(ShutterState, Option[ShutterStateChangeEvent])]]
, shutterType : ShutterType
, selectedEnv : Environment
, killSwitchLink: Option[String]
@(form : Form[?]
, shutterStates : Map[Environment, Seq[(ShutterState, Option[ShutterStateChangeEvent])]]
, shutterType : ShutterType
, selectedEnv : Environment
, selectedTeamName : Option[TeamName]
, selectedDigitalService: Option[DigitalService]
, killSwitchLink : Option[String]
, teams : Seq[TeamName]
, digitalServices : Seq[DigitalService]
)(implicit
request : RequestHeader
, messages : Messages
)

@implicitField: FieldConstructor = @{ FieldConstructor(catalogueFieldConstructor.f) }

@standard_layout(s"Shutter Overview - ${shutterType.asString.capitalize}", active = "shuttering") {

<h1 class="page-heading mt-4">Shutter Overview - @{shutterType.asString.capitalize}</h1>
Expand All @@ -41,6 +49,33 @@ <h1 class="page-heading mt-4">Shutter Overview - @{shutterType.asString.capitali

<section id="shutter-overview">

<div class="row">
<form id="form" method="get">
<div class="row">
<div class="col-md-3">
@select(
field = form("teamName"),
options = teams.map(t => t.asString -> t.asString),
Symbol("_default") -> "All",
Symbol("_label") -> "Team",
Symbol("_labelClass") -> "form-label",
Symbol("id") -> "teamName-dropdown",
Symbol("class") -> "form-select"
)
</div>
<div class="col-md-3">
@select(
field = form("digitalService"),
options = digitalServices.map(ds => ds.asString -> ds.asString),
Symbol("_default") -> "All",
Symbol("_label") -> "Digital Service",
Symbol("_labelClass") -> "form-label",
Symbol("id") -> "digitalService-dropdown",
Symbol("class") -> "form-select"
)
</div>
</form>

<div class="row mb-3">
<div class="col-12 position-relative">
<ul id="environment" class="nav nav-tabs mb2">
Expand Down Expand Up @@ -75,53 +110,62 @@ <h1 class="page-heading mt-4">Shutter Overview - @{shutterType.asString.capitali
</tr>
</thead>
<tbody class="list">
@shutterRows(shutterStates.get(selectedEnv).getOrElse(Seq.empty))
@shutterRows(shutterStates.getOrElse(selectedEnv, Seq.empty))
</tbody>
</table>
</div>
</section>

<script @CSPNonce.attr>
function filterUnshuttered() {
let checkBox = document.getElementById("shuttered-only");
let unshutteredServices = document.querySelectorAll("tr.unshuttered");

if (checkBox.checked === true) {
unshutteredServices.forEach(function(element) {
element.classList.add("d-none");
});
} else {
unshutteredServices.forEach(function(element) {
element.classList.remove("d-none");
});
}
let checkBox = document.getElementById("shuttered-only");
let unshutteredServices = document.querySelectorAll("tr.unshuttered");

if (checkBox.checked === true) {
unshutteredServices.forEach(function(element) {
element.classList.add("d-none");
});
} else {
unshutteredServices.forEach(function(element) {
element.classList.remove("d-none");
});
}
}

document.getElementById("shuttered-only").addEventListener("change", function() {
filterUnshuttered();
filterUnshuttered();
});

let options = { valueNames: [ 'shutter-service', @if(shutterType != ShutterType.Frontend) { 'shutter-context' }, 'shutter-state', 'shutter-user', 'shutter-date' ] };
let serviceList = new List('service-list', options);
["teamName-dropdown", "digitalService-dropdown"]
.forEach(function(id) {
document.getElementById(id).addEventListener("change", function() {
document.getElementById("form").submit();
});
});

@if(shutterStates.getOrElse(selectedEnv, Seq.empty).nonEmpty) {
let options = { valueNames: [ 'shutter-service', @if(shutterType != ShutterType.Frontend) { 'shutter-context', } 'shutter-state', 'shutter-user', 'shutter-date' ] };
let serviceList = new List('service-list', options);
}
</script>
}

@envOption(env: Environment) = {
@defining(env == selectedEnv) { active =>
<li id="[email protected]" class="nav-item">
<a class="nav-link @if(active) {active}" href="@uk.gov.hmrc.cataloguefrontend.shuttering.routes.ShutterOverviewController.allStatesForEnv(shutterType, env)">
<a class="nav-link @if(active) {active}" href="@uk.gov.hmrc.cataloguefrontend.shuttering.routes.ShutterOverviewController.allStatesForEnv(shutterType, env, selectedTeamName, selectedDigitalService)">
@env.displayString (@shutteredCount(env) / @serviceCount(env))
</a>
</li>
}
}

@serviceCount(env: Environment) = {
@shutterStates.get(env).getOrElse(Seq.empty).size
@shutterStates.getOrElse(env, Seq.empty).size
}

@shutteredCount(env: Environment) = {
@shutterStates.get(env).getOrElse(Seq.empty).filter(_._1.status.value == ShutterStatusValue.Shuttered).size
@shutterStates.getOrElse(env, Seq.empty).filter(_._1.status.value == ShutterStatusValue.Shuttered).size
}

@shutterRows(states: Seq[(ShutterState, Option[ShutterStateChangeEvent])]) = {
Expand Down
4 changes: 2 additions & 2 deletions conf/app.routes
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ GET /bobbyrules uk.gov.hmrc.cataloguefro

GET /pr-commenter/recommendations uk.gov.hmrc.cataloguefrontend.prcommenter.PrCommenterController.recommendations(name: Option[String] ?= None, teamName: Option[TeamName] ?= None, commentType: Option[String] ?= None)

GET /shuttering-overview/:shutterType uk.gov.hmrc.cataloguefrontend.shuttering.ShutterOverviewController.allStates(shutterType: ShutterType)
GET /shuttering-overview/:shutterType/:env uk.gov.hmrc.cataloguefrontend.shuttering.ShutterOverviewController.allStatesForEnv(shutterType: ShutterType, env: Environment)
GET /shuttering-overview/:shutterType uk.gov.hmrc.cataloguefrontend.shuttering.ShutterOverviewController.allStates(shutterType: ShutterType, teamName: Option[TeamName] ?= None, digitalService: Option[DigitalService] ?= None)
GET /shuttering-overview/:shutterType/:env uk.gov.hmrc.cataloguefrontend.shuttering.ShutterOverviewController.allStatesForEnv(shutterType: ShutterType, env: Environment, teamName: Option[TeamName] ?= None, digitalService: Option[DigitalService] ?= None)
GET /frontend-route-warnings/:env/:serviceName uk.gov.hmrc.cataloguefrontend.shuttering.ShutterOverviewController.frontendRouteWarnings(env: Environment, serviceName: ServiceName)

GET /shuttering/1 uk.gov.hmrc.cataloguefrontend.shuttering.ShutterWizardController.step1Get(serviceName: Option[ServiceName], context: Option[String])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,12 @@ class ShutterServiceSpec
val boot = Boot.init
given HeaderCarrier = HeaderCarrier()

when(boot.mockShutterConnector.shutterStates(ShutterType.Frontend, Environment.Production))
when(boot.mockShutterConnector.shutterStates(ShutterType.Frontend, Environment.Production, teamName = None, digitalService = None, serviceName = None))
.thenReturn(Future.successful(mockShutterStates))
when(boot.mockShutterConnector.latestShutterEvents(ShutterType.Frontend, Environment.Production))
.thenReturn(Future.successful(mockEvents))

val states = boot.shutterService.findCurrentStates(ShutterType.Frontend, Environment.Production).futureValue
val states = boot.shutterService.findCurrentStates(ShutterType.Frontend, Environment.Production, teamName = None, digitalService = None).futureValue
states.map(_._1.status) shouldBe Seq(
ShutterStatus.Shuttered(reason = None, outageMessage = None, outageMessageWelsh = None, useDefaultOutagePage = false)
, ShutterStatus.Shuttered(reason = None, outageMessage = None, outageMessageWelsh = None, useDefaultOutagePage = false)
Expand Down

0 comments on commit 489051f

Please sign in to comment.