Skip to content

Commit

Permalink
#1275 List of issue service configurations
Browse files Browse the repository at this point in the history
  • Loading branch information
dcoraboeuf committed Jul 29, 2024
1 parent 3d37be7 commit d02e08c
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 120 deletions.
7 changes: 7 additions & 0 deletions ontrack-extension-issues/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ dependencies {
implementation("org.apache.commons:commons-lang3")
implementation("org.apache.commons:commons-text")
implementation("org.slf4j:slf4j-api")

testImplementation(project(":ontrack-test-utils"))
testImplementation(project(":ontrack-it-utils"))
testImplementation(project(path = ":ontrack-ui-graphql", configuration = "tests"))

testRuntimeOnly(project(":ontrack-service"))
testRuntimeOnly(project(":ontrack-repository-impl"))
}

val testJar by tasks.registering(Jar::class) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,34 @@
package net.nemerosa.ontrack.extension.issues;
package net.nemerosa.ontrack.extension.issues

import net.nemerosa.ontrack.extension.issues.model.ConfiguredIssueService;
import net.nemerosa.ontrack.extension.issues.model.IssueServiceConfigurationRepresentation;
import org.jetbrains.annotations.Nullable;
import net.nemerosa.ontrack.common.asOptional
import net.nemerosa.ontrack.extension.issues.model.ConfiguredIssueService
import net.nemerosa.ontrack.extension.issues.model.IssueServiceConfigurationRepresentation
import java.util.*

import java.util.Collection;
import java.util.List;
import java.util.Optional;

