420 lines
12 KiB
Vue
420 lines
12 KiB
Vue
<template>
|
|
<div class="page-container md-layout-column" id="dashboard-container">
|
|
|
|
<!-- toolbar -->
|
|
<toolbar @showDrawerClicked="showDrawer = true"
|
|
@applyClicked="performApply"
|
|
:processing="anyPendingNodeMoves || processingNodeChanges || processingModelDownload"
|
|
:model-changed="modelChanged"
|
|
/>
|
|
|
|
<!-- drawer -->
|
|
<md-drawer :md-active.sync="showDrawer" md-swipeable>
|
|
<workspace-drawer-content :pending-changes="pendingChanges"/>
|
|
</md-drawer>
|
|
|
|
<!-- workspace -->
|
|
<md-content id="diagram-container">
|
|
<simple-flowchart
|
|
:scene.sync="model"
|
|
@nodeDelete="enqueuePendingNodeDeletion"
|
|
@nodeDataEdited="handleNodeDataEdit"
|
|
@linkBreak="handleLinkBreak"
|
|
@linkAdded="handleLinkAdd"
|
|
@nodeMoved="handleNodeMove"
|
|
/>
|
|
<element-adder @addElement="newElement"/>
|
|
</md-content>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
|
|
import SimpleFlowchart from '@/simple-flowchart';
|
|
import ElementAdder from "@/components/ElementAdder";
|
|
import WorkspaceDrawerContent from "@/components/WorkspaceDrawerContent";
|
|
import Toolbar from "@/components/Toolbar";
|
|
|
|
import {some, map, cloneDeep} from 'lodash';
|
|
|
|
export default {
|
|
name: 'Dashboard',
|
|
components: {
|
|
SimpleFlowchart,
|
|
ElementAdder,
|
|
WorkspaceDrawerContent,
|
|
Toolbar
|
|
},
|
|
data: () => ({
|
|
showDrawer: false,
|
|
idGenerator: 1,
|
|
pendingChanges: {
|
|
created: [],
|
|
updated: [],
|
|
deleted: [] // Stores apiId instead of localId
|
|
},
|
|
pendingNodeMoves: 0, // count of inflight coord modifications
|
|
processingNodeChanges: false,
|
|
processingModelDownload: false,
|
|
model: {
|
|
centerX: 0,
|
|
centerY: 0,
|
|
scale: 1,
|
|
nodes: [],
|
|
links: []
|
|
}
|
|
}),
|
|
methods: {
|
|
newElement(type) {
|
|
this.showNewElementChooser = false;
|
|
|
|
let newNode = {
|
|
x: 10,
|
|
y: 10,
|
|
type,
|
|
apiId: null
|
|
}
|
|
|
|
switch (type) {
|
|
case "ingest":
|
|
newNode.data = {
|
|
url: "",
|
|
streamkey: "demokey"
|
|
}
|
|
break;
|
|
case "encoder":
|
|
newNode.data = {
|
|
bitrate: 0,
|
|
width: 0,
|
|
height: 0
|
|
}
|
|
break;
|
|
case "restreamer":
|
|
newNode.data = {
|
|
url: "",
|
|
streamkey: ""
|
|
}
|
|
break;
|
|
}
|
|
|
|
newNode.id = this.idGenerator;
|
|
this.idGenerator++;
|
|
|
|
this.model.nodes.push(newNode);
|
|
//this.pendingChanges.created.add(newNode.id);
|
|
this.enqueuePendingNodeCreation(newNode.id);
|
|
|
|
},
|
|
// Vuejs can not track changes of a set, so we do this magic hack to fix it
|
|
enqueuePendingNodeDeletion({id, apiId}) {
|
|
let pending_deletes = new Set(this.pendingChanges.deleted);
|
|
let pending_updates = new Set(this.pendingChanges.updated);
|
|
let pending_creations = new Set(this.pendingChanges.created);
|
|
|
|
// This stuff is here to prevent the creation and deletion of an object in the same transaction
|
|
if (pending_creations.has(id)) {
|
|
pending_creations.delete(id);
|
|
} else {
|
|
if (apiId) {
|
|
pending_deletes.add(apiId);
|
|
}
|
|
}
|
|
|
|
pending_updates.delete(id);
|
|
|
|
this.pendingChanges.deleted = Array.from(pending_deletes);
|
|
this.pendingChanges.updated = Array.from(pending_updates);
|
|
this.pendingChanges.created = Array.from(pending_creations);
|
|
},
|
|
enqueuePendingNodeUpdate(id) {
|
|
let pending_updates = new Set(this.pendingChanges.updated);
|
|
pending_updates.add(id);
|
|
this.pendingChanges.updated = Array.from(pending_updates);
|
|
},
|
|
enqueuePendingNodeCreation(id) {
|
|
let pending_creations = new Set(this.pendingChanges.created);
|
|
pending_creations.add(id);
|
|
this.pendingChanges.created = Array.from(pending_creations);
|
|
},
|
|
// --- end of magic hack
|
|
handleNodeDataEdit(id) {
|
|
if (!this.pendingChanges.created.includes(id)) {
|
|
this.enqueuePendingNodeUpdate(id);
|
|
}
|
|
},
|
|
handleNodeMove(id) {
|
|
const moved_node = this.model.nodes.find((node) => node.id === id);
|
|
|
|
if ((!moved_node) || (!moved_node.apiId)) {
|
|
return;
|
|
}
|
|
|
|
this.pendingNodeMoves++;
|
|
this.$api.put(`objects/streamerobjects/coordmodify/${moved_node.apiId}`, {
|
|
x: moved_node.x,
|
|
y: moved_node.y
|
|
}).then(() => {
|
|
this.pendingNodeMoves--;
|
|
}).catch(() => {
|
|
this.pendingNodeMoves--;
|
|
});
|
|
|
|
},
|
|
handleLinkBreak({from, to}) {
|
|
this.enqueuePendingNodeUpdate(from);
|
|
this.enqueuePendingNodeUpdate(to);
|
|
},
|
|
handleLinkAdd({from, to}) {
|
|
this.enqueuePendingNodeUpdate(from);
|
|
this.enqueuePendingNodeUpdate(to);
|
|
},
|
|
performApply() {
|
|
this.processingNodeChanges = true;
|
|
|
|
const pendingChangesSnapshot = cloneDeep(this.pendingChanges);
|
|
const modelSnapshot = cloneDeep(this.model);
|
|
this.pendingChanges = {
|
|
created: [],
|
|
updated: [],
|
|
deleted: []
|
|
}
|
|
|
|
let creation_promises = []
|
|
|
|
// Perform creations
|
|
pendingChangesSnapshot.created.forEach((id) => {
|
|
const new_node = modelSnapshot.nodes.find((n) => n.id === id);
|
|
if (new_node) {
|
|
creation_promises.push(new Promise((resolve, reject) => {
|
|
|
|
let data = {
|
|
x: new_node.x,
|
|
y: new_node.y
|
|
}
|
|
|
|
let translated_type = new_node.type;
|
|
|
|
switch (new_node.type) {
|
|
case "ingest":
|
|
data["outputNeighbours"] = [];
|
|
break;
|
|
case "restreamer":
|
|
data["inputNeighbour"] = null;
|
|
data["outputURLs"] = new_node.data.url ? [new_node.data.url] : [];
|
|
translated_type = "restream";
|
|
break;
|
|
case "encoder":
|
|
data["inputNeighbour"] = null;
|
|
data["outputNeighbours"] = []
|
|
data["bitrate"] = new_node.data.bitrate;
|
|
data["width"] = new_node.data.width;
|
|
data["height"] = new_node.data.height;
|
|
translated_type = "encode";
|
|
break;
|
|
}
|
|
|
|
|
|
this.$api.post(`objects/streamerobjects/${translated_type}`, data).then((resp) => {
|
|
new_node.apiId = resp.data.id;
|
|
|
|
const original_node = this.model.nodes.find((n) => n.id === id);
|
|
if (original_node) {
|
|
original_node.apiId = resp.data.id;
|
|
}
|
|
|
|
if (resp.data.resource_type === 1) {
|
|
new_node.data.url = resp.data.url;
|
|
original_node.data.url = resp.data.url;
|
|
|
|
new_node.data.streamkey = resp.data.stream_key;
|
|
original_node.data.streamkey = resp.data.stream_key;
|
|
}
|
|
|
|
return resolve();
|
|
}).catch(reject);
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
// Perform Updates
|
|
Promise.all(creation_promises).then(() => {
|
|
|
|
let update_promises = [];
|
|
pendingChangesSnapshot.updated.forEach((id) => {
|
|
const changed_node = modelSnapshot.nodes.find((n) => n.id === id);
|
|
if (changed_node) {
|
|
update_promises.push(new Promise((resolve, reject) => {
|
|
|
|
const api_id = changed_node.apiId;
|
|
let data = {
|
|
x: changed_node.x,
|
|
y: changed_node.y
|
|
}
|
|
// Find neighbors
|
|
|
|
|
|
let input_neighbor = null;
|
|
let output_neighbors = [];
|
|
if (modelSnapshot.links.length > 0) {
|
|
// Input first
|
|
const input_link = modelSnapshot.links.find((link) => link.to === changed_node.id);
|
|
if (input_link) {
|
|
input_neighbor = modelSnapshot.nodes.find((node) => node.id === input_link.from);
|
|
}
|
|
|
|
// Then output
|
|
const output_links = modelSnapshot.links.filter((link) => link.from === changed_node.id);
|
|
if (output_links.length > 0) {
|
|
output_neighbors = modelSnapshot.nodes.filter((node) => map(output_links, 'to').includes(node.id));
|
|
}
|
|
}
|
|
|
|
// Compile type specific attributes
|
|
let translated_type = changed_node.type;
|
|
switch (changed_node.type) {
|
|
case "ingest":
|
|
data["outputNeighbours"] = map(output_neighbors, 'apiId');
|
|
break;
|
|
case "restreamer":
|
|
data["inputNeighbour"] = input_neighbor ? input_neighbor.apiId : null;
|
|
data["outputURLs"] = changed_node.data.url ? [changed_node.data.url] : [];
|
|
translated_type = "restream";
|
|
break;
|
|
case "encoder":
|
|
data["inputNeighbour"] = input_neighbor ? input_neighbor.apiId : null;
|
|
data["outputNeighbours"] = map(output_neighbors, 'apiId');
|
|
data["bitrate"] = changed_node.data.bitrate;
|
|
data["width"] = changed_node.data.width;
|
|
data["height"] = changed_node.data.height;
|
|
translated_type = "encode";
|
|
break;
|
|
}
|
|
|
|
// and then send it
|
|
this.$api.put(`objects/streamerobjects/${translated_type}/${api_id}`, data).then(() => {
|
|
return resolve();
|
|
}).catch(reject);
|
|
|
|
}));
|
|
}
|
|
});
|
|
|
|
// Perform deletions
|
|
Promise.all(update_promises).then(() => {
|
|
|
|
let delete_promises = [];
|
|
pendingChangesSnapshot.deleted.forEach((apiId) => {
|
|
delete_promises.push(
|
|
this.$api.delete(`objects/streamerobjects/${apiId}`)
|
|
);
|
|
});
|
|
|
|
Promise.all(delete_promises).then(() => {
|
|
this.processingNodeChanges = false;
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
|
},
|
|
mounted() {
|
|
this.processingModelDownload = true;
|
|
this.$api.get('objects/streamerobjects').then((resp) => {
|
|
|
|
// Load bare nodes first
|
|
resp.data.forEach((apiNode) => {
|
|
|
|
const type_map = {
|
|
1: "ingest",
|
|
2: "encoder",
|
|
3: "restreamer"
|
|
}
|
|
|
|
let newNode = {
|
|
x: apiNode.x,
|
|
y: apiNode.y,
|
|
type: type_map[apiNode.resource_type],
|
|
apiId: apiNode.id
|
|
}
|
|
|
|
switch (newNode.type) {
|
|
case "ingest":
|
|
newNode.data = {
|
|
url: apiNode.url,
|
|
streamkey: apiNode.stream_key
|
|
}
|
|
break;
|
|
case "encoder":
|
|
newNode.data = {
|
|
bitrate: apiNode.bitrate,
|
|
width: apiNode.width,
|
|
height: apiNode.height
|
|
}
|
|
break;
|
|
case "restreamer":
|
|
newNode.data = {
|
|
url: apiNode.output_urls[0] || '',
|
|
streamkey: apiNode.stream_key
|
|
}
|
|
break;
|
|
}
|
|
|
|
newNode.id = this.idGenerator;
|
|
this.idGenerator++;
|
|
|
|
this.model.nodes.push(newNode);
|
|
|
|
});
|
|
|
|
// Load links between nodes
|
|
let linkIdGenerator = 1;
|
|
resp.data.forEach((apiNode) => {
|
|
if (apiNode.inputNeighbour) {
|
|
const dst_node = this.model.nodes.find((node) => node.apiId === apiNode.id);
|
|
const src_node = this.model.nodes.find((node) => node.apiId === apiNode.inputNeighbour);
|
|
if (src_node && dst_node) {
|
|
|
|
const newLink = {
|
|
id: linkIdGenerator,
|
|
from: src_node.id,
|
|
to: dst_node.id
|
|
}
|
|
|
|
linkIdGenerator++;
|
|
|
|
this.model.links.push(newLink);
|
|
|
|
}
|
|
}
|
|
});
|
|
|
|
this.processingModelDownload = false;
|
|
});
|
|
},
|
|
computed: {
|
|
modelChanged() {
|
|
return some(this.pendingChanges, (o) => o.length > 0);
|
|
},
|
|
anyPendingNodeMoves() {
|
|
return this.pendingNodeMoves > 0;
|
|
}
|
|
}
|
|
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
#dashboard-container {
|
|
height: 100%;
|
|
}
|
|
|
|
#diagram-container {
|
|
height: calc(100% - 64px);
|
|
}
|
|
</style> |