Merge branch 'release/v0.7'

This commit is contained in:
Caesar Kabalan 2023-05-23 00:40:03 -07:00
commit cfc6493561
No known key found for this signature in database
GPG key ID: DDFEF5FF6CFAB608
15 changed files with 398 additions and 154 deletions

View file

@ -42,6 +42,10 @@ Compile from source:
The full application should then be available within `./dist/`, open `./dist/index.html` in a browser.
## Credits
Split icon made by [Freepik](https://www.flaticon.com/authors/freepik) from [Flaticon](https://www.flaticon.com/).
## License
Visual Subnet Calculator is released under the [MIT License](https://opensource.org/licenses/MIT)

BIN
dist/android-chrome-192x192.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
dist/android-chrome-512x512.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
dist/apple-touch-icon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

9
dist/browserconfig.xml vendored Normal file
View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

BIN
dist/favicon-16x16.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
dist/favicon-32x32.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
dist/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

72
dist/index.html vendored
View file

@ -8,6 +8,13 @@
<link href="main.css" rel="stylesheet">
<meta name="description" content="Quickly design and collaborate on network design. Visual Subnet Calculator focuses on expediting the work of network administrators, not academic subnetting math.">
<meta name="robots" content="index, follow" />
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
<link rel="manifest" href="site.webmanifest">
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
</head>
<body>
<div class="container-xxl mt-3">
@ -23,30 +30,37 @@
</svg>
</a>
</div>
<h1>Visual Subnet Calculator <span style="font-size:1rem;">(UNDER CONSTRUCTION)</span></h1>
<h1>Visual Subnet Calculator</h1>
<div class="alert alert-primary alert-dismissible show mt-3" role="alert">
Enter the network you wish to subnet and use the Split/Join buttons on the right to start designing!
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<form id="input_form" class="row font-monospace g-2 mb-3" novalidate>
<div class="col-lg-2 col-md-3 col-4">
<form id="input_form" class="row g-2 mb-3" novalidate>
<div class="font-monospace col-lg-2 col-md-3 col-4">
<div><label for="network" class="form-label mb-0 ms-1">Network Address</label></div>
<div><input id="network" type="text" class="form-control" value="10.0.0.0" aria-label="Network Address" pattern="^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" required></div>
</div>
<div class="col-auto">
<div class="font-monospace col-auto">
<div style="height:2rem"></div>
<div>/</div>
</div>
<div class="col-lg-2 col-md-3 col-4">
<div class="font-monospace col-lg-2 col-md-3 col-4">
<div><label for="netsize" class="form-label mb-0 ms-1">Network Size</label></div>
<div><input id="netsize" type="text" class="form-control w-10" value="16" aria-label="Network Size" pattern="^(\d|[12]\d|30)$" required></div>
</div>
<div class="col-lg-2 col-md-3 col-3">
<div class="col-lg-2 col-md-3 col-3 font-">
<div style="height:1.5rem"></div>
<div>
<button id="btn_go" class="btn btn-success mb-0 mt-auto" type="button">Go</button>
<button id="btn_reset" class="btn btn-danger mb-0 mt-auto" type="button">Reset</button>
<div class="dropdown d-inline">
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
Tools
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#importExportModal" id="btn_import_export">Import / Export</a></li>
</ul>
</div>
</div>
</div>
</form>
@ -81,6 +95,23 @@
</tr>
</tbody>
</table>
<div id="bottom_nav">
<div class="d-inline-block align-top pt-1" id="colors_word_open"><span>Colors &#187;</span></div>
<div class="d-inline-block d-none" id="color_palette">
<div id="palette_picker_1"></div>
<div id="palette_picker_2"></div>
<div id="palette_picker_3"></div>
<div id="palette_picker_4"></div>
<div id="palette_picker_5"></div>
<div id="palette_picker_6"></div>
<div id="palette_picker_7"></div>
<div id="palette_picker_8"></div>
<div id="palette_picker_9"></div>
<div id="palette_picker_10"></div>
</div>
<div class="d-inline-block align-top align-top pt-1 ps-2 d-none" id="colors_word_close"><span>&#171; Colors</span></div>
</div>
<div class="modal fade" id="notifyModal" tabindex="-1" aria-labelledby="notifyModalLabel" aria-hidden="true">
<div class="modal-dialog modal-md">
@ -96,6 +127,31 @@
</div>
</div>
<div class="modal fade" id="importExportModal" tabindex="-1" aria-labelledby="importExportModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content">
<div class="modal-header border-bottom-0 pb-1">
<h3 class="modal-title" id="importExportModalLabel">Import/Export</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body pt-1">
<div class="alert alert-primary show mt-3" role="alert">
Copy the content from the box below to EXPORT the current subnet configuration. Or, overwrite/paste a previously exported configuration into the box below and click IMPORT.
</div>
<div class="form-floating font-monospace">
<textarea class="form-control pt-3" id="importExportArea" style="height: 510px"></textarea>
<label for="importExportArea"></label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" id="importBtn">Import</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="aboutModal" tabindex="-1" aria-labelledby="aboutModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered">
@ -119,6 +175,8 @@
<br/>
Special thanks to <a href="https://www.davidc.net/" target="_blank">davidc</a> for the <a href="https://www.davidc.net/sites/default/subnets/subnets.html" target="_blank">original tool</a> this website is inspired by.<br/>
<br/>
Split icon made by <a href="https://www.flaticon.com/authors/freepik" target="_blank">Freepik</a> from <a href="https://www.flaticon.com/" target="_blank">Flaticon</a>.<br />
<br/>
All contributions, even suggestions or bug reports, are welcome at:<br/>
<ul>
<li>GitHub: <a href="https://github.com/ckabalan/visualsubnetcalc" target="_blank">https://github.com/ckabalan/visualsubnetcalc</a></li>

109
dist/main.css vendored
View file

@ -1,8 +1,108 @@
:root {
/*
Color Palette
https://coolors.co/palette/033270-1368aa-4091c9-9dcee2-fedfd4-f29479-f26a4f-ef3c2d-cb1b16-65010c
--yale-blue: #033270ff;
--green-blue: #1368aaff;
--celestial-blue: #4091c9ff;
--light-blue: #9dcee2ff;
--misty-rose: #fedfd4ff;
--salmon: #f29479ff;
--tomato: #f26a4fff;
--vermilion: #ef3c2dff;
--engineering-orange: #cb1b16ff;
--rosewood: #65010cff;
*/
--split-foreground: #000000;
--split-background: indianred;
/* Combination of Salmon/Tomato */
--split-background: #F27F64;
--join-foreground: #000000;
--join-background: skyblue;
/* Combination of Light/Celestial Blue */
--join-background: #6FB0D6;
/* Color Palettes for subnet highlights
https://coolors.co/palette/f0d7df-f9e0e2-f8eaec-f7ddd9-f7e6da-e3e9dd-c4dbd9-d4e5e3-cae0e4-c8c7d6
https://coolors.co/palette/54478c-2c699a-048ba8-0db39e-16db93-83e377-b9e769-efea5a-f1c453-f29e4c
https://coolors.co/palette/e2e2df-d2d2cf-e2cfc4-f7d9c4-faedcb-c9e4de-c6def1-dbcdf0-f2c6de-f9c6c9
https://coolors.co/palette/54478c-2c699a-048ba8-0db39e-16db93-83e377-b9e769-efea5a-f1c453-f29e4c
https://coolors.co/palette/ffadad-ffd6a5-fdffb6-caffbf-9bf6ff-a0c4ff-bdb2ff-ffc6ff-fffffc
*/
--subpal-1-1: #ffadadff;
--subpal-1-2: #ffd6a5ff;
--subpal-1-3: #fdffb6ff;
--subpal-1-4: #caffbfff;
--subpal-1-5: #9bf6ffff;
--subpal-1-6: #a0c4ffff;
--subpal-1-7: #bdb2ffff;
--subpal-1-8: #ffc6ffff;
--subpal-1-9: #fffffcff;
--subpal-1-10: #ffffffff;
}
#bottom_nav {
height:2rem;
}
#bottom_nav span {
border-bottom:1px black dotted;
}
#color_palette {
min-width: 0rem;
}
#color_palette #colors_word_close {
line-height: 2rem;
}
#color_palette #colors_word_open {
display:inline-block;
height:2rem;
cursor:pointer;
white-space: nowrap;
}
#color_palette div {
display:inline-block;
height:2rem;
cursor:pointer;
white-space: nowrap;
}
#color_palette div[id^='palette_picker_'] {
width:2rem;
border:1px solid;
}
#color_palette #palette_picker_1 {
background-color: var(--subpal-1-1);
}
#color_palette #palette_picker_2 {
background-color: var(--subpal-1-2);
}
#color_palette #palette_picker_3 {
background-color: var(--subpal-1-3);
}
#color_palette #palette_picker_4 {
background-color: var(--subpal-1-4);
}
#color_palette #palette_picker_5 {
background-color: var(--subpal-1-5);
}
#color_palette #palette_picker_6 {
background-color: var(--subpal-1-6);
}
#color_palette #palette_picker_7 {
background-color: var(--subpal-1-7);
}
#color_palette #palette_picker_8 {
background-color: var(--subpal-1-8);
}
#color_palette #palette_picker_9 {
background-color: var(--subpal-1-9);
}
#color_palette #palette_picker_10 {
background-color: var(--subpal-1-10);
}
.container-xxl {
@ -119,13 +219,12 @@
padding-left:0.5rem;
padding-right:0.5rem;
}
#calc .note {
width: 30%;
}
#calc .note input {
border: none !important;
border-color: transparent !important;
background-color: transparent;
}
/* https://stackoverflow.com/a/47245068/606974 */

