From e35d1a1726cbc0998539dd323ecc33bbbe7b7a0e Mon Sep 17 00:00:00 2001 From: Justman100 <149507651+Justman100@users.noreply.github.com> Date: Thu, 21 Aug 2025 01:57:46 +0200 Subject: [PATCH] Implement: Split To Size --- dist/index.html | 26 ++++- dist/js/main.js | 285 ++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 291 insertions(+), 20 deletions(-) diff --git a/dist/index.html b/dist/index.html index 401293e..b87a1eb 100644 --- a/dist/index.html +++ b/dist/index.html @@ -95,6 +95,30 @@

Visual Subnet Calculator

pattern="^([0-9]|[12][0-9]|3[0-2])$" required> +
+
+
+
+
+
+
+ + +
+
@@ -287,4 +311,4 @@

Credits

- + \ No newline at end of file diff --git a/dist/js/main.js b/dist/js/main.js index d4b96a5..fd913f5 100644 --- a/dist/js/main.js +++ b/dist/js/main.js @@ -50,6 +50,160 @@ const minSubnetSizes = { OCI: 30, }; +// ============================================================================ +// SPLIT TO SIZE - COMPLETE FUNCTIONALITY +// ============================================================================ + +// Validate target size input and update CSS classes +function validateTargetSize() { + const targetSizeVal = $('#targetsize').val(); + const baseSizeVal = $('#netsize').val(); + + // Empty is valid (optional field) + if (!targetSizeVal || targetSizeVal.trim() === '') { + $('#targetsize').removeClass('is-invalid').addClass('is-valid'); + return true; + } + + // Parse values + const targetSize = parseInt(targetSizeVal, 10); + const baseSize = parseInt(baseSizeVal, 10); + + // Check if both values are valid numbers + if (isNaN(targetSize) || isNaN(baseSize)) { + $('#targetsize').removeClass('is-valid').addClass('is-invalid'); + return false; + } + + // Check if target size is greater than base size + if (targetSize > baseSize) { + $('#targetsize').removeClass('is-invalid').addClass('is-valid'); + return true; + } else { + $('#targetsize').removeClass('is-valid').addClass('is-invalid'); + return false; + } +} + +// Split a subnet to a specific target size +function splitSubnetToSize(subnetCidr, targetSize) { + const [subnetIp, currentSizeStr] = subnetCidr.split('/'); + const currentSize = parseInt(currentSizeStr, 10); + + // Validate target size + if (targetSize <= currentSize) { + return false; // Cannot split to same or larger size + } + + if (targetSize > minSubnetSizes[operatingMode]) { + return false; // Target size exceeds minimum for current mode + } + + // Split recursively until we reach target size + const queue = [subnetCidr]; + + while (queue.length > 0) { + const currentCidr = queue.shift(); + const [currentIp, sizeStr] = currentCidr.split('/'); + const size = parseInt(sizeStr, 10); + + // If we've reached target size, continue to next + if (size === targetSize) { + continue; + } + + // Split this subnet into two smaller ones + const children = split_network(currentIp, size); + + // Insert children into subnetMap + insertChildrenIntoMap(currentCidr, children); + + // Add children to queue for further processing + queue.push(children[0], children[1]); + } + + return true; +} + +// Insert child subnets into the subnetMap +function insertChildrenIntoMap(parentCidr, children) { + // Find parent in subnetMap + const parentNode = findSubnetInMap(subnetMap, parentCidr); + if (!parentNode) return; + + // Store parent properties + const parentNote = parentNode._note; + const parentColor = parentNode._color; + + // Create child nodes + parentNode[children[0]] = {}; + parentNode[children[1]] = {}; + + // Copy parent properties to children + if (parentNote !== undefined) { + parentNode[children[0]]._note = parentNote; + parentNode[children[1]]._note = parentNote; + delete parentNode._note; + } + + if (parentColor !== undefined) { + parentNode[children[0]]._color = parentColor; + parentNode[children[1]]._color = parentColor; + delete parentNode._color; + } +} + +// Find a subnet in the subnetMap recursively +function findSubnetInMap(map, targetCidr) { + for (const key in map) { + if (key.startsWith('_')) continue; + + if (key === targetCidr) { + return map[key]; + } + + if (has_network_sub_keys(map[key])) { + const found = findSubnetInMap(map[key], targetCidr); + if (found) return found; + } + } + return null; +} + +// Handle split to size functionality +function handleSplitToSize(subnetCidr) { + const targetSizeStr = $('#targetsize').val(); + + // If no target size specified, do normal split + if (!targetSizeStr || targetSizeStr.trim() === '') { + return false; + } + + const targetSize = parseInt(targetSizeStr, 10); + const currentSize = parseInt(subnetCidr.split('/')[1], 10); + + // Validate target size + if (isNaN(targetSize) || targetSize <= currentSize) { + show_warning_modal('
Split to Size must be larger than the current subnet size.
'); + return true; // Handled, don't do normal split + } + + if (targetSize > minSubnetSizes[operatingMode]) { + show_warning_modal('
Split to Size exceeds the smallest allowed subnet for the selected mode (max /' + minSubnetSizes[operatingMode] + ').
'); + return true; // Handled, don't do normal split + } + + // Perform the split + const success = splitSubnetToSize(subnetCidr, targetSize); + + if (success) { + subnetMap = sortIPCIDRs(subnetMap); + renderTable(operatingMode); + } + + return true; // Handled, don't do normal split +} + $('input#network').on('paste', function (e) { let pastedData = window.event.clipboardData.getData('text') if (pastedData.includes('/')) { @@ -67,8 +221,19 @@ $("input#network").on('keydown', function (e) { } }); -$('input#network,input#netsize').on('input', function() { +$('input#network,input#netsize,input#targetsize').on('input', function() { $('#input_form')[0].classList.add('was-validated'); + + // Always validate targetsize when any input changes + validateTargetSize(); +}) + +// Handle gateway checkbox changes +$('#gateway_checkbox').on('change', function() { + // Re-render table when gateway setting changes + if (Object.keys(subnetMap).length > 0) { + renderTable(operatingMode); + } }) $('#color_palette div').on('click', function() { @@ -206,6 +371,23 @@ function reset() { subnetMap[rootCidr] = {} } maxNetSize = parseInt($('#netsize').val()) + // Handle initial split to size if specified + const targetSizeStr = $('#targetsize').val(); + if (targetSizeStr && targetSizeStr.trim() !== '') { + const targetSize = parseInt(targetSizeStr, 10); + const baseSize = parseInt($('#netsize').val(), 10); + + if (!isNaN(targetSize) && targetSize > baseSize) { + if (targetSize > minSubnetSizes[operatingMode]) { + show_warning_modal('
Split to Size exceeds the smallest allowed subnet for the selected mode (max /' + minSubnetSizes[operatingMode] + ').
'); + } else { + splitSubnetToSize(rootCidr, targetSize); + subnetMap = sortIPCIDRs(subnetMap); + } + } else if (!isNaN(targetSize) && targetSize <= baseSize) { + show_warning_modal('
Split to Size must be larger than the base size (e.g., /27 when base is /24).
'); + } + } renderTable(operatingMode); } @@ -225,8 +407,20 @@ function isMatchingSize(subnet1, subnet2) { $('#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, '') - this.dataset.subnet = sortIPCIDRs(this.dataset.subnet) + const verb = this.dataset.mutateVerb; + const subnet = this.dataset.subnet; + + if (verb === 'split') { + // Try to handle split to size first + const handled = handleSplitToSize(subnet); + if (handled) { + return; // Split to size handled it, don't do normal split + } + } + + // Do normal split/join operation + mutate_subnet_map(verb, subnet, '') + subnetMap = sortIPCIDRs(subnetMap) renderTable(operatingMode); }) @@ -428,13 +622,15 @@ function subnet_addresses(netSize) { function subnet_usable_first(network, netSize, operatingMode) { if (netSize < 31) { + // Check if gateway checkbox is enabled + const includeGateway = $('#gateway_checkbox').is(':checked'); + // https://docs.aws.amazon.com/vpc/latest/userguide/subnet-sizing.html // AWS reserves 3 additional IPs // https://learn.microsoft.com/en-us/azure/virtual-network/virtual-networks-faq#are-there-any-restrictions-on-using-ip-addresses-within-these-subnets // Azure reserves 3 additional IPs // https://docs.oracle.com/en-us/iaas/Content/Network/Concepts/overview.htm#Reserved__reserved_subnet // OCI reserves 2 additional IPs - //return network + (operatingMode == 'Standard' ? 1 : 4); switch (operatingMode) { case 'AWS': case 'AZURE': @@ -444,7 +640,8 @@ function subnet_usable_first(network, netSize, operatingMode) { return network + 2; break; default: - return network + 1; + // Standard mode: Network + Broadcast (2 IPs) + optional Gateway (1 IP) + return network + (includeGateway ? 2 : 1); break; } } else { @@ -581,6 +778,21 @@ function split_network(networkInput, netSize) { return subnets; } +// ============================================================================ +// LEGACY FUNCTIONS - KEPT FOR BACKWARD COMPATIBILITY +// ============================================================================ + +// Legacy function - now replaced by splitSubnetToSize +function preSplitToSize(baseCidr, targetSize) { + return splitSubnetToSize(baseCidr, targetSize); +} + +// Legacy function - now replaced by insertChildrenIntoMap +function insertChildren(tree, parentCidr, children) { + insertChildrenIntoMap(parentCidr, children); + return true; +} + function mutate_subnet_map(verb, network, subnetTree, propValue = '') { if (subnetTree === '') { subnetTree = subnetMap } for (let mapKey in subnetTree) { @@ -765,6 +977,19 @@ function show_warning_modal(message) { $( document ).ready(function() { + // Add custom validation rule for target size + $.validator.addMethod("validTargetSize", function(value, element) { + return validateTargetSize(); + }, "Split to Size must be larger than the base size"); + + // Force validation on page load and when field loses focus + $('#targetsize').on('blur', function() { + validateTargetSize(); + }); + + // Call validation on page load + validateTargetSize(); + // Initialize the jQuery Validation on the form var validator = $('#input_form').validate({ onfocusout: function (element) { @@ -778,6 +1003,10 @@ $( document ).ready(function() { netsize: { required: true, pattern: '^([0-9]|[12][0-9]|3[0-2])$' + }, + targetsize: { + pattern: '^([0-9]|[12][0-9]|3[0-2])$', + validTargetSize: true } }, messages: { @@ -788,6 +1017,10 @@ $( document ).ready(function() { netsize: { required: 'Please enter a network size', pattern: 'Smallest size is /32' + }, + targetsize: { + pattern: 'Must be a valid subnet size (0-32)', + validTargetSize: 'Split to Size must be larger than the base size' } }, errorPlacement: function(error, element) { @@ -834,20 +1067,22 @@ function exportConfig(isMinified = true) { if (isMinified) { minifySubnetMap(miniSubnetMap, subnetMap, baseNetwork) } + + const config = { + 'config_version': configVersion, + 'base_network': baseNetwork, + 'subnets': isMinified ? miniSubnetMap : subnetMap, + }; + + // Add operating mode if not standard if (operatingMode !== 'Standard') { - return { - 'config_version': configVersion, - 'operating_mode': operatingMode, - 'base_network': baseNetwork, - 'subnets': isMinified ? miniSubnetMap : subnetMap, - } - } else { - return { - 'config_version': configVersion, - 'base_network': baseNetwork, - 'subnets': isMinified ? miniSubnetMap : subnetMap, - } + config['operating_mode'] = operatingMode; } + + // Add gateway setting + config['include_gateway'] = $('#gateway_checkbox').is(':checked'); + + return config; } function getConfigUrl() { @@ -858,6 +1093,9 @@ function getConfigUrl() { if (defaultExport.hasOwnProperty('operating_mode')) { renameKey(defaultExport, 'operating_mode', 'm') } + if (defaultExport.hasOwnProperty('include_gateway')) { + renameKey(defaultExport, 'include_gateway', 'g') + } renameKey(defaultExport, 'subnets', 's') //console.log(JSON.stringify(defaultExport)) return '/index.html?c=' + urlVersion + LZString.compressToEncodedURIComponent(JSON.stringify(defaultExport)) @@ -876,6 +1114,9 @@ function processConfigUrl() { if (urlConfig.hasOwnProperty('m')) { renameKey(urlConfig, 'm', 'operating_mode') } + if (urlConfig.hasOwnProperty('g')) { + renameKey(urlConfig, 'g', 'include_gateway') + } renameKey(urlConfig, 's', 'subnets') if (urlConfig['config_version'] === '1') { // Version 1 Configs used full subnet strings as keys and just shortned the _note->_n and _color->_c keys @@ -973,7 +1214,13 @@ function importConfig(text) { subnetMap = sortIPCIDRs(text['subnets']); operatingMode = text['operating_mode'] || 'Standard' switchMode(operatingMode); - + + // Restore gateway setting if available + if (text.hasOwnProperty('include_gateway')) { + $('#gateway_checkbox').prop('checked', text['include_gateway']); + } else { + $('#gateway_checkbox').prop('checked', false); + } } function sortIPCIDRs(obj) { @@ -1016,4 +1263,4 @@ function sortIPCIDRs(obj) { return sortedObj; } -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('')}` +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('')}` \ No newline at end of file