Merge pull request #73 from nuxxapp/refactor/code-generation

Code generation cleanup
pull/74/head
Samuel Rowe 3 years ago committed by GitHub
commit fa53ce6f63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,7 +9,6 @@ volumes:
name: ctk_django_static
services:
postgres:
container_name: ctk-postgres
image: postgres:11
@ -52,15 +51,10 @@ services:
links:
- backend
volumes:
# configs
- ${PWD}/services/frontend/configs/nginx/uwsgi_params:/home/config/uwsgi/uwsgi_params
- ${PWD}/services/frontend/configs/nginx/localhost.conf:/etc/nginx/conf.d/default.conf
- ${PWD}/services/frontend/configs/nginx/nginx.conf:/etc/nginx/nginx.conf
# serve django static stuff
- ./services/frontend/configs/nginx/uwsgi_params:/home/config/uwsgi/uwsgi_params
- ./services/frontend/configs/nginx/localhost.conf:/etc/nginx/conf.d/default.conf
- ./services/frontend/configs/nginx/nginx.conf:/etc/nginx/nginx.conf
- ./services/frontend/build:/usr/share/nginx/html/
- django-static:/home/server/static/
# serve composer built react app
- ${PWD}/services/frontend/build:/usr/share/nginx/html/
ports:
- "80:80"

@ -12,15 +12,6 @@ psycopg2-binary==2.9.3
uwsgi==2.0.20
botocore==1.24.46
boto3==1.21.46
Jinja2==3.1.1
validators==0.19.0
requests==2.27.1
celery==5.2.3
redis==4.3.1
simple-salesforce==1.11.6
cryptography==37.0.2
chardet==4.0.0
pyaml==21.10.1
docker==5.0.3
ruamel.yaml==0.17.21
better-profanity==0.7.0

