diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index c8806953ae..f52d60bc4e 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -4,6 +4,8 @@ === Shapes +- Add support for a query view + === Architectural decision records @@ -22,6 +24,9 @@ === New Features +- https://github.com/eclipse-sirius/sirius-web/issues/4346[#4346] [query] Add support for a query view. +Specifiers can contribute dedicated AQL services for this feature using implementations of `IInterpreterJavaServiceProvider`. + === Improvements diff --git a/doc/iterations/2025.2/add_support_for_a_query_view.adoc b/doc/iterations/2025.2/add_support_for_a_query_view.adoc new file mode 100644 index 0000000000..32e7c1f49d --- /dev/null +++ b/doc/iterations/2025.2/add_support_for_a_query_view.adoc @@ -0,0 +1,43 @@ += Add support for a query view + +== Problem + +Sirius Web users need a query view to execute some expressions and view the results. + +== Key Result + +It shall be possible to run a query against the editing context and view the result along with its type (for example to distinguish one element from a list containing only one element). + +The current selection shall be usable as an entry point of the query. +An extension point on the backend shall be available to add custom Java services too. +Various interpreter shall be supported on the backend to let specifiers add custom interpreters. + +This new view shall be contributed using an extension point in order to be removal by downstream projects which may not need it. + +The result of the expression will only be computed when the user will ask for it. +This view will not be a synchronized representation. + +=== Scenario + +==== A user wants to query some models + +- The user open the interpreter view +- The user click on an element in the explorer or another representation like a diagram +- They start typing an expression in the interpreter view and click on a button to perform the query +- The result appears in the result viewer underneath + + +=== Breadboarding + +- A view on the right of the workbench with a textarea to enter an expression and a viewer underneath to display the result. + + +=== Cutting backs + +- Content assist in the interpreter. + + +== Rabbit holes + + +== No-gos \ No newline at end of file diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/controllers/MutationEvaluateExpressionDataFetcher.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/controllers/MutationEvaluateExpressionDataFetcher.java new file mode 100644 index 0000000000..91ef4a7296 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/controllers/MutationEvaluateExpressionDataFetcher.java @@ -0,0 +1,57 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.views.query.controllers; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.sirius.components.annotations.spring.graphql.MutationDataFetcher; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.graphql.api.IDataFetcherWithFieldCoordinates; +import org.eclipse.sirius.components.graphql.api.IEditingContextDispatcher; +import org.eclipse.sirius.components.graphql.api.IExceptionWrapper; +import org.eclipse.sirius.web.application.views.query.dto.EvaluateExpressionInput; + +import graphql.schema.DataFetchingEnvironment; + +/** + * The data fetcher used to evaluate an expression for the interpreter view. + * + * @author sbegaudeau + */ +@MutationDataFetcher(type = "Mutation", field = "evaluateExpression") +public class MutationEvaluateExpressionDataFetcher implements IDataFetcherWithFieldCoordinates> { + + private static final String INPUT_ARGUMENT = "input"; + + private final ObjectMapper objectMapper; + + private final IExceptionWrapper exceptionWrapper; + + private final IEditingContextDispatcher editingContextDispatcher; + + public MutationEvaluateExpressionDataFetcher(ObjectMapper objectMapper, IExceptionWrapper exceptionWrapper, IEditingContextDispatcher editingContextDispatcher) { + this.objectMapper = Objects.requireNonNull(objectMapper); + this.exceptionWrapper = Objects.requireNonNull(exceptionWrapper); + this.editingContextDispatcher = Objects.requireNonNull(editingContextDispatcher); + } + + @Override + public CompletableFuture get(DataFetchingEnvironment environment) throws Exception { + Object argument = environment.getArgument(INPUT_ARGUMENT); + var input = this.objectMapper.convertValue(argument, EvaluateExpressionInput.class); + return this.exceptionWrapper.wrapMono(() -> this.editingContextDispatcher.dispatchMutation(input.editingContextId(), input), input).toFuture(); + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/BooleanExpressionResult.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/BooleanExpressionResult.java new file mode 100644 index 0000000000..8479bb4745 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/BooleanExpressionResult.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.views.query.dto; + +/** + * Used to return a boolean value. + * + * @author sbegaudeau + */ +public record BooleanExpressionResult(boolean value) implements IEvaluateExpressionResult { +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/EvaluateExpressionInput.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/EvaluateExpressionInput.java new file mode 100644 index 0000000000..adf9fbf65c --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/EvaluateExpressionInput.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.views.query.dto; + +import java.util.UUID; + +import org.eclipse.sirius.components.core.api.IInput; + +/** + * Used to execute an expression. + * + * @author sbegaudeau + */ +public record EvaluateExpressionInput(UUID id, String editingContextId, String expression) implements IInput { +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/EvaluateExpressionSuccessPayload.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/EvaluateExpressionSuccessPayload.java new file mode 100644 index 0000000000..932068cb46 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/EvaluateExpressionSuccessPayload.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.views.query.dto; + +import java.util.UUID; + +import org.eclipse.sirius.components.core.api.IPayload; + +import jakarta.validation.constraints.NotNull; + +/** + * Used to indicate that the expression has been successfully evaluated. + * + * @author sbegaudeau + */ +public record EvaluateExpressionSuccessPayload( + @NotNull UUID id, + @NotNull IEvaluateExpressionResult result) implements IPayload { +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/IEvaluateExpressionResult.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/IEvaluateExpressionResult.java new file mode 100644 index 0000000000..35320d4a0b --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/IEvaluateExpressionResult.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.views.query.dto; + +/** + * Interface to be implemented by all the evaluation results. + * + * @author sbegaudeau + */ +public interface IEvaluateExpressionResult { +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/IntExpressionResult.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/IntExpressionResult.java new file mode 100644 index 0000000000..0860f374ea --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/IntExpressionResult.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.views.query.dto; + +/** + * Used to return an integer value. + * + * @author sbegaudeau + */ +public record IntExpressionResult(int value) implements IEvaluateExpressionResult { +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/ObjectExpressionResult.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/ObjectExpressionResult.java new file mode 100644 index 0000000000..e80fbb4901 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/ObjectExpressionResult.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.views.query.dto; + +/** + * Used to return a single object. + * + * @author sbegaudeau + */ +public record ObjectExpressionResult(Object value) implements IEvaluateExpressionResult { +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/ObjectsExpressionResult.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/ObjectsExpressionResult.java new file mode 100644 index 0000000000..8cf8f6f15e --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/ObjectsExpressionResult.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.views.query.dto; + +import java.util.List; + +/** + * Used to return a list of objects. + * + * @author sbegaudeau + */ +public record ObjectsExpressionResult(List value) implements IEvaluateExpressionResult { +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/StringExpressionResult.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/StringExpressionResult.java new file mode 100644 index 0000000000..ca0437acb7 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/StringExpressionResult.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.views.query.dto; + +/** + * Used to return a string based value. + * + * @author sbegaudeau + */ +public record StringExpressionResult(String value) implements IEvaluateExpressionResult { +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/VoidExpressionResult.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/VoidExpressionResult.java new file mode 100644 index 0000000000..7905d0dd3f --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/dto/VoidExpressionResult.java @@ -0,0 +1,21 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.views.query.dto; + +/** + * Used to indicate the lack of result. + * + * @author sbegaudeau + */ +public record VoidExpressionResult() implements IEvaluateExpressionResult { +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/services/AQLInterpreterProvider.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/services/AQLInterpreterProvider.java new file mode 100644 index 0000000000..43992fadbf --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/services/AQLInterpreterProvider.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.views.query.services; + +import java.util.List; +import java.util.Objects; + +import org.eclipse.emf.ecore.EPackage; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.emf.query.EditingContextServices; +import org.eclipse.sirius.components.emf.services.api.IEMFEditingContext; +import org.eclipse.sirius.components.interpreter.AQLInterpreter; +import org.eclipse.sirius.web.application.views.query.services.api.IAQLInterpreterProvider; +import org.eclipse.sirius.web.application.views.query.services.api.IInterpreterJavaServiceProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Service; + +/** + * Used to provide the AQL interpreter. + * + * @author sbegaudeau + */ +@Service +public class AQLInterpreterProvider implements IAQLInterpreterProvider { + + private final List interpreterJavaServiceProviders; + + private final ApplicationContext applicationContext; + + private final Logger logger = LoggerFactory.getLogger(AQLInterpreterProvider.class); + + public AQLInterpreterProvider(List interpreterJavaServiceProviders, ApplicationContext applicationContext) { + this.interpreterJavaServiceProviders = Objects.requireNonNull(interpreterJavaServiceProviders); + this.applicationContext = Objects.requireNonNull(applicationContext); + } + + @Override + public AQLInterpreter getInterpreter(IEditingContext editingContext) { + var ePackages = this.getEPackages(editingContext); + var services = this.getServices(editingContext); + return new AQLInterpreter(List.of(EditingContextServices.class), services, ePackages); + } + + private List getEPackages(IEditingContext editingContext) { + if (editingContext instanceof IEMFEditingContext emfEditingContext) { + EPackage.Registry packageRegistry = emfEditingContext.getDomain().getResourceSet().getPackageRegistry(); + return packageRegistry.values().stream() + .filter(EPackage.class::isInstance) + .map(EPackage.class::cast) + .toList(); + } + return List.of(); + } + + private List getServices(IEditingContext editingContext) { + AutowireCapableBeanFactory beanFactory = this.applicationContext.getAutowireCapableBeanFactory(); + return this.interpreterJavaServiceProviders.stream() + .flatMap(provider -> provider.getServiceClasses(editingContext).stream()) + .map(serviceClass -> { + try { + return beanFactory.createBean(serviceClass); + } catch (BeansException beansException) { + this.logger.warn("Error while trying to instantiate Java service class " + serviceClass.getName(), beansException); + return null; + } + }) + .filter(Objects::nonNull) + .map(Object.class::cast) + .toList(); + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/services/EvaluateExpressionEventHandler.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/services/EvaluateExpressionEventHandler.java new file mode 100644 index 0000000000..29e6cae63c --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/services/EvaluateExpressionEventHandler.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.views.query.services; + +import java.util.Collection; +import java.util.Objects; +import java.util.UUID; + +import org.eclipse.sirius.components.collaborative.api.ChangeDescription; +import org.eclipse.sirius.components.collaborative.api.ChangeKind; +import org.eclipse.sirius.components.collaborative.api.IEditingContextEventHandler; +import org.eclipse.sirius.components.collaborative.api.Monitoring; +import org.eclipse.sirius.components.core.api.ErrorPayload; +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.core.api.IInput; +import org.eclipse.sirius.components.core.api.IPayload; +import org.eclipse.sirius.components.interpreter.Result; +import org.eclipse.sirius.components.interpreter.Status; +import org.eclipse.sirius.components.representations.VariableManager; +import org.eclipse.sirius.web.application.views.query.dto.BooleanExpressionResult; +import org.eclipse.sirius.web.application.views.query.dto.EvaluateExpressionInput; +import org.eclipse.sirius.web.application.views.query.dto.EvaluateExpressionSuccessPayload; +import org.eclipse.sirius.web.application.views.query.dto.IntExpressionResult; +import org.eclipse.sirius.web.application.views.query.dto.ObjectExpressionResult; +import org.eclipse.sirius.web.application.views.query.dto.ObjectsExpressionResult; +import org.eclipse.sirius.web.application.views.query.dto.StringExpressionResult; +import org.eclipse.sirius.web.application.views.query.dto.VoidExpressionResult; +import org.eclipse.sirius.web.application.views.query.services.api.IAQLInterpreterProvider; +import org.eclipse.sirius.web.domain.services.api.IMessageService; +import org.springframework.stereotype.Service; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import reactor.core.publisher.Sinks; + +/** + * Used to evaluate an expression for the interpreter view. + * + * @author sbegaudeau + */ +@Service +public class EvaluateExpressionEventHandler implements IEditingContextEventHandler { + + private final IAQLInterpreterProvider aqlInterpreterProvider; + + private final IMessageService messageService; + + private final Counter counter; + + public EvaluateExpressionEventHandler(IAQLInterpreterProvider aqlInterpreterProvider, IMessageService messageService, MeterRegistry meterRegistry) { + this.aqlInterpreterProvider = Objects.requireNonNull(aqlInterpreterProvider); + this.messageService = Objects.requireNonNull(messageService); + this.counter = Counter.builder(Monitoring.EVENT_HANDLER) + .tag(Monitoring.NAME, this.getClass().getSimpleName()) + .register(meterRegistry); + } + + @Override + public boolean canHandle(IEditingContext editingContext, IInput input) { + return input instanceof EvaluateExpressionInput; + } + + @Override + public void handle(Sinks.One payloadSink, Sinks.Many changeDescriptionSink, IEditingContext editingContext, IInput input) { + this.counter.increment(); + + String message = this.messageService.invalidInput(input.getClass().getSimpleName(), EvaluateExpressionInput.class.getSimpleName()); + IPayload payload = new ErrorPayload(input.id(), message); + ChangeDescription changeDescription = new ChangeDescription(ChangeKind.NOTHING, editingContext.getId(), input); + + if (input instanceof EvaluateExpressionInput evaluateExpressionInput) { + var interpreter = this.aqlInterpreterProvider.getInterpreter(editingContext); + + var variableManager = new VariableManager(); + variableManager.put(IEditingContext.EDITING_CONTEXT, editingContext); + var evaluationResult = interpreter.evaluateExpression(variableManager.getVariables(), evaluateExpressionInput.expression()); + + payload = this.toPayload(input.id(), evaluationResult); + changeDescription = new ChangeDescription(ChangeKind.SEMANTIC_CHANGE, editingContext.getId(), input); + } + + payloadSink.tryEmitValue(payload); + changeDescriptionSink.tryEmitNext(changeDescription); + } + + private IPayload toPayload(UUID inputId, Result evaluationResult) { + IPayload payload = null; + if (evaluationResult.getStatus() == Status.ERROR) { + payload = new ErrorPayload(inputId, this.messageService.unexpectedError()); + } else { + var optionalObject = evaluationResult.asObject(); + if (optionalObject.isPresent()) { + var object = optionalObject.get(); + if (object instanceof Collection collectionValue) { + var value = collectionValue.stream().map(Object.class::cast).toList(); + payload = new EvaluateExpressionSuccessPayload(inputId, new ObjectsExpressionResult(value)); + } else if (object instanceof Boolean booleanValue) { + payload = new EvaluateExpressionSuccessPayload(inputId, new BooleanExpressionResult(booleanValue)); + } else if (object instanceof String stringValue) { + payload = new EvaluateExpressionSuccessPayload(inputId, new StringExpressionResult(stringValue)); + } else if (object instanceof Integer intValue) { + payload = new EvaluateExpressionSuccessPayload(inputId, new IntExpressionResult(intValue)); + } else { + payload = new EvaluateExpressionSuccessPayload(inputId, new ObjectExpressionResult(object)); + } + } else { + payload = new EvaluateExpressionSuccessPayload(inputId, new VoidExpressionResult()); + } + } + return payload; + } +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/services/api/IAQLInterpreterProvider.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/services/api/IAQLInterpreterProvider.java new file mode 100644 index 0000000000..6363882b69 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/services/api/IAQLInterpreterProvider.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.views.query.services.api; + +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.components.interpreter.AQLInterpreter; + +/** + * Used to receive a properly initialized instance of the AQL interpreter. + * + * @author sbegaudeau + */ +public interface IAQLInterpreterProvider { + AQLInterpreter getInterpreter(IEditingContext editingContext); +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/services/api/IInterpreterJavaServiceProvider.java b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/services/api/IInterpreterJavaServiceProvider.java new file mode 100644 index 0000000000..0253252be7 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/java/org/eclipse/sirius/web/application/views/query/services/api/IInterpreterJavaServiceProvider.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.views.query.services.api; + +import java.util.List; + +import org.eclipse.sirius.components.core.api.IEditingContext; + +/** + * Used to provide Java services for the interpreter view. + * + * @author sbegaudeau + */ +public interface IInterpreterJavaServiceProvider { + List> getServiceClasses(IEditingContext editingContext); +} diff --git a/packages/sirius-web/backend/sirius-web-application/src/main/resources/schema/query.graphqls b/packages/sirius-web/backend/sirius-web-application/src/main/resources/schema/query.graphqls new file mode 100644 index 0000000000..92faf2ce21 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-application/src/main/resources/schema/query.graphqls @@ -0,0 +1,42 @@ +extend type Mutation { + evaluateExpression(input: EvaluateExpressionInput!): EvaluateExpressionPayload! +} + +input EvaluateExpressionInput { + id: ID! + editingContextId: ID! + expression: String! +} + +union EvaluateExpressionPayload = ErrorPayload | EvaluateExpressionSuccessPayload + +type EvaluateExpressionSuccessPayload { + id: ID! + result: EvaluateExpressionResult! +} + +union EvaluateExpressionResult = ObjectExpressionResult | ObjectsExpressionResult | BooleanExpressionResult | IntExpressionResult | StringExpressionResult | VoidExpressionResult + +type ObjectExpressionResult { + value: Object! +} + +type ObjectsExpressionResult { + value: [Object!]! +} + +type BooleanExpressionResult { + value: Boolean! +} + +type IntExpressionResult { + value: Int! +} + +type StringExpressionResult { + value: String! +} + +type VoidExpressionResult { + value: String! +} \ No newline at end of file diff --git a/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/graphql/EvaluateExpressionMutationRunner.java b/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/graphql/EvaluateExpressionMutationRunner.java new file mode 100644 index 0000000000..bca171675f --- /dev/null +++ b/packages/sirius-web/backend/sirius-web-tests/src/main/java/org/eclipse/sirius/web/tests/graphql/EvaluateExpressionMutationRunner.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.tests.graphql; + +import java.util.Objects; + +import org.eclipse.sirius.components.graphql.tests.api.IGraphQLRequestor; +import org.eclipse.sirius.components.graphql.tests.api.IMutationRunner; +import org.eclipse.sirius.web.application.views.query.dto.EvaluateExpressionInput; +import org.springframework.stereotype.Service; + +/** + * Used to evaluate expressions. + * + * @author sbegaudeau + */ +@Service +public class EvaluateExpressionMutationRunner implements IMutationRunner { + + private static final String EVALUATE_EXPRESSION_MUTATION = """ + mutation evaluateExpression($input: EvaluateExpressionInput!) { + evaluateExpression(input: $input) { + __typename + ... on EvaluateExpressionSuccessPayload { + result { + __typename + ... on ObjectsExpressionResult { + objectsValue: value { + id + label + kind + } + } + ... on ObjectExpressionResult { + objectValue: value { + id + label + kind + } + } + ... on BooleanExpressionResult { + booleanValue: value + } + ... on IntExpressionResult { + intValue: value + } + ... on StringExpressionResult { + stringValue: value + } + } + } + ... on ErrorPayload { + messages { + level + body + } + } + } + } + """; + + private final IGraphQLRequestor graphQLRequestor; + + public EvaluateExpressionMutationRunner(IGraphQLRequestor graphQLRequestor) { + this.graphQLRequestor = Objects.requireNonNull(graphQLRequestor); + } + + @Override + public String run(EvaluateExpressionInput input) { + return this.graphQLRequestor.execute(EVALUATE_EXPRESSION_MUTATION, input); + } +} diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/query/QueryIntegrationTests.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/query/QueryIntegrationTests.java new file mode 100644 index 0000000000..8433baca64 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/application/controllers/query/QueryIntegrationTests.java @@ -0,0 +1,175 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.application.controllers.query; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.jayway.jsonpath.JsonPath; + +import java.util.List; +import java.util.UUID; + +import org.eclipse.sirius.web.AbstractIntegrationTests; +import org.eclipse.sirius.web.application.views.query.dto.BooleanExpressionResult; +import org.eclipse.sirius.web.application.views.query.dto.EvaluateExpressionInput; +import org.eclipse.sirius.web.application.views.query.dto.EvaluateExpressionSuccessPayload; +import org.eclipse.sirius.web.application.views.query.dto.IntExpressionResult; +import org.eclipse.sirius.web.application.views.query.dto.ObjectExpressionResult; +import org.eclipse.sirius.web.application.views.query.dto.ObjectsExpressionResult; +import org.eclipse.sirius.web.application.views.query.dto.StringExpressionResult; +import org.eclipse.sirius.web.application.views.query.dto.VoidExpressionResult; +import org.eclipse.sirius.web.data.PapayaIdentifiers; +import org.eclipse.sirius.web.tests.graphql.EvaluateExpressionMutationRunner; +import org.eclipse.sirius.web.tests.services.api.IGivenInitialServerState; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests of the query view. + * + * @author sbegaudeau + */ +@Transactional +@SuppressWarnings("checkstyle:MultipleStringLiterals") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class QueryIntegrationTests extends AbstractIntegrationTests { + + @Autowired + private IGivenInitialServerState givenInitialServerState; + + @Autowired + private EvaluateExpressionMutationRunner evaluateExpressionMutationRunner; + + @BeforeEach + public void beforeEach() { + this.givenInitialServerState.initialize(); + } + + @Test + @DisplayName("Given a project, when we execute an expression returning a collection of objects, then the result is returned") + @Sql(scripts = {"/scripts/papaya.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"/scripts/cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) + public void givenProjectWhenWeExecuteAnExpressionReturningCollectionOfObjectsThenTheResultIsReturned() { + var expression = "aql:editingContext.allContents()->filter(papaya::Component)"; + var result = this.evaluateExpressionMutationRunner.run(new EvaluateExpressionInput(UUID.randomUUID(), PapayaIdentifiers.PAPAYA_PROJECT.toString(), expression)); + + String payloadTypename = JsonPath.read(result, "$.data.evaluateExpression.__typename"); + assertThat(payloadTypename).isEqualTo(EvaluateExpressionSuccessPayload.class.getSimpleName()); + + String resultTypename = JsonPath.read(result, "$.data.evaluateExpression.result.__typename"); + assertThat(resultTypename).isEqualTo(ObjectsExpressionResult.class.getSimpleName()); + + List labels = JsonPath.read(result, "$.data.evaluateExpression.result.objectsValue[*].label"); + assertThat(labels) + .isNotEmpty() + .containsExactly("Component sirius-web-domain", "Component sirius-web-application", "Component sirius-web-infrastructure", "Component sirius-web-starter", "Component sirius-web"); + } + + @Test + @DisplayName("Given a project, when we execute an expression returning a single object, then the result is returned") + @Sql(scripts = {"/scripts/papaya.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"/scripts/cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) + public void givenProjectWhenWeExecuteAnExpressionReturningSingleObjectThenTheResultIsReturned() { + var expression = "aql:editingContext.allContents()->filter(papaya::Component)->first()"; + var result = this.evaluateExpressionMutationRunner.run(new EvaluateExpressionInput(UUID.randomUUID(), PapayaIdentifiers.PAPAYA_PROJECT.toString(), expression)); + + String payloadTypename = JsonPath.read(result, "$.data.evaluateExpression.__typename"); + assertThat(payloadTypename).isEqualTo(EvaluateExpressionSuccessPayload.class.getSimpleName()); + + String resultTypename = JsonPath.read(result, "$.data.evaluateExpression.result.__typename"); + assertThat(resultTypename).isEqualTo(ObjectExpressionResult.class.getSimpleName()); + + String label = JsonPath.read(result, "$.data.evaluateExpression.result.objectValue.label"); + assertThat(label).isEqualTo("Component sirius-web-domain"); + } + + @Test + @DisplayName("Given a project, when we execute an expression returning an integer, then the result is returned") + @Sql(scripts = {"/scripts/papaya.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"/scripts/cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) + public void givenProjectWhenWeExecuteAnExpressionReturningIntegerThenTheResultIsReturned() { + var expression = "aql:editingContext.allContents()->filter(papaya::Component)->size()"; + var result = this.evaluateExpressionMutationRunner.run(new EvaluateExpressionInput(UUID.randomUUID(), PapayaIdentifiers.PAPAYA_PROJECT.toString(), expression)); + + String payloadTypename = JsonPath.read(result, "$.data.evaluateExpression.__typename"); + assertThat(payloadTypename).isEqualTo(EvaluateExpressionSuccessPayload.class.getSimpleName()); + + String resultTypename = JsonPath.read(result, "$.data.evaluateExpression.result.__typename"); + assertThat(resultTypename).isEqualTo(IntExpressionResult.class.getSimpleName()); + + int size = JsonPath.read(result, "$.data.evaluateExpression.result.intValue"); + assertThat(size).isEqualTo(5); + } + + @Test + @DisplayName("Given a project, when we execute an expression returning a boolean, then the result is returned") + @Sql(scripts = {"/scripts/papaya.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"/scripts/cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) + public void givenProjectWhenWeExecuteAnExpressionReturningBooleanThenTheResultIsReturned() { + var expression = "aql:editingContext.allContents()->filter(papaya::Component)->isEmpty()"; + var result = this.evaluateExpressionMutationRunner.run(new EvaluateExpressionInput(UUID.randomUUID(), PapayaIdentifiers.PAPAYA_PROJECT.toString(), expression)); + + String payloadTypename = JsonPath.read(result, "$.data.evaluateExpression.__typename"); + assertThat(payloadTypename).isEqualTo(EvaluateExpressionSuccessPayload.class.getSimpleName()); + + String resultTypename = JsonPath.read(result, "$.data.evaluateExpression.result.__typename"); + assertThat(resultTypename).isEqualTo(BooleanExpressionResult.class.getSimpleName()); + + boolean isEmpty = JsonPath.read(result, "$.data.evaluateExpression.result.booleanValue"); + assertThat(isEmpty).isFalse(); + } + + @Test + @DisplayName("Given a project, when we execute an expression returning a string, then the result is returned") + @Sql(scripts = {"/scripts/papaya.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"/scripts/cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) + public void givenProjectWhenWeExecuteAnExpressionReturningStringThenTheResultIsReturned() { + var expression = "aql:editingContext.allContents()->filter(papaya::Component)->first().name"; + var result = this.evaluateExpressionMutationRunner.run(new EvaluateExpressionInput(UUID.randomUUID(), PapayaIdentifiers.PAPAYA_PROJECT.toString(), expression)); + + String payloadTypename = JsonPath.read(result, "$.data.evaluateExpression.__typename"); + assertThat(payloadTypename).isEqualTo(EvaluateExpressionSuccessPayload.class.getSimpleName()); + + String resultTypename = JsonPath.read(result, "$.data.evaluateExpression.result.__typename"); + assertThat(resultTypename).isEqualTo(StringExpressionResult.class.getSimpleName()); + + String name = JsonPath.read(result, "$.data.evaluateExpression.result.stringValue"); + assertThat(name).isEqualTo("sirius-web-domain"); + } + + @Test + @DisplayName("Given a project, when we execute an expression which does not return anything, then the result is returned") + @Sql(scripts = {"/scripts/papaya.sql"}, executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + @Sql(scripts = {"/scripts/cleanup.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD, config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) + public void givenProjectWhenWeExecuteAnExpressionWhichDoesNotReturnAnythingThenTheResultIsReturned() { + var expression = "aql:editingContext.allContents()->filter(papaya::Component)->first().newPackage('newpackage')"; + var result = this.evaluateExpressionMutationRunner.run(new EvaluateExpressionInput(UUID.randomUUID(), PapayaIdentifiers.PAPAYA_PROJECT.toString(), expression)); + + String payloadTypename = JsonPath.read(result, "$.data.evaluateExpression.__typename"); + assertThat(payloadTypename).isEqualTo(EvaluateExpressionSuccessPayload.class.getSimpleName()); + + String resultTypename = JsonPath.read(result, "$.data.evaluateExpression.result.__typename"); + assertThat(resultTypename).isEqualTo(VoidExpressionResult.class.getSimpleName()); + + expression = "aql:editingContext.allContents()->filter(papaya::Component)->first().packages->last().name"; + result = this.evaluateExpressionMutationRunner.run(new EvaluateExpressionInput(UUID.randomUUID(), PapayaIdentifiers.PAPAYA_PROJECT.toString(), expression)); + String name = JsonPath.read(result, "$.data.evaluateExpression.result.stringValue"); + assertThat(name).isEqualTo("newpackage"); + } +} diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/services/query/PapayaInterpreterJavaServiceProvider.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/services/query/PapayaInterpreterJavaServiceProvider.java new file mode 100644 index 0000000000..c6914dd6e0 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/services/query/PapayaInterpreterJavaServiceProvider.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.services.query; + +import java.util.List; + +import org.eclipse.sirius.components.core.api.IEditingContext; +import org.eclipse.sirius.web.application.views.query.services.api.IInterpreterJavaServiceProvider; +import org.springframework.stereotype.Service; + +/** + * Used to contribute additional services while executing queries. + * + * @author sbegaudeau + */ +@Service +public class PapayaInterpreterJavaServiceProvider implements IInterpreterJavaServiceProvider { + @Override + public List> getServiceClasses(IEditingContext editingContext) { + return List.of(PapayaQueryServices.class); + } +} diff --git a/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/services/query/PapayaQueryServices.java b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/services/query/PapayaQueryServices.java new file mode 100644 index 0000000000..9623890c19 --- /dev/null +++ b/packages/sirius-web/backend/sirius-web/src/test/java/org/eclipse/sirius/web/services/query/PapayaQueryServices.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.web.services.query; + +import org.eclipse.sirius.components.papaya.Component; +import org.eclipse.sirius.components.papaya.PapayaFactory; + +/** + * Custom services to manipulate Papaya objects. + * + * @author sbegaudeau + */ +public class PapayaQueryServices { + + public void newPackage(Component component, String name) { + var papayaPackage = PapayaFactory.eINSTANCE.createPackage(); + papayaPackage.setName(name); + component.getPackages().add(papayaPackage); + } +} diff --git a/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx b/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx index 632f05f24e..57af77d51e 100644 --- a/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx +++ b/packages/sirius-web/frontend/sirius-web-application/src/extension/DefaultExtensionRegistry.tsx @@ -51,6 +51,7 @@ import Filter from '@mui/icons-material/Filter'; import ImageIcon from '@mui/icons-material/Image'; import LinkIcon from '@mui/icons-material/Link'; import MenuIcon from '@mui/icons-material/Menu'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import WarningIcon from '@mui/icons-material/Warning'; import { useMatch } from 'react-router-dom'; import { DiagramFilter } from '../diagrams/DiagramFilter'; @@ -67,6 +68,7 @@ import { editProjectNavbarMenuEntryExtensionPoint } from '../views/edit-project/ import { ObjectTreeItemContextMenuContribution } from '../views/edit-project/ObjectTreeItemContextMenuContribution'; import { DetailsView } from '../views/edit-project/workbench-views/details/DetailsView'; import { ExplorerView } from '../views/edit-project/workbench-views/explorer/ExplorerView'; +import { QueryView } from '../views/edit-project/workbench-views/query/QueryView'; import { RelatedElementsView } from '../views/edit-project/workbench-views/related-elements/RelatedElementsView'; import { RepresentationsView } from '../views/edit-project/workbench-views/representations/RepresentationsView'; import { createProjectAreaCardExtensionPoint } from '../views/project-browser/create-projects-area/CreateProjectAreaExtensionPoints'; @@ -128,6 +130,12 @@ const workbenchViewContributions: WorkbenchViewContribution[] = [ icon: , component: DetailsView, }, + { + side: 'right', + title: 'Query', + icon: , + component: QueryView, + }, { side: 'right', title: 'Representations', diff --git a/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/workbench-views/query/QueryView.tsx b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/workbench-views/query/QueryView.tsx new file mode 100644 index 0000000000..d841836f23 --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/workbench-views/query/QueryView.tsx @@ -0,0 +1,258 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +import { WorkbenchViewComponentProps } from '@eclipse-sirius/sirius-components-core'; +import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemText from '@mui/material/ListItemText'; +import { SxProps, Theme } from '@mui/material/styles'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import { ComponentType, useState } from 'react'; +import { + ExpressionAreaProps, + ExpressionAreaState, + ExpressionResultViewerProps, + ResultAreaProps, +} from './QueryView.types'; +import { useEvaluateExpression } from './useEvaluateExpression'; +import { + GQLBooleanExpressionResult, + GQLIntExpressionResult, + GQLObjectExpressionResult, + GQLObjectsExpressionResult, + GQLStringExpressionResult, +} from './useEvaluateExpression.types'; + +export const QueryView = ({ editingContextId, readOnly }: WorkbenchViewComponentProps) => { + const interpreterStyle: SxProps = (theme) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingX: theme.spacing(1), + }); + + const { evaluateExpression, loading, result } = useEvaluateExpression(); + + const handleEvaluateExpression = (expression: string) => evaluateExpression(editingContextId, expression); + + return ( + + + + + ); +}; + +const ExpressionArea = ({ onEvaluateExpression, disabled }: ExpressionAreaProps) => { + const [state, setState] = useState({ + expression: '', + }); + + const handleExpressionChange: React.ChangeEventHandler = (event) => { + const { + target: { value }, + } = event; + setState((prevState) => ({ ...prevState, expression: value })); + }; + + const handleKeyDown: React.KeyboardEventHandler = (event) => { + if ('Enter' === event.key && event.altKey) { + onEvaluateExpression(state.expression); + } + }; + + const expressionAreaToolbarStyle: SxProps = { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }; + + return ( +
+ + Expression + + onEvaluateExpression(state.expression)} + color={state.expression.trim().length > 0 ? 'primary' : undefined} + disabled={disabled || state.expression.trim().length === 0}> + + + +
+ +
+
+ ); +}; + +const ObjectExpressionResultViewer = ({ result }: ExpressionResultViewerProps) => { + if (result.__typename !== 'ObjectExpressionResult') { + return null; + } + + const { objectValue } = result as GQLObjectExpressionResult; + return ( + + + One object returned + + + + + + + + ); +}; + +const ObjectsExpressionResultViewer = ({ result }: ExpressionResultViewerProps) => { + if (result.__typename !== 'ObjectsExpressionResult') { + return null; + } + + const { objectsValue } = result as GQLObjectsExpressionResult; + return ( + + + {objectsValue.length} object(s) returned + + + {objectsValue.map((object) => { + return ( + + + + ); + })} + + + ); +}; + +const BooleanExpressionResultViewer = ({ result }: ExpressionResultViewerProps) => { + if (result.__typename !== 'BooleanExpressionResult') { + return null; + } + + const { booleanValue } = result as GQLBooleanExpressionResult; + return ( + + + One boolean returned + + + + + + + + ); +}; + +const StringExpressionResultViewer = ({ result }: ExpressionResultViewerProps) => { + if (result.__typename !== 'StringExpressionResult') { + return null; + } + + const { stringValue } = result as GQLStringExpressionResult; + return ( + + + One string returned + + + + + + + + ); +}; + +const IntExpressionResultViewer = ({ result }: ExpressionResultViewerProps) => { + if (result.__typename !== 'IntExpressionResult') { + return null; + } + + const { intValue } = result as GQLIntExpressionResult; + return ( + + + One integer returned + + + + + + + + ); +}; + +const resultType2viewer: Record> = { + ObjectExpressionResult: ObjectExpressionResultViewer, + ObjectsExpressionResult: ObjectsExpressionResultViewer, + BooleanExpressionResult: BooleanExpressionResultViewer, + StringExpressionResult: StringExpressionResultViewer, + IntExpressionResult: IntExpressionResultViewer, +}; + +const ResultArea = ({ payload }: ResultAreaProps) => { + const resultAreaToolbarStyle: SxProps = { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }; + + const titleAreaStyle: SxProps = { + display: 'flex', + flexDirection: 'column', + }; + + let content: JSX.Element | null = null; + if (payload) { + const Viewer = resultType2viewer[payload.result.__typename]; + if (Viewer) { + content = ; + } + } + + return ( +
+ + + Evaluation result + + + + {content} +
+ ); +}; diff --git a/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/workbench-views/query/QueryView.types.ts b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/workbench-views/query/QueryView.types.ts new file mode 100644 index 0000000000..77926faade --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/workbench-views/query/QueryView.types.ts @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +import { GQLEvaluateExpressionSuccessPayload, GQLExpressionResult } from './useEvaluateExpression.types'; + +export interface ExpressionAreaProps { + onEvaluateExpression: (expression: string) => void; + disabled: boolean; +} + +export interface ExpressionAreaState { + expression: string; +} + +export interface ResultAreaProps { + payload: GQLEvaluateExpressionSuccessPayload | null; +} + +export interface ExpressionResultViewerProps { + result: GQLExpressionResult; +} diff --git a/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/workbench-views/query/useEvaluateExpression.tsx b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/workbench-views/query/useEvaluateExpression.tsx new file mode 100644 index 0000000000..27e7d97033 --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/workbench-views/query/useEvaluateExpression.tsx @@ -0,0 +1,115 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +import { gql, useMutation } from '@apollo/client'; +import { useMultiToast } from '@eclipse-sirius/sirius-components-core'; +import { useEffect } from 'react'; +import { + GQLErrorPayload, + GQLEvaluateExpressionMutationData, + GQLEvaluateExpressionMutationVariables, + GQLEvaluateExpressionPayload, + GQLEvaluateExpressionSuccessPayload, + UseEvaluateExpressionResult, +} from './useEvaluateExpression.types'; + +const evaluateExpressionMutation = gql` + mutation evaluateExpression($input: EvaluateExpressionInput!) { + evaluateExpression(input: $input) { + __typename + ... on EvaluateExpressionSuccessPayload { + result { + __typename + ... on ObjectExpressionResult { + objectValue: value { + id + kind + label + } + } + ... on ObjectsExpressionResult { + objectsValue: value { + id + kind + label + } + } + ... on BooleanExpressionResult { + booleanValue: value + } + ... on IntExpressionResult { + intValue: value + } + ... on StringExpressionResult { + stringValue: value + } + } + } + ... on ErrorPayload { + messages { + body + level + } + } + } + } +`; + +const isErrorPayload = (payload: GQLEvaluateExpressionPayload): payload is GQLErrorPayload => + payload.__typename === 'ErrorPayload'; +const isEvaluateExpressionSuccessPayload = ( + payload: GQLEvaluateExpressionPayload +): payload is GQLEvaluateExpressionSuccessPayload => payload.__typename === 'EvaluateExpressionSuccessPayload'; + +export const useEvaluateExpression = (): UseEvaluateExpressionResult => { + const [performExpressionEvaluation, { loading, data, error }] = useMutation< + GQLEvaluateExpressionMutationData, + GQLEvaluateExpressionMutationVariables + >(evaluateExpressionMutation); + + const { addErrorMessage, addMessages } = useMultiToast(); + useEffect(() => { + if (error) { + addErrorMessage('An unexpected error has occurred, please refresh the page'); + } + if (data) { + const { evaluateExpression } = data; + if (isErrorPayload(evaluateExpression)) { + addMessages(evaluateExpression.messages); + } + } + }, [data, error]); + + const evaluateExpression = (editingContextId: string, expression: string) => { + const variables: GQLEvaluateExpressionMutationVariables = { + input: { + id: crypto.randomUUID(), + editingContextId, + expression, + }, + }; + + performExpressionEvaluation({ variables }); + }; + + let result: GQLEvaluateExpressionSuccessPayload | null = null; + if (data && isEvaluateExpressionSuccessPayload(data.evaluateExpression)) { + result = data.evaluateExpression; + } + + return { + evaluateExpression, + loading, + result, + }; +}; diff --git a/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/workbench-views/query/useEvaluateExpression.types.ts b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/workbench-views/query/useEvaluateExpression.types.ts new file mode 100644 index 0000000000..248030cb9f --- /dev/null +++ b/packages/sirius-web/frontend/sirius-web-application/src/views/edit-project/workbench-views/query/useEvaluateExpression.types.ts @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (c) 2024 Obeo. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ + +import { GQLMessage } from '@eclipse-sirius/sirius-components-core'; + +export interface UseEvaluateExpressionResult { + evaluateExpression: (editingContextId: string, expression: string) => void; + loading: boolean; + result: GQLEvaluateExpressionSuccessPayload | null; +} + +export interface GQLEvaluateExpressionMutationData { + evaluateExpression: GQLEvaluateExpressionPayload; +} + +export interface GQLEvaluateExpressionPayload { + __typename: string; +} + +export interface GQLEvaluateExpressionSuccessPayload extends GQLEvaluateExpressionPayload { + result: GQLExpressionResult; +} + +export interface GQLExpressionResult { + __typename: string; +} + +export interface GQLObjectExpressionResult extends GQLExpressionResult { + objectValue: GQLObject; +} + +export interface GQLObject { + id: string; + kind: string; + label: string; +} + +export interface GQLObjectsExpressionResult extends GQLExpressionResult { + objectsValue: GQLObject[]; +} + +export interface GQLBooleanExpressionResult extends GQLExpressionResult { + booleanValue: boolean; +} + +export interface GQLIntExpressionResult extends GQLExpressionResult { + intValue: number; +} + +export interface GQLStringExpressionResult extends GQLExpressionResult { + stringValue: string; +} + +export interface GQLVoidExpressionResult extends GQLExpressionResult {} + +export interface GQLErrorPayload extends GQLEvaluateExpressionPayload { + messages: GQLMessage[]; +} + +export interface GQLEvaluateExpressionMutationVariables { + input: GQLEvaluateExpressionInput; +} + +export interface GQLEvaluateExpressionInput { + id: string; + editingContextId: string; + expression: string; +}