Skip to content

Commit

Permalink
[3533] Add a dedicated component to handle diagram subscription
Browse files Browse the repository at this point in the history
Bug: #3533
Signed-off-by: Michaël Charfadi <[email protected]>
  • Loading branch information
mcharfadi committed Sep 4, 2024
1 parent e5fbf3c commit b625582
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 153 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
Expand All @@ -37,21 +38,24 @@
*/
@QueryDataFetcher(type = "DiagramDescription", field = "dropNodeCompatibility")
public class DiagramDescriptionDropNodeCompatibilityDataFetcher implements IDataFetcherWithFieldCoordinates<CompletableFuture<List<DropNodeCompatibilityEntry>>> {
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<List<DropNodeCompatibilityEntry>> get(DataFetchingEnvironment environment) throws Exception {
Map<String, Object> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -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<DiagramRepresentationState>({
id: crypto.randomUUID(),
message: null,
});
const { addErrorMessage } = useMultiToast();

export const DiagramRepresentation = ({
editingContextId,
representationId,
readOnly,
}: RepresentationComponentProps) => {
const [state, setState] = useState<DiagramRepresentationState>({
id: crypto.randomUUID(),
diagramRefreshedEventPayload: null,
payload: null,
complete: false,
message: null,
});
const { addErrorMessage } = useMultiToast();
const {
loading: diagramDescriptionLoading,
data: diagramDescriptionData,
error: diagramDescriptionError,
} = useQuery<GQLDiagramDescriptionData, GQLDiagramDescriptionVariables>(getDiagramDescription, {
variables: {
editingContextId,
representationId,
},
});

const variables: GQLDiagramEventVariables = {
input: {
id: state.id,
editingContextId,
diagramId: representationId,
},
};

const onData = ({ data }: OnDataOptions<GQLDiagramEventData>) => {
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<GQLDiagramDescriptionData, GQLDiagramDescriptionVariables>(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 <div>{state.message}</div>;
}
}, [diagramDescriptionLoading, diagramDescriptionData, diagramDescriptionError]);

const onComplete = () => {
setState((prevState) => ({ ...prevState, diagramRefreshedEventPayload: null, complete: true }));
};

const { error } = useSubscription<GQLDiagramEventData>(subscription, {
variables,
fetchPolicy: 'no-cache',
onData,
onComplete,
});

const diagramDescription: GQLDiagramDescription | undefined =
diagramDescriptionData?.viewer.editingContext.representation.description;
if (!diagramDescription) {
return <div></div>;
}

if (state.message) {
return <div>{state.message}</div>;
}
if (error) {
return <div>{error.message}</div>;
return (
<ReactFlowProvider>
<StoreContextProvider>
<DiagramDirectEditContextProvider>
<DiagramPaletteContextProvider>
<DiagramElementPaletteContextProvider>
<ConnectorContextProvider>
<DropNodeContextProvider>
<NodeContextProvider>
<MarkerDefinitions />
<FullscreenContextProvider>
<DiagramDescriptionContext.Provider value={{ diagramDescription }}>
<DiagramSubscriptionProvider
diagramId={representationId}
editingContextId={editingContextId}
readOnly={readOnly}></DiagramSubscriptionProvider>
</DiagramDescriptionContext.Provider>
</FullscreenContextProvider>
</NodeContextProvider>
</DropNodeContextProvider>
</ConnectorContextProvider>
</DiagramElementPaletteContextProvider>
</DiagramPaletteContextProvider>
</DiagramDirectEditContextProvider>
</StoreContextProvider>
</ReactFlowProvider>
);
}
if (state.complete) {
return <div>The representation is not available anymore</div>;
}
if (!state.diagramRefreshedEventPayload || !diagramDescription) {
return <div></div>;
}

return (
<ReactFlowProvider>
<DiagramContext.Provider
value={{
editingContextId,
diagramId: representationId,
refreshEventPayloadId: state.diagramRefreshedEventPayload.id,
payload: state.payload,
readOnly,
}}>
<DiagramDescriptionContext.Provider value={{ diagramDescription }}>
<StoreContextProvider>
<DiagramDirectEditContextProvider>
<DiagramPaletteContextProvider>
<DiagramElementPaletteContextProvider>
<ConnectorContextProvider>
<DropNodeContextProvider>
<NodeContextProvider>
<div
style={{ display: 'inline-block', position: 'relative' }}
data-representation-kind="diagram"
data-representation-label={state.diagramRefreshedEventPayload.diagram.metadata.label}>
<MarkerDefinitions />
<FullscreenContextProvider>
<DialogContextProvider>
<DiagramRenderer
key={state.diagramRefreshedEventPayload.diagram.id}
diagramRefreshedEventPayload={state.diagramRefreshedEventPayload}
/>
</DialogContextProvider>
</FullscreenContextProvider>
</div>
</NodeContextProvider>
</DropNodeContextProvider>
</ConnectorContextProvider>
</DiagramElementPaletteContextProvider>
</DiagramPaletteContextProvider>
</DiagramDirectEditContextProvider>
</StoreContextProvider>
</DiagramDescriptionContext.Provider>
</DiagramContext.Provider>
</ReactFlowProvider>
);
};
);
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<DiagramSubscriptionState>({
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 <LinearProgress />;
}

if (complete) {
return (
<div>
<Typography variant="subtitle2">The representation is not available anymore</Typography>
</div>
);
}

return (
<DiagramContext.Provider
value={{
editingContextId,
diagramId: diagramId,
refreshEventPayloadId: state.diagramRefreshedEventPayload.id,
payload: payload,
readOnly,
}}>
<DiagramRenderer diagramRefreshedEventPayload={state.diagramRefreshedEventPayload} />
</DiagramContext.Provider>
);
}
);
Loading

0 comments on commit b625582

Please sign in to comment.