Skip to content

Commit

Permalink
#1334 Notifications records in the promotion run page
Browse files Browse the repository at this point in the history
  • Loading branch information
dcoraboeuf committed Jul 24, 2024
1 parent 87fdd51 commit e9e28fe
Show file tree
Hide file tree
Showing 21 changed files with 1,511 additions and 204 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,16 @@ class DefaultNotificationRecordingService(
queries += "data::jsonb->'source'->>'id' = :sourceId"
queryVariables["sourceId"] = filter.sourceId
if (filter.sourceData != null) {
queries += "data::jsonb->'source'->'data' @> CAST(:sourceData AS JSONB)"
queries += "data::jsonb->'source'->'data' @> CAST(:sourceData AS JSONB)"
queryVariables["sourceData"] = filter.sourceData.format()
}
}

if (filter.eventEntityId != null) {
queries += "(data::jsonb->'event'->'entities'->'${filter.eventEntityId.type.name}'->>'id')::int = :eventEntityId"
queryVariables["eventEntityId"] = filter.eventEntityId.id
}

val query = queries.joinToString(" AND ") { "( $it )" }

val total = storageService.count(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ import net.nemerosa.ontrack.extension.notifications.channels.NotificationResultT
import net.nemerosa.ontrack.graphql.schema.GQLRootQuery
import net.nemerosa.ontrack.graphql.schema.GQLTypeCache
import net.nemerosa.ontrack.graphql.support.GQLScalarJSON
import net.nemerosa.ontrack.graphql.support.enumArgument
import net.nemerosa.ontrack.graphql.support.intArgument
import net.nemerosa.ontrack.graphql.support.pagination.GQLPaginatedListFactory
import net.nemerosa.ontrack.graphql.support.stringArgument
import net.nemerosa.ontrack.model.structure.ProjectEntityID
import net.nemerosa.ontrack.model.structure.ProjectEntityType
import org.springframework.stereotype.Component

@Component
Expand Down Expand Up @@ -43,6 +47,14 @@ class GQLRootQueryNotificationRecords(
.description("Filtering on the source data")
.type(GQLScalarJSON.INSTANCE)
.build(),
enumArgument<ProjectEntityType>(
ARG_FILTER_EVENT_ENTITY_TYPE,
"Filtering on the entity type targeted by the event (${ARG_FILTER_EVENT_ENTITY_ID} must be provided as well)"
),
intArgument(
ARG_FILTER_EVENT_ENTITY_ID,
"Filtering on the entity ID targeted by the event (${ARG_FILTER_EVENT_ENTITY_TYPE} must be provided as well)"
),
),
itemPaginatedListProvider = { env, _, offset, size ->
val resultType = env.getArgument<String?>(ARG_FILTER_RESULT_TYPE)?.let {
Expand All @@ -51,13 +63,25 @@ class GQLRootQueryNotificationRecords(
val channel: String? = env.getArgument(ARG_FILTER_CHANNEL)
val sourceId: String? = env.getArgument(ARG_FILTER_SOURCE_ID)
val sourceData: JsonNode? = env.getArgument(ARG_FILTER_SOURCE_DATA)

val eventEntityType = env.getArgument<String?>(ARG_FILTER_EVENT_ENTITY_TYPE)
?.takeIf { it.isNotBlank() }
?.let { ProjectEntityType.valueOf(it) }
val eventEntityNumericId: Int? = env.getArgument(ARG_FILTER_EVENT_ENTITY_ID)
val eventEntityId: ProjectEntityID? = if (eventEntityType != null && eventEntityNumericId != null) {
ProjectEntityID(eventEntityType, eventEntityNumericId)
} else {
null
}

val filter = NotificationRecordFilter(
offset = offset,
size = size,
channel = channel,
resultType = resultType,
sourceId = sourceId,
sourceData = sourceData,
eventEntityId = eventEntityId,
)
notificationRecordingService.filter(filter)
}
Expand All @@ -68,5 +92,7 @@ class GQLRootQueryNotificationRecords(
private const val ARG_FILTER_RESULT_TYPE = "resultType"
private const val ARG_FILTER_SOURCE_ID = "sourceId"
private const val ARG_FILTER_SOURCE_DATA = "sourceData"
private const val ARG_FILTER_EVENT_ENTITY_TYPE = "eventEntityType"
private const val ARG_FILTER_EVENT_ENTITY_ID = "eventEntityId"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package net.nemerosa.ontrack.extension.notifications.recording

import com.fasterxml.jackson.databind.JsonNode
import net.nemerosa.ontrack.extension.notifications.channels.NotificationResultType
import net.nemerosa.ontrack.model.annotations.APIDescription
import net.nemerosa.ontrack.model.structure.ProjectEntityID

data class NotificationRecordFilter(
val offset: Int = 0,
Expand All @@ -10,4 +12,6 @@ data class NotificationRecordFilter(
val resultType: NotificationResultType? = null,
val sourceId: String? = null,
val sourceData: JsonNode? = null,
@APIDescription("Entity targeted by the event which has triggered the notification")
val eventEntityId: ProjectEntityID? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import net.nemerosa.ontrack.extension.notifications.channels.NotificationResult
import net.nemerosa.ontrack.extension.notifications.mock.MockNotificationChannelConfig
import net.nemerosa.ontrack.extension.notifications.mock.MockNotificationChannelOutput
import net.nemerosa.ontrack.extension.notifications.subscriptions.EventSubscription
import net.nemerosa.ontrack.extension.notifications.subscriptions.subscribe
import net.nemerosa.ontrack.json.asJson
import net.nemerosa.ontrack.json.getRequiredIntField
import net.nemerosa.ontrack.json.getRequiredTextField
import net.nemerosa.ontrack.json.getTextField
import net.nemerosa.ontrack.model.events.EventFactory
import net.nemerosa.ontrack.model.structure.toProjectEntityID
import net.nemerosa.ontrack.test.TestUtils.uid
import net.nemerosa.ontrack.test.assertJsonNotNull
import net.nemerosa.ontrack.test.assertJsonNull
import org.junit.jupiter.api.Test
Expand Down Expand Up @@ -256,4 +259,64 @@ internal class GQLRootQueryNotificationRecordsIT : AbstractNotificationTestSuppo
}
}

@Test
fun `Getting the notifications linked to a promotion run`() {
asAdmin {
project {
branch {
val pl = promotionLevel()
// Subscription at promotion level
val target = uid("pl-")
val subscriptionName = uid("pl-sub-")
eventSubscriptionService.subscribe(
name = subscriptionName,
channel = mockNotificationChannel,
channelConfig = MockNotificationChannelConfig(target),
projectEntity = pl,
keywords = null,
origin = "test",
contentTemplate = null,
EventFactory.NEW_PROMOTION_RUN
)
// Creating a separate promotion
build {
promote(pl)
}
// Tracking a specific promotion
build {
val run = promote(pl)
// Looking at the records for THIS run to get its ID
val records = notificationRecordingService.filter(
filter = NotificationRecordFilter(
eventEntityId = run.toProjectEntityID(),
)
).pageItems
assertEquals(1, records.size)
val recordId = records.first().id
// Looking at the records for THIS run
run(
"""{
notificationRecords(
eventEntityType: PROMOTION_RUN,
eventEntityId: ${run.id},
) {
pageItems {
id
}
}
}"""
) { data ->
val record = data.path("notificationRecords").path("pageItems")
.path(0)
assertEquals(
recordId,
record.path("id").asText()
)
}
}
}
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package net.nemerosa.ontrack.extension.notifications.recording

import net.nemerosa.ontrack.extension.notifications.AbstractNotificationTestSupport
import net.nemerosa.ontrack.extension.notifications.mock.MockNotificationChannelConfig
import net.nemerosa.ontrack.extension.notifications.sources.EntitySubscriptionNotificationSourceDataType
import net.nemerosa.ontrack.extension.notifications.subscriptions.subscribe
import net.nemerosa.ontrack.json.asJson
import net.nemerosa.ontrack.model.events.EventFactory
import net.nemerosa.ontrack.model.structure.ProjectEntityType
import net.nemerosa.ontrack.model.structure.toProjectEntityID
import net.nemerosa.ontrack.test.TestUtils.uid
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import kotlin.test.assertEquals

class NotificationRecordsIT : AbstractNotificationTestSupport() {

@Autowired
private lateinit var notificationRecordingService: NotificationRecordingService

@Test
fun `Getting the notifications linked to a promotion run`() {
asAdmin {
project {
branch {
val pl = promotionLevel()
// Subscription at promotion level
val target = uid("pl-")
val subscriptionName = uid("pl-sub-")
eventSubscriptionService.subscribe(
name = subscriptionName,
channel = mockNotificationChannel,
channelConfig = MockNotificationChannelConfig(target),
projectEntity = pl,
keywords = null,
origin = "test",
contentTemplate = null,
EventFactory.NEW_PROMOTION_RUN
)
// Creating a separate promotion
build {
promote(pl)
}
// Tracking a specific promotion
build {
val run = promote(pl)
// Looking at the records for THIS run
val records = notificationRecordingService.filter(
filter = NotificationRecordFilter(
eventEntityId = run.toProjectEntityID(),
)
).pageItems
assertEquals(1, records.size)
val record = records.first()
assertEquals(
"entity-subscription",
record.source?.id
)
assertEquals(
EntitySubscriptionNotificationSourceDataType(
entityType = ProjectEntityType.PROMOTION_LEVEL,
entityId = pl.id(),
subscriptionName = subscriptionName,
).asJson(),
record.source?.data
)
}
}
}
}
}

}
3 changes: 1 addition & 2 deletions ontrack-web-core/components/core/model/EventDetails.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import EventDisplay from "@components/core/model/EventDisplay";
import {Space} from "antd";
import {extractProjectEntityInfo} from "@components/entities/ProjectEntityPageInfo";
import Link from "next/link";

export default function EventDetails({event}) {
return (
Expand All @@ -17,7 +16,7 @@ export default function EventDetails({event}) {
<>
<Space>
{info.type}
<Link href={info.href}>{info.name}</Link>
{info.component}
</Space>
</>
)
Expand Down
28 changes: 25 additions & 3 deletions ontrack-web-core/components/entities/ProjectEntityPageInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,21 @@ import {
validationStampBreadcrumbs
} from "@components/common/Breadcrumbs";
import PromotionLevelViewTitle from "@components/promotionLevels/PromotionLevelViewTitle";
import {branchUri, buildUri, projectUri, promotionLevelUri, validationStampUri} from "@components/common/Links";
import {
branchUri,
buildUri,
projectUri,
promotionLevelUri,
promotionRunUri,
validationStampUri
} from "@components/common/Links";
import ProjectLink from "@components/projects/ProjectLink";
import ValidationStampViewTitle from "@components/validationStamps/ValidationStampViewTitle";
import BranchLink from "@components/branches/BranchLink";
import PromotionLevelLink from "@components/promotionLevels/PromotionLevelLink";
import ValidationStampLink from "@components/validationStamps/ValidationStampLink";
import BuildLink from "@components/builds/BuildLink";
import PromotionRunLink from "@components/promotionRuns/PromotionRunLink";

export const extractProjectEntityInfo = (type, entity) => {
switch (type) {
Expand All @@ -26,6 +38,7 @@ export const extractProjectEntityInfo = (type, entity) => {
name: entity.name,
compositeName: entity.name,
href: projectUri(entity),
component: <ProjectLink project={entity}/>,
}
}
case 'BRANCH': {
Expand All @@ -34,6 +47,7 @@ export const extractProjectEntityInfo = (type, entity) => {
name: entity.name,
compositeName: `${entity.project.name}/${entity.name}`,
href: branchUri(entity),
component: <BranchLink branch={entity}/>,
}
}
case 'PROMOTION_LEVEL': {
Expand All @@ -42,6 +56,7 @@ export const extractProjectEntityInfo = (type, entity) => {
name: entity.name,
compositeName: `${entity.branch.project.name}/${entity.branch.name}/${entity.name}`,
href: promotionLevelUri(entity),
component: <PromotionLevelLink promotionLevel={entity}/>,
}
}
case 'VALIDATION_STAMP': {
Expand All @@ -50,6 +65,7 @@ export const extractProjectEntityInfo = (type, entity) => {
name: entity.name,
compositeName: `${entity.branch.project.name}/${entity.branch.name}/${entity.name}`,
href: validationStampUri(entity),
component: <ValidationStampLink validationStamp={entity}/>,
}
}
case 'BUILD': {
Expand All @@ -58,15 +74,21 @@ export const extractProjectEntityInfo = (type, entity) => {
name: entity.name,
compositeName: `${entity.branch.project.name}/${entity.branch.name}/${entity.name}`,
href: buildUri(entity),
component: <BuildLink build={entity}/>,
}
}
case 'VALIDATION_RUN': {
// TODO
break
}
case 'PROMOTION_RUN': {
// No information available for this type of entity
break
return {
type: 'Promotion run',
name: `${entity.build.name} x ${entity.promotionLevel.name}`,
compositeName: `${entity.build.branch.project.name}/${entity.build.branch.name}/${entity.promotionLevel.name}/${entity.build.name}`,
href: promotionRunUri(entity),
component: <PromotionRunLink promotionRun={entity}/>,
}
}
}
}
Expand Down
Loading

0 comments on commit e9e28fe

Please sign in to comment.