From bf1dfc9c5a33a1c14aa91901769d6bcc528cfbf6 Mon Sep 17 00:00:00 2001 From: Damien Coraboeuf Date: Mon, 8 Jul 2024 13:14:24 +0200 Subject: [PATCH] #1319 Displaying the list of subscriptions in the promotions page --- .../subscriptions/EntitySubscriptionStore.kt | 8 +- .../GQLRootQueryEventSubscriptionsIT.kt | 51 ++++++++++- .../ontrack/model/structure/EntityStore.kt | 2 + .../model/structure/EntityStoreExtensions.kt | 4 +- .../pagination/PaginatedListExtensionsTest.kt | 31 +++++++ .../repository/EntityStoreJdbcRepository.kt | 6 +- .../repository/EntityStoreJdbcRepositoryIT.kt | 2 +- .../BranchPromotionLevelsView.js | 6 +- .../notifications/EntitySubscriptions.js | 84 +++++++++++++++++++ .../notifications/SubscriptionName.js | 5 +- .../notifications/SubscriptionsLink.js | 4 +- 11 files changed, 191 insertions(+), 12 deletions(-) create mode 100644 ontrack-model/src/test/java/net/nemerosa/ontrack/model/pagination/PaginatedListExtensionsTest.kt create mode 100644 ontrack-web-core/components/extension/notifications/EntitySubscriptions.js diff --git a/ontrack-extension-notifications/src/main/java/net/nemerosa/ontrack/extension/notifications/subscriptions/EntitySubscriptionStore.kt b/ontrack-extension-notifications/src/main/java/net/nemerosa/ontrack/extension/notifications/subscriptions/EntitySubscriptionStore.kt index 284f0e687e9..1ac88bfa461 100644 --- a/ontrack-extension-notifications/src/main/java/net/nemerosa/ontrack/extension/notifications/subscriptions/EntitySubscriptionStore.kt +++ b/ontrack-extension-notifications/src/main/java/net/nemerosa/ontrack/extension/notifications/subscriptions/EntitySubscriptionStore.kt @@ -98,7 +98,13 @@ class EntitySubscriptionStore( // Total count for THIS entity val count = entityStore.getCountByFilter(entity, ENTITY_STORE, jsonFilter) // Loading & converting the records - val items = entityStore.getByFilter(entity, ENTITY_STORE, jsonFilter) + val items = entityStore.getByFilter( + entity = entity, + store = ENTITY_STORE, + offset = offset, + size = size, + jsonFilter = jsonFilter + ) // OK return count to items } diff --git a/ontrack-extension-notifications/src/test/java/net/nemerosa/ontrack/extension/notifications/subscriptions/GQLRootQueryEventSubscriptionsIT.kt b/ontrack-extension-notifications/src/test/java/net/nemerosa/ontrack/extension/notifications/subscriptions/GQLRootQueryEventSubscriptionsIT.kt index 702754708f8..da0814c3705 100644 --- a/ontrack-extension-notifications/src/test/java/net/nemerosa/ontrack/extension/notifications/subscriptions/GQLRootQueryEventSubscriptionsIT.kt +++ b/ontrack-extension-notifications/src/test/java/net/nemerosa/ontrack/extension/notifications/subscriptions/GQLRootQueryEventSubscriptionsIT.kt @@ -116,7 +116,7 @@ internal class GQLRootQueryEventSubscriptionsIT : AbstractNotificationTestSuppor EventFactory.NEW_PROMOTION_RUN ) // Query - asUser().withProjectFunction(this, ProjectSubscriptionsRead::class.java).call { + asUser().withView(this).withProjectFunction(this, ProjectSubscriptionsRead::class.java).call { run( """ query { @@ -138,7 +138,7 @@ internal class GQLRootQueryEventSubscriptionsIT : AbstractNotificationTestSuppor } } // Query - asUser().withProjectFunction(this, ProjectSubscriptionsRead::class.java) + asUser().withView(this).withProjectFunction(this, ProjectSubscriptionsRead::class.java) .withProjectFunction(this, ProjectSubscriptionsWrite::class.java) .call { run( @@ -277,6 +277,53 @@ internal class GQLRootQueryEventSubscriptionsIT : AbstractNotificationTestSuppor } } + @Test + fun `Getting the first subscriptions for an entity`() { + project { + // Creating 10 subscriptions + (1..10).forEach { + eventSubscriptionService.subscribe( + name = uid("p"), + channel = mockNotificationChannel, + channelConfig = MockNotificationChannelConfig("#channel-$it"), + projectEntity = this, + keywords = null, + origin = "test", + contentTemplate = "Subscription #$it", + EventFactory.NEW_PROMOTION_RUN + ) + } + // Query + run( + """ + query { + eventSubscriptions( + size: 2, + filter: { + entity: { + type: PROJECT, + id: $id + } + } + ) { + pageInfo { + totalSize + } + pageItems { + contentTemplate + } + } + } + """ + ) { data -> + val eventSubscriptions = data.path("eventSubscriptions") + assertEquals(10, eventSubscriptions.path("pageInfo").path("totalSize").asInt()) + val items = eventSubscriptions.path("pageItems") + assertEquals(2, items.size()) + } + } + } + @Test fun `Getting a global subscription with a content template`() { asAdmin { diff --git a/ontrack-model/src/main/java/net/nemerosa/ontrack/model/structure/EntityStore.kt b/ontrack-model/src/main/java/net/nemerosa/ontrack/model/structure/EntityStore.kt index 9a8e8617d97..3bceade2da8 100644 --- a/ontrack-model/src/main/java/net/nemerosa/ontrack/model/structure/EntityStore.kt +++ b/ontrack-model/src/main/java/net/nemerosa/ontrack/model/structure/EntityStore.kt @@ -44,6 +44,8 @@ interface EntityStore { fun getByFilter( entity: ProjectEntity, store: String, + offset: Int = 0, + size: Int = 10, filter: EntityStoreFilter, type: KClass ): List diff --git a/ontrack-model/src/main/java/net/nemerosa/ontrack/model/structure/EntityStoreExtensions.kt b/ontrack-model/src/main/java/net/nemerosa/ontrack/model/structure/EntityStoreExtensions.kt index 3b2ef82067d..fc68b20864b 100644 --- a/ontrack-model/src/main/java/net/nemerosa/ontrack/model/structure/EntityStoreExtensions.kt +++ b/ontrack-model/src/main/java/net/nemerosa/ontrack/model/structure/EntityStoreExtensions.kt @@ -9,9 +9,11 @@ inline fun EntityStore.findByName(entity: ProjectEntity, entit inline fun EntityStore.getByFilter( entity: ProjectEntity, store: String, + offset: Int = 0, + size: Int = 10, jsonFilter: EntityStoreFilter ): List = - getByFilter(entity, store, jsonFilter, T::class) + getByFilter(entity, store, offset, size, jsonFilter, T::class) inline fun EntityStore.forEachByFilter( entity: ProjectEntity, diff --git a/ontrack-model/src/test/java/net/nemerosa/ontrack/model/pagination/PaginatedListExtensionsTest.kt b/ontrack-model/src/test/java/net/nemerosa/ontrack/model/pagination/PaginatedListExtensionsTest.kt new file mode 100644 index 00000000000..b9d541a6c3c --- /dev/null +++ b/ontrack-model/src/test/java/net/nemerosa/ontrack/model/pagination/PaginatedListExtensionsTest.kt @@ -0,0 +1,31 @@ +package net.nemerosa.ontrack.model.pagination + +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class PaginatedListExtensionsTest { + + @Test + fun `Spanning pagination with one seed`() { + val items = (1..3).map { "Item $it" } + val seed: (offset: Int, size: Int) -> Pair> = { offset, size: Int -> + 3 to items.drop(offset).take(size) + } + val pl = spanningPaginatedList( + offset = 0, + size = 2, + seeds = listOf(seed) + ) + assertEquals(3, pl.pageInfo.totalSize) + assertNotNull(pl.pageInfo.nextPage) + assertEquals( + listOf( + "Item 1", + "Item 2" + ), + pl.pageItems + ) + } + +} \ No newline at end of file diff --git a/ontrack-repository-impl/src/main/java/net/nemerosa/ontrack/repository/EntityStoreJdbcRepository.kt b/ontrack-repository-impl/src/main/java/net/nemerosa/ontrack/repository/EntityStoreJdbcRepository.kt index cc76f555830..e565aa660a2 100644 --- a/ontrack-repository-impl/src/main/java/net/nemerosa/ontrack/repository/EntityStoreJdbcRepository.kt +++ b/ontrack-repository-impl/src/main/java/net/nemerosa/ontrack/repository/EntityStoreJdbcRepository.kt @@ -108,6 +108,8 @@ class EntityStoreJdbcRepository(dataSource: DataSource) : AbstractJdbcRepository override fun getByFilter( entity: ProjectEntity, store: String, + offset: Int, + size: Int, filter: EntityStoreFilter, type: KClass ): List { @@ -115,10 +117,12 @@ class EntityStoreJdbcRepository(dataSource: DataSource) : AbstractJdbcRepository val criteria = StringBuilder() val params = mutableMapOf() buildCriteria(entity, store, filter, context, criteria, params) + params["offset"] = offset + params["size"] = size // Runs the query @Suppress("SqlSourceToSinkFlow") return namedParameterJdbcTemplate!!.query( - "SELECT DATA FROM ENTITY_STORE $context WHERE $criteria", + "SELECT DATA FROM ENTITY_STORE $context WHERE $criteria ORDER BY ID DESC OFFSET :offset LIMIT :size", params ) { rs: ResultSet, _ -> readJson(rs, "DATA").parseInto(type) diff --git a/ontrack-repository-impl/src/test/java/net/nemerosa/ontrack/repository/EntityStoreJdbcRepositoryIT.kt b/ontrack-repository-impl/src/test/java/net/nemerosa/ontrack/repository/EntityStoreJdbcRepositoryIT.kt index 899c1877567..8aeeb4953b7 100644 --- a/ontrack-repository-impl/src/test/java/net/nemerosa/ontrack/repository/EntityStoreJdbcRepositoryIT.kt +++ b/ontrack-repository-impl/src/test/java/net/nemerosa/ontrack/repository/EntityStoreJdbcRepositoryIT.kt @@ -88,7 +88,7 @@ class EntityStoreJdbcRepositoryIT : AbstractRepositoryTestSupport() { assertEquals( listOf(r1), - repository.getByFilter(branch, STORE, filter) + repository.getByFilter(branch, STORE, jsonFilter = filter) ) } diff --git a/ontrack-web-core/components/branches/promotionLevels/BranchPromotionLevelsView.js b/ontrack-web-core/components/branches/promotionLevels/BranchPromotionLevelsView.js index 4172b70c392..bd409114ebc 100644 --- a/ontrack-web-core/components/branches/promotionLevels/BranchPromotionLevelsView.js +++ b/ontrack-web-core/components/branches/promotionLevels/BranchPromotionLevelsView.js @@ -4,7 +4,7 @@ import Head from "next/head"; import {subBranchTitle} from "@components/common/Titles"; import {downToBranchBreadcrumbs} from "@components/common/Breadcrumbs"; import MainPage from "@components/layouts/MainPage"; -import {List, Skeleton, Space} from "antd"; +import {List, Skeleton, Space, Typography} from "antd"; import {gql} from "graphql-request"; import {CloseCommand} from "@components/common/Commands"; import {branchUri} from "@components/common/Links"; @@ -15,6 +15,7 @@ import {isAuthorized} from "@components/common/authorizations"; import PromotionLevelCreateCommand from "@components/promotionLevels/PromotionLevelCreateCommand"; import {EventsContext, useEventForRefresh} from "@components/common/EventsContext"; import SortableList, {SortableItem, SortableKnob} from "react-easy-sort"; +import EntitySubscriptions from "@components/extension/notifications/EntitySubscriptions"; export default function BranchPromotionLevelsView({id}) { @@ -148,6 +149,9 @@ export default function BranchPromotionLevelsView({id}) { } description={pl.description} /> +
+ +
)} diff --git a/ontrack-web-core/components/extension/notifications/EntitySubscriptions.js b/ontrack-web-core/components/extension/notifications/EntitySubscriptions.js new file mode 100644 index 00000000000..e31ea4eecd5 --- /dev/null +++ b/ontrack-web-core/components/extension/notifications/EntitySubscriptions.js @@ -0,0 +1,84 @@ +import {useGraphQLClient} from "@components/providers/ConnectionContextProvider"; +import {useEffect, useState} from "react"; +import {gql} from "graphql-request"; +import {List, Space, Tag, Typography} from "antd"; +import SubscriptionLink from "@components/extension/notifications/SubscriptionLink"; +import Link from "next/link"; +import SubscriptionsLink from "@components/extension/notifications/SubscriptionsLink"; + +/** + * This component displays a list of the subscriptions attached to a given entity. + * + * Each subscription can be navigated to and a general link allows to go the list + * of all subscriptions for this entity. + * + * @param type Project entity type + * @param id Project entity ID + */ +export default function EntitySubscriptions({type, id}) { + + const client = useGraphQLClient() + const [subscriptions, setSubscriptions] = useState([]) + const [pageInfo, setPageInfo] = useState({}) + + useEffect(() => { + if (client) { + client.request( + gql` + query GetEntitySubscriptions($entity: ProjectEntityIDInput!) { + eventSubscriptions(size: 10, filter: { + entity: $entity, + }) { + pageInfo { + totalSize + nextPage { + offset + } + } + pageItems { + name + channel + } + } + } + `, + {entity: {type, id}} + ).then(data => { + setSubscriptions(data.eventSubscriptions.pageItems) + setPageInfo(data.eventSubscriptions.pageInfo) + }) + } + }, [client, type, id]) + + return ( + <> + Subscriptions () + } + size="small" + renderItem={(subscription) => ( + + } + avatar={{subscription.channel}} + /> + + )} + footer={ +
+ { + pageInfo.nextPage && + + } +
+ } + /> + + ) +} \ No newline at end of file diff --git a/ontrack-web-core/components/extension/notifications/SubscriptionName.js b/ontrack-web-core/components/extension/notifications/SubscriptionName.js index a3beb9a24fd..7900a1df88d 100644 --- a/ontrack-web-core/components/extension/notifications/SubscriptionName.js +++ b/ontrack-web-core/components/extension/notifications/SubscriptionName.js @@ -48,11 +48,10 @@ export default function SubscriptionName({subscription, entity, managePermission <> {contextHolder} : , - }} + } : false} > {subscription.name} diff --git a/ontrack-web-core/components/extension/notifications/SubscriptionsLink.js b/ontrack-web-core/components/extension/notifications/SubscriptionsLink.js index 2ff20ea9042..5bfd612195e 100644 --- a/ontrack-web-core/components/extension/notifications/SubscriptionsLink.js +++ b/ontrack-web-core/components/extension/notifications/SubscriptionsLink.js @@ -8,6 +8,6 @@ export const subscriptionsLink = (entity) => { } } -export default function SubscriptionsLink({entity}) { - return Subscriptions +export default function SubscriptionsLink({entity, text = 'Subscriptions'}) { + return {text} } \ No newline at end of file