From b62558257203b5158307cb45668e88625b99a469 Mon Sep 17 00:00:00 2001 From: Michael Charfadi Date: Mon, 27 May 2024 11:28:35 +0200 Subject: [PATCH] [3533] Add a dedicated component to handle diagram subscription MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: https://github.com/eclipse-sirius/sirius-web/issues/3533 Signed-off-by: Michaƫl Charfadi --- CHANGELOG.adoc | 2 + ...ptionDropNodeCompatibilityDataFetcher.java | 24 ++- .../representation/DiagramRepresentation.tsx | 202 ++++++------------ .../DiagramRepresentation.types.ts | 8 +- .../DiagramSubscriptionProvider.tsx | 72 +++++++ .../DiagramSubscriptionProvider.types.ts | 31 +++ .../representation/useDiagramSubscription.tsx | 65 ++++++ .../useDiagramSubscription.types.ts | 38 ++++ .../web/starter/EditingContextDispatcher.java | 6 + 9 files changed, 295 insertions(+), 153 deletions(-) create mode 100644 packages/diagrams/frontend/sirius-components-diagrams/src/representation/DiagramSubscriptionProvider.tsx create mode 100644 packages/diagrams/frontend/sirius-components-diagrams/src/representation/DiagramSubscriptionProvider.types.ts create mode 100644 packages/diagrams/frontend/sirius-components-diagrams/src/representation/useDiagramSubscription.tsx create mode 100644 packages/diagrams/frontend/sirius-components-diagrams/src/representation/useDiagramSubscription.types.ts diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 77785698e3..32e825e328 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -128,6 +128,7 @@ A migration participant has been added to automatically keep compatible all diag - https://github.com/eclipse-sirius/sirius-web/issues/3927[#3927] [sirius-web] Make the viewer object configurable - https://github.com/eclipse-sirius/sirius-web/issues/3928[#3928] [sirius-web] Derive the read-only state from the project GraphQL query - https://github.com/eclipse-sirius/sirius-web/issues/3932[#3932] [forms] Do not display the 'Delete' button (even disabled) for non-deletable lists +- https://github.com/eclipse-sirius/sirius-web/issues/3533[#3553] [diagram] Add a dedicated component to handle diagram subscription == v2024.7.0 @@ -276,6 +277,7 @@ We cannot rely on the error message recaived from the backend since Cross Origin - https://github.com/eclipse-sirius/sirius-web/issues/3701[#3701] [domain] Add edge and node tools to domain diagram including creation tools as well as delete and direct edit tools. - https://github.com/eclipse-sirius/sirius-web/issues/3859[#3859] [view] Allow name duplication in ecore by grouping builders in dedicated packages + == v2024.5.0 === Shapes diff --git a/packages/diagrams/backend/sirius-components-diagrams-graphql/src/main/java/org/eclipse/sirius/components/diagrams/graphql/datafetchers/diagram/DiagramDescriptionDropNodeCompatibilityDataFetcher.java b/packages/diagrams/backend/sirius-components-diagrams-graphql/src/main/java/org/eclipse/sirius/components/diagrams/graphql/datafetchers/diagram/DiagramDescriptionDropNodeCompatibilityDataFetcher.java index 12631909c5..f1043cbb74 100644 --- a/packages/diagrams/backend/sirius-components-diagrams-graphql/src/main/java/org/eclipse/sirius/components/diagrams/graphql/datafetchers/diagram/DiagramDescriptionDropNodeCompatibilityDataFetcher.java +++ b/packages/diagrams/backend/sirius-components-diagrams-graphql/src/main/java/org/eclipse/sirius/components/diagrams/graphql/datafetchers/diagram/DiagramDescriptionDropNodeCompatibilityDataFetcher.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Obeo. + * Copyright (c) 2023, 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 @@ -20,11 +20,12 @@ import java.util.concurrent.CompletableFuture; import org.eclipse.sirius.components.annotations.spring.graphql.QueryDataFetcher; -import org.eclipse.sirius.components.collaborative.api.IEditingContextEventProcessorRegistry; import org.eclipse.sirius.components.collaborative.diagrams.dto.DropNodeCompatibilityEntry; import org.eclipse.sirius.components.collaborative.diagrams.dto.GetDropNodeCompatibilitySuccessPayload; import org.eclipse.sirius.components.collaborative.diagrams.dto.GetDropNodeCompatibiliyInput; 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.components.graphql.api.LocalContextConstants; import graphql.schema.DataFetchingEnvironment; @@ -37,21 +38,24 @@ */ @QueryDataFetcher(type = "DiagramDescription", field = "dropNodeCompatibility") public class DiagramDescriptionDropNodeCompatibilityDataFetcher implements IDataFetcherWithFieldCoordinates>> { - private final IEditingContextEventProcessorRegistry editingContextEventProcessorRegistry; - public DiagramDescriptionDropNodeCompatibilityDataFetcher(IEditingContextEventProcessorRegistry editingContextEventProcessorRegistry) { - this.editingContextEventProcessorRegistry = Objects.requireNonNull(editingContextEventProcessorRegistry); + private final IExceptionWrapper exceptionWrapper; + private final IEditingContextDispatcher editingContextDispatcher; + + public DiagramDescriptionDropNodeCompatibilityDataFetcher(IExceptionWrapper exceptionWrapper, IEditingContextDispatcher editingContextDispatcher) { + this.exceptionWrapper = Objects.requireNonNull(exceptionWrapper); + this.editingContextDispatcher = Objects.requireNonNull(editingContextDispatcher); } @Override public CompletableFuture> get(DataFetchingEnvironment environment) throws Exception { Map localContext = environment.getLocalContext(); - String editingContextId = Optional.ofNullable(localContext.get(LocalContextConstants.EDITING_CONTEXT_ID)).map(Object::toString).orElse(null); - String representationId = Optional.ofNullable(localContext.get(LocalContextConstants.REPRESENTATION_ID)).map(Object::toString).orElse(null); - if (editingContextId != null && representationId != null) { - GetDropNodeCompatibiliyInput input = new GetDropNodeCompatibiliyInput(UUID.randomUUID(), editingContextId, representationId); + var editingContextId = Optional.ofNullable(localContext.get(LocalContextConstants.EDITING_CONTEXT_ID)).map(Object::toString); + var representationId = Optional.ofNullable(localContext.get(LocalContextConstants.REPRESENTATION_ID)).map(Object::toString); - return this.editingContextEventProcessorRegistry.dispatchEvent(input.editingContextId(), input) + if (editingContextId.isPresent() && representationId.isPresent()) { + GetDropNodeCompatibiliyInput input = new GetDropNodeCompatibiliyInput(UUID.randomUUID(), editingContextId.get(), representationId.get()); + return this.exceptionWrapper.wrapMono(() -> this.editingContextDispatcher.dispatchQuery(input.editingContextId(), input), input) .filter(GetDropNodeCompatibilitySuccessPayload.class::isInstance) .map(GetDropNodeCompatibilitySuccessPayload.class::cast) .map(GetDropNodeCompatibilitySuccessPayload::entries) diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/representation/DiagramRepresentation.tsx b/packages/diagrams/frontend/sirius-components-diagrams/src/representation/DiagramRepresentation.tsx index a5ff0b9619..dd0ab130fe 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/representation/DiagramRepresentation.tsx +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/representation/DiagramRepresentation.tsx @@ -11,20 +11,12 @@ * Obeo - initial API and implementation *******************************************************************************/ -import { gql, OnDataOptions, useQuery, useSubscription } from '@apollo/client'; +import { gql, useQuery } from '@apollo/client'; import { RepresentationComponentProps, useMultiToast } from '@eclipse-sirius/sirius-components-core'; -import { useEffect, useState } from 'react'; +import { memo, useEffect, useState } from 'react'; import { ReactFlowProvider } from 'reactflow'; -import { DiagramContext } from '../contexts/DiagramContext'; import { DiagramDescriptionContext } from '../contexts/DiagramDescriptionContext'; -import { DialogContextProvider } from '../dialog/DialogContext'; -import { diagramEventSubscription } from '../graphql/subscription/diagramEventSubscription'; -import { - GQLDiagramEventPayload, - GQLDiagramRefreshedEventPayload, -} from '../graphql/subscription/diagramEventSubscription.types'; import { ConnectorContextProvider } from '../renderer/connector/ConnectorContext'; -import { DiagramRenderer } from '../renderer/DiagramRenderer'; import { DiagramDirectEditContextProvider } from '../renderer/direct-edit/DiagramDirectEditContext'; import { DropNodeContextProvider } from '../renderer/dropNode/DropNodeContext'; import { MarkerDefinitions } from '../renderer/edge/MarkerDefinitions'; @@ -37,13 +29,10 @@ import { GQLDiagramDescription, GQLDiagramDescriptionData, GQLDiagramDescriptionVariables, - GQLDiagramEventData, - GQLDiagramEventVariables, } from './DiagramRepresentation.types'; +import { DiagramSubscriptionProvider } from './DiagramSubscriptionProvider'; import { StoreContextProvider } from './StoreContext'; -const subscription = gql(diagramEventSubscription); - export const getDiagramDescription = gql` query getDiagramDescription($editingContextId: ID!, $representationId: ID!) { viewer { @@ -75,134 +64,75 @@ export const getDiagramDescription = gql` } `; -const isDiagramRefreshedEventPayload = (payload: GQLDiagramEventPayload): payload is GQLDiagramRefreshedEventPayload => - payload.__typename === 'DiagramRefreshedEventPayload'; +export const DiagramRepresentation = memo( + ({ editingContextId, representationId, readOnly }: RepresentationComponentProps) => { + const [state, setState] = useState({ + id: crypto.randomUUID(), + message: null, + }); + const { addErrorMessage } = useMultiToast(); -export const DiagramRepresentation = ({ - editingContextId, - representationId, - readOnly, -}: RepresentationComponentProps) => { - const [state, setState] = useState({ - id: crypto.randomUUID(), - diagramRefreshedEventPayload: null, - payload: null, - complete: false, - message: null, - }); - const { addErrorMessage } = useMultiToast(); + const { + loading: diagramDescriptionLoading, + data: diagramDescriptionData, + error: diagramDescriptionError, + } = useQuery(getDiagramDescription, { + variables: { + editingContextId, + representationId, + }, + }); - const variables: GQLDiagramEventVariables = { - input: { - id: state.id, - editingContextId, - diagramId: representationId, - }, - }; - - const onData = ({ data }: OnDataOptions) => { - if (data.data) { - const { diagramEvent } = data.data; - if (isDiagramRefreshedEventPayload(diagramEvent)) { - setState((prevState) => ({ ...prevState, diagramRefreshedEventPayload: diagramEvent })); + useEffect(() => { + if (!diagramDescriptionLoading) { + setState((prevState) => ({ + ...prevState, + diagramDescription: diagramDescriptionData?.viewer.editingContext.representation.description, + })); } - setState((prevState) => ({ ...prevState, payload: diagramEvent })); - } - }; + if (diagramDescriptionError) { + const { message } = diagramDescriptionError; + addErrorMessage(message); + } + }, [diagramDescriptionLoading, diagramDescriptionData, diagramDescriptionError]); - const { - loading: diagramDescriptionLoading, - data: diagramDescriptionData, - error: diagramDescriptionError, - } = useQuery(getDiagramDescription, { - variables: { - editingContextId, - representationId, - }, - skip: state.diagramRefreshedEventPayload === null, - }); + const diagramDescription: GQLDiagramDescription | undefined = + diagramDescriptionData?.viewer.editingContext.representation.description; - useEffect(() => { - if (!diagramDescriptionLoading) { - setState((prevState) => ({ - ...prevState, - diagramDescription: diagramDescriptionData?.viewer.editingContext.representation.description, - })); - } - if (diagramDescriptionError) { - const { message } = diagramDescriptionError; - addErrorMessage(message); + if (state.message) { + return
{state.message}
; } - }, [diagramDescriptionLoading, diagramDescriptionData, diagramDescriptionError]); - - const onComplete = () => { - setState((prevState) => ({ ...prevState, diagramRefreshedEventPayload: null, complete: true })); - }; - const { error } = useSubscription(subscription, { - variables, - fetchPolicy: 'no-cache', - onData, - onComplete, - }); - - const diagramDescription: GQLDiagramDescription | undefined = - diagramDescriptionData?.viewer.editingContext.representation.description; + if (!diagramDescription) { + return
; + } - if (state.message) { - return
{state.message}
; - } - if (error) { - return
{error.message}
; + return ( + + + + + + + + + + + + + + + + + + + + + + + ); } - if (state.complete) { - return
The representation is not available anymore
; - } - if (!state.diagramRefreshedEventPayload || !diagramDescription) { - return
; - } - - return ( - - - - - - - - - - -
- - - - - - -
-
-
-
-
-
-
-
-
-
-
- ); -}; +); diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/representation/DiagramRepresentation.types.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/representation/DiagramRepresentation.types.ts index 98224e5bed..30ff08eb3a 100644 --- a/packages/diagrams/frontend/sirius-components-diagrams/src/representation/DiagramRepresentation.types.ts +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/representation/DiagramRepresentation.types.ts @@ -12,16 +12,10 @@ *******************************************************************************/ import { GQLNodeDescription } from '../graphql/query/nodeDescriptionFragment.types'; -import { - GQLDiagramEventPayload, - GQLDiagramRefreshedEventPayload, -} from '../graphql/subscription/diagramEventSubscription.types'; +import { GQLDiagramEventPayload } from '../graphql/subscription/diagramEventSubscription.types'; export interface DiagramRepresentationState { id: string; - diagramRefreshedEventPayload: GQLDiagramRefreshedEventPayload | null; - payload: GQLDiagramEventPayload | null; - complete: boolean; message: string | null; } diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/representation/DiagramSubscriptionProvider.tsx b/packages/diagrams/frontend/sirius-components-diagrams/src/representation/DiagramSubscriptionProvider.tsx new file mode 100644 index 0000000000..92980296e2 --- /dev/null +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/representation/DiagramSubscriptionProvider.tsx @@ -0,0 +1,72 @@ +/******************************************************************************* + * 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 LinearProgress from '@mui/material/LinearProgress'; +import Typography from '@mui/material/Typography'; +import { memo, useEffect, useState } from 'react'; +import { DiagramContext } from '../contexts/DiagramContext'; +import { + GQLDiagramEventPayload, + GQLDiagramRefreshedEventPayload, +} from '../graphql/subscription/diagramEventSubscription.types'; +import { DiagramRenderer } from '../renderer/DiagramRenderer'; +import { DiagramSubscriptionProviderProps, DiagramSubscriptionState } from './DiagramSubscriptionProvider.types'; +import { useDiagramSubscription } from './useDiagramSubscription'; + +const isDiagramRefreshedEventPayload = ( + payload: GQLDiagramEventPayload | null +): payload is GQLDiagramRefreshedEventPayload => !!payload && payload.__typename === 'DiagramRefreshedEventPayload'; + +export const DiagramSubscriptionProvider = memo( + ({ diagramId, editingContextId, readOnly }: DiagramSubscriptionProviderProps) => { + const [state, setState] = useState({ + id: crypto.randomUUID(), + diagramRefreshedEventPayload: null, + complete: false, + message: '', + }); + + const { loading, complete, payload } = useDiagramSubscription(editingContextId, diagramId); + + useEffect(() => { + if (isDiagramRefreshedEventPayload(payload)) { + setState((prevState) => ({ ...prevState, diagramRefreshedEventPayload: payload })); + } + }, [payload]); + + if (loading || !state.diagramRefreshedEventPayload) { + return ; + } + + if (complete) { + return ( +
+ The representation is not available anymore +
+ ); + } + + return ( + + + + ); + } +); diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/representation/DiagramSubscriptionProvider.types.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/representation/DiagramSubscriptionProvider.types.ts new file mode 100644 index 0000000000..5896db7323 --- /dev/null +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/representation/DiagramSubscriptionProvider.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 { GQLDiagramRefreshedEventPayload } from '../graphql/subscription/diagramEventSubscription.types'; + +export type DiagramSubscriptionContextValue = { + diagramRefreshedEventPayload: GQLDiagramRefreshedEventPayload | null; + refreshEventPayloadId: string; +}; + +export type DiagramSubscriptionState = { + id: string; + diagramRefreshedEventPayload: GQLDiagramRefreshedEventPayload | null; + complete: boolean; + message: string; +}; + +export interface DiagramSubscriptionProviderProps { + editingContextId: string; + diagramId: string; + readOnly: boolean; +} diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/representation/useDiagramSubscription.tsx b/packages/diagrams/frontend/sirius-components-diagrams/src/representation/useDiagramSubscription.tsx new file mode 100644 index 0000000000..3b457d985d --- /dev/null +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/representation/useDiagramSubscription.tsx @@ -0,0 +1,65 @@ +/******************************************************************************* + * 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, useSubscription } from '@apollo/client'; +import { useMultiToast } from '@eclipse-sirius/sirius-components-core'; +import { useEffect, useState } from 'react'; +import { diagramEventSubscription } from '../graphql/subscription/diagramEventSubscription'; +import { + GQLDiagramEventSubscription, + GQLDiagramEventVariables, + UseDiagramSubscriptionState, + UseDiagramSubscriptionValue, +} from './useDiagramSubscription.types'; + +export const useDiagramSubscription = (editingContextId: string, diagramId: string): UseDiagramSubscriptionValue => { + const [state, setState] = useState({ + id: crypto.randomUUID(), + complete: false, + payload: null, + }); + + const variables: GQLDiagramEventVariables = { + input: { + id: state.id, + editingContextId, + diagramId, + }, + }; + + const onComplete = () => { + setState((prevState) => ({ ...prevState, diagramRefreshedEventPayload: null, complete: true })); + }; + + const { error, loading, data } = useSubscription( + gql(diagramEventSubscription), + { + variables, + fetchPolicy: 'no-cache', + onComplete, + } + ); + + const { addErrorMessage } = useMultiToast(); + useEffect(() => { + if (error) { + addErrorMessage('An unexpected error has occurred, please refresh the page'); + } + }, [error]); + + return { + loading, + payload: data?.diagramEvent ?? null, + complete: state.complete, + }; +}; diff --git a/packages/diagrams/frontend/sirius-components-diagrams/src/representation/useDiagramSubscription.types.ts b/packages/diagrams/frontend/sirius-components-diagrams/src/representation/useDiagramSubscription.types.ts new file mode 100644 index 0000000000..ab8be76c53 --- /dev/null +++ b/packages/diagrams/frontend/sirius-components-diagrams/src/representation/useDiagramSubscription.types.ts @@ -0,0 +1,38 @@ +/******************************************************************************* + * 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 { GQLDiagramEventPayload } from '../graphql/subscription/diagramEventSubscription.types'; +export interface UseDiagramSubscriptionValue { + loading: boolean; + payload: GQLDiagramEventPayload | null; + complete: boolean; +} + +export interface UseDiagramSubscriptionState { + id: string; + complete: boolean; + payload: GQLDiagramEventPayload | null; +} + +export interface GQLDiagramEventInput { + id: string; + editingContextId: string; + diagramId: string; +} + +export interface GQLDiagramEventVariables { + input: GQLDiagramEventInput; +} + +export interface GQLDiagramEventSubscription { + diagramEvent: GQLDiagramEventPayload; +} diff --git a/packages/sirius-web/backend/sirius-web-starter/src/main/java/org/eclipse/sirius/web/starter/EditingContextDispatcher.java b/packages/sirius-web/backend/sirius-web-starter/src/main/java/org/eclipse/sirius/web/starter/EditingContextDispatcher.java index 6a70c872aa..9cf91ed88f 100644 --- a/packages/sirius-web/backend/sirius-web-starter/src/main/java/org/eclipse/sirius/web/starter/EditingContextDispatcher.java +++ b/packages/sirius-web/backend/sirius-web-starter/src/main/java/org/eclipse/sirius/web/starter/EditingContextDispatcher.java @@ -15,6 +15,8 @@ import java.util.Objects; import org.eclipse.sirius.components.collaborative.api.IEditingContextEventProcessorRegistry; +import org.eclipse.sirius.components.collaborative.diagrams.api.DiagramConfiguration; +import org.eclipse.sirius.components.collaborative.diagrams.api.IDiagramInput; import org.eclipse.sirius.components.core.api.ErrorPayload; import org.eclipse.sirius.components.core.api.IInput; import org.eclipse.sirius.components.core.api.IPayload; @@ -42,6 +44,10 @@ public EditingContextDispatcher(IEditingContextEventProcessorRegistry editingCon @Override public Mono dispatchQuery(String editingContextId, IInput input) { + if (input instanceof IDiagramInput diagramInput) { + var editingContextEventProcessor = this.editingContextEventProcessorRegistry.getOrCreateEditingContextEventProcessor(editingContextId); + editingContextEventProcessor.ifPresent(iEditingContextEventProcessor -> iEditingContextEventProcessor.acquireRepresentationEventProcessor(new DiagramConfiguration(diagramInput.representationId()), diagramInput)); + } return this.editingContextEventProcessorRegistry.dispatchEvent(editingContextId, input) .defaultIfEmpty(new ErrorPayload(input.id(), this.messageService.unexpectedError())); }