public interface IssueServiceRegistry {
interface IssueServiceRegistry {

/**
* Gets all the issue services
*/
Collection<IssueServiceExtension> getIssueServices();
val issueServices: Collection<IssueServiceExtension>

/**
* Gets an issue service by its ID. It may be present or not.
*/
fun findIssueServiceById(id: String): IssueServiceExtension?

/**
* Gets an issue service by its ID. It may be present or not.
*/
Optional<IssueServiceExtension> getOptionalIssueService(String id);
@Deprecated("Use findIssueServiceById", replaceWith = ReplaceWith("findIssueServiceById"))
fun getOptionalIssueService(id: String): Optional<IssueServiceExtension> =
findIssueServiceById(id).asOptional()

List<IssueServiceConfigurationRepresentation> getAvailableIssueServiceConfigurations();
val availableIssueServiceConfigurations: List<IssueServiceConfigurationRepresentation>

/**
* Gets the association between a service and a configuration, or <code>null</code>
* Gets the association between a service and a configuration, or `null`
* if neither service nor configuration can be found.
*/
@Nullable
ConfiguredIssueService getConfiguredIssueService(String issueServiceConfigurationIdentifier);
fun getConfiguredIssueService(issueServiceConfigurationIdentifier: String): ConfiguredIssueService?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package net.nemerosa.ontrack.extension.issues.graphql

import graphql.schema.GraphQLFieldDefinition
import net.nemerosa.ontrack.extension.issues.IssueServiceRegistry
import net.nemerosa.ontrack.extension.issues.model.IssueServiceConfigurationRepresentation
import net.nemerosa.ontrack.graphql.schema.GQLRootQuery
import net.nemerosa.ontrack.graphql.support.listType
import org.springframework.stereotype.Component

@Component
class GQLRootQueryIssueServiceConfigurations(
private val issueServiceConfigurationRepresentationGQLType: IssueServiceConfigurationRepresentationGQLType,
private val issueServiceRegistry: IssueServiceRegistry,
) : GQLRootQuery {

override fun getFieldDefinition(): GraphQLFieldDefinition =
GraphQLFieldDefinition.newFieldDefinition()
.name("issueServiceConfigurations")
.description("List of issue services")
.type(listType(issueServiceConfigurationRepresentationGQLType.typeRef))
.dataFetcher {
issueServiceRegistry.issueServices.flatMap { service ->
service.configurationList.map { config ->
IssueServiceConfigurationRepresentation.of(
issueServiceExtension = service,
issueServiceConfiguration = config,
)
}
}
}
.build()

}
Original file line number Diff line number Diff line change
@@ -1,56 +1,47 @@
package net.nemerosa.ontrack.extension.issues.model;
package net.nemerosa.ontrack.extension.issues.model

import lombok.Data;
import net.nemerosa.ontrack.extension.issues.IssueServiceExtension;
import net.nemerosa.ontrack.model.structure.Project;
import net.nemerosa.ontrack.model.support.MessageAnnotationUtils;
import net.nemerosa.ontrack.model.support.MessageAnnotator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;
import net.nemerosa.ontrack.extension.issues.IssueServiceExtension
import net.nemerosa.ontrack.extension.issues.model.IssueServiceConfigurationRepresentation.Companion.of
import net.nemerosa.ontrack.model.support.MessageAnnotationUtils
import net.nemerosa.ontrack.model.support.MessageAnnotator

/**
* Association between an {@link net.nemerosa.ontrack.extension.issues.IssueServiceExtension} and
* one of its {@link net.nemerosa.ontrack.extension.issues.model.IssueServiceConfiguration configuration}s.
* Association between an [net.nemerosa.ontrack.extension.issues.IssueServiceExtension] and
* one of its [configuration][net.nemerosa.ontrack.extension.issues.model.IssueServiceConfiguration]s.
*/
@Data
public class ConfiguredIssueService {

private final IssueServiceExtension issueServiceExtension;
private final IssueServiceConfiguration issueServiceConfiguration;

public String formatIssuesInMessage(String message) {
MessageAnnotator messageAnnotator = issueServiceExtension.getMessageAnnotator(issueServiceConfiguration);
if (messageAnnotator != null) {
return MessageAnnotationUtils.annotate(message, Collections.singletonList(messageAnnotator));
data class ConfiguredIssueService(
val issueServiceExtension: IssueServiceExtension,
val issueServiceConfiguration: IssueServiceConfiguration,
) {

fun formatIssuesInMessage(message: String?): String {
val messageAnnotator = issueServiceExtension.getMessageAnnotator(issueServiceConfiguration)
return if (messageAnnotator != null) {
MessageAnnotationUtils.annotate(message, listOf(messageAnnotator))
} else {
return "";
""
}
}

@Nullable
public Issue getIssue(String issueKey) {
return issueServiceExtension.getIssue(issueServiceConfiguration, issueKey);
fun getIssue(issueKey: String): Issue? {
return issueServiceExtension.getIssue(issueServiceConfiguration, issueKey)
}

public IssueServiceConfigurationRepresentation getIssueServiceConfigurationRepresentation() {
return IssueServiceConfigurationRepresentation.Companion.of(
issueServiceExtension,
issueServiceConfiguration
);
}
val issueServiceConfigurationRepresentation: IssueServiceConfigurationRepresentation
get() = of(
issueServiceExtension,
issueServiceConfiguration
)

public MessageAnnotator getMessageAnnotator() {
return issueServiceExtension.getMessageAnnotator(issueServiceConfiguration);
}
val messageAnnotator: MessageAnnotator?
get() = issueServiceExtension.getMessageAnnotator(issueServiceConfiguration)

public Set<String> extractIssueKeysFromMessage(String message) {
return issueServiceExtension.extractIssueKeysFromMessage(issueServiceConfiguration, message);
fun extractIssueKeysFromMessage(message: String): Set<String> {
return issueServiceExtension.extractIssueKeysFromMessage(issueServiceConfiguration, message)
}

public @Nullable String getIssueId(@NotNull String token) {
return issueServiceExtension.getIssueId(issueServiceConfiguration, token);
fun getIssueId(token: String): String? {
return issueServiceExtension.getIssueId(issueServiceConfiguration, token)
}

/**
Expand All @@ -59,12 +50,11 @@ public class ConfiguredIssueService {
* @param key Key ID
* @return Display key
*/
public String getDisplayKey(String key) {
return issueServiceExtension.getDisplayKey(issueServiceConfiguration, key);
fun getDisplayKey(key: String): String {
return issueServiceExtension.getDisplayKey(issueServiceConfiguration, key)
}

@NotNull
public String getMessageRegex(@NotNull Issue issue) {
return issueServiceExtension.getMessageRegex(issueServiceConfiguration, issue);
fun getMessageRegex(issue: Issue): String {
return issueServiceExtension.getMessageRegex(issueServiceConfiguration, issue)
}
}
Original file line number Diff line number Diff line change
@@ -1,77 +1,63 @@
package net.nemerosa.ontrack.extension.issues.support;
package net.nemerosa.ontrack.extension.issues.support

import net.nemerosa.ontrack.extension.api.ExtensionManager;
import net.nemerosa.ontrack.extension.issues.IssueServiceExtension;
import net.nemerosa.ontrack.extension.issues.IssueServiceRegistry;
import net.nemerosa.ontrack.extension.issues.model.ConfiguredIssueService;
import net.nemerosa.ontrack.extension.issues.model.IssueServiceConfiguration;
import net.nemerosa.ontrack.extension.issues.model.IssueServiceConfigurationIdentifier;
import net.nemerosa.ontrack.extension.issues.model.IssueServiceConfigurationRepresentation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;
import net.nemerosa.ontrack.extension.api.ExtensionManager
import net.nemerosa.ontrack.extension.issues.IssueServiceExtension
import net.nemerosa.ontrack.extension.issues.IssueServiceRegistry
import net.nemerosa.ontrack.extension.issues.model.ConfiguredIssueService
import net.nemerosa.ontrack.extension.issues.model.IssueServiceConfigurationIdentifier.Companion.parse
import net.nemerosa.ontrack.extension.issues.model.IssueServiceConfigurationRepresentation
import net.nemerosa.ontrack.extension.issues.model.IssueServiceConfigurationRepresentation.Companion.of
import org.springframework.stereotype.Service

@Service
public class IssueServiceRegistryImpl implements IssueServiceRegistry {

private final ExtensionManager extensionManager;

@Autowired
public IssueServiceRegistryImpl(ExtensionManager extensionManager) {
this.extensionManager = extensionManager;
}

protected Map<String, IssueServiceExtension> getIssueServiceExtensionMap() {
return extensionManager.getExtensions(IssueServiceExtension.class).stream()
.collect(Collectors.toMap(
IssueServiceExtension::getId,
x -> x
)
);
}
class IssueServiceRegistryImpl(
private val extensionManager: ExtensionManager
) : IssueServiceRegistry {

@Override
public Collection<IssueServiceExtension> getIssueServices() {
return getIssueServiceExtensionMap().values();
private val issueServiceExtensionMap: Map<String, IssueServiceExtension> by lazy {
extensionManager.getExtensions(IssueServiceExtension::class.java).associateBy { it.id }
}

@Override
public Optional<IssueServiceExtension> getOptionalIssueService(String id) {
return Optional.ofNullable(getIssueServiceExtensionMap().get(id));
}
override val issueServices: Collection<IssueServiceExtension>
get() {
return issueServiceExtensionMap.values
}

@Override
public List<IssueServiceConfigurationRepresentation> getAvailableIssueServiceConfigurations() {
List<IssueServiceConfigurationRepresentation> issueServiceConfigurationRepresentations = new ArrayList<>();
for (IssueServiceExtension issueServiceExtension : getIssueServiceExtensionMap().values()) {
List<? extends IssueServiceConfiguration> configurationList = issueServiceExtension.getConfigurationList();
for (IssueServiceConfiguration issueServiceConfiguration : configurationList) {
issueServiceConfigurationRepresentations.add(
IssueServiceConfigurationRepresentation.Companion.of(
issueServiceExtension,
issueServiceConfiguration
override fun findIssueServiceById(id: String): IssueServiceExtension? = issueServiceExtensionMap[id]

override val availableIssueServiceConfigurations: List<IssueServiceConfigurationRepresentation>
get() {
val issueServiceConfigurationRepresentations = mutableListOf<IssueServiceConfigurationRepresentation>()
for (issueServiceExtension in issueServiceExtensionMap.values) {
val configurationList = issueServiceExtension.configurationList
for (issueServiceConfiguration in configurationList) {
issueServiceConfigurationRepresentations.add(
of(
issueServiceExtension,
issueServiceConfiguration
)
);
)
}
}
return issueServiceConfigurationRepresentations
}
return issueServiceConfigurationRepresentations;
}

@Override
public ConfiguredIssueService getConfiguredIssueService(String issueServiceConfigurationIdentifier) {
override fun getConfiguredIssueService(issueServiceConfigurationIdentifier: String): ConfiguredIssueService? {
// Parsing
IssueServiceConfigurationIdentifier identifier = IssueServiceConfigurationIdentifier.parse(issueServiceConfigurationIdentifier);
val identifier = parse(issueServiceConfigurationIdentifier)
if (identifier != null) {
Optional<IssueServiceExtension> issueService = getOptionalIssueService(identifier.getServiceId());
return issueService.map(issueServiceExtension -> new ConfiguredIssueService(
issueServiceExtension,
issueServiceExtension.getConfigurationByName(identifier.getName())
)).orElse(null);
val issueService = findIssueServiceById(identifier.serviceId)
return issueService?.run {
val config = getConfigurationByName(identifier.name)
config?.let {
ConfiguredIssueService(
this,
it
)
}
}
} else {
return null;
return null
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package net.nemerosa.ontrack.extension.issues.graphql

import net.nemerosa.ontrack.graphql.AbstractQLKTITSupport
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

class GQLRootQueryIssueServiceConfigurationsIT : AbstractQLKTITSupport() {

@Test
fun `Getting the issue service configurations with GraphQL`() {
asAdmin {
run(
"""
{
issueServiceConfigurations {
id
name
serviceId
}
}
"""
) { data ->
val configs = data.path("issueServiceConfigurations")
val testConfigs = configs.filter { it.path("serviceId").asText() == "test" }
assertEquals(1, testConfigs.size)
val testConfig = testConfigs.first()
assertEquals("default (Test issues)", testConfig?.path("name")?.asText())
assertEquals("test//default", testConfig?.path("id")?.asText())
}
}
}

}
32 changes: 32 additions & 0 deletions ontrack-web-core/components/extension/issues/SelectIssueService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {Select, Space, Typography} from "antd";
import {useEffect, useState} from "react";

export default function SelectIssueService({value, onChange, self}) {

const [options, setOptions] = useState([])
const [selfAdded, setSelfAdded] = useState(false)

useEffect(() => {
if (self && !selfAdded) {
setOptions(items => [...items, {
value: "self",
label: <Space>
<Typography.Text>{self}</Typography.Text>
<Typography.Text type="secondary">[self]</Typography.Text>
</Space>,
}])
setSelfAdded(true)
}
}, [self, selfAdded])

return (
<>
<Select
options={options}
value={value}
onChange={onChange}
allowClear={true}
/>
</>
)
}
Loading

0 comments on commit d02e08c

Please sign in to comment.