Beyond the no code approach, Sirius Web is an open and extensible platform that you can customize in order to support your needs. Discover how to develop specific features in Sirius Web and integrate your modeler with other web applications.
Stéphane Bégaudeau, Obeo
Stéphane Bégaudeau graduated from the Nantes University of Sciences and Technology and is currently working as an Eclipse Modeling consultant at Obeo in France.
Sirius Web Advanced : Customize and Extend the Platform
1. Sirius Web Advanced:
Customize and extend the platform
Stéphane Bégaudeau
Sirius Web Architect
stephane.begaudeau@obeo.fr | sbegaudeau
2. Sirius Web
■ Everything you liked in Sirius Desktop, available on a modern cloud-based stack
■ Graphical and Domain specific tooling
■ Defined by a configuration file
■ Deployed on a web server
■ Rendered in a web browser
■ Collaborative support
https://www.eclipse.org/sirius/sirius-web.html
3. Obeo Studio
■ All of Sirius Web with additional collaborative and access control features
■ Authentication and authorization
■ Public/Private projects
■ Role based access control
■ Indicators of active users
https://www.obeosoft.com/en/products/obeo-studio
4. Completely customizable
■ Configure Sirius Web and Obeo Studio with the concepts from your domain
■ Define the graphical representation that you need
■ Diagrams
■ Tools
■ Forms
■ Validation
■ Import existing Sirius desktop configuration easily
5. What’s in Sirius Web?
READY-TO-USE
Modeling framework
to define and render
graphical applications
in the web
6. What’s in Sirius Web?
READY-TO-USE
Modeling framework
to define and render
graphical applications
in the web MODEL SERVER
Open source model
server components
with a GraphQL API
7. What’s in Sirius Web?
READY-TO-USE
Modeling framework
to define and render
graphical applications
in the web MODEL SERVER
MODEL APPLICATION
Open source model
application (diagram,
properties, forms…)
Open source model
server components
with a GraphQL API
11. Code-based customization
■ Customize icons
■ Customize the child creation proposals available in the explorer
■ Advanced behavior for diagram tools
■ Java services
■ Java based representation descriptions
12. Provide Java services
@Service
public class CustomServicesProvider implements IJavaServiceProvider {
@Override
public List<Class<?>> getServiceClasses(View view) {
return List.of(CustomServices.class);
}
}
public class CustomServices {
public String getValue(EObject self, String name) {
return self.eClass().eGet(self.eClass().getEStructuralFeature(name)).toString();
}
}
aql:self.getValue('name')
13. Register representation descriptions
@Configuration
public class RepresentationDescriptionRegistryConfigurer implements IRepresentationDescriptionRegistryConfigurer {
@Override
public void addRepresentationDescriptions(IRepresentationDescriptionRegistry registry) {
DiagramDescription diagramDescription = DiagramDescription.newDiagramDescription("customDiagram")
.nodeDescriptions(List.of())
.edgeDescriptions(List.of())
.toolSections(List.of())
.build();
registry.add(diagramDescription);
}
}
15. Let’s extend the platform!
■ Add a new kind of representation
■ Contribute it to our backend
■ Describe it in our GraphQL API
■ Integrate it in the frontend of your application
■ Synchronize our data with another application
16.
17. Map based representation
■ Map representation based on Google Maps
■ A dedicated metamodel
■ to create semantic elements with map-related attributes
■ Display and refresh our map in real time when the objects are modified
18. Backend
■ Add support for the map representation
■ Representation description and instance
■ Creation process
■ Event processor
■ GraphQL API
19. Register metamodel
@Configuration
public class MapPackageConfiguration {
@Bean
public EPackage mapPackage() {
EClass mapEClass = EcoreFactory.eINSTANCE.createEClass();
mapEClass.setName("Map");
// Contribute the attributes of the class: lat, lng, zoom
EPackage mapEPackage = EcoreFactory.eINSTANCE.createEPackage();
mapEPackage.setName("Map");
mapEPackage.setNsPrefix("map");
mapEPackage.setNsURI("https://www.eclipse.org/sirius/map");
mapEPackage.getEClassifiers().add(mapEClass);
return mapEPackage;
}
}
20.
21.
22. MapDescription.java
public class MapDescription implements IRepresentationDescription {
@Override
public String getId() {
return "map";
}
@Override
public String getLabel() {
return "Map";
}
@Override
public Predicate<VariableManager> getCanCreatePredicate() {
return variableManager -> variableManager.get("class", EClass.class)
.filter(eClass -> eClass.getEPackage().getNsURI().equals("https://www.eclipse.org/sirius/map"))
.isPresent();
}
}
23. Map.java
public class Map implements IRepresentation, ISemanticRepresentation {
public static final String KIND = IRepresentation.KIND_PREFIX + "?type=Map";
private String id;
private String descriptionId = "map";
private String targetObjectId;
private String label;
private double lng;
private double lat;
private int zoom;
}
24. Representation description registration
@Configuration
public class MapRepresentationDescriptionRegistryConfigurer implements IRepresentationDescriptionRegistryConfigurer
{
@Override
public void addRepresentationDescriptions(IRepresentationDescriptionRegistry registry) {
registry.add(new MapDescription());
}
}
25. Create maps
@Service
public class CreateMapEventHandler implements IEditingContextEventHandler {
@Override
public boolean canHandle(IEditingContext editingContext, IInput input) {
// Check that the input is for the creation of a map representation
}
@Override
public void handle(One<IPayload> payloadSink, Many<ChangeDescription> changeDescriptionSink,
IEditingContext editingContext, IInput input) {
Map map = new Map(...);
this.representationPersistenceService.save(editingContext, map);
changeDescriptionSink.tryEmitNext(
new ChangeDescription(ChangeKind.REPRESENTATION_CREATION, editingContext.getId(), input)
);
payloadSink.tryEmitValue(new CreateRepresentationSuccessPayload(input.getId(), map));
}
}
26. Tell Jackson how to read the map
@Service
public class MapDeserialiser implements IRepresentationDeserializer {
@Override
public boolean canHandle(ObjectNode root) {
return Optional.ofNullable(root.get("kind"))
.map(JsonNode::asText)
.filter(Map.KIND::equals)
.isPresent();
}
@Override
public Optional<IRepresentation> handle(ObjectMapper mapper, ObjectNode root) {
try {
return Optional.of(mapper.readValue(root.toString(), Map.class));
} catch (JsonProcessingException exception) {}
return Optional.empty();
}
}
30. GraphQL Subscription
@SubscriptionDataFetcher(type = "Subscription", field = "mapEvent")
public class SubscriptionMapEventDataFetcher implements IDataFetcherWithFieldCoordinates<Publisher<IPayload>> {
@Override
public Publisher<IPayload> get(DataFetchingEnvironment environment) throws Exception {
Object argument = environment.getArgument("input");
var input = this.objectMapper.convertValue(argument, MapEventInput.class);
var mapConfiguration = new MapConfiguration(input.getMapId());
return this.editingContextEventProcessorRegistry
.getOrCreateEditingContextEventProcessor(input.getEditingContextId())
.flatMap(processor -> processor.acquireRepresentationEventProcessor(
IMapEventProcessor.class, mapConfiguration, input
))
.map(representationEventProcessor -> representationEventProcessor.getOutputEvents(input))
.orElse(Flux.empty());
}
}
31. Subscribe to the representation
@Service
public class MapEventProcessorFactory implements IRepresentationEventProcessorFactory {
@Override
public <T extends IRepresentationEventProcessor> Optional<T> createRepresentationEventProcessor(Class<T>
representationEventProcessorClass, IRepresentationConfiguration configuration, IEditingContext editingContext) {
var optionalMap = this.representationSearchService.findById(editingContext, mapConfiguration.getId(),
Map.class);
if (optionalMap.isPresent()) {
Map map = optionalMap.get();
IRepresentationEventProcessor mapEventProcessor = new MapEventProcessor(...);
return Optional.of(mapEventProcessor)
.filter(representationEventProcessorClass::isInstance)
.map(representationEventProcessorClass::cast);
}
return Optional.empty();
}
}
32. Subscribe to the representation
public class MapEventProcessor implements IMapEventProcessor {
@Override
public void refresh(ChangeDescription changeDescription) {
// Refresh, save the new version of the map and send it using the mapEventFlux
}
@Override
public Flux<IPayload> getOutputEvents(IInput input) {
return Flux.merge(
this.mapEventFlux.getFlux(input),
this.subscriptionManager.getFlux(input)
);
}
@Override
public void dispose() {
this.subscriptionManager.dispose();
this.mapEventFlux.dispose();
}
}
33.
34. Frontend
■ On the frontend side
■ New representation component using React
■ Register the component in the entry point of the application
■ Subscribe to map events using our GraphQL API
43. Additional ideas
■ Update the properties of the object when the map is modified
■ Listen to change in coordinates or zoom level
■ Send a mutation to the backend
■ Update the data in the MapEventProcessor and refresh the map
46. MapEventProcessor.java
public class MapEventProcessor implements IMapEventProcessor {
@Override
public void refresh(ChangeDescription changeDescription) {
// Refresh, save the new version of the map and send it using the mapEventFlux
LocalDateTime now = LocalDateTime.now();
String datetime = now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
String content = datetime + " --- Latitude: " + lat + ", Longitude: " + lng; //$NON-NLS-1$//$NON-NLS-2$
String body = "{"description":"Update","files":{"MapData":{"content":"" + content + ""}}}";
var uri = URI.create("https://api.github.com/gists/58875db0a0146ccd7d17945079f489e1");
var httpClient = HttpClient.newHttpClient();
var httpRequest = HttpRequest.newBuilder()
.uri(uri)
.method("PATCH", HttpRequest.BodyPublishers.ofString(body))
.header("Accept", "application/vnd.github.v3+json")
.header("Authorization", "token XXXXXXXXXXXXXXXXXXX")
.build();
httpClient.send(httpRequest, BodyHandlers.ofString());
}
}
47. Synchronize with a remote service
■ You can check to see if there are any difference with the previous version first
■ Perform the request asynchronously to improve performance
■ In order to synchronize data the other way
■ Send a GraphQL query to Sirius Web
■ Contribute an IEditingContextEventHandler to perform the change
50. Integration in web applications
MODEL SERVER
Leverage our GraphQL API
over HTTP and WebSocket to
interact with your servers
GRAPHICAL EDITORS
Manipulate your Sirius Web
graphical editors from your
app (diagrams, forms, etc)
51. Getting started
■ To start integrating Sirius Web in a cloud IDE, you’ll need
■ The latest release of @eclipse-sirius/sirius-components
■ React components for our graphical editors
■ An instance of a Sirius Web server
■ HTTP and WebSocket GraphQL API
56. Project TreeView
■ Used to manipulate Sirius Web / Obeo Studio projects
■ Leverage our GraphQL API over HTTP
57. Project TreeView
■ Retrieve the projects using a GraphQL query
private fetchProjects(): Promise<ProjectData[]> {
const queryURL = `${this.serverAddress}/api/graphql`;
const headers = { headers: { Cookie: this.cookie } };
const graphQLQuery = `
query getProjects($page: Int!) {
viewer {
id
projects(page: $page) {
edges {
node {
id
name
visibility
}
}
}
}
}
`;
58. Explorer TreeView
■ Used to display the model elements from the project
■ Based on the configuration of the explorer of the server
■ Can be parameterized
■ Based on a tree representation
■ Using a GraphQL subscription for real time update
■ Based on the graphql-ws protocol