301
dist/main.js vendored
View file

@ -18,17 +18,51 @@ let infoColumnCount = 5
let operatingMode = 'NORMAL'
let noteTimeout;
let minSubnetSize = 30
let inflightColor = 'NONE'
$('input#network,input#netsize').on('input', function() {
$('#input_form')[0].classList.add('was-validated');
})
$('#color_palette div').on('click', function() {
// We don't really NEED to convert this to hex, but it's really low overhead to do the
// conversion here and saves us space in the export/save
inflightColor = rgba2hex($(this).css('background-color'))
})
$('#calcbody').on('click', '.row_address, .row_range, .row_usable, .row_hosts, .note, input', function(event) {
if (inflightColor !== 'NONE') {
mutate_subnet_map('color', this.dataset.subnet, '', inflightColor)
// We could re-render here, but there is really no point, keep performant and just change the background color now
//renderTable();
$(this).parent().css('background-color', inflightColor)
}
})
$('#btn_go').on('click', function() {
reset();
})
$('#btn_reset').on('click', function() {
reset();
$('#importBtn').on('click', function() {
importConfig($('#importExportArea').val())
})
$('#bottom_nav #colors_word_open').on('click', function() {
$('#bottom_nav #color_palette').removeClass('d-none');
$('#bottom_nav #colors_word_close').removeClass('d-none');
$('#bottom_nav #colors_word_open').addClass('d-none');
})
$('#bottom_nav #colors_word_close').on('click', function() {
$('#bottom_nav #color_palette').addClass('d-none');
$('#bottom_nav #colors_word_close').addClass('d-none');
$('#bottom_nav #colors_word_open').removeClass('d-none');
inflightColor = 'NONE'
})
$('#btn_import_export').on('click', function() {
$('#importExportArea').val(JSON.stringify(exportConfig(), null, 2))
})
function reset() {
@ -47,56 +81,12 @@ function reset() {
subnetMap = {}
subnetMap[rootCidr] = {}
maxNetSize = parseInt($('#netsize').val())
/*
subnetMap = {
'10.0.0.0/16': {
'10.0.0.0/17': {},
'10.0.128.0/17': {
'10.0.128.0/18': {
'10.0.128.0/19': {},
'10.0.160.0/19': {
'10.0.160.0/20': {},
'10.0.176.0/20': {
'10.0.176.0/21': {
'10.0.176.0/22': {
'10.0.176.0/23': {},
'10.0.178.0/23': {}
},
'10.0.180.0/22': {}
},
'10.0.184.0/21': {}
}
}
},
'10.0.192.0/18': {
'10.0.192.0/19': {},
'10.0.224.0/19': {
'10.0.224.0/20': {},
'10.0.240.0/20': {
'10.0.240.0/21': {},
'10.0.248.0/21': {
'10.0.248.0/22': {},
'10.0.252.0/22': {
'10.0.252.0/23': {},
'10.0.254.0/23': {
'10.0.254.0/24': {},
'10.0.255.0/24': {}
}
}
}
}
}
}
}
}
}
*/
renderTable();
}
$('#calcbody').on('click', 'td.split,td.join', function(event) {
// HTML DOM Data elements! Yay! See the `data-*` attributes of the HTML tags
mutate_subnet_map(this.dataset.mutateVerb, this.dataset.subnet, subnetMap)
mutate_subnet_map(this.dataset.mutateVerb, this.dataset.subnet, '')
renderTable();
})
@ -105,16 +95,14 @@ $('#calcbody').on('keyup', 'td.note input', function(event) {
let delay = 1000;
clearTimeout(noteTimeout);
noteTimeout = setTimeout(function(element) {
console.log('CAP')
subnetNotes[element.dataset.subnet] = element.value
mutate_subnet_map('note', element.dataset.subnet, '', element.value)
}, delay, this);
})
$('#calcbody').on('focusout', 'td.note input', function(event) {
// HTML DOM Data elements! Yay! See the `data-*` attributes of the HTML tags
clearTimeout(noteTimeout);
console.log('CAP')
subnetNotes[this.dataset.subnet] = this.value
mutate_subnet_map('note', this.dataset.subnet, '', this.value)
})
@ -127,16 +115,27 @@ function renderTable() {
function addRowTree(subnetTree, depth, maxDepth) {
for (let mapKey in subnetTree) {
if (Object.keys(subnetTree[mapKey]).length > 0) {
if (mapKey.startsWith('_')) { continue; }
if (has_network_sub_keys(subnetTree[mapKey])) {
addRowTree(subnetTree[mapKey], depth + 1, maxDepth)
} else {
let subnet_split = mapKey.split('/')
addRow(subnet_split[0], parseInt(subnet_split[1]), (infoColumnCount + maxDepth - depth))
let notesWidth = '30%';
if ((maxDepth > 5) && (maxDepth <= 10)) {
notesWidth = '25%';
} else if ((maxDepth > 10) && (maxDepth <= 15)) {
notesWidth = '20%';
} else if ((maxDepth > 15) && (maxDepth <= 20)) {
notesWidth = '15%';
} else if (maxDepth > 20) {
notesWidth = '10%';
}
addRow(subnet_split[0], parseInt(subnet_split[1]), (infoColumnCount + maxDepth - depth), (subnetTree[mapKey]['_note'] || ''), notesWidth, (subnetTree[mapKey]['_color'] || ''))
}
}
}
function addRow(network, netSize, colspan) {
function addRow(network, netSize, colspan, note, notesWidth, color) {
// TODO: do some checking here for smaller networks like /32, probably some edge cases to watch for.
let addressFirst = ip2int(network)
let addressLast = subnet_last_address(addressFirst, netSize)
@ -144,13 +143,18 @@ function addRow(network, netSize, colspan) {
let usableFirst = addressFirst + 1
let usableLast = addressLast - 1
let hostCount = 1 + usableLast - usableFirst
let styleTag = ''
if (color !== '') {
styleTag = ' style="background-color: ' + color + '"'
console.log(styleTag)
}
let newRow =
' <tr id="row_' + network.replace('.', '-') + '_' + netSize + '">\n' +
' <td class="row_address">' + network + '/' + netSize + '</td>\n' +
' <td class="row_range">' + int2ip(addressFirst) + ' - ' + int2ip(addressLast) + '</td>\n' +
' <td class="row_usable">' + int2ip(usableFirst) + ' - ' + int2ip(usableLast) + '</td>\n' +
' <td class="row_hosts">' + hostCount + '</td>\n' +
' <td class="note"><label><input type="text" class="form-control shadow-none p-0" data-subnet="' + network + '/' + netSize + '" value="' + (subnetNotes[network + '/' + netSize] || '') + '"></label></td>\n' +
' <tr id="row_' + network.replace('.', '-') + '_' + netSize + '"' + styleTag + '>\n' +
' <td data-subnet="' + network + '/' + netSize + '" class="row_address">' + network + '/' + netSize + '</td>\n' +
' <td data-subnet="' + network + '/' + netSize + '" class="row_range">' + int2ip(addressFirst) + ' - ' + int2ip(addressLast) + '</td>\n' +
' <td data-subnet="' + network + '/' + netSize + '" class="row_usable">' + int2ip(usableFirst) + ' - ' + int2ip(usableLast) + '</td>\n' +
' <td data-subnet="' + network + '/' + netSize + '" class="row_hosts">' + hostCount + '</td>\n' +
' <td class="note" style="width:' + notesWidth + '"><label><input type="text" class="form-control shadow-none p-0" data-subnet="' + network + '/' + netSize + '" value="' + note + '"></label></td>\n' +
' <td rowspan="1" colspan="' + colspan + '" class="split rotate" data-subnet="' + network + '/' + netSize + '" data-mutate-verb="split"><span>/' + netSize + '</span></td>\n'
if (netSize > maxNetSize) {
// This is wrong. Need to figure out a way to get the number of children so you can set rowspan and the number
@ -191,6 +195,7 @@ function subnet_addresses(netSize) {
function get_dict_max_depth(dict, curDepth) {
let maxDepth = curDepth
for (let mapKey in dict) {
if (mapKey.startsWith('_')) { continue; }
let newDepth = get_dict_max_depth(dict[mapKey], curDepth + 1)
if (newDepth > maxDepth) { maxDepth = newDepth }
}
@ -200,7 +205,8 @@ function get_dict_max_depth(dict, curDepth) {
function get_join_children(subnetTree, childCount) {
for (let mapKey in subnetTree) {
if (Object.keys(subnetTree[mapKey]).length > 0) {
if (mapKey.startsWith('_')) { continue; }
if (has_network_sub_keys(subnetTree[mapKey])) {
childCount += get_join_children(subnetTree[mapKey])
} else {
return childCount
@ -208,12 +214,24 @@ function get_join_children(subnetTree, childCount) {
}
}
function has_network_sub_keys(dict) {
let allKeys = Object.keys(dict)
// Maybe an efficient way to do this with a Lambda?
for (let i in allKeys) {
if (!allKeys[i].startsWith('_')) {
return true
}
}
return false
}
function count_network_children(network, subnetTree, ancestryList) {
// TODO: This might be able to be optimized. Ultimately it needs to count the number of keys underneath
// the current key are unsplit networks (IE rows in the table, IE keys with a value of {}).
let childCount = 0
for (let mapKey in subnetTree) {
if (Object.keys(subnetTree[mapKey]).length > 0) {
if (mapKey.startsWith('_')) { continue; }
if (has_network_sub_keys(subnetTree[mapKey])) {
childCount += count_network_children(network, subnetTree[mapKey], ancestryList.concat([mapKey]))
} else {
if (ancestryList.includes(network)) {
@ -229,7 +247,8 @@ function get_network_children(network, subnetTree) {
// the current key are unsplit networks (IE rows in the table, IE keys with a value of {}).
let subnetList = []
for (let mapKey in subnetTree) {
if (Object.keys(subnetTree[mapKey]).length > 0) {
if (mapKey.startsWith('_')) { continue; }
if (has_network_sub_keys(subnetTree[mapKey])) {
subnetList.push.apply(subnetList, get_network_children(network, subnetTree[mapKey]))
} else {
subnetList.push(mapKey)
@ -241,7 +260,8 @@ function get_network_children(network, subnetTree) {
function get_matching_network_list(network, subnetTree) {
let subnetList = []
for (let mapKey in subnetTree) {
if (Object.keys(subnetTree[mapKey]).length > 0) {
if (mapKey.startsWith('_')) { continue; }
if (has_network_sub_keys(subnetTree[mapKey])) {
subnetList.push.apply(subnetList, get_matching_network_list(network, subnetTree[mapKey]))
}
if (mapKey.split('/')[0] === network) {
@ -251,6 +271,31 @@ function get_matching_network_list(network, subnetTree) {
return subnetList
}
function get_consolidated_property(subnetTree, property) {
let allValues = get_property_values(subnetTree, property)
// https://stackoverflow.com/questions/14832603/check-if-all-values-of-array-are-equal
let allValuesMatch = allValues.every( (val, i, arr) => val === arr[0] )
if (allValuesMatch) {
return allValues[0]
} else {
return ''
}
}
function get_property_values(subnetTree, property) {
let propValues = []
for (let mapKey in subnetTree) {
if (has_network_sub_keys(subnetTree[mapKey])) {
propValues.push.apply(propValues, get_property_values(subnetTree[mapKey], property))
} else {
// The "else" above is a bit different because it will start tracking values for subnets which are
// in the hierarchy, but not displayed. Those are always blank so it messes up the value list
propValues.push(subnetTree[mapKey][property] || '')
}
}
return propValues
}
function get_network(networkInput, netSize) {
let ipInt = ip2int(networkInput)
netSize = parseInt(netSize)
@ -267,10 +312,12 @@ function split_network(networkInput, netSize) {
return subnets;
}
function mutate_subnet_map(verb, network, subnetTree) {
function mutate_subnet_map(verb, network, subnetTree, propValue = '') {
if (subnetTree === '') { subnetTree = subnetMap }
for (let mapKey in subnetTree) {
if (Object.keys(subnetTree[mapKey]).length > 0) {
mutate_subnet_map(verb, network, subnetTree[mapKey])
if (mapKey.startsWith('_')) { continue; }
if (has_network_sub_keys(subnetTree[mapKey])) {
mutate_subnet_map(verb, network, subnetTree[mapKey], propValue)
}
if (mapKey === network) {
let netSplit = mapKey.split('/')
@ -278,90 +325,46 @@ function mutate_subnet_map(verb, network, subnetTree) {
if (verb === 'split') {
if (netSize < minSubnetSize) {
let new_networks = split_network(netSplit[0], netSize)
// Could maybe optimize this for readability with some null coalescing
subnetTree[mapKey][new_networks[0]] = {}
subnetTree[mapKey][new_networks[1]] = {}
// Copy note to both children and delete Delete parent note
subnetNotes[new_networks[0]] = subnetNotes[mapKey]
subnetNotes[new_networks[1]] = subnetNotes[mapKey]
delete subnetNotes[mapKey]
// Options:
// [ Selected ] Copy note to both children and delete parent note
// [ Possible ] Blank out the new and old subnet notes
if (subnetTree[mapKey].hasOwnProperty('_note')) {
subnetTree[mapKey][new_networks[0]]['_note'] = subnetTree[mapKey]['_note']
subnetTree[mapKey][new_networks[1]]['_note'] = subnetTree[mapKey]['_note']
}
delete subnetTree[mapKey]['_note']
if (subnetTree[mapKey].hasOwnProperty('_color')) {
subnetTree[mapKey][new_networks[0]]['_color'] = subnetTree[mapKey]['_color']
subnetTree[mapKey][new_networks[1]]['_color'] = subnetTree[mapKey]['_color']
}
delete subnetTree[mapKey]['_color']
}
} else if (verb === 'join') {
// Keep the note of the first subnet (which matches the network address) and lose the second subnet's note
// Could consider changing this to concatenate the notes into the parent, but I think this is more intuitive
// Find first (smallest) subnet note which matches the exact network address (this would be the top network in the join scope)
let smallestMatchingNetworkSize = 0
for (let subnetCidr in subnetNotes) {
if (subnetCidr.startsWith(netSplit[0])) {
if (parseInt(subnetCidr.split('/')[1]) > smallestMatchingNetworkSize) {
smallestMatchingNetworkSize = subnetCidr.split('/')[1]
// Options:
// [ Selected ] Keep note if all the notes are the same, blank them out if they differ. Most intuitive
// [ Possible ] Lose note data for all deleted subnets.
// [ Possible ] Keep note from first subnet in the join scope. Reasonable but I think rarely will the note be kept by the user
// [ Possible ] Concatenate all notes. Ugly and won't really be useful for more than two subnets being joined
subnetTree[mapKey] = {
'_note': get_consolidated_property(subnetTree[mapKey], '_note'),
'_color': get_consolidated_property(subnetTree[mapKey], '_color')
}
}
}
subnetNotes[mapKey] = subnetNotes[netSplit[0] + '/' + smallestMatchingNetworkSize]
// Delete all notes of subnets under this collapsed subnet
let removeKeys = get_network_children(mapKey, subnetTree[mapKey], [])
for (let removeKey in removeKeys) {
subnetNotes[removeKey] = ''
}
// And delete the subnets themselves
subnetTree[mapKey] = {}
} else if (verb === 'note') {
subnetTree[mapKey]['_note'] = propValue
console.log('Note Verb - ' + propValue)
} else if (verb === 'color') {
subnetTree[mapKey]['_color'] = propValue
console.log('Color Verb - ' + propValue)
} else {
// How did you get here?
}
}
}
}
/*
function validate_cidr(network, netSize) {
let returnObj = {
'valid': false,
'errorNetwork': true,
'errorSize': true,
'cidr': false,
'network': false,
'netSize': false
}
returnObj['network'] = validate_network(network)
if (returnObj['network']) {
returnObj['errorNetwork'] = false;
}
if (!/^\d+$/.test(netSize)) {
returnObj['errorSize'] = true;
} else {
netSize = parseInt(netSize)
if ((netSize > 32) || (netSize < 0)) {
returnObj['errorSize'] = true;
} else {
returnObj['errorSize'] = false;
returnObj['netSize'] = netSize.toString()
}
}
if ((returnObj['errorNetwork'] === false) && (returnObj['errorSize'] === false)) {
returnObj['cidr'] = returnObj['network'] + '/' + returnObj['netSize']
returnObj['valid'] = true
}
return returnObj;
}
function validate_network(network) {
// This can probably be done with Regex but this is better.
let octets = network.split('.');
if (octets.length !== 4) { return false }
if (!/^\d+$/.test(octets[0])) { return false }
if (!/^\d+$/.test(octets[1])) { return false }
if (!/^\d+$/.test(octets[2])) { return false }
if (!/^\d+$/.test(octets[3])) { return false }
octets[0] = parseInt(octets[0])
octets[1] = parseInt(octets[1])
octets[2] = parseInt(octets[2])
octets[3] = parseInt(octets[3])
if ((octets[0] < 0) || (octets[0] > 255)) { return false }
if ((octets[1] < 0) || (octets[1] > 255)) { return false }
if ((octets[2] < 0) || (octets[2] > 255)) { return false }
if ((octets[3] < 0) || (octets[3] > 255)) { return false }
return octets.join('.')
}
*/
function show_warning_modal(message) {
var notifyModal = new bootstrap.Modal(document.getElementById("notifyModal"), {});
@ -371,4 +374,24 @@ function show_warning_modal(message) {
$( document ).ready(function() {
reset();
//importConfig('{"config_version":"1","subnets":{"10.0.0.0/16":{"10.0.0.0/17":{"10.0.0.0/18":{},"10.0.64.0/18":{}},"10.0.128.0/17":{"10.0.128.0/18":{"10.0.128.0/19":{},"10.0.160.0/19":{"10.0.160.0/20":{"10.0.160.0/21":{"10.0.160.0/22":{},"10.0.164.0/22":{}},"10.0.168.0/21":{}},"10.0.176.0/20":{"10.0.176.0/21":{"10.0.176.0/22":{"10.0.176.0/23":{},"10.0.178.0/23":{}},"10.0.180.0/22":{}},"10.0.184.0/21":{}}}},"10.0.192.0/18":{"10.0.192.0/19":{},"10.0.224.0/19":{}}}}},"notes":{}}')
//importConfig('{"config_version":"1","subnets":{"10.0.0.0/16":{"10.0.0.0/17":{"10.0.0.0/18":{"_note":"Note 1"},"10.0.64.0/18":{"_note":"Note 2"}},"10.0.128.0/17":{"10.0.128.0/18":{"10.0.128.0/19":{"_note":"Note 3"},"10.0.160.0/19":{"10.0.160.0/20":{"10.0.160.0/21":{"10.0.160.0/22":{"_note":"Note 4"},"10.0.164.0/22":{"_note":"Note 5"}},"10.0.168.0/21":{"_note":"Note 6"}},"10.0.176.0/20":{"10.0.176.0/21":{"10.0.176.0/22":{"10.0.176.0/23":{"_note":"Note 7"},"10.0.178.0/23":{"_note":"Note 8"}},"10.0.180.0/22":{"_note":"Note 9"}},"10.0.184.0/21":{"_note":"Note 10"}}}},"10.0.192.0/18":{"10.0.192.0/19":{"_note":"Note 11"},"10.0.224.0/19":{"_note":"Note 12"}}}}},"notes":{}}')
});
function exportConfig() {
return {
'config_version': '1',
'subnets': subnetMap,
}
}
function importConfig(text) {
// TODO: Probably need error checking here
text = JSON.parse(text)
if (text['config_version'] === '1') {
subnetMap = text['subnets'];
renderTable()
}
}
const rgba2hex = (rgba) => `#${rgba.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+\.{0,1}\d*))?\)$/).slice(1).map((n, i) => (i === 3 ? Math.round(parseFloat(n) * 255) : parseFloat(n)).toString(16).padStart(2, '0').replace('NaN', '')).join('')}`

BIN
dist/mstile-150x150.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

32
dist/safari-pinned-tab.svg vendored Normal file
View file

@ -0,0 +1,32 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M520 5112 c-219 -30 -407 -186 -484 -400 l-31 -87 -1 -2048 c-1
-1615 2 -2059 12 -2100 18 -74 34 -115 67 -172 80 -137 191 -226 344 -276 66
-22 82 -22 551 -25 l482 -2 0 199 c0 152 -3 199 -12 200 -7 0 -206 1 -441 1
-238 0 -444 4 -463 10 -46 12 -103 64 -124 111 -16 35 -17 110 -17 938 l0 899
2157 0 2158 0 -2 -907 -1 -908 -23 -40 c-26 -45 -73 -82 -123 -95 -19 -4 -218
-8 -444 -9 -225 0 -420 0 -432 0 l-23 -1 0 -199 0 -199 483 2 482 2 71 27 c40
15 93 39 119 53 74 42 175 149 219 233 75 144 69 -51 71 2221 2 2192 4 2093
-47 2213 -70 164 -200 281 -375 338 -66 22 -82 22 -545 25 l-478 2 0 -199 0
-199 23 -1 c12 0 213 -1 445 -1 424 -1 424 -1 470 -24 52 -26 90 -74 102 -128
4 -19 8 -433 8 -920 l0 -886 -2158 0 -2158 0 0 888 c0 537 3 902 9 925 13 48
63 105 112 127 34 16 84 18 455 18 229 0 432 1 450 1 l32 1 0 199 0 198 -457
0 c-252 -1 -469 -3 -483 -5z"/>
<path d="M2423 4984 c-76 -75 -307 -304 -515 -510 l-376 -373 139 -143 140
-142 142 141 c78 78 202 200 275 272 l132 131 0 -590 0 -590 200 0 200 0 0
587 0 588 269 -269 269 -269 141 142 141 142 -509 509 -509 510 -139 -136z"/>
<path d="M2360 1355 c0 -322 -2 -585 -5 -585 -3 0 -125 118 -271 263 -146 144
-267 264 -270 266 -2 3 -67 -60 -144 -138 l-139 -143 139 -137 c76 -75 308
-304 515 -509 l376 -372 510 509 510 510 -142 142 -141 141 -269 -268 -269
-269 0 588 0 587 -200 0 -200 0 0 -585z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

19
dist/site.webmanifest vendored Normal file
View file

@ -0,0 +1,19 @@
{
"name": "Visual Subnet Calc",
"short_name": "Visual Subnet Calc",
"icons": [
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

BIN
src/split_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB