Compare commits
9
Commits
v4.2.1
...
release_4.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ca128a76b | ||
|
|
db76752a82 | ||
|
|
47f263f5c2 | ||
|
|
6eb20004f9 | ||
|
|
836d9f40d7 | ||
|
|
607347ff18 | ||
|
|
0f6051731a | ||
|
|
3c9aebfd2f | ||
|
|
60a5fe1eae |
@@ -14,31 +14,15 @@ Options to export to xml (for draw.io/diagrams.net) or png.
|
|||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
**_NOTE:_** For docker please see: [Docker install](https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins)
|
**_NOTE:_** For Docker please see: [Docker install](https://github.com/netbox-community/netbox-docker/wiki/Using-Netbox-Plugins)
|
||||||
|
|
||||||
**_NOTE:_** Add `RUN mkdir -p /opt/netbox/netbox/static/netbox_topology_views/img` to the Dockerfile-Plugins file to create the image folder
|
**_NOTE:_** Add `RUN mkdir -p /opt/netbox/netbox/static/netbox_topology_views/img` to the Dockerfile-Plugins file to create the image folder.
|
||||||
|
|
||||||
The plugin is available as a Python package and can be installed with pip.
|
The plugin is available as a Python package and can be installed with pip.
|
||||||
|
|
||||||
Run `pip install netbox-topology-views` in your virtual env.
|
|
||||||
|
|
||||||
To ensure NetBox Topology Views plugin is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the NetBox root directory (alongside `requirements.txt`) and list the `netbox-topology-views` package:
|
|
||||||
|
|
||||||
```no-highlight
|
|
||||||
# echo netbox-topology-views >> local_requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
Once installed, the plugin needs to be enabled in your `configuration.py`
|
|
||||||
|
|
||||||
```python
|
|
||||||
# In your configuration.py
|
|
||||||
PLUGINS = ["netbox_topology_views"]
|
|
||||||
```
|
|
||||||
|
|
||||||
First run `source /opt/netbox/venv/bin/activate` to enter the Python virtual environment.
|
First run `source /opt/netbox/venv/bin/activate` to enter the Python virtual environment.
|
||||||
|
|
||||||
|
Then run:
|
||||||
Then run
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/netbox/netbox
|
cd /opt/netbox/netbox
|
||||||
pip3 install netbox-topology-views
|
pip3 install netbox-topology-views
|
||||||
@@ -46,6 +30,26 @@ python3 manage.py migrate netbox_topology_views
|
|||||||
python3 manage.py collectstatic --no-input
|
python3 manage.py collectstatic --no-input
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To ensure NetBox Topology Views plugin is automatically re-installed during future upgrades, create a file named `local_requirements.txt` (if not already existing) in the NetBox root directory (alongside `requirements.txt`) and list the `netbox-topology-views` package:
|
||||||
|
|
||||||
|
```no-highlight
|
||||||
|
echo netbox-topology-views >> /opt/netbox/local_requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Once installed, the plugin needs to be enabled in your `configuration.py`:
|
||||||
|
```no-highlight
|
||||||
|
vim /opt/netbox/netbox/netbox/configuration.py
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
PLUGINS = ["netbox_topology_views"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, restart NetBox:
|
||||||
|
```no-highlight
|
||||||
|
systemctl restart netbox
|
||||||
|
```
|
||||||
|
|
||||||
### Versions
|
### Versions
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
@@ -53,6 +57,8 @@ python3 manage.py collectstatic --no-input
|
|||||||
|
|
||||||
| netbox version | netbox-topology-views version |
|
| netbox version | netbox-topology-views version |
|
||||||
| -------------- | ----------------------------- |
|
| -------------- | ----------------------------- |
|
||||||
|
| = 4.3.X | = v4.3.0 |
|
||||||
|
| = 4.2.1 | = v4.2.1 |
|
||||||
| = 4.2.0 | = v4.2.0 |
|
| = 4.2.0 | = v4.2.0 |
|
||||||
| = 4.1.X | = v4.1.0 |
|
| = 4.1.X | = v4.1.0 |
|
||||||
| = 4.0.X | = v4.0.0 |
|
| = 4.0.X | = v4.0.0 |
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
version: 0.1
|
version: 0.1
|
||||||
package_name: netbox-topology-views
|
package_name: netbox-topology-views
|
||||||
compatibility:
|
compatibility:
|
||||||
|
- release: 4.3.0
|
||||||
|
netbox_min: 4.3.0
|
||||||
|
netbox_max: 4.3.1
|
||||||
- release: 4.2.0
|
- release: 4.2.0
|
||||||
netbox_min: 4.2.0
|
netbox_min: 4.2.0
|
||||||
netbox_max: 4.2.1
|
netbox_max: 4.2.1
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class TopologyViewsConfig(PluginConfig):
|
|||||||
name = "netbox_topology_views"
|
name = "netbox_topology_views"
|
||||||
verbose_name = "Topology views"
|
verbose_name = "Topology views"
|
||||||
description = "An plugin to render topology maps"
|
description = "An plugin to render topology maps"
|
||||||
version = "4.2.1"
|
version = "4.3.0"
|
||||||
author = "Mattijs Vanhaverbeke"
|
author = "Mattijs Vanhaverbeke"
|
||||||
author_email = "author@example.com"
|
author_email = "author@example.com"
|
||||||
base_url = "netbox_topology_views"
|
base_url = "netbox_topology_views"
|
||||||
|
|||||||
@@ -516,7 +516,6 @@ class IndividualOptionsForm(NetBoxModelForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet
|
FieldSet
|
||||||
(
|
(
|
||||||
'user_id',
|
|
||||||
'ignore_cable_type',
|
'ignore_cable_type',
|
||||||
'preselected_device_roles',
|
'preselected_device_roles',
|
||||||
'preselected_tags',
|
'preselected_tags',
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
(()=>{var l=(e,t,o)=>new Promise((n,r)=>{var c=s=>{try{i(o.next(s))}catch(a){r(a)}},m=s=>{try{i(o.throw(s))}catch(a){r(a)}},i=s=>s.done?n(s.value):Promise.resolve(s.value).then(c,m);i((o=o.apply(e,t)).next())});var d=e=>{if(!document.cookie)return;let t=null,o=document.cookie.split(";");for(let n=0;n<o.length;n++){let r=o[n].trim();if(r.substring(0,e.length+1)===e+"="){t=decodeURIComponent(r.substring(e.length+1));break}}return t};var u={success:e=>{let t=document.querySelector("#topology-plugin-success-toast");if(!t)return console.error("Could not find toast component!");let o=t.querySelector("span");o.textContent=e,new window.Toast(t).show()},error:e=>{let t=document.querySelector("#topology-plugin-error-toast");if(!t)return console.error("Could not find toast component!");let o=t.querySelector("span");o.textContent=e,new window.Toast(t).show()}};var g={},p=d("csrftoken");document.querySelector("form#images").addEventListener("submit",e=>l(void 0,null,function*(){e.preventDefault();try{let t=yield fetch("/"+basePath+"api/plugins/netbox_topology_views/images/save/",{method:"POST",body:JSON.stringify(g),headers:{"X-CSRFToken":p,"Content-Type":"application/json"}});if(!t.ok)throw new Error(yield t.text());u.success("Saved settings")}catch(t){console.dir(t),u.error(t.message)}}));document.querySelectorAll("form#images .dropdown-menu img").forEach(e=>{e.addEventListener("click",t=>{var c;if(!(t.currentTarget instanceof HTMLElement))return;let{dataset:{role:o,image:n}}=t.currentTarget;g[o]=n;let r=(c=t.currentTarget.closest(".dropdown"))==null?void 0:c.querySelector(`#dropdownMenuButton${o}`);r&&(r.innerHTML=`<img src="${n}" />`)})});})();
|
(()=>{var l=(o,t)=>()=>(o&&(t=o(o=0)),t);var k=(o,t)=>()=>(t||o((t={exports:{}}).exports,t),t.exports);var d=(o,t,e)=>new Promise((n,r)=>{var c=s=>{try{i(e.next(s))}catch(a){r(a)}},y=s=>{try{i(e.throw(s))}catch(a){r(a)}},i=s=>s.done?n(s.value):Promise.resolve(s.value).then(c,y);i((e=e.apply(o,t)).next())});var g,m=l(()=>{g=o=>{if(!document.cookie)return;let t=null,e=document.cookie.split(";");for(let n=0;n<e.length;n++){let r=e[n].trim();if(r.substring(0,o.length+1)===o+"="){t=decodeURIComponent(r.substring(o.length+1));break}}return t}});var u,p=l(()=>{u={success:o=>{let t=document.querySelector("#topology-plugin-success-toast");if(!t)return console.error("Could not find toast component!");let e=t.querySelector("span");e.textContent=o,new window.Toast(t).show()},error:o=>{let t=document.querySelector("#topology-plugin-error-toast");if(!t)return console.error("Could not find toast component!");let e=t.querySelector("span");e.textContent=o,new window.Toast(t).show()}}});var S=k(w=>{m();p();var f={},h=g("csrftoken");document.querySelector("form#images").addEventListener("submit",o=>d(null,null,function*(){o.preventDefault();try{let t=yield fetch("/"+basePath+"api/plugins/netbox_topology_views/images/save/",{method:"POST",body:JSON.stringify(f),headers:{"X-CSRFToken":h,"Content-Type":"application/json"}});if(!t.ok)throw new Error(yield t.text());u.success("Saved settings")}catch(t){console.dir(t),u.error(t.message)}}));document.querySelectorAll("form#images .dropdown-menu img").forEach(o=>{o.addEventListener("click",t=>{var c;if(!(t.currentTarget instanceof HTMLElement))return;let{dataset:{role:e,image:n}}=t.currentTarget;f[e]=n;let r=(c=t.currentTarget.closest(".dropdown"))==null?void 0:c.querySelector(`#dropdownMenuButton${e}`);r&&(r.innerHTML=`<img src="${n}" />`)})})});S();})();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"sources": ["../../../static_dev/js/csrftoken.js", "../../../static_dev/js/toast.js", "../../../static_dev/js/images.js"],
|
"sources": ["../../../static_dev/js/csrftoken.js", "../../../static_dev/js/toast.js", "../../../static_dev/js/images.js"],
|
||||||
"mappings": "mNAAO,GAAM,GAAY,AAAC,GAAS,CAC/B,GAAI,CAAC,SAAS,OAAQ,OAEtB,GAAI,GAAc,KACZ,EAAU,SAAS,OAAO,MAAM,KAEtC,OAAS,GAAI,EAAG,EAAI,EAAQ,OAAQ,IAAK,CACrC,GAAM,GAAS,EAAQ,GAAG,OAE1B,GAAI,EAAO,UAAU,EAAG,EAAK,OAAS,KAAO,EAAO,IAAK,CACrD,EAAc,mBAAmB,EAAO,UAAU,EAAK,OAAS,IAChE,OAIR,MAAO,ICfJ,GAAM,GAAQ,CACjB,QAAS,AAAC,GAAY,CAClB,GAAM,GAAK,SAAS,cAAc,kCAClC,GAAI,CAAC,EAAI,MAAO,SAAQ,MAAM,mCAC9B,GAAM,GAAU,EAAG,cAAc,QACjC,EAAQ,YAAc,EAEtB,AADc,GAAI,QAAO,MAAM,GACzB,QAEV,MAAO,AAAC,GAAY,CAChB,GAAM,GAAK,SAAS,cAAc,gCAClC,GAAI,CAAC,EAAI,MAAO,SAAQ,MAAM,mCAC9B,GAAM,GAAU,EAAG,cAAc,QACjC,EAAQ,YAAc,EAEtB,AADc,GAAI,QAAO,MAAM,GACzB,SCZd,GAAM,GAAU,GACV,EAAY,EAAU,aAE5B,SAAS,cAAc,eAAe,iBAAiB,SAAU,AAAO,GAAM,0BAC1E,EAAE,iBACF,GAAI,CACA,GAAM,GAAM,KAAM,OAAM,IAAM,SAAW,iDAAkD,CACvF,OAAQ,OACR,KAAM,KAAK,UAAU,GACrB,QAAS,CACL,cAAe,EACf,eAAgB,sBAIxB,GAAI,CAAC,EAAI,GAAI,KAAM,IAAI,OAAM,KAAM,GAAI,QACvC,EAAM,QAAQ,wBACT,EAAP,CACE,QAAQ,IAAI,GACZ,EAAM,MAAM,EAAI,aAIxB,SAAS,iBAAiB,kCAAkC,QAAQ,AAAC,GAAO,CACxE,EAAG,iBAAiB,QAAS,AAAC,GAAM,CA3BxC,MA4BQ,GAAI,CAAE,GAAE,wBAAyB,cAAc,OAC/C,GAAM,CACF,QAAS,CAAE,OAAM,UACjB,EAAE,cAEN,EAAQ,GAAQ,EAEhB,GAAM,GAAS,KAAE,cACZ,QAAQ,eADE,cAET,cAAc,sBAAsB,KAC1C,AAAI,GAAQ,GAAO,UAAY,aAAa",
|
"mappings": "oTAAA,IAAaA,EAAbC,EAAAC,EAAA,KAAaF,EAAaG,GAAS,CAC/B,GAAI,CAAC,SAAS,OAAQ,OAEtB,IAAIC,EAAc,KACZC,EAAU,SAAS,OAAO,MAAM,GAAG,EAEzC,QAASC,EAAI,EAAGA,EAAID,EAAQ,OAAQC,IAAK,CACrC,IAAMC,EAASF,EAAQC,CAAC,EAAE,KAAK,EAE/B,GAAIC,EAAO,UAAU,EAAGJ,EAAK,OAAS,CAAC,IAAMA,EAAO,IAAK,CACrDC,EAAc,mBAAmBG,EAAO,UAAUJ,EAAK,OAAS,CAAC,CAAC,EAClE,KACJ,CACJ,CAEA,OAAOC,CACX,IChBA,IAAaI,EAAbC,EAAAC,EAAA,KAAaF,EAAQ,CACjB,QAAUG,GAAY,CAClB,IAAMC,EAAK,SAAS,cAAc,gCAAgC,EAClE,GAAI,CAACA,EAAI,OAAO,QAAQ,MAAM,iCAAiC,EAC/D,IAAMC,EAAUD,EAAG,cAAc,MAAM,EACvCC,EAAQ,YAAcF,EACR,IAAI,OAAO,MAAMC,CAAE,EAC3B,KAAK,CACf,EACA,MAAQD,GAAY,CAChB,IAAMC,EAAK,SAAS,cAAc,8BAA8B,EAChE,GAAI,CAACA,EAAI,OAAO,QAAQ,MAAM,iCAAiC,EAC/D,IAAMC,EAAUD,EAAG,cAAc,MAAM,EACvCC,EAAQ,YAAcF,EACR,IAAI,OAAO,MAAMC,CAAE,EAC3B,KAAK,CACf,CACJ,ICjBA,IAAAE,EAAAC,EAAAC,GAAA,CAAAC,IACAC,IAEA,IAAMC,EAAU,CAAC,EACXC,EAAYC,EAAU,WAAW,EAEvC,SAAS,cAAc,aAAa,EAAE,iBAAiB,SAAiBC,GAAMC,EAAA,sBAC1ED,EAAE,eAAe,EACjB,GAAI,CACA,IAAME,EAAM,MAAM,MAAM,IAAM,SAAW,iDAAkD,CACvF,OAAQ,OACR,KAAM,KAAK,UAAUL,CAAO,EAC5B,QAAS,CACL,cAAeC,EACf,eAAgB,kBACpB,CACJ,CAAC,EAED,GAAI,CAACI,EAAI,GAAI,MAAM,IAAI,MAAM,MAAMA,EAAI,KAAK,CAAC,EAC7CC,EAAM,QAAQ,gBAAgB,CAClC,OAASC,EAAK,CACV,QAAQ,IAAIA,CAAG,EACfD,EAAM,MAAMC,EAAI,OAAO,CAC3B,CACJ,EAAC,EAED,SAAS,iBAAiB,gCAAgC,EAAE,QAASC,GAAO,CACxEA,EAAG,iBAAiB,QAAUL,GAAM,CA3BxC,IAAAM,EA4BQ,GAAI,EAAEN,EAAE,yBAAyB,aAAc,OAC/C,GAAM,CACF,QAAS,CAAE,KAAAO,EAAM,MAAAC,CAAM,CAC3B,EAAIR,EAAE,cAENH,EAAQU,CAAI,EAAIC,EAEhB,IAAMC,GAASH,EAAAN,EAAE,cACZ,QAAQ,WAAW,IADT,YAAAM,EAET,cAAc,sBAAsBC,CAAI,IAC1CE,IAAQA,EAAO,UAAY,aAAaD,CAAK,OACrD,CAAC,CACL,CAAC",
|
||||||
"names": []
|
"names": ["getCookie", "init_csrftoken", "__esmMin", "name", "cookieValue", "cookies", "i", "cookie", "toast", "init_toast", "__esmMin", "message", "el", "content", "require_images", "__commonJSMin", "exports", "init_csrftoken", "init_toast", "mapping", "csrftoken", "getCookie", "e", "__async", "res", "toast", "err", "el", "_a", "role", "image", "button"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
|
|||||||
// Extract node ids and node type ids from all nodes
|
// Extract node ids and node type ids from all nodes
|
||||||
for (let [key, value] of nodes._data) {
|
for (let [key, value] of nodes._data) {
|
||||||
if (value[typeId] != undefined) {
|
if (value[typeId] != undefined) {
|
||||||
nodesArray.push([value.id, value[typeId], value[type]]);
|
nodesArray.push([value.id, value[typeId], value[type], value.label_num_lines]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Split single array above into arrays grouped by node id
|
// Split single array above into arrays grouped by node id
|
||||||
@@ -390,16 +390,24 @@ const coordSaveCheckbox = document.querySelector('#id_save_coords')
|
|||||||
const rectangles = [];
|
const rectangles = [];
|
||||||
const xValues = [];
|
const xValues = [];
|
||||||
const yValues = [];
|
const yValues = [];
|
||||||
|
const yValuesWithLabel = [];
|
||||||
|
const labelLineHeight = 16;
|
||||||
|
|
||||||
for(let val of value[1]) {
|
// Get coordinates for every node in a given group
|
||||||
|
for(let val of value[1]) {
|
||||||
xValues.push(graph.getPosition(val[0]).x);
|
xValues.push(graph.getPosition(val[0]).x);
|
||||||
yValues.push(graph.getPosition(val[0]).y);
|
yValues.push(graph.getPosition(val[0]).y);
|
||||||
|
yValuesWithLabel.push(yValues[yValues.length -1] + val[3] * labelLineHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rectX = Math.min(...xValues) - rectParams.paddingX;
|
const minX = Math.min(...xValues);
|
||||||
const rectY = Math.min(...yValues) - rectParams.paddingY;
|
const maxX = Math.max(...xValues);
|
||||||
const rectSizeX = Math.max(...xValues) - Math.min(...xValues) + 2*rectParams.paddingX;
|
const minY = Math.min(...yValues);
|
||||||
const rectSizeY = Math.max(...yValues) - Math.min(...yValues) + 2*rectParams.paddingY;
|
const maxY = Math.max(...yValuesWithLabel);
|
||||||
|
const rectX = minX - rectParams.paddingX;
|
||||||
|
const rectY = minY - rectParams.paddingY;
|
||||||
|
const rectSizeX = maxX - minX + 2*rectParams.paddingX;
|
||||||
|
const rectSizeY = maxY - minY + 2*rectParams.paddingY;
|
||||||
|
|
||||||
rectangles.push({
|
rectangles.push({
|
||||||
ctx: canvascontext,
|
ctx: canvascontext,
|
||||||
|
|||||||
+264
-323
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"name": "netbox_topology_views",
|
"name": "netbox_topology_views",
|
||||||
"version": "4.2.0",
|
"version": "4.3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"bundle": "node bundle.js",
|
"bundle": "node bundle.js",
|
||||||
"bundle:styles": "node bundle.js --styles",
|
"bundle:styles": "node bundle.js --styles",
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ NETBOX_CURRENT_VERSION = version.parse(settings.RELEASE.version)
|
|||||||
|
|
||||||
|
|
||||||
class SiteButtons(PluginTemplateExtension):
|
class SiteButtons(PluginTemplateExtension):
|
||||||
model = 'dcim.site'
|
models = ('dcim.site', )
|
||||||
def buttons(self):
|
def buttons(self):
|
||||||
return self.render('netbox_topology_views/site_button.html')
|
return self.render('netbox_topology_views/site_button.html')
|
||||||
|
|
||||||
class LocationButtons(PluginTemplateExtension):
|
class LocationButtons(PluginTemplateExtension):
|
||||||
model = 'dcim.location'
|
models = ('dcim.location', )
|
||||||
def buttons(self):
|
def buttons(self):
|
||||||
return self.render('netbox_topology_views/location_button.html')
|
return self.render('netbox_topology_views/location_button.html')
|
||||||
|
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ def create_node(
|
|||||||
node["shape"] = "image"
|
node["shape"] = "image"
|
||||||
node["href"] = device.get_absolute_url()
|
node["href"] = device.get_absolute_url()
|
||||||
node["image"] = get_image_for_entity(device)
|
node["image"] = get_image_for_entity(device)
|
||||||
|
node["label_num_lines"] = len(label_items)
|
||||||
|
|
||||||
return node
|
return node
|
||||||
|
|
||||||
@@ -443,14 +444,14 @@ def get_topology_data(
|
|||||||
termination_b = create_circuit_termination(
|
termination_b = create_circuit_termination(
|
||||||
circuit_termination.cable.b_terminations[0]
|
circuit_termination.cable.b_terminations[0]
|
||||||
)
|
)
|
||||||
elif circuit_termination.provider_network is not None:
|
elif circuit_termination.termination is not None:
|
||||||
if (
|
if (
|
||||||
circuit_termination.provider_network_id
|
circuit_termination.termination_id
|
||||||
not in nodes_provider_networks
|
not in nodes_provider_networks
|
||||||
):
|
):
|
||||||
nodes_provider_networks[
|
nodes_provider_networks[
|
||||||
circuit_termination.provider_network.pk
|
circuit_termination.termination.pk
|
||||||
] = circuit_termination.provider_network
|
] = circuit_termination.termination
|
||||||
|
|
||||||
if bool(termination_a) and bool(termination_b):
|
if bool(termination_a) and bool(termination_b):
|
||||||
circuit_model = {
|
circuit_model = {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ long_description = readme.read_text()
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="netbox-topology-views",
|
name="netbox-topology-views",
|
||||||
version="4.2.1",
|
version="4.3.0",
|
||||||
description="An NetBox plugin to create Topology maps",
|
description="An NetBox plugin to create Topology maps",
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
|
|||||||
Reference in New Issue
Block a user