Skip to content

Commit

Permalink
feat: add collaboration demo
Browse files Browse the repository at this point in the history
  • Loading branch information
NewByVector committed Nov 22, 2023
1 parent b767453 commit 12ca0e1
Show file tree
Hide file tree
Showing 7 changed files with 27,593 additions and 6,111 deletions.
5 changes: 4 additions & 1 deletion examples/x6-example-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@
"react-test-renderer": "^16.7.0",
"umi": "^2.9.0",
"umi-plugin-react": "^1.8.0",
"umi-types": "^0.3.0"
"umi-types": "^0.3.0",
"@fluidframework/aqueduct": "^0.33.5",
"@fluidframework/get-tinylicious-container": "^0.33.5",
"@fluidframework/map": "^0.33.5"
},
"engines": {
"node": ">=8.0.0"
Expand Down
52 changes: 52 additions & 0 deletions examples/x6-example-features/src/collaboration/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import type { Graph } from '@antv/x6'
import { getDefaultObjectFromContainer } from '@fluidframework/aqueduct'
import { getTinyliciousContainer } from '@fluidframework/get-tinylicious-container'

import { GraphDataObjectContainerRuntimeFactory } from './containerCode'
import type { IGraphDataObject } from './dataObject'
import { connectGraph } from './view'

// In interacting with the service, we need to be explicit about whether we're creating a new document vs. loading
// an existing one. We also need to provide the unique ID for the document we are creating or loading from.

// In this app, we'll choose to create a new document when navigating directly to http://localhost:8080. For the ID,
// we'll choose to use the current timestamp. We'll also choose to interpret the URL hash as an existing document's
// ID to load from, so the URL for a document load will look something like http://localhost:8080/#1596520748752.
// These policy choices are arbitrary for demo purposes, and can be changed however you'd like.

const createNew = false
// if (location.hash.length === 0) {
// createNew = true;
// location.hash = Date.now().toString();
// }
// const documentId = location.hash.substring(1);
// document.title = documentId;

export async function start(graph: Graph): Promise<void> {
// The getTinyliciousContainer helper function facilitates loading our container code into a Container and
// connecting to a locally-running test service called Tinylicious. This will look different when moving to a
// production service, but ultimately we'll still be getting a reference to a Container object. The helper
// function takes the ID of the document we're creating or loading, the container code to load into it, and a
// flag to specify whether we're creating a new document or loading an existing one.
const container = await getTinyliciousContainer(
'test',
GraphDataObjectContainerRuntimeFactory,
createNew,
)

// In this app, we know our container code provides a default data object that is an IGraphDataObject.
const graphDataObject: IGraphDataObject =
await getDefaultObjectFromContainer<IGraphDataObject>(container)

connectGraph(graphDataObject, graph)

// Reload the page on any further hash changes, e.g. in case you want to paste in a different document ID.
// window.addEventListener('hashchange', () => {
// location.reload();
// });
}
24 changes: 24 additions & 0 deletions examples/x6-example-features/src/collaboration/containerCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { ContainerRuntimeFactoryWithDefaultDataStore } from '@fluidframework/aqueduct'

import { GraphDataObjectInstantiationFactory } from './dataObject'

/**
* The GraphDataObjectContainerRuntimeFactory is the container code for our scenario.
*
* Since we only need to instantiate and retrieve a single dice roller for our scenario, we can use a
* ContainerRuntimeFactoryWithDefaultDataStore. We provide it with the type of the data object we want to create
* and retrieve by default, and the registry entry mapping the type to the factory.
*
* This container code will create the single default data object on our behalf and make it available on the
* Container with a URL of "/", so it can be retrieved via container.request("/").
*/
export const GraphDataObjectContainerRuntimeFactory =
new ContainerRuntimeFactoryWithDefaultDataStore(
GraphDataObjectInstantiationFactory,
new Map([GraphDataObjectInstantiationFactory.registryEntry]),
)
252 changes: 252 additions & 0 deletions examples/x6-example-features/src/collaboration/dataObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
/*!
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import type { EventEmitter } from 'events'

import { StringExt } from '@antv/x6'
import { DataObject, DataObjectFactory } from '@fluidframework/aqueduct'
import type { IFluidHandle } from '@fluidframework/core-interfaces'
import type { IValueChanged } from '@fluidframework/map'
import { SharedMap } from '@fluidframework/map'

/**
* IGraphDataObject describes the public API surface for our graph data object.
*/
export interface IGraphDataObject extends EventEmitter {
cells: SharedMap
users: SharedMap

userId: string

on(event: string, listener: (id: string, attributes: any) => void): this
on(
event: string,
listener: (id: string, attributes: any, prevAttributes: any) => void,
): this

setCell(id: string, attributes: any): void

deleteCell(id: string): void

updateUser(attributes: any): void
}

export interface IUser {
id: string
name: string
color: string
x?: number
y?: number
selection?: string
}

export const initialCells = [
{
id: 'test_node_1',
x: 100,
y: 100,
shape: 'dag-node',
data: {
id: 'test_node_1',
codeName: 'ss_sgd_train',
label: '逻辑回归',
status: 0,
},
ports: [
{
id: 'test_node_1_input_0',
group: 'top',
type: 'sf.table.vtable',
},
{
id: 'test_node_1_input_1',
group: 'top',
type: 'sf.table.individual',
},
{
id: 'test_node_1_output_0',
group: 'bottom',
type: 'sf.model.ss_sgb',
},
],
},
{
id: 'test_node_2',
x: 300,
y: 300,
shape: 'dag-node',
data: {
id: 'test_node_2',
codeName: 'ss_sgd_predict',
label: '模型预测',
status: 2,
},
ports: [
{
id: 'test_node_2_input_0',
group: 'top',
type: 'sf.model.ss_sgb',
},
{
id: 'test_node_2_input_1',
group: 'top',
type: 'sf.table.vtable',
},
{
id: 'test_node_2_output_0',
group: 'bottom',
type: 'sf.table.individual',
},
],
},
{
shape: 'dag-edge',
id: 'test_node_1_output_0__test_node_2_input_0',
source: {
cell: 'test_node_1',
port: 'test_node_1_output_0',
},
target: {
cell: 'test_node_2',
port: 'test_node_2_input_0',
},
data: {
id: 'test_node_1_output_0__test_node_2_input_0',
source: 'test_node_1',
sourceAnchor: 'test_node_1_output_0',
target: 'test_node_2',
targetAnchor: 'test_node_2_input_0',
},
zIndex: -1,
},
]

/**
* The GraphDataObject is our data object that implements the IGraphDataObject interface.
*/
export class GraphDataObject extends DataObject implements IGraphDataObject {
/**
* initializingFirstTime is run only once by the first client to create the DataObject. Here we use it to
* initialize the state of the DataObject.
*/

private _cells: SharedMap
private _users: SharedMap

protected async initializingFirstTime() {
const cellsMap = SharedMap.create(this.runtime)
const usersMap = SharedMap.create(this.runtime)

this.root.set('cellsMap', cellsMap.handle)
this.root.set('usersMap', usersMap.handle)

await this.initMaps()
initialCells.forEach((cell) => {
this.setCell(cell.id, { ...cell })
})
}

/**
* hasInitialized is run by each client as they load the DataObject. Here we use it to set up usage of the
* DataObject, by registering an event listener for graph data object.
*/
protected async hasInitialized() {
await this.initMaps()

this.cells.on('valueChanged', (changed: IValueChanged) => {
this.emit('cellChanged', changed.key, this.cells.get(changed.key))
})

this.users.on('valueChanged', (changed: IValueChanged) => {
this.emit(
'userChanged',
changed.key,
this.users.get(changed.key),
changed.previousValue,
)
})

this.addUser()
}

private async initMaps() {
if (!this._cells) {
const mapHandle = this.root.get<IFluidHandle<SharedMap>>('cellsMap')
if (!mapHandle) throw Error('Something went wrong')
this._cells = await mapHandle.get()
}
if (!this._users) {
const mapHandle = this.root.get<IFluidHandle<SharedMap>>('usersMap')
if (!mapHandle) throw Error('Something went wrong')
this._users = await mapHandle.get()
}
}

public get cells() {
return this._cells
}

public get users() {
return this._users
}

public setCell(id: string, attributes: any) {
this.cells.set(id, attributes)
}

public deleteCell(id: string) {
if (this.cells.has(id)) {
this.cells.delete(id)
}
}

public userId: string

public addUser() {
if (
sessionStorage.getItem('userId') &&
this.users.get<IUser>(<string>sessionStorage.getItem('userId'))
) {
this.userId = <string>sessionStorage.getItem('userId') //This session might have has a user
} else {
const user: IUser = {
id: StringExt.uuid(),
name: 'Fake',
color: getRandomColor(),
}
this.userId = user.id
sessionStorage.setItem('userId', user.id)
this.users.set(user.id, user)
}
}

public getUser(): IUser | undefined {
return this.users.get<IUser>(this.userId)
}

public updateUser(attributes: any) {
return this.users.set(this.userId, { ...this.getUser(), ...attributes })
}
}

function getRandomColor(): string {
const letters = '0123456789ABCDEF'
let color = '#'
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)]
}
return color
}

/**
* The DataObjectFactory is used by Fluid Framework to instantiate our DataObject. We provide it with a unique name
* and the constructor it will call. In this scenario, the third and fourth arguments are not used.
*/
export const GraphDataObjectInstantiationFactory = new DataObjectFactory(
'graph-data-object',
GraphDataObject,
[],
{},
)
1 change: 1 addition & 0 deletions examples/x6-example-features/src/collaboration/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './app'
Loading

0 comments on commit 12ca0e1

Please sign in to comment.