Skip to content

Commit

Permalink
#1275 Adding properties
Browse files Browse the repository at this point in the history
  • Loading branch information
dcoraboeuf committed Jun 2, 2024
1 parent 910a77d commit e551887
Show file tree
Hide file tree
Showing 19 changed files with 531 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,10 @@ val ProjectEntityType.varName: String
*/
val ProjectEntityType.typeName: String
get() = varName.replaceFirstChar { it.titlecase() }

/**
* Given an entity type & ID, looks for it.
*/
@Suppress("UNCHECKED_CAST")
fun <E : ProjectEntity> StructureService.findEntity(type: ProjectEntityType, id: Int): E? =
type.getFindEntityFn(this).apply(ID.of(id)) as? E?
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package net.nemerosa.ontrack.graphql.schema

import net.nemerosa.ontrack.model.exceptions.NotFoundException
import net.nemerosa.ontrack.model.structure.ProjectEntityType

class EntityNotFoundByIdException(
type: ProjectEntityType,
id: Int,
) : NotFoundException(
"""Cannot find ${type.displayName} with ID $id."""
)
Original file line number Diff line number Diff line change
Expand Up @@ -14,55 +14,59 @@ import org.springframework.stereotype.Component

@Component
class GQLProjectEntityPropertyListFieldContributor(
private val propertyService: PropertyService,
private val property: GQLTypeProperty,
private val propertyService: PropertyService,
private val property: GQLTypeProperty,
) : GQLProjectEntityFieldContributor {

override fun getFields(projectEntityClass: Class<out ProjectEntity>, projectEntityType: ProjectEntityType): List<GraphQLFieldDefinition>? {
override fun getFields(
projectEntityClass: Class<out ProjectEntity>,
projectEntityType: ProjectEntityType
): List<GraphQLFieldDefinition>? {
return listOf<GraphQLFieldDefinition>(
GraphQLFieldDefinition.newFieldDefinition()
.name("properties")
.description("List of properties")
.argument(
GraphQLArgument.newArgument()
.name("type")
.description("Fully qualified name of the property type")
.type(Scalars.GraphQLString)
.build()
)
.argument(
GraphQLArgument.newArgument()
.name("hasValue")
.description("Keeps properties having a value")
.type(Scalars.GraphQLBoolean)
.defaultValue(false)
.build()
)
.type(listType(property.typeRef))
.dataFetcher(projectEntityPropertiesDataFetcher(projectEntityClass))
GraphQLFieldDefinition.newFieldDefinition()
.name("properties")
.description("List of properties")
.argument(
GraphQLArgument.newArgument()
.name("type")
.description("Fully qualified name of the property type")
.type(Scalars.GraphQLString)
.build()
)
.argument(
GraphQLArgument.newArgument()
.name("hasValue")
.description("Keeps properties having a value")
.type(Scalars.GraphQLBoolean)
.defaultValue(false)
.build()
)
.type(listType(property.typeRef))
.dataFetcher(projectEntityPropertiesDataFetcher(projectEntityClass))
.build()
)
}

private fun projectEntityPropertiesDataFetcher(projectEntityClass: Class<out ProjectEntity>) =
DataFetcher { environment: DataFetchingEnvironment ->
val o = environment.getSource<Any>()
if (projectEntityClass.isInstance(o)) {
// Filters
val typeFilter: String? = environment.getArgument("type")
val hasValue: Boolean = environment.getArgument<Boolean?>("hasValue") ?: false
// Gets the raw list
propertyService.getProperties(o as ProjectEntity)
.filter { property: Property<*> ->
typeFilter?.let {
it == property.typeDescriptor.typeName
} ?: true
}
.filter { property: Property<*> ->
!hasValue || !property.isEmpty
}
} else {
return@DataFetcher null
}
DataFetcher { environment: DataFetchingEnvironment ->
val o = environment.getSource<Any>()
if (projectEntityClass.isInstance(o)) {
// Filters
val typeFilter: String? = environment.getArgument("type")
val hasValue: Boolean = environment.getArgument<Boolean?>("hasValue") ?: false
// Gets the raw list
propertyService.getProperties(o as ProjectEntity)
.filter { property: Property<*> ->
typeFilter?.let {
it == property.typeDescriptor.typeName
} ?: true
}
.filter { property: Property<*> ->
!hasValue || !property.isEmpty
}
.sortedBy { it.type.name }
} else {
return@DataFetcher null
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,32 @@ class PropertiesMutations(
}

override val mutations: List<Mutation>
get() = genericMutations + specificMutations
get() = listOf(genericMutation) + genericMutations + specificMutations

private val genericMutation: Mutation
get() = simpleMutation(
name = "setGenericProperty",
description = "Generic update for a property and an entity",
input = SetGenericPropertyInput::class,
outputName = "property",
outputDescription = "Updated property",
outputType = Property::class
) { input ->
// Gets the entity
val entity = structureService.findEntity<ProjectEntity>(input.entityType, input.entityId)
?: throw EntityNotFoundByIdException(input.entityType, input.entityId)
// Saving the property
propertyService.editProperty(
entity = entity,
propertyTypeName = input.type,
data = input.value,
)
// Returning the property
propertyService.getProperty<Any>(
entity = entity,
propertyTypeName = input.type,
)
}

private fun createGenericMutationById(type: ProjectEntityType) = object : Mutation {

Expand Down Expand Up @@ -82,6 +107,7 @@ class PropertiesMutations(
providers.size > 1 -> {
throw MultiplePropertyMutationProviderException(propertyType)
}

providers.size == 1 -> {
@Suppress("UNCHECKED_CAST")
val provider = providers.first() as PropertyMutationProvider<T>
Expand All @@ -95,6 +121,7 @@ class PropertiesMutations(
)
}
}

else -> {
return emptyList()
}
Expand Down Expand Up @@ -145,9 +172,10 @@ class PropertiesMutations(
override val description: String =
"Set the ${propertyType.name.decapitalize()} property on a ${type.displayName} identified by name."

override fun inputFields(dictionary: MutableSet<GraphQLType>): List<GraphQLInputObjectField> = type.names.map {
name(it)
} + provider.inputFields
override fun inputFields(dictionary: MutableSet<GraphQLType>): List<GraphQLInputObjectField> =
type.names.map {
name(it)
} + provider.inputFields

override val outputFields: List<GraphQLFieldDefinition> = listOf(
projectEntityTypeField(type)
Expand Down Expand Up @@ -182,9 +210,10 @@ class PropertiesMutations(
override val description: String =
"Deletes the ${propertyType.name.decapitalize()} property on a ${type.displayName} identified by name."

override fun inputFields(dictionary: MutableSet<GraphQLType>): List<GraphQLInputObjectField> = type.names.map {
name(it)
}
override fun inputFields(dictionary: MutableSet<GraphQLType>): List<GraphQLInputObjectField> =
type.names.map {
name(it)
}

override val outputFields: List<GraphQLFieldDefinition> = listOf(
projectEntityTypeField(type)
Expand Down Expand Up @@ -323,6 +352,13 @@ class PropertiesMutations(
.type(GraphQLTypeReference(type.typeName))
.build()

data class SetGenericPropertyInput(
val entityType: ProjectEntityType,
val entityId: Int,
val type: String,
val value: JsonNode,
)

companion object {
const val ARG_ID = "id"
const val ARG_PROPERTY_TYPE = "property"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package net.nemerosa.ontrack.graphql.schema

import net.nemerosa.ontrack.extension.api.support.TestPropertyType
import net.nemerosa.ontrack.graphql.AbstractQLKTITSupport
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull

class PropertiesMutationsIT : AbstractQLKTITSupport() {

@Test
fun `Setting a property using the generic mutation`() {
asAdmin {
project {
run(
"""
mutation {
setGenericProperty(input: {
entityType: PROJECT,
entityId: $id,
type: "${TestPropertyType::class.java.name}",
value: {
configuration: "test-config",
value: "test-value"
}
}) {
errors {
message
}
}
}
"""
)
val property = propertyService.getPropertyValue(this, TestPropertyType::class.java)
assertNotNull(property) {
assertEquals("test-config", it.configuration.name)
assertEquals("test-value", it.value)
}
}
}
}

}
4 changes: 2 additions & 2 deletions ontrack-web-core/components/branches/BranchInfoViewDrawer.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export default function BranchInfoViewDrawer({branch, loadingBranch}) {
width="40%"
>
<PropertiesSection
entity={branch}
loading={loadingBranch}
entityType="BRANCH"
entityId={branch.id}
/>
<InformationSection
entity={branch}
Expand Down
4 changes: 2 additions & 2 deletions ontrack-web-core/components/builds/BuildInfoViewDrawer.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export default function BuildInfoViewDrawer({build, loading}) {
width="33%"
>
<PropertiesSection
entity={build}
loading={loading}
entityType="BUILD"
entityId={build.id}
/>
<InformationSection
entity={build}
Expand Down
4 changes: 2 additions & 2 deletions ontrack-web-core/components/common/ListSection.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import {List, Skeleton, Space, Typography} from "antd";
import {FaCog} from "react-icons/fa";

export default function ListSection({title, icon, loading, items, renderItem}) {
export default function ListSection({title, extraTitle, icon, loading, items}) {
return (
<>
<Space direction="vertical" className="ot-line">
<Typography.Title level={3}>
<Space>
{icon}
{title}
{extraTitle}
</Space>
</Typography.Title>
<Skeleton active loading={loading}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {Button} from "antd";
import {FaPlus} from "react-icons/fa";
import PropertyDialog, {usePropertyDialog} from "@components/core/model/properties/PropertyDialog";

export default function PropertyAddButton({entityType, entityId, propertyList}) {

const dialog = usePropertyDialog({})

const startDialog = () => {
dialog.start({entityType, entityId, propertyList})
}

return (
<>
<Button size="small" icon={<FaPlus/>} title="Add a property" onClick={startDialog}/>
<PropertyDialog dialog={dialog}/>
</>
)
}
Loading

0 comments on commit e551887

Please sign in to comment.