videon-frontend/src/views/Dashboard.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>