@ -1,7 +1,7 @@
from rest_framework import generics, status
from rest_framework.response import Response
from .utils import generate_dc
from .utils import generate
class GenerateGenericAPIView(generics.GenericAPIView):
@ -14,19 +14,13 @@ class GenerateGenericAPIView(generics.GenericAPIView):
request_data = request.data
version = request_data['data'].get('version', '3')
services = request_data['data'].get('services', None)
connections = request_data['data'].get('connections', None)
volumes = request_data['data'].get('volumes', None)
networks = request_data['data'].get('networks', None)
secrets = request_data['data'].get('secrets', None)
configs = request_data['data'].get('configs', None)
code = generate_dc(
code = generate(
services,
connections,
volumes,
networks,
secrets,
configs,
version=version,
return_format='yaml')
resp = {'code': code}

@ -1,18 +1,7 @@
import io
import re
import sys
import random
import string
import ast
import uuid
import contextlib
import docker
from better_profanity import profanity
from ruamel.yaml import YAML
from ruamel.yaml.scalarstring import (
SingleQuotedScalarString,
DoubleQuotedScalarString)
from ruamel.yaml.scalarstring import DoubleQuotedScalarString
from api.models import Project
@ -28,18 +17,11 @@ else:
def indent(text, amount, ch=' '):
return textwrap.indent(text, amount * ch)
def get_project_obj_by_id(id):
with contextlib.suppress(Project.DoesNotExist):
return Project.objects.get(pk=id)
return None
def get_project_obj_by_uuid(uuid):
with contextlib.suppress(Project.DoesNotExist):
return Project.objects.get(uuid=uuid)
return None
def sequence_indent_four(s):
ret_val = ''
first_indent = True
@ -58,7 +40,6 @@ def sequence_indent_four(s):
return ret_val
def sequence_indent_one(s):
ret_val = ''
first_indent = True
@ -77,405 +58,13 @@ def sequence_indent_one(s):
return ret_val
def format_quotes(s):
if '\'' in s:
return SingleQuotedScalarString(s.replace("'", ''))
if '"' in s:
return DoubleQuotedScalarString(s.replace('"', ''))
return SingleQuotedScalarString(s)
def format_volumes_top_level(volumes, compose_version):
ret = {}
for volume in volumes:
volume_custom_name = volume.get('volume_name', None)
volume_driver = volume.get('driver', None)
external = volume.get('external', False)
external_name = volume.get('external_name', False)
ret[volume['name']] = {}
if external:
ret[volume['name']]['external'] = True
if external_name:
ret[volume['name']]['external'] = {
'name': external_name
}
if volume_custom_name:
ret[volume['name']]['name'] = volume_custom_name
if volume_driver:
ret[volume['name']]['driver'] = volume_driver
if compose_version in [2, 3]:
if labels := volume.get('labels', None):
ret[volume['name']]['labels'] = {}
for label in labels:
ret[volume['name']]['labels'][label['key']] = format_quotes(label['value'])
if not ret[volume['name']]:
ret[volume['name']] = None
return ret
def format_networks_top_level(networks, compose_version):
ret = {}
for network in networks:
network_custom_name = network.get('object_name', None)
network_driver = network.get('driver', None)
network_custom_driver = network.get('driver_custom', False)
external = network.get('external', False)
external_name = network.get('external_name', False)
driver_opts = network.get('driver_opts', None)
ret[network['name']] = {}
if external:
ret[network['name']]['external'] = True
ret[network['name']]['name'] = external_name
if network_custom_name:
ret[network['name']]['name'] = network_custom_name
if network_driver:
ret[network['name']]['driver'] = network_driver
if driver_opts:
ret[network['name']]['driver_opts'] = {}
for driver_opt in driver_opts:
ret[network['name']]['driver_opts'][driver_opt['key']] = format_quotes(driver_opt['value'])
if compose_version in [2, 3]:
if labels := network.get('labels', None):
ret[network['name']]['labels'] = {}
for label in labels:
ret[network['name']]['labels'][label['key']] = format_quotes(label['value'])
if not ret[network['name']]:
ret[network['name']] = None
return ret
def format_key_val_pairs(pairs):
return {pair_part['key']: pair_part['value'] for pair_part in pairs}
def format_ports(ports):
service_ports_formatted = []
for port in ports:
port_published = port['published']
port_target = port['target']
formatter_string = DoubleQuotedScalarString(f"{port_published}")
if port_target:
formatter_string = DoubleQuotedScalarString(f"{port_published}:{port_target}")
service_ports_formatted.append(formatter_string)
return service_ports_formatted
def format_volumes(service_volumes, volumes):
ret = []
for service_volume in service_volumes:
for volume in volumes:
if 'volume' in service_volume and service_volume['volume'] == volume['uuid']:
volume_mount_str = f"{volume['name']}:{service_volume['destination']}"
ret.append(volume_mount_str)
if 'relativePathSource' in service_volume:
volume_mount_str = f"{service_volume['relativePathSource']}:{service_volume['destination']}"
ret.append(volume_mount_str)
return ret
def format_networks(service_networks, networks):
ret = []
if service_networks:
for service_network_uuid in service_networks:
for network in networks:
if service_network_uuid == network['uuid']:
network_str = f"{network['name']}"
ret.append(network_str)
return ret
def clean_string(string):
string = " ".join(re.findall(r"[a-zA-Z0-9]+", string))
string = string.lower()
string = string.replace(' ', '-')
return string
def format_command_string(command):
"""
Format command list of string for v1, v2, v3.
param: command: string
return: list
"""
command_list = []
command = str(command)
command_list = command.replace("\n", "")
try:
# try to convert the string into list
command_list = ast.literal_eval(command_list)
except (ValueError, SyntaxError) as e:
# special case
if "\n" in command:
command_list = command.split("\n")
else:
return command
except Exception as e:
return command
if len(command_list) > 1:
longest_str = max(command_list, key=len)
if len(longest_str) >= 30:
return [format_quotes(i) for i in command_list]
return FSlist(command_list)
return command_list[0]
def format_build(specified_version, build):
if isinstance(build, str):
return build
build_str = build.get('build', None)
context_str = build.get('context', None)
ret = {}
if specified_version < 2:
if build_str:
return build_str
elif context_str:
return context_str
else:
return None
if build_str:
return build_str
for _key, _val in build.items():
if _val:
if _key in ['args', 'cache_from', 'labels']:
ret[_key] = format_key_val_pairs(_val)
else:
ret[_key] = _val
return ret
def _remove_missing_and_underscored_keys(str):
if not str: return str
for key in list(str.keys()):
if isinstance(str[key], list):
str[key] = list(filter(None, str[key]))
if not str.get(key):
del str[key]
elif isinstance(str[key], dict):
str[key] = _remove_missing_and_underscored_keys(str[key])
if str[key] is None or str[key] == {}:
del str[key]
return str
def format_deploy(deploy):
ret = deploy
with contextlib.suppress(Exception):
placement_preferences = deploy['placement']['preferences']
ret['placement']['preferences'] = format_key_val_pairs(placement_preferences)
with contextlib.suppress(Exception):
labels = deploy['labels']
ret['labels'] = format_key_val_pairs(labels)
ret = _remove_missing_and_underscored_keys(ret)
return ret
def get_version(verion):
try:
return int(verion)
except ValueError:
return float(verion)
def format_services_version_one(specified_version, services, volumes, networks):
services_formatted = {}
for service in services:
service_formatted = {}
if image := service.get('image', None):
image_tag = "latest"
try:
image_tag = service['tag']
service_formatted['image'] = f"{image}:{image_tag}"
except KeyError:
service_formatted['image'] = f"{image}"
with contextlib.suppress(KeyError):
if service['container_name']:
service_formatted['container_name'] = service['container_name']
with contextlib.suppress(KeyError):
if service['restart']:
service_formatted['restart'] = service['restart']
with contextlib.suppress(KeyError):
if service['command']:
service_formatted['command'] = format_command_string(service['command'])
with contextlib.suppress(KeyError):
if service['entrypoint']:
service_formatted['entrypoint'] = format_command_string(service['entrypoint'])
with contextlib.suppress(KeyError):
if service['working_dir']:
service_formatted['working_dir'] = service['working_dir']
with contextlib.suppress(KeyError):
if service['ports']:
service_formatted['ports'] = format_ports(service['ports'])
with contextlib.suppress(KeyError):
if links := service.get('links', []):
service_formatted['links'] = []
for link in links:
for service_obj in services:
if link == service_obj['uuid']:
service_formatted['links'].append(f"{service_obj['name']}")
with contextlib.suppress(KeyError):
if service['environment']:
envs = service['environment']
service_formatted['environment'] = format_key_val_pairs(envs)
with contextlib.suppress(KeyError):
service_volumes = service['volumes']
if formatted_volumes := format_volumes(service_volumes, volumes):
service_formatted['volumes'] = formatted_volumes
else:
del service_formatted['volumes']
with contextlib.suppress(KeyError):
if build := format_build(specified_version, service['build']):
service_formatted['build'] = build
services_formatted[service['name']] = service_formatted
return services_formatted
def get_service_by_label_key(key, services):
for service in services:
with contextlib.suppress(KeyError):
if key == service["labels"]["key"]:
return service
return None
def get_connected_services(service_key, connections, services):
connected_services = []
for connection in connections:
if service_key == connection[0]:
if connected_service := get_service_by_label_key(connection[1], services):
connected_services.append(connected_service)
return connected_services
def format_services_version_three(specified_version, services, connections, volumes, networks):
services_formatted = {}
for service in services:
service_formatted = {}
service_key = ""
# add labels excluding certain keys
if labels := service.get('labels', {}):
clean_labels = {x: labels[x] for x in labels if x not in ["key"]}
if "key" in labels:
service_key = labels["key"]
if bool(clean_labels):
service_formatted['labels'] = clean_labels
# image name
if image := service.get('image', None):
image_tag = "latest"
try:
image_tag = service['tag']
service_formatted['image'] = f"{image}:{image_tag}"
except KeyError:
service_formatted['image'] = f"{image}"
# dependencies
with contextlib.suppress(KeyError):
if connected_services := get_connected_services(service_key, connections, services):
service_formatted['depends_on'] = []
for connected_service in connected_services:
service_formatted['depends_on'].append(f"{connected_service['service_name']}")
with contextlib.suppress(KeyError):
if service['container_name']:
service_formatted['container_name'] = service['container_name']
with contextlib.suppress(KeyError):
if service['restart']:
service_formatted['restart'] = service['restart']
with contextlib.suppress(KeyError):
if service['command']:
service_formatted['command'] = format_command_string(service['command'])
with contextlib.suppress(KeyError):
if service['entrypoint']:
service_formatted['entrypoint'] = format_command_string(service['entrypoint'])
with contextlib.suppress(KeyError):
if working_dir_str := service['working_dir']:
service_formatted['working_dir'] = working_dir_str
with contextlib.suppress(KeyError):
if service['ports']:
service_formatted['ports'] = format_ports(service['ports'])
with contextlib.suppress(KeyError):
if service['environment']:
envs = service['environment']
service_formatted['environment'] = format_key_val_pairs(envs)
with contextlib.suppress(KeyError):
service_volumes = service['volumes']
if formatted_volumes := format_volumes(service_volumes, volumes):
service_formatted['volumes'] = formatted_volumes
else:
del service_formatted['volumes']
with contextlib.suppress(KeyError):
service_networks = service.get('networks', [])
if formatted_networks := format_networks(service_networks, networks):
service_formatted['networks'] = formatted_networks
else:
del service_formatted['networks']
with contextlib.suppress(KeyError):
if build := format_build(specified_version, service['build']):
service_formatted['build'] = build
if int(float(specified_version)) >= 3:
with contextlib.suppress(KeyError):
if deploy := format_deploy(service['deploy']):
service_formatted['deploy'] = deploy
services_formatted[service['service_name']] = service_formatted
return services_formatted
def FSlist(l): # concert list into flow-style (default is block style)
from ruamel.yaml.comments import CommentedSeq
double_quoted_list = [DoubleQuotedScalarString(x) for x in l]
cs = CommentedSeq(double_quoted_list)
cs.fa.set_flow_style()
return cs
def generate_dc(services, connections, volumes, networks, secrets, configs, version="3", return_format='yaml'):
def generate(services, volumes, networks, version="3", return_format='yaml'):
if return_format != 'yaml':
return
@ -487,153 +76,26 @@ def generate_dc(services, connections, volumes, networks, secrets, configs, vers
base_version = int(specified_version)
if services:
ret_yaml.dump({'version': DoubleQuotedScalarString(specified_version)}, s)
ret_yaml.explicit_start = False
s.write('\n')
if base_version in {2, 3}:
ret_yaml.dump({'version': DoubleQuotedScalarString(specified_version)}, s)
ret_yaml.explicit_start = False
s.write('\n')
services_formatted = format_services_version_three(specified_version, services, connections, volumes, networks)
ret_yaml.dump({'services': services_formatted}, s, transform=sequence_indent_four)
ret_yaml.dump({'services': services}, s, transform=sequence_indent_four)
if base_version == 1:
ret_yaml.dump({'version': DoubleQuotedScalarString(specified_version)}, s)
ret_yaml.explicit_start = False
s.write('\n')
services_formatted = format_services_version_one(specified_version, services, volumes, networks)
ret_yaml.dump(services_formatted, s, transform=sequence_indent_one)
ret_yaml.dump(services, s, transform=sequence_indent_one)
s.write('\n')
if base_version in {3, 2} and networks:
networks_formatted = format_networks_top_level(networks, version)
ret_yaml.dump({'networks': networks_formatted}, s)
ret_yaml.dump({'networks': networks}, s)
s.write('\n')
if volumes:
volumes_formatted = format_volumes_top_level(volumes, version)
ret_yaml.dump({'volumes': volumes_formatted}, s)
s.write('\n')
if secrets:
ret_yaml.dump({'secrets': secrets}, s)
s.write('\n')
if configs:
ret_yaml.dump({'configs': configs}, s)
ret_yaml.dump({'volumes': volumes}, s)
s.write('\n')
s.seek(0)
return s
def generate(cname):
c = docker.from_env()
try:
cid = [x.short_id for x in c.containers.list() if cname == x.name or x.short_id in cname][0]
except IndexError:
sys.exit(1)
cattrs = c.containers.get(cid).attrs
cfile = {cattrs['Name'][1:]: {}}
ct = cfile[cattrs['Name'][1:]]
values = {
'cap_add': cattrs['HostConfig']['CapAdd'],
'cap_drop': cattrs['HostConfig']['CapDrop'],
'cgroup_parent': cattrs['HostConfig']['CgroupParent'],
'container_name': cattrs['Name'][1:],
'devices': cattrs['HostConfig']['Devices'],
'dns': cattrs['HostConfig']['Dns'],
'dns_search': cattrs['HostConfig']['DnsSearch'],
'environment': cattrs['Config']['Env'],
'extra_hosts': cattrs['HostConfig']['ExtraHosts'],
'image': cattrs['Config']['Image'],
'labels': cattrs['Config']['Labels'],
'links': cattrs['HostConfig']['Links'],
#'log_driver': cattrs['HostConfig']['LogConfig']['Type'],
#'log_opt': cattrs['HostConfig']['LogConfig']['Config'],
'logging': {'driver': cattrs['HostConfig']['LogConfig']['Type'], 'options': cattrs['HostConfig']['LogConfig']['Config']},
'networks': {x: {'aliases': cattrs['NetworkSettings']['Networks'][x]['Aliases']} for x in cattrs['NetworkSettings']['Networks'].keys()},
'security_opt': cattrs['HostConfig']['SecurityOpt'],
'ulimits': cattrs['HostConfig']['Ulimits'],
'volumes': cattrs['HostConfig']['Binds'],
'volume_driver': cattrs['HostConfig']['VolumeDriver'],
'volumes_from': cattrs['HostConfig']['VolumesFrom'],
'cpu_shares': cattrs['HostConfig']['CpuShares'],
'cpuset': cattrs['HostConfig']['CpusetCpus']+','+cattrs['HostConfig']['CpusetMems'],
'entrypoint': cattrs['Config']['Entrypoint'],
'user': cattrs['Config']['User'],
'working_dir': cattrs['Config']['WorkingDir'],
'domainname': cattrs['Config']['Domainname'],
'hostname': cattrs['Config']['Hostname'],
'ipc': cattrs['HostConfig']['IpcMode'],
'mac_address': cattrs['NetworkSettings']['MacAddress'],
'mem_limit': cattrs['HostConfig']['Memory'],
'memswap_limit': cattrs['HostConfig']['MemorySwap'],
'privileged': cattrs['HostConfig']['Privileged'],
'restart': cattrs['HostConfig']['RestartPolicy']['Name'],
'read_only': cattrs['HostConfig']['ReadonlyRootfs'],
'stdin_open': cattrs['Config']['OpenStdin'],
'tty': cattrs['Config']['Tty']
}
networklist = c.networks.list()
networks = {network.attrs['Name']: {'external': (not network.attrs['Internal'])} for network in networklist if network.attrs['Name'] in values['networks'].keys()}
# Check for command and add it if present.
if cattrs['Config']['Cmd'] != None:
values['command'] = " ".join(cattrs['Config']['Cmd']),
# Check for exposed/bound ports and add them if needed.
try:
expose_value = list(cattrs['Config']['ExposedPorts'].keys())
ports_value = [cattrs['HostConfig']['PortBindings'][key][0]['HostIp']+':'+cattrs['HostConfig']['PortBindings'][key][0]['HostPort']+':'+key for key in cattrs['HostConfig']['PortBindings']]
# If bound ports found, don't use the 'expose' value.
if ports_value not in [None, "", [], 'null', {}, "default", 0, ",", "no"]:
for index, port in enumerate(ports_value):
if port[0] == ':':
ports_value[index] = port[1:]
values['ports'] = ports_value
else:
values['expose'] = expose_value
except (KeyError, TypeError):
# No ports exposed/bound. Continue without them.
ports = None
# Iterate through values to finish building yaml dict.
for key, value in values.items():
if value not in [None, "", [], 'null', {}, "default", 0, ",", "no"]:
ct[key] = value
return cfile, networks
def generate_uuid():
return uuid.uuid4().hex[:6].upper()
def random_string(string_length=10):
"""
Generate a random string of fixed length
:param string_length: integer
:return: string
"""
final_string = ''.join(random.choice(
string.ascii_uppercase +
string.ascii_lowercase +
string.digits) for _ in range(string_length))
final_string = profanity.censor(final_string, '').lower()
return final_string
def generate_rand_string():
return "".join(
random.choice(
string.ascii_uppercase + string.ascii_lowercase + string.digits
) for _ in range(16)
)

@ -1,4 +1,3 @@
user www-data;
worker_processes 4;
pid /run/nginx.pid;

@ -56,8 +56,14 @@ export default function VolumeNode(props: INodeProps) {
)}
<div className="relative node-label w-full py-2 px-4">
<>
{node.volumeConfig.name && (
{node.canvasConfig.node_name && (
<div className="text-sm font-semibold overflow-x-hidden">
{truncateStr(node.canvasConfig.node_name, 12)}
</div>
)}
{node.volumeConfig.name && (
<div className="text-xs text-gray-500 overflow-x-hidden">
{truncateStr(node.volumeConfig.name, 20)}
</div>
)}

@ -23,6 +23,12 @@ const ModalVolumeCreate = (props: IModalVolumeCreate) => {
formik.resetForm();
};
const validationSchema = yup.object({
canvasConfig: yup.object({
node_name: yup
.string()
.max(256, "volume name should be 256 characters or less")
.required("volume name is required")
}),
volumeConfig: yup.object({
name: yup
.string()

@ -4,7 +4,12 @@ import * as yup from "yup";
import { XIcon } from "@heroicons/react/outline";
import General from "./General";
import Labels from "./Labels";
import { CallbackFunction, IVolumeNodeItem } from "../../../types";
import {
CallbackFunction,
ICanvasConfig,
IVolumeNodeItem,
IVolumeTopLevel
} from "../../../types";
interface IModalVolumeEdit {
node: IVolumeNodeItem;
@ -18,10 +23,17 @@ const ModalVolumeEdit = (props: IModalVolumeEdit) => {
const [selectedNode, setSelectedNode] = useState<IVolumeNodeItem>();
const handleUpdate = (values: any) => {
const updated = { ...selectedNode };
updated.canvasConfig = values.canvasConfig;
updated.volumeConfig = values.volumeConfig;
onUpdateEndpoint(updated);
};
const validationSchema = yup.object({
canvasConfig: yup.object({
node_name: yup
.string()
.max(256, "volume name should be 256 characters or less")
.required("volume name is required")
}),
volumeConfig: yup.object({
name: yup
.string()
@ -79,9 +91,12 @@ const ModalVolumeEdit = (props: IModalVolumeEdit) => {
{selectedNode && (
<Formik
initialValues={{
canvasConfig: {
...selectedNode.canvasConfig
} as ICanvasConfig,
volumeConfig: {
...selectedNode.volumeConfig
}
} as IVolumeTopLevel
}}
enableReinitialize={true}
onSubmit={(values) => {

@ -3,6 +3,7 @@ import TextField from "../../global/FormElements/InputField";
const General = () => {
return (
<>
<TextField label="Volume name" name="canvasConfig.node_name" />
<TextField label="Name" name="volumeConfig.name" />
</>
);

@ -343,16 +343,11 @@ export interface IProjectPayload {
};
}
export interface ISaturatedService extends Partial<IService>, ICanvasConfig {}
export interface IGeneratePayload {
data: {
configs: [];
networks: [];
secrets: [];
services: ISaturatedService[];
connections: [[string, string]];
version: number;
volumes: [];
networks: Partial<INetworkTopLevel>[];
services: Record<string, Partial<IService>>;
volumes: Record<string, Partial<IVolumeTopLevel>>;
};
}

@ -1,40 +1,26 @@
import {
IServiceNodeItem,
IGeneratePayload,
ISaturatedService
} from "../types";
import { Dictionary } from "lodash";
const getServices = (
graphNodes: Dictionary<IServiceNodeItem>
): ISaturatedService[] => {
const ret: ISaturatedService[] = [];
for (const [, value] of Object.entries(graphNodes)) {
ret.push({
...value.canvasConfig,
...value.serviceConfig
});
}
return ret;
};
import { IGeneratePayload } from "../types";
export const flattenGraphData = (graphData: any): IGeneratePayload => {
const nodes = graphData["nodes"];
const base: IGeneratePayload = {
data: {
version: 3,
configs: [],
networks: [],
secrets: [],
services: [],
connections: graphData["connections"],
volumes: []
services: {},
volumes: {}
}
};
getServices(nodes).forEach((x) => {
base.data.services.push(x);
Object.keys(nodes).forEach((key) => {
if (nodes[key].type === "SERVICE") {
base.data.services[nodes[key].canvasConfig.node_name] =
nodes[key].serviceConfig;
}
if (nodes[key].type === "VOLUME") {
base.data.volumes[nodes[key].canvasConfig.node_name] =
nodes[key].volumeConfig;
}
});
return base;

@ -204,6 +204,7 @@ export const topLevelNetworkConfigInitialValues =
export const volumeConfigCanvasInitialValues = (): ICanvasConfig => {
return {
node_name: "unnamed",
node_icon: ""
};
};

Loading…
Cancel
Save