First Upload

This commit is contained in:
2024-11-19 03:32:02 +00:00
commit c205e717a5
41 changed files with 3599 additions and 0 deletions

14
Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM node:20-alpine as build
ARG REACT_APP_SERVICES_HOST=/services/m
COPY . /app
WORKDIR /app/src
RUN npm install
RUN npm run build
FROM nginx
COPY --from=build /app/dist /usr/share/nginx/html

22
LICENSE Normal file
View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2023 Caesar Kabalan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

57
NOTES.md Normal file
View File

@@ -0,0 +1,57 @@
# Notes
## Efficient Saves
Looks like the most efficient way to store an address is to track the base network (say 10.0.0.0/8) and then represent
all other addresses as offsets from that base network. This way, we can store a subnet as a base network and a mask. The
most efficient way to store this is to store two values:
- The base network offset (0 to 4,294,967,296)
- 0 being the best case, a subnet within the base subnet with the same network address (10.0.0.0/24)
- 255.255.255.255 being the worst case, the last address in 0.0.0.0/0 (unrealistic)
- The mask (0 to 32)
Combine both of these values into a single binary string that is 32+5 bits. Round the storage up to the nearest byte
(40 bits = 5 bytes), padding the remaining bits with 0s, then encode this 5 byte string into a URL-safe base64 string.
Example for 10.0.0.0/8 and representing the network 10.0.15.0/24:
Find the offset:
10.0.0.0 (decimal):
00001010 00000000 00000000 00000000 → 167772160
10.0.15.0 (decimal):
00001010 00000000 00001111 00000000 → 167775232
Offset: 167775232 - 167772160 = 3072
Hmmm, this above works good for close together smaller networks but gets ugly when you're dealing with larger networks
because the offset is huge.
I'm thinking about a coordinate system. Let's say you have a base network of 10.0.0.0/8 and you want to as concisely as
possible represent 10.166.64.0/20. We could represent this as the nth /20 within the /8 and just store N-20, and
probably shorten that even more with the last digit always being base32 for the mask.
If I'm doing my math right you can say a /20 has 4096 addresses.
10.0.0.0 = 167772160
10.166.64.0 = 178667520
178667520 - 167772160 = 10895360
10895360 / 4096 = 2660
Which means 10.166.64.0 is the 2,660th /20 within the /8. You could store this as 2660x20 (inefficient)
You can reverse this 2660x20 from 10.0.0.0/8 by doing the following:
a /21 has 4096 addresses, times the 2660th network is 10895360. Add that to the base network address to get the network
10.0.0.0 = 167772160
167772160 + 10895360 = 178667520
178667520 = 10.166.64.0 then you can tack back on the /20
This has the advantage overall that you're unlikely to store wildly different subnet sizes in the same page. Your "N" in
the "Nth" subnet is likely to be very small. You're more likely to store the Nth /24 in a /20 than you are a /20 in an
/8. So the numbers will mostly be 1-2 digits, rarely 3 digits, and almost never 4 digits as depicted above.
I then for efficienty I could use this format:
`[Nth Network as Integer][Network Size as Base32]`
So lets say you're wanting to represent the 0th /24 in a /20 you would represent it as `00`, always knowing the last
digit is the network size. Or the 0th /32 would be `07` (32 in base32 is 7). or the 5th /28 would be `54`.

110
README.md Normal file
View File

@@ -0,0 +1,110 @@
# Visual Subnet Calculator - [visualsubnetcalc.com](https://visualsubnetcalc.com)
![demo.gif](src%2Fdemo.gif)
Visual Subnet Calculator is a modernized tool based on the original work by [davidc](https://github.com/davidc/subnets).
It strives to be a tool for quickly designing networks and collaborating on that design with others. It focuses on
expediting the work of network administrators, not academic subnetting math.
## Design Tenets
The following tenets are the most important values that drive the design of the tool. New features, pull requests, etc
should align to these tenets, or propose an adjustment to the tenets.
- **Simplicity is king.** Network admins are busy and Visual Subnet Calculator should always be easy for FIRST TIME USERS to
quickly and intuitively use.
- **Subnetting is design work.** Promote features that enhance visual clarity and easy mental processing of even the most
complex architectures.
- **Users control the data.** We store nothing, but provide convenient ways for users to save and share their designs.
- **Embrace community contributions.** Consider and respond to all feedback and pull requests in the context of these
tenets.
## Building From Source
If you have a more opinionated best-practice way to lay out this repository please open an issue.
Build prerequisites:
- (Optional but recommended) NVM to manage node version
- node.js (version 20) and associated NPM.
- sass (Globally installed, following instructions below.)
Compile from source:
```shell
# Clone the repository
> git clone https://github.com/ckabalan/visualsubnetcalc
# Change to the repository directory
> cd visualsubnetcalc
# Use recommended NVM version
> nvm use
# Change to the sources directory
> cd src
# Install Bootstrap
> npm install
# Compile Bootstrap (Also install sass command line globally)
> npm run build
# Run the local webserver
> npm start
```
The full application should then be available within `./dist/`, open `./dist/index.html` in a browser.
### Run with certificates (Optional)
***NB:*** *required for testing clipboard.writeText() in the browser. Feature is only available in secure (https) mode.*
```shell
#Install mkcert
> brew install mkcert
# generate CA Certs to be trusted by local browsers
> mkcert install
# generate certs for local development
> cd visualsubnetcalc/src
# generate certs for local development
> npm run setup:certs
# run the local webserver with https
> npm run local-secure-start
````
# Cloud Subnet Notes
- [AWS reserves 3 additional IPs](https://docs.aws.amazon.com/vpc/latest/userguide/subnet-sizing.html)
- [Azure 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)
## Standard mode:
- Smallest subnet: /32
- Two reserved addresses per subnet of size <= 30:
- Network Address (network + 0)
- Broadcast Address (last network address)
## AWS mode :
- Smallest subnet: /28
- Five reserved addresses per subnet:
- Network Address (network + 0)
- AWS Reserved - VPC Router
- AWS Reserved - VPC DNS
- AWS Reserved - Future Use
- Broadcast Address (last network address)
## Azure mode :
- Smallest subnet: /29
- Five reserved addresses per subnet:
- Network Address (network + 0)
- Azure Reserved - Default Gateway
- Azure Reserved - DNS Mapping
- Azure Reserved - DNS Mapping
- Broadcast Address (last network address)
## 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)

5
dist/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/css/bootstrap.min.css.map vendored Normal file

File diff suppressed because one or more lines are too long

272
dist/css/main.css vendored Normal file
View File

@@ -0,0 +1,272 @@
: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;
/* Combination of Salmon/Tomato */
--split-background: #F27F64;
--join-foreground: #000000;
/* 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-e6e6e6
*/
--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: #e6e6e6ff;
--subpal-1-10: #ffffffff;
}
.table>:not(caption)>*>* {
background-color: unset;
}
#bottom_nav {
height:2rem;
}
#bottom_nav span {
border-bottom:1px black dotted;
}
#whats_new {
cursor:pointer !important;
text-align: right;
width:15rem;
float:right;
}
#whats_new a {
width:15rem;
text-align: right;
text-decoration: none;
border-bottom:1px var(--bs-success) dotted;
}
#copy_url {
cursor:pointer !important;
text-align: center;
width:11rem;
}
#copy_url span {
width:11rem;
text-align: center;
}
#color_palette {
min-width: 0rem;
}
#color_palette #colors_word_close {
line-height: 2rem;
cursor:pointer;
}
#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 {
min-width: 576px;
}
#navigation a {
color:#000000;
text-decoration: none;
}
#subnet_input #network {
flex-grow: 0;
flex-basis: 11rem;
}
#subnet_input #netsize {
flex-grow: 0;
flex-basis: 4rem;
}
#calc {
vertical-align: middle;
}
#calc>tbody>tr>td, #calc>tbody>tr>th, #calc>tfoot>tr>td, #calc>tfoot>tr>th, #calc>thead>tr>td, #calc>thead>tr>th {
/* Equivalent to p-1 */
padding: 0.25rem;
/* Equivalent to p-2 */
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
/*
#joinHeader {
border:none;
}
#calc thead {
border-right-width: 1px;
border-bottom-width: 0px;
}*/
#calc span.split {
color: var(--split-background);
}
#calc span.join {
color: var(--join-background);
}
#calc td.split {
background-color: var(--split-background);
color: var(--split-foreground);
cursor: pointer;
min-width: 2.3rem;
width: 1%;
font-size:1rem;
}
#notifyModal .modal-content {
background-color: var(--bs-alert-bg);
}
#calc td.join {
background-color: var(--join-background);
color: var(--join-foreground);
cursor: pointer;
min-width: 2.3rem;
width: 1%;
font-size:1rem;
}
#calc td.split, #calc td.join {
padding: 0;
}
#calc td.split span, #calc td.join span {
padding-right: 0.4rem;
}
#calc .note {
padding-left:0.5rem;
padding-right:0.5rem;
}
#calc .note label,input {
width: 100%;
}
#calc .row_address {
white-space: nowrap;
padding-left:0.5rem;
padding-right:0.5rem;
}
#calc .row_range {
/* TODO: Make this a checkbox?
white-space: nowrap;
*/
padding-left:0.5rem;
padding-right:0.5rem;
}
#calc .row_usable {
/* TODO: Make this a checkbox?
white-space: nowrap;
*/
padding-left:0.5rem;
padding-right:0.5rem;
}
#calc .row_hosts {
width:1%;
white-space: nowrap;
padding-left:0.5rem;
padding-right:0.5rem;
}
#calc .note input {
border: none !important;
border-color: transparent !important;
background-color: transparent;
}
/* https://stackoverflow.com/a/47245068/606974 */
.rotate {
vertical-align: middle;
text-align: end;
}
.rotate span {
-ms-writing-mode: tb-rl;
-webkit-writing-mode: vertical-rl;
writing-mode: vertical-rl;
white-space: nowrap;
padding-top: 0.25rem;
}

BIN
dist/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

9
dist/icon/browserconfig.xml vendored Normal file
View File

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

32
dist/icon/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/icon/site.webmanifest vendored Normal file
View File

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

237
dist/index.html vendored Normal file
View File

@@ -0,0 +1,237 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Visual Subnet Calculator - Split/Join</title>
<link rel="stylesheet" href="css/bootstrap.min.css">
<link href="css/main.css" rel="stylesheet">
<meta name="description" content="Quickly and easily design network layouts. Split and join subnets, add notes and color, then collaborate with others by sharing a custom link to your design.">
<meta name="robots" content="index, follow" />
<link rel="apple-touch-icon" sizes="180x180" href="icon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="icon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="icon/favicon-16x16.png">
<link rel="manifest" href="icon/site.webmanifest">
<link rel="mask-icon" href="icon/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">
<div class="float-end" id="navigation">
<a href="#" id="info_icon" data-bs-toggle="modal" data-bs-target="#aboutModal" aria-label="About">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-info-circle" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
</svg>
</a><a href="https://github.com/ckabalan/visualsubnetcalc" target="_blank" aria-label="GitHub">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-github" viewBox="0 0 16 16">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
</svg>
</a>
</div>
<h1>Visual Subnet Calculator</h1>
<div class="alert alert-primary alert-dismissible show mt-3" role="alert">
<p class="mb-1">Quickly and easily design network layouts. Split and join subnets, add notes and color, then collaborate with others by sharing a custom link to your design.</p>
<p class="mb-0">Enter the network you wish to subnet and use the Split/Join buttons on the right to start designing!</p>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<form id="input_form" class="row g-2 mb-3">
<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 data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-custom-class="custom-tooltip"
data-bs-title="Error"
data-bs-trigger="manual"
name="network"
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="font-monospace col-auto">
<div style="height:2rem"></div>
<div>/</div>
</div>
<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 data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-custom-class="custom-tooltip"
data-bs-title="Error"
data-bs-trigger="manual"
name="netsize"
id="netsize"
type="text"
class="form-control w-10"
value="16"
aria-label="Network Size"
pattern="^([0-9]|[12][0-9]|3[0-2])$"
required></div>
</div>
<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>
<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 active" href="#" data-bs-toggle="operatingMode" data-bs-target="#operatingMode" id="dropdown_standard" aria-current="true">Mode - Standard</a></li>
<li><a class="dropdown-item" href="#" data-bs-toggle="operatingMode" data-bs-target="#operatingMode" id="dropdown_aws">Mode - AWS</a></li>
<li><a class="dropdown-item" href="#" data-bs-toggle="operatingMode" data-bs-target="#operatingMode" id="dropdown_azure">Mode - Azure</a></li>
<li><hr class="dropdown-divider"></li>
<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>
<table id="calc" class="table table-bordered font-monospace" aria-label="Subnet Table">
<thead>
<tr>
<th aria-label="Subnet Address" id="subnetHeader" style="display: table-cell;">Subnet Address</th>
<th aria-label="Range of Addresses" id="rangeHeader" style="display: table-cell;">Range of Addresses</th>
<th aria-label="Usable IPs" id="useableHeader" style="display: table-cell;">Usable IPs</th>
<th aria-label="Hosts" id="hostsHeader" style="display: table-cell;">Hosts</th>
<th aria-label="Note" id="noteHeader" colspan="100%" style="display: table-cell;">
Note
<div style="float:right;"><span id="splitHeader" aria-label="Split" class="split">Split</span>/<span id="joinHeader" aria-label="Join" class="join">Join</span></div>
</th>
</tr>
</thead>
<tbody id="calcbody">
<tr id="row_10-0-0-0_16" aria-label="10.0.0.0/16">
<td aria-labelledby="subnetHeader" class="row_address">Loading...</td>
<td aria-labelledby="rangeHeader" class="row_range"></td>
<td aria-labelledby="useableHeader" class="row_usable"></td>
<td aria-labelledby="hostsHeader" class="row_hosts"></td>
<td class="note"><label><input aria-labelledby="noteHeader" type="text" class="form-control shadow-none p-0"></label></td>
<td aria-labelledby="splitHeader" rowspan="1" colspan="13" class="split rotate"><span></span></td>
<td aria-labelledby="joinHeader" rowspan="14" colspan="1" class="join rotate"><span></span></td>
</tr>
</tbody>
</table>
<div id="bottom_nav">
<div class="d-inline-block align-top pt-1" id="colors_word_open" aria-label="Change Colors"><span>Change Colors &#187;</span></div>
<div class="d-inline-block d-none" id="color_palette" aria-label="Color Palette">
<div role="button" aria-label="Color 1" id="palette_picker_1"></div>
<div role="button" aria-label="Color 2" id="palette_picker_2"></div>
<div role="button" aria-label="Color 3" id="palette_picker_3"></div>
<div role="button" aria-label="Color 4" id="palette_picker_4"></div>
<div role="button" aria-label="Color 5" id="palette_picker_5"></div>
<div role="button" aria-label="Color 6" id="palette_picker_6"></div>
<div role="button" aria-label="Color 7" id="palette_picker_7"></div>
<div role="button" aria-label="Color 8" id="palette_picker_8"></div>
<div role="button" aria-label="Color 9" id="palette_picker_9"></div>
<div role="button" aria-label="Color 10" 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" aria-label="Stop Changing Colors"><span>&#171; Stop Changing Colors</span></div>
<div class="d-inline-block align-top pt-1 ps-3" id="copy_url"><span>Copy Shareable URL</span></div>
<div class="d-inline-block align-top pt-1 ps-3" id="whats_new">
<a title="Released 2024-10-15" class="link-success" href="https://github.com/ckabalan/visualsubnetcalc/releases/tag/v1.3.0" target="_blank" aria-label="What's New">v1.3.0 (What's New?)</a>
</div>
</div>
<div class="modal fade" id="notifyModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-md">
<div class="modal-content alert-warning" role="alertdialog" aria-labelledby="notifyModalLabel" aria-describedby="notifyModalDescription">
<div class="modal-header border-bottom-0 pb-1">
<h3 class="modal-title" id="notifyModalLabel">Warning!</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body pt-1" id="notifyModalDescription">
Notification Text Here
</div>
</div>
</div>
</div>
<div class="modal fade" id="importExportModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content" role="alertdialog" aria-labelledby="importExportModalLabel" aria-describedby="importExportModalDescription">
<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" id="importExportModalDescription">
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" aria-label="Import/Export Content"></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">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title" id="aboutModalLabel">About Visual Subnet Calculator</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Visual Subnet Calculator strives to be a tool for quickly designing networks and collaborating on that design with others. It focuses on expediting the work of network administrators, not academic subnetting math.<br />
<br/>
<h4>Design Tenets</h4>
<ul>
<li><span style="font-weight:bold">Simplicity is king.</span> Network admins are busy and Visual Subnet Calculator should always be easy for FIRST TIME USERS to quickly and intuitively use.</li>
<li><span style="font-weight:bold">Subnetting is design work.</span> Promote features that enhance visual clarity and easy mental processing of even the most complex architectures.</li>
<li><span style="font-weight:bold">Users control the data.</span> We store nothing, but provide convenient ways for users to save and share their designs.</li>
<li><span style="font-weight:bold">Embrace community contributions.</span> Consider and respond to all feedback and pull requests in the context of these tenets.</li>
</ul>
<h4 class="mt-4">Credits</h4>
Developed by <a href="https://www.caesarkabalan.com/" target="_blank">Caesar Kabalan</a> with help from <a href="https://github.com/ckabalan/visualsubnetcalc/graphs/contributors" target="_blank">GitHub Contributors</a>!<br/>
<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>
<li>LinkedIn: <a href="https://www.linkedin.com/in/caesarkabalan/" target="_blank">Caesar Kabalan</a></li>
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.7.0.min.js"
integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g="
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery-validation@1.20.1/dist/jquery.validate.min.js"
integrity="sha256-0xVRcEF27BnewkTwGDpseENfeitZEOsQAVSlDc7PgG0="
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery-validation@1.20.1/dist/additional-methods.min.js"
integrity="sha384-Wxa7enUK34eRCZgjQIh81NeuzGHX2YaEkcLPCBIvASsdqlXBtAvVUiM7Tb56UvtC"
crossorigin="anonymous"></script>
<script src="js/lz-string.min.js"></script>
<script src="js/main.js"></script>
</div>
</body>
</html>

1
dist/js/lz-string.min.js vendored Normal file
View File

@@ -0,0 +1 @@
var LZString=function(){var r=String.fromCharCode,o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",e={};function t(r,o){if(!e[r]){e[r]={};for(var n=0;n<r.length;n++)e[r][r.charAt(n)]=n}return e[r][o]}var i={compressToBase64:function(r){if(null==r)return"";var n=i._compress(r,6,function(r){return o.charAt(r)});switch(n.length%4){default:case 0:return n;case 1:return n+"===";case 2:return n+"==";case 3:return n+"="}},decompressFromBase64:function(r){return null==r?"":""==r?null:i._decompress(r.length,32,function(n){return t(o,r.charAt(n))})},compressToUTF16:function(o){return null==o?"":i._compress(o,15,function(o){return r(o+32)})+" "},decompressFromUTF16:function(r){return null==r?"":""==r?null:i._decompress(r.length,16384,function(o){return r.charCodeAt(o)-32})},compressToUint8Array:function(r){for(var o=i.compress(r),n=new Uint8Array(2*o.length),e=0,t=o.length;e<t;e++){var s=o.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null==o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;e<t;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(r){return null==r?"":i._compress(r,6,function(r){return n.charAt(r)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(o){return t(n,r.charAt(o))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(r,o,n){if(null==r)return"";var e,t,i,s={},u={},a="",p="",c="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;i<r.length;i+=1)if(a=r.charAt(i),Object.prototype.hasOwnProperty.call(s,a)||(s[a]=f++,u[a]=!0),p=c+a,Object.prototype.hasOwnProperty.call(s,p))c=p;else{if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e<h;e++)m<<=1,v==o-1?(v=0,d.push(n(m)),m=0):v++;for(t=c.charCodeAt(0),e=0;e<8;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;e<h;e++)m=m<<1|t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=c.charCodeAt(0),e=0;e<16;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;0==--l&&(l=Math.pow(2,h),h++),s[p]=f++,c=String(a)}if(""!==c){if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e<h;e++)m<<=1,v==o-1?(v=0,d.push(n(m)),m=0):v++;for(t=c.charCodeAt(0),e=0;e<8;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;e<h;e++)m=m<<1|t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=c.charCodeAt(0),e=0;e<16;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;0==--l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==o-1){d.push(n(m));break}v++}return d.join("")},decompress:function(r){return null==r?"":""==r?null:i._decompress(r.length,32768,function(o){return r.charCodeAt(o)})},_decompress:function(o,n,e){var t,i,s,u,a,p,c,l=[],f=4,h=4,d=3,m="",v=[],g={val:e(0),position:n,index:1};for(t=0;t<3;t+=1)l[t]=t;for(s=0,a=Math.pow(2,2),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 2:return""}for(l[3]=c,i=c,v.push(c);;){if(g.index>o)return"";for(s=0,a=Math.pow(2,d),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(c=s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 2:return v.join("")}if(0==f&&(f=Math.pow(2,d),d++),l[c])m=l[c];else{if(c!==h)return null;m=i+i.charAt(0)}v.push(m),l[h++]=i+m.charAt(0),i=m,0==--f&&(f=Math.pow(2,d),d++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module?module.exports=LZString:"undefined"!=typeof angular&&null!=angular&&angular.module("LZString",[]).factory("LZString",function(){return LZString});

933
dist/js/main.js vendored Normal file
View File

@@ -0,0 +1,933 @@
let subnetMap = {};
let subnetNotes = {};
let maxNetSize = 0;
let infoColumnCount = 5
// NORMAL mode:
// - Smallest subnet: /32
// - Two reserved addresses per subnet of size <= 30:
// - Net+0 = Network Address
// - Last = Broadcast Address
// AWS mode:
// - Smallest subnet: /28
// - Two reserved addresses per subnet:
// - Net+0 = Network Address
// - Net+1 = AWS Reserved - VPC Router
// - Net+2 = AWS Reserved - VPC DNS
// - Net+3 = AWS Reserved - Future Use
// - Last = Broadcast Address
// Azure mode:
// - Smallest subnet: /29
// - Two reserved addresses per subnet:
// - Net+0 = Network Address
// - Net+1 = Reserved - Default Gateway
// - Net+2 = Reserved - DNS Mapping
// - Net+3 = Reserved - DNS Mapping
// - Last = Broadcast Address
let noteTimeout;
let operatingMode = 'Standard'
let previousOperatingMode = 'Standard'
let inflightColor = 'NONE'
let urlVersion = '1'
let configVersion = '2'
const netsizePatterns = {
Standard: '^([12]?[0-9]|3[0-2])$',
AZURE: '^([12]?[0-9])$',
AWS: '^(1?[0-9]|2[0-8])$',
};
const minSubnetSizes = {
Standard: 32,
AZURE: 29,
AWS: 28,
};
$('input#network').on('paste', function (e) {
let pastedData = window.event.clipboardData.getData('text')
if (pastedData.includes('/')) {
let [network, netSize] = pastedData.split('/')
$('#network').val(network)
$('#netsize').val(netSize)
}
e.preventDefault()
});
$("input#network").on('keydown', function (e) {
if (e.key === '/') {
e.preventDefault()
$('input#netsize').focus().select()
}
});
$('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).closest('tr').css('background-color', inflightColor)
}
})
$('#btn_go').on('click', function() {
$('#input_form').removeClass('was-validated');
$('#input_form').validate();
if ($('#input_form').valid()) {
$('#input_form')[0].classList.add('was-validated');
reset();
// Additional actions upon validation can be added here
} else {
show_warning_modal('<div>Please correct the errors in the form!</div>');
}
})
$('#dropdown_standard').click(function() {
previousOperatingMode = operatingMode;
operatingMode = 'Standard';
if(!switchMode(operatingMode)) {
operatingMode = previousOperatingMode;
$('#dropdown_'+ operatingMode.toLowerCase()).addClass('active');
}
});
$('#dropdown_azure').click(function() {
previousOperatingMode = operatingMode;
operatingMode = 'AZURE';
if(!switchMode(operatingMode)) {
operatingMode = previousOperatingMode;
$('#dropdown_'+ operatingMode.toLowerCase()).addClass('active');
}
});
$('#dropdown_aws').click(function() {
previousOperatingMode = operatingMode;
operatingMode = 'AWS';
if(!switchMode(operatingMode)) {
operatingMode = previousOperatingMode;
$('#dropdown_'+ operatingMode.toLowerCase()).addClass('active');
}
});
$('#importBtn').on('click', function() {
importConfig(JSON.parse($('#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'
})
$('#bottom_nav #copy_url').on('click', function() {
// TODO: Provide a warning here if the URL is longer than 2000 characters, probably using a modal.
let url = window.location.origin + getConfigUrl()
navigator.clipboard.writeText(url);
$('#bottom_nav #copy_url span').text('Copied!')
// Swap the text back after 3sec
setTimeout(function(){
$('#bottom_nav #copy_url span').text('Copy Shareable URL')
}, 2000)
})
$('#btn_import_export').on('click', function() {
$('#importExportArea').val(JSON.stringify(exportConfig(false), null, 2))
})
function reset() {
set_usable_ips_title(operatingMode);
let cidrInput = $('#network').val() + '/' + $('#netsize').val()
let rootNetwork = get_network($('#network').val(), $('#netsize').val())
let rootCidr = rootNetwork + '/' + $('#netsize').val()
if (cidrInput !== rootCidr) {
show_warning_modal('<div>Your network input is not on a network boundary for this network size. It has been automatically changed:</div><div class="font-monospace pt-2">' + $('#network').val() + ' -> ' + rootNetwork + '</div>')
$('#network').val(rootNetwork)
cidrInput = $('#network').val() + '/' + $('#netsize').val()
}
if (Object.keys(subnetMap).length > 0) {
// This page already has data imported, so lets see if we can just change the range
if (isMatchingSize(Object.keys(subnetMap)[0], cidrInput)) {
subnetMap = changeBaseNetwork(cidrInput)
} else {
// This is a page with existing data of a different subnet size, so make it blank
// Could be an opportunity here to do the following:
// - Prompt the user to confirm they want to clear the existing data
// - Resize the existing data anyway by making the existing network a subnetwork of their new input (if it
// is a larger network), or by just trimming the network to the new size (if it is a smaller network),
// or even resizing all of the containing networks by change in size of the base network. For example a
// base network going from /16 -> /18 would be all containing networks would be resized smaller (/+2),
// or bigger (/-2) if going from /18 -> /16.
subnetMap = {}
subnetMap[rootCidr] = {}
}
} else {
// This is a fresh page load with no existing data
subnetMap[rootCidr] = {}
}
maxNetSize = parseInt($('#netsize').val())
renderTable(operatingMode);
}
function changeBaseNetwork(newBaseNetwork) {
// Minifiy it, to make all the keys in the subnetMap relative to their original base network
// Then expand it, but with the new CIDR as the base network, effectively converting from old to new.
let miniSubnetMap = {}
minifySubnetMap(miniSubnetMap, subnetMap, Object.keys(subnetMap)[0])
let newSubnetMap = {}
expandSubnetMap(newSubnetMap, miniSubnetMap, newBaseNetwork)
return newSubnetMap
}
function isMatchingSize(subnet1, subnet2) {
return subnet1.split('/')[1] === subnet2.split('/')[1];
}
$('#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, '')
renderTable(operatingMode);
})
$('#calcbody').on('keyup', 'td.note input', function(event) {
// HTML DOM Data elements! Yay! See the `data-*` attributes of the HTML tags
let delay = 1000;
clearTimeout(noteTimeout);
noteTimeout = setTimeout(function(element) {
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);
mutate_subnet_map('note', this.dataset.subnet, '', this.value)
})
function renderTable(operatingMode) {
// TODO: Validation Code
$('#calcbody').empty();
let maxDepth = get_dict_max_depth(subnetMap, 0)
addRowTree(subnetMap, 0, maxDepth, operatingMode)
}
function addRowTree(subnetTree, depth, maxDepth,operatingMode) {
for (let mapKey in subnetTree) {
if (mapKey.startsWith('_')) { continue; }
if (has_network_sub_keys(subnetTree[mapKey])) {
addRowTree(subnetTree[mapKey], depth + 1, maxDepth,operatingMode)
} else {
let subnet_split = mapKey.split('/')
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'] || ''),operatingMode)
}
}
}
function addRow(network, netSize, colspan, note, notesWidth, color, operatingMode) {
let addressFirst = ip2int(network)
let addressLast = subnet_last_address(addressFirst, netSize)
let usableFirst = subnet_usable_first(addressFirst, netSize, operatingMode)
let usableLast = subnet_usable_last(addressFirst, netSize)
let hostCount = 1 + usableLast - usableFirst
let styleTag = ''
if (color !== '') {
styleTag = ' style="background-color: ' + color + '"'
}
let rangeCol, usableCol;
if (netSize < 32) {
rangeCol = int2ip(addressFirst) + ' - ' + int2ip(addressLast);
usableCol = int2ip(usableFirst) + ' - ' + int2ip(usableLast);
} else {
rangeCol = int2ip(addressFirst);
usableCol = int2ip(usableFirst);
}
let rowId = 'row_' + network.replace('.', '-') + '_' + netSize
let rowCIDR = network + '/' + netSize
let newRow =
' <tr id="' + rowId + '"' + styleTag + ' aria-label="' + rowCIDR + '">\n' +
' <td data-subnet="' + rowCIDR + '" aria-labelledby="' + rowId + ' subnetHeader" class="row_address">' + rowCIDR + '</td>\n' +
' <td data-subnet="' + rowCIDR + '" aria-labelledby="' + rowId + ' rangeHeader" class="row_range">' + rangeCol + '</td>\n' +
' <td data-subnet="' + rowCIDR + '" aria-labelledby="' + rowId + ' useableHeader" class="row_usable">' + usableCol + '</td>\n' +
' <td data-subnet="' + rowCIDR + '" aria-labelledby="' + rowId + ' hostsHeader" class="row_hosts">' + hostCount + '</td>\n' +
' <td class="note" style="width:' + notesWidth + '"><label><input aria-labelledby="' + rowId + ' noteHeader" type="text" class="form-control shadow-none p-0" data-subnet="' + rowCIDR + '" value="' + note + '"></label></td>\n' +
' <td data-subnet="' + rowCIDR + '" aria-labelledby="' + rowId + ' splitHeader" rowspan="1" colspan="' + colspan + '" class="split rotate" 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
// of ancestors so you can set colspan.
// DONE: If the subnet address (without the mask) matches a larger subnet address
// in the heirarchy that is a signal to add more join buttons to that row, since they start at the top row and
// via rowspan extend downward.
let matchingNetworkList = get_matching_network_list(network, subnetMap).slice(1)
for (const i in matchingNetworkList) {
let matchingNetwork = matchingNetworkList[i]
let networkChildrenCount = count_network_children(matchingNetwork, subnetMap, [])
newRow += ' <td aria-label="' + matchingNetwork + ' Join" rowspan="' + networkChildrenCount + '" colspan="1" class="join rotate" data-subnet="' + matchingNetwork + '" data-mutate-verb="join"><span>/' + matchingNetwork.split('/')[1] + '</span></td>\n'
}
}
newRow += ' </tr>';
$('#calcbody').append(newRow)
}
// Helper Functions
function ip2int(ip) {
return ip.split('.').reduce(function(ipInt, octet) { return (ipInt<<8) + parseInt(octet, 10)}, 0) >>> 0;
}
function int2ip (ipInt) {
return ((ipInt>>>24) + '.' + (ipInt>>16 & 255) + '.' + (ipInt>>8 & 255) + '.' + (ipInt & 255));
}
function toBase36(num) {
return num.toString(36);
}
function fromBase36(str) {
return parseInt(str, 36);
}
/**
* Coordinate System for Subnet Representation
*
* This system aims to represent subnets efficiently within a larger network space.
* The goal is to produce the shortest possible string representation for subnets,
* which is particularly effective when dealing with hierarchical network designs.
*
* Key concept:
* - We represent a subnet by its ordinal position within a larger network,
* along with its mask size.
* - This approach is most efficient when subnets are relatively close together
* in the address space and of similar sizes.
*
* Benefits:
* 1. Compact representation: Often results in very short strings (e.g., "7k").
* 2. Hierarchical: Naturally represents subnet hierarchy.
* 3. Efficient for common cases: Works best for typical network designs where
* subnets are grouped and of similar sizes.
*
* Trade-offs:
* - Less efficient for representing widely dispersed or highly varied subnet sizes.
* - Requires knowledge of the base network to interpret.
*
* Extreme Example... Representing the value 192.168.200.210/31 within the base
* network of 192.168.200.192/27. These are arbitrary but long subnets to represent
* as a string.
* - Normal Way - '192.168.200.210/31'
* - Nth Position Way - '9v'
* - '9' represents the 9th /31 subnet within the /27
* - 'v' represents the /31 mask size converted to Base 36 (31 -> 'v')
*/
/**
* Converts a specific subnet to its Nth position representation within a base network.
*
* @param {string} baseNetwork - The larger network containing the subnet (e.g., "10.0.0.0/16")
* @param {string} specificSubnet - The subnet to be represented (e.g., "10.0.112.0/20")
* @returns {string} A compact string representing the subnet's position and size (e.g., "7k")
*/
function getNthSubnet(baseNetwork, specificSubnet) {
const [baseIp, baseMask] = baseNetwork.split('/');
const [specificIp, specificMask] = specificSubnet.split('/');
const baseInt = ip2int(baseIp);
const specificInt = ip2int(specificIp);
const baseSize = 32 - parseInt(baseMask, 10);
const specificSize = 32 - parseInt(specificMask, 10);
const offset = specificInt - baseInt;
const nthSubnet = offset >>> specificSize;
return `${nthSubnet}${toBase36(parseInt(specificMask, 10))}`;
}
/**
* Reconstructs a subnet from its Nth position representation within a base network.
*
* @param {string} baseNetwork - The larger network containing the subnet (e.g., "10.0.0.0/16")
* @param {string} nthString - The compact representation of the subnet (e.g., "7k")
* @returns {string} The full subnet representation (e.g., "10.0.112.0/20")
*/
// Takes 10.0.0.0/16 and '7k' and returns 10.0.96.0/20
// '10.0.96.0/20' being the 7th /20 (base36 'k' is 20 int) within the /16.
function getSubnetFromNth(baseNetwork, nthString) {
const [baseIp, baseMask] = baseNetwork.split('/');
const baseInt = ip2int(baseIp);
const size = fromBase36(nthString.slice(-1));
const nth = parseInt(nthString.slice(0, -1), 10);
const innerSizeInt = 32 - size;
const subnetInt = baseInt + (nth << innerSizeInt);
return `${int2ip(subnetInt)}/${size}`;
}
function subnet_last_address(subnet, netSize) {
return subnet + subnet_addresses(netSize) - 1;
}
function subnet_addresses(netSize) {
return 2**(32-netSize);
}
function subnet_usable_first(network, netSize, operatingMode) {
if (netSize < 31) {
// 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
return network + (operatingMode == 'Standard' ? 1 : 4);
} else {
return network;
}
}
function subnet_usable_last(network, netSize) {
let last_address = subnet_last_address(network, netSize);
if (netSize < 31) {
return last_address - 1;
} else {
return last_address;
}
}
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 }
}
return maxDepth
}
function get_join_children(subnetTree, childCount) {
for (let mapKey in subnetTree) {
if (mapKey.startsWith('_')) { continue; }
if (has_network_sub_keys(subnetTree[mapKey])) {
childCount += get_join_children(subnetTree[mapKey])
} else {
return 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('_') && allKeys[i] !== 'n' && allKeys[i] !== 'c') {
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 (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)) {
childCount += 1
}
}
}
return childCount
}
function get_network_children(network, subnetTree) {
// 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 subnetList = []
for (let mapKey in subnetTree) {
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)
}
}
return subnetList
}
function get_matching_network_list(network, subnetTree) {
let subnetList = []
for (let mapKey in subnetTree) {
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) {
subnetList.push(mapKey)
}
}
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)
for (let i=31-netSize; i>=0; i--) {
ipInt &= ~ 1<<i;
}
return int2ip(ipInt);
}
function split_network(networkInput, netSize) {
let subnets = [networkInput + '/' + (netSize + 1)]
let newSubnet = ip2int(networkInput) + 2**(32-netSize-1);
subnets.push(int2ip(newSubnet) + '/' + (netSize + 1))
return subnets;
}
function mutate_subnet_map(verb, network, subnetTree, propValue = '') {
if (subnetTree === '') { subnetTree = subnetMap }
for (let mapKey in subnetTree) {
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('/')
let netSize = parseInt(netSplit[1])
if (verb === 'split') {
if (netSize < minSubnetSizes[operatingMode]) {
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]] = {}
// 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 {
switch (operatingMode) {
case 'AWS':
var modal_error_message = 'The minimum IPv4 subnet size for AWS is /' + minSubnetSizes[operatingMode] + '.<br/><br/>More Information:<br/><a href="https://docs.aws.amazon.com/vpc/latest/userguide/subnet-sizing.html#subnet-sizing-ipv4" target="_blank">Amazon Virtual Private Cloud > User Guide > Subnet CIDR Blocks > Subnet Sizing for IPv4</a>'
break;
case 'AZURE':
var modal_error_message = 'The minimum IPv4 subnet size for Azure is /' + minSubnetSizes[operatingMode] + '.<br/><br/>More Information:<br/><a href="https://learn.microsoft.com/en-us/azure/virtual-network/virtual-networks-faq#how-small-and-how-large-can-virtual-networks-and-subnets-be" target="_blank">Azure Virtual Network FAQ > How small and how large can virtual networks and subnets be?</a>'
break;
default:
var modal_error_message = 'The minimum size for an IPv4 subnet is /' + minSubnetSizes[operatingMode] + '.<br/><br/>More Information:<br/><a href="https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing" target="_blank">Wikipedia - Classless Inter-Domain Routing</a>'
break;
}
show_warning_modal('<div>' + modal_error_message + '</div>')
}
} else if (verb === 'join') {
// 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')
}
} else if (verb === 'note') {
subnetTree[mapKey]['_note'] = propValue
} else if (verb === 'color') {
subnetTree[mapKey]['_color'] = propValue
} else {
// How did you get here?
}
}
}
}
function switchMode(operatingMode) {
let isSwitched = true;
if (subnetMap !== null) {
if (validateSubnetSizes(subnetMap, minSubnetSizes[operatingMode])) {
renderTable(operatingMode);
set_usable_ips_title(operatingMode);
$('#netsize').attr('pattern', netsizePatterns[operatingMode]);
$('#input_form').removeClass('was-validated');
$('#input_form').rules('remove', 'netsize');
switch (operatingMode) {
case 'AWS':
var validate_error_message = 'AWS Mode - Smallest size is /' + minSubnetSizes[operatingMode]
break;
case 'AZURE':
var validate_error_message = 'Azure Mode - Smallest size is /' + minSubnetSizes[operatingMode]
break;
default:
var validate_error_message = 'Smallest size is /' + minSubnetSizes[operatingMode]
break;
}
// Modify jquery validation rule
$('#input_form #netsize').rules('add', {
required: true,
pattern: netsizePatterns[operatingMode],
messages: {
required: 'Please enter a network size',
pattern: validate_error_message
}
});
// Remove active class from all buttons if needed
$('#dropdown_standard, #dropdown_azure, #dropdown_aws').removeClass('active');
$('#dropdown_' + operatingMode.toLowerCase()).addClass('active');
isSwitched = true;
} else {
switch (operatingMode) {
case 'AWS':
var modal_error_message = 'One or more subnets are smaller than the minimum allowed for AWS.<br/>The smallest size allowed is /' + minSubnetSizes[operatingMode] + '.<br/>See: <a href="https://docs.aws.amazon.com/vpc/latest/userguide/subnet-sizing.html#subnet-sizing-ipv4" target="_blank">Amazon Virtual Private Cloud > User Guide > Subnet CIDR Blocks > Subnet Sizing for IPv4</a>'
break;
case 'AZURE':
var modal_error_message = 'One or more subnets are smaller than the minimum allowed for Azure.<br/>The smallest size allowed is /' + minSubnetSizes[operatingMode] + '.<br/>See: <a href="https://learn.microsoft.com/en-us/azure/virtual-network/virtual-networks-faq#how-small-and-how-large-can-virtual-networks-and-subnets-be" target="_blank">Azure Virtual Network FAQ > How small and how large can virtual networks and subnets be?</a>'
break;
default:
var validate_error_message = 'Unknown Error'
break;
}
show_warning_modal('<div>' + modal_error_message + '</div>');
isSwitched = false;
}
} else {
//unlikely to get here.
reset();
}
return isSwitched;
}
function validateSubnetSizes(subnetMap, minSubnetSize) {
let isValid = true;
const validate = (subnetTree) => {
for (let key in subnetTree) {
if (key.startsWith('_')) continue; // Skip special keys
let [_, size] = key.split('/');
if (parseInt(size) > minSubnetSize) {
isValid = false;
return; // Early exit if any subnet is invalid
}
if (typeof subnetTree[key] === 'object') {
validate(subnetTree[key]); // Recursively validate subnets
}
}
};
validate(subnetMap);
return isValid;
}
function set_usable_ips_title(operatingMode) {
switch (operatingMode) {
case 'AWS':
$('#useableHeader').html('Usable IPs (<a href="https://docs.aws.amazon.com/vpc/latest/userguide/subnet-sizing.html#subnet-sizing-ipv4" target="_blank" style="color:#000; border-bottom: 1px dotted #000; text-decoration: dotted" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-html="true" title="AWS reserves 5 addresses in each subnet for platform use.<br/>Click to navigate to the AWS documentation.">AWS</a>)')
break;
case 'AZURE':
$('#useableHeader').html('Usable IPs (<a href="https://learn.microsoft.com/en-us/azure/virtual-network/virtual-networks-faq#are-there-any-restrictions-on-using-ip-addresses-within-these-subnets" target="_blank" style="color:#000; border-bottom: 1px dotted #000; text-decoration: dotted" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-html="true" title="Azure reserves 5 addresses in each subnet for platform use.<br/>Click to navigate to the Azure documentation.">Azure</a>)')
break;
default:
$('#useableHeader').html('Usable IPs')
break;
}
$('[data-bs-toggle="tooltip"]').tooltip()
}
function show_warning_modal(message) {
var notifyModal = new bootstrap.Modal(document.getElementById('notifyModal'), {});
$('#notifyModal .modal-body').html(message)
notifyModal.show()
}
$( document ).ready(function() {
// Initialize the jQuery Validation on the form
var validator = $('#input_form').validate({
onfocusout: function (element) {
$(element).valid();
},
rules: {
network: {
required: true,
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]?)$'
},
netsize: {
required: true,
pattern: '^([0-9]|[12][0-9]|3[0-2])$'
}
},
messages: {
network: {
required: 'Please enter a network',
pattern: 'Must be a valid IPv4 Address'
},
netsize: {
required: 'Please enter a network size',
pattern: 'Smallest size is /32'
}
},
errorPlacement: function(error, element) {
//console.log(error);
//console.log(element);
if (error[0].innerHTML !== '') {
//console.log('Error Placement - Text')
if (!element.data('errorIsVisible')) {
bootstrap.Tooltip.getInstance(element).setContent({'.tooltip-inner': error[0].innerHTML})
element.tooltip('show');
element.data('errorIsVisible', true)
}
} else {
//console.log('Error Placement - Empty')
//console.log(element);
if (element.data('errorIsVisible')) {
element.tooltip('hide');
element.data('errorIsVisible', false)
}
}
//console.log(element);
},
// This success function appears to be required as errorPlacement() does not fire without the success function
// being defined.
success: function(label, element) { },
// When the form is valid, add the 'was-validated' class
submitHandler: function(form) {
form.classList.add('was-validated');
form.submit(); // Submit the form
}
});
let autoConfigResult = processConfigUrl();
if (!autoConfigResult) {
reset();
}
});
function exportConfig(isMinified = true) {
const baseNetwork = Object.keys(subnetMap)[0]
let miniSubnetMap = {};
if (isMinified) {
minifySubnetMap(miniSubnetMap, subnetMap, baseNetwork)
}
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,
}
}
}
function getConfigUrl() {
// Deep Copy
let defaultExport = JSON.parse(JSON.stringify(exportConfig(true)));
renameKey(defaultExport, 'config_version', 'v')
renameKey(defaultExport, 'base_network', 'b')
if (defaultExport.hasOwnProperty('operating_mode')) {
renameKey(defaultExport, 'operating_mode', 'm')
}
renameKey(defaultExport, 'subnets', 's')
//console.log(JSON.stringify(defaultExport))
return '/index.html?c=' + urlVersion + LZString.compressToEncodedURIComponent(JSON.stringify(defaultExport))
}
function processConfigUrl() {
const params = new Proxy(new URLSearchParams(window.location.search), {
get: (searchParams, prop) => searchParams.get(prop),
});
if (params['c'] !== null) {
// First character is the version of the URL string, in case the mechanism of encoding changes
let urlVersion = params['c'].substring(0, 1)
let urlData = params['c'].substring(1)
let urlConfig = JSON.parse(LZString.decompressFromEncodedURIComponent(params['c'].substring(1)))
renameKey(urlConfig, 'v', 'config_version')
if (urlConfig.hasOwnProperty('m')) {
renameKey(urlConfig, 'm', 'operating_mode')
}
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
expandKeys(urlConfig['subnets'])
} else if (urlConfig['config_version'] === '2') {
// Version 2 Configs uses the Nth Position representation for subnet keys and requires the base_network
// option. It also uses n/c for note/color
if (urlConfig.hasOwnProperty('b')) {
renameKey(urlConfig, 'b', 'base_network')
}
let expandedSubnetMap = {};
expandSubnetMap(expandedSubnetMap, urlConfig['subnets'], urlConfig['base_network'])
urlConfig['subnets'] = expandedSubnetMap
}
importConfig(urlConfig)
return true
}
}
function minifySubnetMap(minifiedMap, referenceMap, baseNetwork) {
for (let subnet in referenceMap) {
if (subnet.startsWith('_')) continue;
const nthRepresentation = getNthSubnet(baseNetwork, subnet);
minifiedMap[nthRepresentation] = {}
if (referenceMap[subnet].hasOwnProperty('_note')) {
minifiedMap[nthRepresentation]['n'] = referenceMap[subnet]['_note']
}
if (referenceMap[subnet].hasOwnProperty('_color')) {
minifiedMap[nthRepresentation]['c'] = referenceMap[subnet]['_color']
}
if (Object.keys(referenceMap[subnet]).some(key => !key.startsWith('_'))) {
minifySubnetMap(minifiedMap[nthRepresentation], referenceMap[subnet], baseNetwork);
}
}
}
function expandSubnetMap(expandedMap, miniMap, baseNetwork) {
for (let mapKey in miniMap) {
if (mapKey === 'n' || mapKey === 'c') {
continue;
}
let subnetKey = getSubnetFromNth(baseNetwork, mapKey)
expandedMap[subnetKey] = {}
if (has_network_sub_keys(miniMap[mapKey])) {
expandSubnetMap(expandedMap[subnetKey], miniMap[mapKey], baseNetwork)
} else {
if (miniMap[mapKey].hasOwnProperty('n')) {
expandedMap[subnetKey]['_note'] = miniMap[mapKey]['n']
}
if (miniMap[mapKey].hasOwnProperty('c')) {
expandedMap[subnetKey]['_color'] = miniMap[mapKey]['c']
}
}
}
}
// For Config Version 1 Backwards Compatibility
function expandKeys(subnetTree) {
for (let mapKey in subnetTree) {
if (mapKey.startsWith('_')) {
continue;
}
if (has_network_sub_keys(subnetTree[mapKey])) {
expandKeys(subnetTree[mapKey])
} else {
if (subnetTree[mapKey].hasOwnProperty('_n')) {
renameKey(subnetTree[mapKey], '_n', '_note')
}
if (subnetTree[mapKey].hasOwnProperty('_c')) {
renameKey(subnetTree[mapKey], '_c', '_color')
}
}
}
}
function renameKey(obj, oldKey, newKey) {
if (oldKey !== newKey) {
Object.defineProperty(obj, newKey,
Object.getOwnPropertyDescriptor(obj, oldKey));
delete obj[oldKey];
}
}
function importConfig(text) {
if (text['config_version'] === '1') {
var [subnetNet, subnetSize] = Object.keys(text['subnets'])[0].split('/')
} else if (text['config_version'] === '2') {
var [subnetNet, subnetSize] = text['base_network'].split('/')
}
$('#network').val(subnetNet)
$('#netsize').val(subnetSize)
subnetMap = text['subnets'];
operatingMode = text['operating_mode'] || 'Standard'
switchMode(operatingMode);
}
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('')}`

2
dist/robots.txt vendored Normal file
View File

@@ -0,0 +1,2 @@
User-Agent: *
Allow: /

5
dist/sitemap.xml vendored Normal file
View File

@@ -0,0 +1,5 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url>
<loc>https://visualsubnetcalc.com/</loc>
</url>
</urlset>

6
package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "visualsubnetcalc",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

19
repopack.config.json Normal file
View File

@@ -0,0 +1,19 @@
{
"output": {
"filePath": "repopack-output.txt",
"style": "xml",
"removeComments": false,
"removeEmptyLines": false,
"topFilesLength": 5,
"showLineNumbers": false
},
"include": [],
"ignore": {
"useGitignore": true,
"useDefaultPatterns": false,
"customPatterns": []
},
"security": {
"enableSecurityCheck": true
}
}

5
src/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

215
src/cloudformation.yaml Normal file
View File

@@ -0,0 +1,215 @@
---
AWSTemplateFormatVersion: '2010-09-09'
Description: Visual Subnet Calculator
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Naming
Parameters:
- DomainName
ParameterLabels:
DomainName:
default: Domain Name
Parameters:
DomainName:
Type: String
Default: ''
Description: Domain Name for Route53 Hosted Zone and Static S3 Site
Resources:
OriginAccessIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: Visual Subnet Calculator Static Website Amazon CloudFront Identity
StaticWebsite:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub
- visualsubnetcalc-static-website-${Unique}
- Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ]
AccessControl: Private
LoggingConfiguration:
DestinationBucketName: !Ref LoggingBucket
LogFilePrefix: s3-static-website/
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
VersioningConfiguration:
Status: Enabled
LifecycleConfiguration:
Rules:
- Id: DeleteOldVersionAfter7Days
Status: Enabled
NoncurrentVersionExpiration:
NoncurrentDays: 7
StaticWebsiteBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Sub
- visualsubnetcalc-static-website-${Unique}
- Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ]
PolicyDocument:
Version: '2012-10-17'
Id: WebAccess
Statement:
- Sid: CloudFrontReadForGetBucketObjects
Principal:
AWS: !Sub 'arn:${AWS::Partition}:iam::cloudfront:user/CloudFront Origin Access Identity ${OriginAccessIdentity}'
Effect: Allow
Action:
- s3:GetObject
- s3:GetObjectVersion
Resource: !Sub
- arn:${AWS::Partition}:s3:::visualsubnetcalc-static-website-${Unique}/*
- Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ]
- Sid: DenyPlaintextAccess
Principal: '*'
Effect: Deny
Action: s3:*
Resource:
- !Sub
- arn:${AWS::Partition}:s3:::visualsubnetcalc-static-website-${Unique}
- Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ]
- !Sub
- arn:${AWS::Partition}:s3:::visualsubnetcalc-static-website-${Unique}/*
- Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ]
Condition:
Bool:
aws:SecureTransport: 'false'
LoggingBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
Properties:
BucketName: !Sub
- visualsubnetcalc-logging-${Unique}
- Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ]
AccessControl: Private
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
OwnershipControls:
Rules:
- ObjectOwnership: BucketOwnerPreferred
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
VersioningConfiguration:
Status: Enabled
LifecycleConfiguration:
Rules:
- Id: DeleteOldVersionAfter7Days
Status: Enabled
NoncurrentVersionExpiration:
NoncurrentDays: 7
LoggingBucketBucketPolicy:
Type: AWS::S3::BucketPolicy
DependsOn: LoggingBucket
Properties:
Bucket: !Sub
- visualsubnetcalc-logging-${Unique}
- Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ]
PolicyDocument:
Version: '2012-10-17'
Id: WebAccess
Statement:
- Sid: S3ServerAccessLogsPolicy
Effect: Allow
Principal:
Service: logging.s3.amazonaws.com
Action:
- s3:PutObject
Resource: !Sub
- arn:${AWS::Partition}:s3:::visualsubnetcalc-logging-${Unique}/*
- Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ]
Condition:
ArnEquals:
aws:SourceArn:
- !Sub
- arn:${AWS::Partition}:s3:::visualsubnetcalc-static-website-${Unique}
- Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ]
StringEquals:
aws:SourceAccount: !Ref 'AWS::AccountId'
- Sid: DenyPlaintextAccess
Principal: '*'
Effect: Deny
Action: s3:*
Resource:
- !Sub
- arn:${AWS::Partition}:s3:::visualsubnetcalc-logging-${Unique}
- Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ]
- !Sub
- arn:${AWS::Partition}:s3:::visualsubnetcalc-logging-${Unique}/*
- Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ]
Condition:
Bool:
aws:SecureTransport: 'false'
HostedZone:
Type: AWS::Route53::HostedZone
Properties:
HostedZoneConfig:
Comment: !Sub 'VisualSubnetCalc'
Name: !Ref 'DomainName'
Certificate:
Type: AWS::CertificateManager::Certificate
Properties:
DomainName: !Ref 'DomainName'
DomainValidationOptions:
- DomainName: !Ref 'DomainName'
HostedZoneId: !Ref 'HostedZone'
ValidationMethod: DNS
Tags:
- Key: Name
Value: !Sub 'VisualSubnetCalc'
CloudFrontAlias:
Type: AWS::Route53::RecordSet
Properties:
AliasTarget:
DNSName: !GetAtt 'CloudFront.DomainName'
HostedZoneId: Z2FDTNDATAQYW2
Comment: To CloudFront S3
HostedZoneId: !Ref 'HostedZone'
Name: !Sub '${DomainName}.'
Type: A
CloudFront:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Comment: Visual Subnet Calculator Static Website
Aliases:
- !Ref 'DomainName'
Logging:
Bucket: !GetAtt LoggingBucket.DomainName
Prefix: cloudfront
IncludeCookies: true
DefaultCacheBehavior:
ForwardedValues:
QueryString: false
TargetOriginId: !Sub 'S3-Static-Website'
ViewerProtocolPolicy: redirect-to-https
Compress: true
DefaultRootObject: index.html
Enabled: true
HttpVersion: http2
Origins:
- DomainName: !Sub
- visualsubnetcalc-static-website-${Unique}.s3.${AWS::Region}.amazonaws.com
- Unique: !Select [ 4, !Split [ '-', !Select [ 2, !Split [ '/', !Ref 'AWS::StackId' ] ] ] ]
Id: !Sub 'S3-Static-Website'
S3OriginConfig:
OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/${OriginAccessIdentity}'
PriceClass: PriceClass_100
IPV6Enabled: false
ViewerCertificate:
AcmCertificateArn: !Ref 'Certificate'
MinimumProtocolVersion: TLSv1.2_2021
SslSupportMethod: sni-only

BIN
src/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

675
src/package-lock.json generated Normal file
View File

@@ -0,0 +1,675 @@
{
"name": "src",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"hasInstallScript": true,
"dependencies": {
"bootstrap": "^5.3.3",
"http-server": "^14.1.1",
"lz-string": "^1.5.0"
},
"devDependencies": {
"@playwright/test": "^1.48.0",
"@types/node": "^22.7.5"
}
},
"node_modules/@playwright/test": {
"version": "1.48.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.0.tgz",
"integrity": "sha512-W5lhqPUVPqhtc/ySvZI5Q8X2ztBOUgZ8LbAFy0JQgrXZs2xaILrUcNO3rQjwbLPfGK13+rZsDa1FpG+tqYkT5w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.48.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@types/node": {
"version": "22.7.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.2"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/async": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.14"
}
},
"node_modules/basic-auth": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.1.2"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/bootstrap": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
"integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"license": "MIT",
"peerDependencies": {
"@popperjs/core": "^2.11.8"
}
},
"node_modules/call-bind": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/corser": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
"integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.1.3"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"license": "MIT",
"bin": {
"he": "bin/he"
}
},
"node_modules/html-encoding-sniffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
"integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
"license": "MIT",
"dependencies": {
"whatwg-encoding": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/http-proxy": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
"license": "MIT",
"dependencies": {
"eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0",
"requires-port": "^1.0.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/http-server": {
"version": "14.1.1",
"resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz",
"integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==",
"license": "MIT",
"dependencies": {
"basic-auth": "^2.0.1",
"chalk": "^4.1.2",
"corser": "^2.0.1",
"he": "^1.2.0",
"html-encoding-sniffer": "^3.0.0",
"http-proxy": "^1.18.1",
"mime": "^1.6.0",
"minimist": "^1.2.6",
"opener": "^1.5.1",
"portfinder": "^1.0.28",
"secure-compare": "3.0.1",
"union": "~0.5.0",
"url-join": "^4.0.1"
},
"bin": {
"http-server": "bin/http-server"
},
"engines": {
"node": ">=12"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"license": "MIT",
"bin": {
"lz-string": "bin/bin.js"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/object-inspect": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/opener": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
"license": "(WTFPL OR MIT)",
"bin": {
"opener": "bin/opener-bin.js"
}
},
"node_modules/playwright": {
"version": "1.48.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.0.tgz",
"integrity": "sha512-qPqFaMEHuY/ug8o0uteYJSRfMGFikhUysk8ZvAtfKmUK3kc/6oNl/y3EczF8OFGYIi/Ex2HspMfzYArk6+XQSA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.48.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.48.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.0.tgz",
"integrity": "sha512-RBvzjM9rdpP7UUFrQzRwR8L/xR4HyC1QXMzGYTbf1vjw25/ya9NRAVnXi/0fvFopjebvyPzsmoK58xxeEOaVvA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/portfinder": {
"version": "1.0.32",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz",
"integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==",
"license": "MIT",
"dependencies": {
"async": "^2.6.4",
"debug": "^3.2.7",
"mkdirp": "^0.5.6"
},
"engines": {
"node": ">= 0.12.0"
}
},
"node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"license": "MIT"
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/secure-compare": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
"integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==",
"license": "MIT"
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/side-channel": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
"integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.7",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.4",
"object-inspect": "^1.13.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true,
"license": "MIT"
},
"node_modules/union": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
"integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
"dependencies": {
"qs": "^6.4.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/url-join": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
"integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
"license": "MIT"
},
"node_modules/whatwg-encoding": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
"integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=12"
}
}
}
}

19
src/package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"dependencies": {
"bootstrap": "^5.3.3",
"http-server": "^14.1.1",
"lz-string": "^1.5.0"
},
"scripts": {
"postinstall": "sudo npm install -g sass@1.77.6",
"build": "sass --style compressed scss/custom.scss:../dist/css/bootstrap.min.css && cp node_modules/lz-string/libs/lz-string.min.js ../dist/js/lz-string.min.js",
"test": "npx playwright test",
"setup:certs": "mkdir -p certs; mkcert -cert-file certs/cert.pem -key-file certs/cert.key localhost 127.0.0.1",
"start": "node node_modules/http-server/bin/http-server ../dist -c-1",
"local-secure-start": "node node_modules/http-server/bin/http-server ../dist -c-1 -C certs/cert.pem -K certs/cert.key -S -p 8443"
},
"devDependencies": {
"@playwright/test": "^1.48.0",
"@types/node": "^22.7.5"
}
}

99
src/playwright.config.ts Normal file
View File

@@ -0,0 +1,99 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'https://localhost:8443',
ignoreHTTPSErrors: true,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
permissions: ['clipboard-read', 'clipboard-write'],
viewport: {
width: 1920,
height: 1080,
},
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
viewport: {
width: 1920,
height: 1080,
},
},
},
//{
// name: 'webkit',
// use: {
// ...devices['Desktop Safari'],
// permissions: ['clipboard-read'],
// viewport: {
// width: 1920,
// height: 1080,
// },
// },
//},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: 'npm run build && npm run local-secure-start',
port: 8443,
reuseExistingServer: !process.env.CI,
},
});

27
src/scss/custom.scss Normal file
View File

@@ -0,0 +1,27 @@
@import '../node_modules/bootstrap/scss/functions';
@import '../node_modules/bootstrap/scss/variables';
$new-breakpoints: (
xxxl:1600px,
xxxxl:1800px,
//xxxxx:2000px,
);
$grid-breakpoints: map-merge($grid-breakpoints, $new-breakpoints);
$new-container-max-widths: (
xxxl:1520px,
xxxxl:1720px,
//xxxxxl:1920px
);
$container-max-widths: map-merge($container-max-widths, $new-container-max-widths);
.custom-tooltip {
--bs-tooltip-bg: var(--bs-danger) !important;
--bs-tooltip-color: var(--bs-white) !important;
--bs-tooltip-opacity: 1 !important;
}
@import "../node_modules/bootstrap/scss/bootstrap";

BIN
src/split_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

2
src/tests/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
# This file contains a private dataset used for testing real world use cases
real-world-functional.spec.ts

View File

@@ -0,0 +1,192 @@
import { test, expect } from '@playwright/test';
async function getClipboardText(page) {
return page.evaluate(async () => {
return await navigator.clipboard.readText();
});
}
test('Deep Functional Test', async ({ page }) => {
// The goal of this test is to identify any weird interdependencies or issues that may arise
// from doing a variety of actions on one page load. It's meant to emulate a complex human
// user interaction often with steps that don't make sense.
// This does a little of everything:
// - Manual Network Input
// - Subnet splitting/joining
// - Colors
// - Sharable URLs
// - AWS/Azure Mode
// - Import Reddit Example Config
// - Change Network Size
await page.goto('/');
// Change 10.0.0.0/8 -> 172.16.0.0/12
await page.getByLabel('Network Address').click();
await page.getByLabel('Network Address').press('Shift+Home');
await page.getByLabel('Network Address').fill('172.16.0.0');
await page.getByLabel('Network Size').fill('12');
await page.getByRole('button', { name: 'Go' }).click();
// Do a bunch of splitting
await page.getByText('/12', { exact: true }).click();
await page.getByLabel('172.24.0.0/13', { exact: true }).getByText('/13', { exact: true }).click();
await page.getByLabel('172.24.0.0/14', { exact: true }).getByText('/14', { exact: true }).click();
await page.getByLabel('172.26.0.0/15', { exact: true }).getByText('/15', { exact: true }).click();
await page.getByLabel('172.26.0.0/16', { exact: true }).getByText('/16', { exact: true }).click();
await page.getByRole('cell', { name: '/15 Split' }).click();
await page.getByRole('cell', { name: '172.24.0.0/16 Split' }).click();
await page.getByRole('cell', { name: '172.25.0.0/16 Split' }).click();
await page.getByRole('cell', { name: '/14 Split' }).click();
await page.getByRole('cell', { name: '172.30.0.0/15 Split' }).click();
await page.getByRole('cell', { name: '172.31.0.0/16 Split' }).click();
await page.getByLabel('172.31.128.0/17', { exact: true }).getByText('/17', { exact: true }).click();
await page.getByLabel('172.31.192.0/18', { exact: true }).getByText('/18', { exact: true }).click();
await page.getByLabel('172.31.224.0/19', { exact: true }).getByText('/19', { exact: true }).click();
await page.getByLabel('172.31.192.0/19', { exact: true }).getByText('/19', { exact: true }).click();
await page.getByRole('textbox', { name: '172.31.240.0/20 Note' }).click();
await page.getByRole('textbox', { name: '172.31.240.0/20 Note' }).fill('Test A');
await page.getByRole('textbox', { name: '172.31.224.0/20 Note' }).click();
await page.getByRole('textbox', { name: '172.31.224.0/20 Note' }).fill('Test B');
await page.getByRole('cell', { name: '172.31.240.0/20 Split' }).click();
await page.getByRole('textbox', { name: '172.31.240.0/21 Note' }).click();
await page.getByRole('textbox', { name: '172.31.240.0/21 Note' }).fill('Test A - 1');
await page.getByRole('cell', { name: '172.31.248.0/21 Note' }).click();
await page.getByRole('textbox', { name: '172.31.248.0/21 Note' }).fill('Test A - 2');
await page.getByLabel('172.31.248.0/21', { exact: true }).getByText('/21', { exact: true }).click();
await page.getByLabel('172.31.252.0/22', { exact: true }).getByText('/22', { exact: true }).click();
await page.getByRole('cell', { name: '/21 Split' }).click();
await page.getByRole('textbox', { name: '172.31.240.0/22 Note' }).click();
await page.getByRole('textbox', { name: '172.31.240.0/22 Note' }).fill('Test A - 1A');
await page.getByRole('textbox', { name: '172.31.244.0/22 Note' }).click();
await page.getByRole('textbox', { name: '172.31.244.0/22 Note' }).fill('Test A - 1B');
// Join a subnet
await page.getByLabel('172.31.240.0/21 Join').click();
// Change some colors and do some more splitting
await page.getByText('Change Colors »').click();
await page.getByLabel('Color 4').click();
await page.getByRole('cell', { name: '172.26.128.0/17 Usable IPs' }).click();
await page.getByText('« Stop Changing Colors').click();
await page.getByRole('cell', { name: '172.26.128.0/17 Split' }).click();
await page.getByRole('cell', { name: '172.26.192.0/18 Split' }).click();
await page.getByText('Change Colors »').click();
await page.getByLabel('Color 8').click();
await page.getByRole('cell', { name: '172.26.128.0/18 Usable IPs' }).click();
await page.getByText('« Stop Changing Colors').click();
// Make sure we're still not changing colors
await page.getByRole('cell', { name: '172.26.128.0/18 Split' }).click();
// Check a bunch of specific items
await expect(page.getByLabel('Network Address')).toHaveValue('172.16.0.0');
await expect(page.getByLabel('Network Size')).toHaveValue('12');
await expect(page.getByRole('textbox', { name: '172.31.254.0/23 Note' })).toHaveValue('Test A - 2');
await expect(page.getByRole('textbox', { name: '172.31.252.0/23 Note' })).toHaveValue('Test A - 2');
await expect(page.getByRole('textbox', { name: '/22 Note' })).toHaveValue('Test A - 2');
await expect(page.getByRole('textbox', { name: '/21 Note' })).toBeEmpty();
await expect(page.getByRole('textbox', { name: '172.31.224.0/20 Note' })).toHaveValue('Test B');
await expect(page.getByLabel('172.16.0.0/13', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/13');
await expect(page.getByLabel('172.24.0.0/17', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/17');
await expect(page.getByLabel('172.26.128.0/19', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/19');
await expect(page.getByLabel('172.27.0.0/16', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/16');
await expect(page.getByLabel('172.28.0.0/15', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/15');
await expect(page.getByLabel('172.30.0.0/16', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/16');
await expect(page.getByLabel('172.31.0.0/17', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/17');
await expect(page.getByLabel('172.31.128.0/18', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/18');
await expect(page.getByLabel('172.31.192.0/20', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/20');
await expect(page.getByLabel('172.31.240.0/21', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/21');
await expect(page.getByLabel('172.31.248.0/22', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/22');
await expect(page.getByLabel('172.31.252.0/23', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/23');
await expect(page.getByLabel('/12 Join')).toContainText('/12');
await expect(page.getByLabel('/13 Join')).toContainText('/13');
await expect(page.getByLabel('172.26.128.0/17 Join')).toContainText('/17');
await expect(page.getByLabel('172.31.128.0/17 Join')).toContainText('/17');
await expect(page.getByLabel('172.31.192.0/19 Join')).toContainText('/19');
await expect(page.getByLabel('172.31.224.0/19 Join')).toContainText('/19');
await expect(page.getByLabel('/21 Join')).toContainText('/21');
await expect(page.getByLabel('/22 Join')).toContainText('/22');
await expect(page.getByRole('row', { name: '172.26.128.0/19' })).toHaveCSS('background-color', 'rgb(255, 198, 255)');
await expect(page.getByRole('row', { name: '172.26.160.0/19' })).toHaveCSS('background-color', 'rgb(255, 198, 255)');
await expect(page.getByRole('row', { name: '172.26.192.0/19' })).toHaveCSS('background-color', 'rgb(202, 255, 191)');
await expect(page.getByRole('row', { name: '172.26.224.0/19' })).toHaveCSS('background-color', 'rgb(202, 255, 191)');
// Check the Shareable URL
await page.getByText('Copy Shareable URL').click();
let clipboardUrl = await getClipboardText(page);
expect(clipboardUrl).toContain('/index.html?c=1N4IgbiBcIEwgNCARlEBGA7DAdGgbNgAxED0aciAzlKIQMY0iEAmNAvomq5KDAKaMALADNGADgDmjfAAt2nDHJ5sOIAJxSe6MUuCq0a3StUBWUVrSFNvQkcQw0ukIJgBLcYIBWjBtADEwsJ0eIEgqmIm3lq+IAFBIaIqiIIAzO5aYnhRoDF+dACGgUiJiGIY2SC5BUWJxpxo1nUgKQJaIfIgGOagaIKNnCbWzbYdKY6MeG4deGnSMFmMMCYwANYdSylryvow5YsmglugAHaoACp8lAAuAAQAQmH2JiZHICaWADaMp9AIlaiPN5oNBfCyEGAwAC233Ol1uAEEbgBaG5wfTglLQrQwQiCPA-E6w643REotH2XEYAkgH4gC7E0mosLGFmsthAA');
// Check the Export
await page.getByRole('button', { name: 'Tools' }).click();
await page.getByRole('link', { name: 'Import / Export' }).click();
await expect(page.getByLabel('Import/Export Content')).toHaveValue('{\n "config_version": "2",\n "base_network": "172.16.0.0/12",\n "subnets": {\n "172.16.0.0/12": {\n "172.16.0.0/13": {},\n "172.24.0.0/13": {\n "172.24.0.0/14": {\n "172.24.0.0/15": {\n "172.24.0.0/16": {\n "172.24.0.0/17": {},\n "172.24.128.0/17": {}\n },\n "172.25.0.0/16": {\n "172.25.0.0/17": {},\n "172.25.128.0/17": {}\n }\n },\n "172.26.0.0/15": {\n "172.26.0.0/16": {\n "172.26.0.0/17": {},\n "172.26.128.0/17": {\n "172.26.128.0/18": {\n "172.26.128.0/19": {\n "_color": "#ffc6ff"\n },\n "172.26.160.0/19": {\n "_color": "#ffc6ff"\n }\n },\n "172.26.192.0/18": {\n "172.26.192.0/19": {\n "_color": "#caffbf"\n },\n "172.26.224.0/19": {\n "_color": "#caffbf"\n }\n }\n }\n },\n "172.27.0.0/16": {}\n }\n },\n "172.28.0.0/14": {\n "172.28.0.0/15": {},\n "172.30.0.0/15": {\n "172.30.0.0/16": {},\n "172.31.0.0/16": {\n "172.31.0.0/17": {},\n "172.31.128.0/17": {\n "172.31.128.0/18": {},\n "172.31.192.0/18": {\n "172.31.192.0/19": {\n "172.31.192.0/20": {},\n "172.31.208.0/20": {}\n },\n "172.31.224.0/19": {\n "172.31.224.0/20": {\n "_note": "Test B"\n },\n "172.31.240.0/20": {\n "172.31.240.0/21": {\n "_note": "",\n "_color": ""\n },\n "172.31.248.0/21": {\n "172.31.248.0/22": {\n "_note": "Test A - 2"\n },\n "172.31.252.0/22": {\n "172.31.252.0/23": {\n "_note": "Test A - 2"\n },\n "172.31.254.0/23": {\n "_note": "Test A - 2"\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n}');
await page.getByLabel('Import/Export', { exact: true }).getByText('Close').click();
// Set to AWS Mode
await page.getByRole('button', { name: 'Tools' }).click();
await page.getByRole('link', { name: 'Mode - AWS' }).click();
// Check AWS Mode Settings
await expect(page.getByLabel('172.31.254.0/23', { exact: true }).getByLabel('Usable IPs (AWS)')).toContainText('172.31.254.4 - 172.31.255.254');
await expect(page.getByLabel('172.31.254.0/23', { exact: true }).getByLabel('Hosts')).toContainText('507');
await page.getByText('Copy Shareable URL').click();
clipboardUrl = await getClipboardText(page);
expect(clipboardUrl).toContain('/index.html?c=1N4IgbiBcIEwgNCARlEBGA7DAdGgbNgAxED0aciAtqgIIDqAygiAM5SiEDG7IhAJuwC+iNAMigYAUx4AWAGY8AHAHMe+ABZCRGTeMHCQATlXj0i3cANpDF-QYCsC02kImJhW4hhoLIGTABLJRkAKx5uaABiOTlOPBiQA0V7MNMIkGjY+IV9RBkAZiDTRTxU0HTIzgBDGKQcxEUMMpAK6tqcuxE0N06QfOlTeK0QDCdQNBkekXs3Po9h-J8ePEDhvEK1GFKeGHsYAGth3fzDvSsYJp37GVPQADtUABVJFgAXAAIAIUSve3tbkD2FwAGx4D2gzHSP0BaDQoOchBgMGopnBIGeb3eNHeAFp3nArIj8ij3DI8OD7k8Xh9sXiCV5CDIMBSQGiMTTcfjEnYebzBEA');
// Set to Azure Mode
await page.getByRole('button', { name: 'Tools' }).click();
await page.getByRole('link', { name: 'Mode - Azure' }).click();
// Check Azure Mode Settings
await expect(page.getByLabel('172.31.254.0/23', { exact: true }).getByLabel('Usable IPs (Azure)')).toContainText('172.31.254.4 - 172.31.255.254');
await expect(page.getByLabel('172.31.254.0/23', { exact: true }).getByLabel('Hosts')).toContainText('507');
await page.getByText('Copy Shareable URL').click();
clipboardUrl = await getClipboardText(page);
expect(clipboardUrl).toContain('/index.html?c=1N4IgbiBcIEwgNCARlEBGA7DAdGgbNgAxED0aciAtqgIIBaAqgEoCiCIAzlKIQMbchCAE24BfRGhGRQMAKYCALADMBADgDmA-AAsxEjLumjxIAJybp6VYeAm0pm8ZMBWFZbSELMwo8Qw0NiAKMACWagoAVgL80ADESkq8eAkgJqrOUZYxIPGJySrGiAoAzGGWqniZoNmxvACGCUgFiKoYVSA19Y0FThJoXr0gxfKWyXogGG6gaAoDEs5eQz7jxQECeKHjeKVaMJUCMM4wANbjh8WnRnYwbQfOCpegAHaoACqyHAAuAAQAQql+ZzOR4gZweAA2Ahe0HY2QBoLQaEh7kIMBg1Es0JA7y+3xo3wAtN84HZUcUMd4FHhoc83h8fviiSS-IQFBgaSAsTiGYTiaknALBaIgA');
// Import Default Reddit Config
await page.getByRole('button', { name: 'Tools' }).click();
await page.getByRole('link', { name: 'Import / Export' }).click();
await page.getByLabel('Import/Export Content').click();
await page.getByLabel('Import/Export Content').press('ControlOrMeta+a');
await page.getByLabel('Import/Export Content').fill('{\n "config_version": "2",\n "base_network": "10.0.0.0/20",\n "subnets": {\n "10.0.0.0/20": {\n "10.0.0.0/21": {\n "10.0.0.0/22": {\n "10.0.0.0/23": {\n "10.0.0.0/24": {\n "_note": "Data Center - Virtual Servers",\n "_color": "#9bf6ff"\n },\n "10.0.1.0/24": {\n "_note": "Data Center - Virtual Servers",\n "_color": "#9bf6ff"\n }\n },\n "10.0.2.0/23": {\n "10.0.2.0/24": {\n "_note": "Data Center - Virtual Servers",\n "_color": "#9bf6ff"\n },\n "10.0.3.0/24": {\n "_note": "Data Center - Physical Servers",\n "_color": "#a0c4ff"\n }\n }\n },\n "10.0.4.0/22": {\n "10.0.4.0/23": {\n "_note": "Building A - Wifi",\n "_color": "#ffd6a5"\n },\n "10.0.6.0/23": {\n "_note": "Building A - LAN",\n "_color": "#ffd6a5"\n }\n }\n },\n "10.0.8.0/21": {\n "10.0.8.0/22": {\n "10.0.8.0/23": {\n "10.0.8.0/24": {\n "_note": "Building A - Printers",\n "_color": "#ffd6a5"\n },\n "10.0.9.0/24": {\n "_note": "Building A - Voice",\n "_color": "#ffd6a5"\n }\n },\n "10.0.10.0/23": {\n "_note": "Building B - Wifi",\n "_color": "#fdffb6"\n }\n },\n "10.0.12.0/22": {\n "10.0.12.0/23": {\n "_note": "Building B - LAN",\n "_color": "#fdffb6"\n },\n "10.0.14.0/23": {\n "10.0.14.0/24": {\n "_note": "Building B - Printers",\n "_color": "#fdffb6"\n },\n "10.0.15.0/24": {\n "_note": "Building B - Voice",\n "_color": "#fdffb6"\n }\n }\n }\n }\n }\n }\n}');
await page.getByRole('button', { name: 'Import' }).click();
// Do all the Reddit Default Checks
await expect(page.getByLabel('Network Address')).toHaveValue('10.0.0.0');
await expect(page.getByLabel('Network Size')).toHaveValue('20');
await expect(page.getByLabel('10.0.0.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.0.0/24');
await expect(page.getByLabel('10.0.1.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.1.0/24');
await expect(page.getByLabel('10.0.2.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.2.0/24');
await expect(page.getByLabel('10.0.3.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.3.0/24');
await expect(page.getByLabel('10.0.4.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.4.0/23');
await expect(page.getByLabel('10.0.6.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.6.0/23');
await expect(page.getByLabel('10.0.8.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.8.0/24');
await expect(page.getByLabel('10.0.9.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.9.0/24');
await expect(page.getByLabel('10.0.10.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.10.0/23');
await expect(page.getByLabel('10.0.12.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.12.0/23');
await expect(page.getByLabel('10.0.14.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.14.0/24');
await expect(page.getByLabel('10.0.15.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.15.0/24');
await expect(page.getByRole('textbox', { name: '10.0.0.0/24 Note' })).toHaveValue('Data Center - Virtual Servers');
await expect(page.getByRole('textbox', { name: '10.0.1.0/24 Note' })).toHaveValue('Data Center - Virtual Servers');
await expect(page.getByRole('textbox', { name: '10.0.2.0/24 Note' })).toHaveValue('Data Center - Virtual Servers');
await expect(page.getByRole('textbox', { name: '10.0.3.0/24 Note' })).toHaveValue('Data Center - Physical Servers');
await expect(page.getByRole('textbox', { name: '10.0.4.0/23 Note' })).toHaveValue('Building A - Wifi');
await expect(page.getByRole('textbox', { name: '10.0.6.0/23 Note' })).toHaveValue('Building A - LAN');
await expect(page.getByRole('textbox', { name: '10.0.8.0/24 Note' })).toHaveValue('Building A - Printers');
await expect(page.getByRole('textbox', { name: '10.0.9.0/24 Note' })).toHaveValue('Building A - Voice');
await expect(page.getByRole('textbox', { name: '10.0.10.0/23 Note' })).toHaveValue('Building B - Wifi');
await expect(page.getByRole('textbox', { name: '10.0.12.0/23 Note' })).toHaveValue('Building B - LAN');
await expect(page.getByRole('textbox', { name: '10.0.14.0/24 Note' })).toHaveValue('Building B - Printers');
await expect(page.getByRole('textbox', { name: '10.0.15.0/24 Note' })).toHaveValue('Building B - Voice');
await expect(page.getByRole('row', { name: '10.0.0.0/24' })).toHaveCSS('background-color', 'rgb(155, 246, 255)');
await expect(page.getByRole('row', { name: '10.0.1.0/24' })).toHaveCSS('background-color', 'rgb(155, 246, 255)');
await expect(page.getByRole('row', { name: '10.0.2.0/24' })).toHaveCSS('background-color', 'rgb(155, 246, 255)');
await expect(page.getByRole('row', { name: '10.0.3.0/24' })).toHaveCSS('background-color', 'rgb(160, 196, 255)');
await expect(page.getByRole('row', { name: '10.0.4.0/23' })).toHaveCSS('background-color', 'rgb(255, 214, 165)');
await expect(page.getByRole('row', { name: '10.0.6.0/23' })).toHaveCSS('background-color', 'rgb(255, 214, 165)');
await expect(page.getByRole('row', { name: '10.0.8.0/24' })).toHaveCSS('background-color', 'rgb(255, 214, 165)');
await expect(page.getByRole('row', { name: '10.0.9.0/24' })).toHaveCSS('background-color', 'rgb(255, 214, 165)');
await expect(page.getByRole('row', { name: '10.0.10.0/23' })).toHaveCSS('background-color', 'rgb(253, 255, 182)');
await expect(page.getByRole('row', { name: '10.0.12.0/23' })).toHaveCSS('background-color', 'rgb(253, 255, 182)');
await expect(page.getByRole('row', { name: '10.0.14.0/24' })).toHaveCSS('background-color', 'rgb(253, 255, 182)');
await expect(page.getByRole('row', { name: '10.0.15.0/24' })).toHaveCSS('background-color', 'rgb(253, 255, 182)');
// Now change the whole network address
await page.getByLabel('Network Address').click();
await page.getByLabel('Network Address').press('ControlOrMeta+a');
await page.getByLabel('Network Address').fill('192.168.0.0');
await page.getByRole('button', { name: 'Go' }).click();
await expect(page.getByLabel('192.168.0.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('192.168.0.0/24');
});
//test('Test', async ({ page }) => {
// await page.goto('/');
//});

View File

@@ -0,0 +1,22 @@
import { test, expect } from '@playwright/test';
test('Default Homepage Rendering', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/Visual Subnet Calculator/);
await expect(page.getByRole('heading')).toContainText('Visual Subnet Calculator');
await expect(page.getByLabel('Network Address')).toHaveValue('10.0.0.0');
await expect(page.getByLabel('Network Size')).toHaveValue('16');
await expect(page.locator('#useableHeader')).toContainText('Usable IPs');
await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.0.0/16');
await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Range of Addresses')).toContainText('10.0.0.0 - 10.0.255.255');
await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Usable IPs')).toContainText('10.0.0.1 - 10.0.255.254');
await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Hosts')).toContainText('65534');
await expect(page.getByRole('textbox', { name: '10.0.0.0/16 Note' })).toBeEmpty();
await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/16');
// This "default no color" check could maybe be improved. May not be reliable cross-browser.
await expect(page.getByRole('row', { name: '10.0.0.0/16' })).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
await expect(page.getByLabel('Change Colors').locator('span')).toContainText('Change Colors »');
await expect(page.locator('#copy_url')).toContainText('Copy Shareable URL');
});

View File

@@ -0,0 +1,47 @@
import { test, expect } from '@playwright/test';
test('Default Export Content', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Tools' }).click();
await page.getByRole('link', { name: 'Import / Export' }).click();
await expect(page.locator('#importExportModalLabel')).toContainText('Import/Export');
await expect(page.getByLabel('Import/Export', { exact: true })).toContainText('Close');
await expect(page.locator('#importBtn')).toContainText('Import');
await expect(page.getByLabel('Import/Export Content')).toHaveValue('{\n "config_version": "2",\n "base_network": "10.0.0.0/16",\n "subnets": {\n "10.0.0.0/16": {}\n }\n}');
});
test('Default (AWS) Export Content', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Tools' }).click();
await page.getByRole('link', { name: 'Mode - AWS' }).click();
await page.getByRole('button', { name: 'Tools' }).click();
await page.getByRole('link', { name: 'Import / Export' }).click();
await expect(page.getByLabel('Import/Export Content')).toHaveValue('{\n "config_version": "2",\n "operating_mode": "AWS",\n "base_network": "10.0.0.0/16",\n "subnets": {\n "10.0.0.0/16": {}\n }\n}');
});
test('Default (Azure) Export Content', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Tools' }).click();
await page.getByRole('link', { name: 'Mode - Azure' }).click();
await page.getByRole('button', { name: 'Tools' }).click();
await page.getByRole('link', { name: 'Import / Export' }).click();
await expect(page.getByLabel('Import/Export Content')).toHaveValue('{\n "config_version": "2",\n "operating_mode": "AZURE",\n "base_network": "10.0.0.0/16",\n "subnets": {\n "10.0.0.0/16": {}\n }\n}');
await page.getByLabel('Import/Export', { exact: true }).getByText('Close').click();
});
test('Import 192.168.0.0/24', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Tools' }).click();
await page.getByRole('link', { name: 'Import / Export' }).click();
await page.getByLabel('Import/Export Content').click();
await page.getByLabel('Import/Export Content').fill('{\n "config_version": "2",\n "base_network": "192.168.0.0/24",\n "subnets": {\n "192.168.0.0/24": {}\n }\n}');
await page.getByRole('button', { name: 'Import' }).click();
await expect(page.getByLabel('Network Address')).toHaveValue('192.168.0.0');
await expect(page.getByLabel('Network Size')).toHaveValue('24');
await expect(page.getByLabel('192.168.0.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('192.168.0.0/24');
});
//test('Test', async ({ page }) => {
// await page.goto('/');
//});

Binary file not shown.

View File

@@ -0,0 +1,174 @@
import { test, expect } from '@playwright/test';
async function getClipboardText(page) {
return page.evaluate(async () => {
return await navigator.clipboard.readText();
});
}
test('Renders Max Depth /0 to /32', async ({ page }) => {
await page.goto('/');
await page.getByLabel('Network Address').click();
await page.getByLabel('Network Address').press('Shift+Home');
await page.getByLabel('Network Address').fill('0.0.0.0');
await page.getByLabel('Network Address').press('Tab');
await page.getByLabel('Network Size').fill('0');
await page.getByRole('button', { name: 'Go' }).click();
await page.getByRole('cell', { name: '0.0.0.0/0 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/1 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/2 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/3 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/4 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/5 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/6 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/7 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/8 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/9 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/10 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/11 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/12 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/13 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/14 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/15 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/16 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/17 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/18 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/19 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/20 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/21 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/22 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/23 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/24 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/25 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/26 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/27 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/28 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/29 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/30 Split' }).click();
await page.getByRole('cell', { name: '0.0.0.0/31 Split' }).click();
await expect(page.getByLabel('0.0.0.0/32', { exact: true }).getByLabel('Subnet Address')).toContainText('0.0.0.0/32');
await expect(page.getByLabel('0.0.0.0/32', { exact: true }).getByLabel('Range of Addresses')).toContainText('0.0.0.0');
await expect(page.getByLabel('0.0.0.0/32', { exact: true }).getByLabel('Usable IPs')).toContainText('0.0.0.0');
await expect(page.getByLabel('0.0.0.0/32', { exact: true }).getByLabel('Hosts')).toContainText('1');
await expect(page.getByLabel('0.0.0.0/32', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/32');
await expect(page.getByLabel('/31 Join')).toContainText('/31');
await expect(page.getByLabel('128.0.0.0/1', { exact: true }).getByLabel('Subnet Address')).toContainText('128.0.0.0/1');
});
test('Change To 192.168.0.0/24', async ({ page }) => {
await page.goto('/');
await page.getByLabel('Network Address').click();
await page.getByLabel('Network Address').fill('192.168.0.0');
await page.getByLabel('Network Size').click();
await page.getByLabel('Network Size').fill('24');
await page.getByRole('button', { name: 'Go' }).click();
await expect(page.getByLabel('192.168.0.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('192.168.0.0/24');
await expect(page.getByLabel('192.168.0.0/24', { exact: true }).getByLabel('Range of Addresses')).toContainText('192.168.0.0 - 192.168.0.255');
await expect(page.getByLabel('192.168.0.0/24', { exact: true }).getByLabel('Usable IPs')).toContainText('192.168.0.1 - 192.168.0.254');
await expect(page.getByLabel('192.168.0.0/24', { exact: true }).getByLabel('Hosts')).toContainText('254');
await expect(page.getByLabel('192.168.0.0/24', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/24');
await expect(page.getByRole('textbox', { name: '192.168.0.0/24 Note' })).toBeEmpty();
});
test('Deep /32 Split', async ({ page }) => {
await page.goto('/');
await page.getByText('/16', { exact: true }).click();
await page.getByLabel('10.0.128.0/17', { exact: true }).getByText('/17', { exact: true }).click();
await page.getByLabel('10.0.128.0/18', { exact: true }).getByText('/18', { exact: true }).click();
await page.getByLabel('10.0.160.0/19', { exact: true }).getByText('/19', { exact: true }).click();
await page.getByLabel('10.0.176.0/20', { exact: true }).getByText('/20', { exact: true }).click();
await page.getByLabel('10.0.184.0/21', { exact: true }).getByText('/21', { exact: true }).click();
await page.getByLabel('10.0.176.0/21', { exact: true }).getByText('/21', { exact: true }).click();
await page.getByLabel('10.0.176.0/22', { exact: true }).getByText('/22', { exact: true }).click();
await page.getByLabel('10.0.176.0/23', { exact: true }).getByText('/23', { exact: true }).click();
await page.getByLabel('10.0.176.0/24', { exact: true }).getByText('/24', { exact: true }).click();
await page.getByLabel('10.0.176.0/25', { exact: true }).getByText('/25', { exact: true }).click();
await page.getByLabel('10.0.176.0/26', { exact: true }).getByText('/26', { exact: true }).click();
await page.getByLabel('10.0.176.0/27', { exact: true }).getByText('/27', { exact: true }).click();
await page.getByLabel('10.0.176.0/28', { exact: true }).getByText('/28', { exact: true }).click();
await page.getByLabel('10.0.176.0/29', { exact: true }).getByText('/29', { exact: true }).click();
await page.getByLabel('10.0.176.0/30', { exact: true }).getByText('/30', { exact: true }).click();
await page.getByLabel('10.0.176.0/31', { exact: true }).getByText('/31', { exact: true }).click();
await page.getByRole('textbox', { name: '10.0.176.0/32 Note' }).click();
await page.getByRole('textbox', { name: '10.0.176.0/32 Note' }).fill('Test Text');
await page.getByText('Change Colors »').click();
await page.locator('#palette_picker_6').click();
await page.getByRole('cell', { name: '10.0.176.0/32 Subnet Address' }).click();
await page.getByText('« Stop Changing Colors').click();
await page.getByLabel('Network Address').click();
await page.getByLabel('Network Address').fill('99.0.0.0');
await page.getByRole('button', { name: 'Go' }).click();
await expect(page.getByLabel('99.0.176.0/32', { exact: true }).getByLabel('Subnet Address')).toContainText('99.0.176.0/32');
await expect(page.getByLabel('99.0.176.0/32', { exact: true }).getByLabel('Hosts')).toContainText('1');
await expect(page.getByRole('textbox', { name: '99.0.176.0/32 Note' })).toHaveValue('Test Text');
await expect(page.getByLabel('99.0.176.0/32', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/32');
});
test('Usable IPs - Standard', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Tools' }).click();
await page.getByRole('link', { name: 'Mode - Standard' }).click();
await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Usable IPs')).toContainText('10.0.0.1 - 10.0.255.254');
});
test('Usable IPs - AWS', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Tools' }).click();
await page.getByRole('link', { name: 'Mode - AWS' }).click();
await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Usable IPs')).toContainText('10.0.0.4 - 10.0.255.254');
});
test('Usable IPs - Azure', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Tools' }).click();
await page.getByRole('link', { name: 'Mode - Azure' }).click();
await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Usable IPs')).toContainText('10.0.0.4 - 10.0.255.254');
});
test('Note Splitting', async ({ page }) => {
await page.goto('/');
await page.getByLabel('Note Split/Join').click();
await page.getByLabel('Note Split/Join').fill('This should be duplicated!');
await page.getByRole('cell', { name: '/16 Split' }).click();
await expect(page.getByRole('textbox', { name: '10.0.0.0/17 Note' })).toHaveValue('This should be duplicated!');
await expect(page.getByRole('textbox', { name: '10.0.128.0/17 Note' })).toHaveValue('This should be duplicated!');
});
test('Note Joining Same', async ({ page }) => {
await page.goto('/index.html?c=1N4IgbiBcIEwgNCARlEBGADAOm7g9GgGwIgDOUoGA5hSBgBa0B2qAKvQJakAEp9A9gFcANgBNuSAKbdRggA7COAYwCGAF0miAhCAC+iNI0igW0dl14CR4qTPmLVG7Xt2ugA');
await page.getByLabel('/16 Join').click();
await expect(page.getByLabel('Note Split/Join')).toHaveValue('This should be duplicated!');
});
test('Note Joining Different', async ({ page }) => {
await page.goto('/index.html?c=1N4IgbiBcIEwgNCARlEBGADAOm7g9GgGwIgDOUoGA5hSBgBa0B2qAKvQJakAEp9A9gFcANgBNuSAKbdRggA7COAYwCGAF0miAhNwCCIAL6I0jSKBbR2XXgJHipM+YtUbt3AEKGD3oA');
await page.getByLabel('/16 Join').click();
await expect(page.getByLabel('Note Split/Join')).toBeEmpty();
});
test('Color Splitting', async ({ page }) => {
await page.goto('/');
await page.getByText('Change Colors »').click();
await page.getByLabel('Color 5').click();
await page.getByRole('cell', { name: '/16 Subnet Address' }).click();
await page.getByText('« Stop Changing Colors').click();
await page.getByText('/16', { exact: true }).click();
await expect(page.getByRole('row', { name: '10.0.0.0/17' })).toHaveCSS('background-color', 'rgb(155, 246, 255)');
await expect(page.getByRole('row', { name: '10.0.128.0/17' })).toHaveCSS('background-color', 'rgb(155, 246, 255)');
});
test('Color Joining Same', async ({ page }) => {
await page.goto('/index.html?c=1N4IgbiBcIEwgNCARlEBGADAOm7g9GgGwIgDOUoGA5hSBgBa0DGqAxAJxIBmhXXIAX0RpGkUC2gduvfgLkCgA');
await page.getByLabel('/16 Join').click();
await expect(page.getByRole('row', { name: '10.0.0.0/16' })).toHaveCSS('background-color', 'rgb(155, 246, 255)');
});
test('Color Joining Different', async ({ page }) => {
await page.goto('/index.html?c=1N4IgbiBcIEwgNCARlEBGADAOm7g9GgGwIgDOUoGA5hSBgBa0DGqAxAJxIBmhXXIAX0RpGkUC2isuAEz5JiAxQKA');
await page.getByLabel('/16 Join').click();
await expect(page.getByRole('row', { name: '10.0.0.0/16' })).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
});
//test('Test', async ({ page }) => {
// await page.goto('/');
//});

View File

@@ -0,0 +1,76 @@
import { test, expect } from '@playwright/test';
test('Bad Network Address', async ({ page }) => {
await page.goto('/');
await page.getByLabel('Network Address').click();
await page.getByLabel('Network Address').fill('1');
await page.locator('html').click();
await expect(page.locator('#network')).toHaveClass(/error/i);
await expect(page.getByText('Must be a valid IPv4 Address')).toBeVisible();
});
test('Bad Network Size', async ({ page }) => {
await page.goto('/');
await page.getByLabel('Network Size').click();
await page.getByLabel('Network Size').fill('33');
await page.locator('html').click();
await expect(page.locator('#netsize')).toHaveClass(/error/i);
await expect(page.getByText('Smallest size is /32')).toBeVisible();
});
test('Prevent Go on Bad Input', async ({ page }) => {
await page.goto('/');
await page.getByLabel('Network Size').click();
await page.getByLabel('Network Size').fill('33');
await page.locator('html').click();
await page.getByRole('button', { name: 'Go' }).click();
await expect(page.locator('#notifyModalLabel')).toContainText('Warning!');
await expect(page.locator('#notifyModalDescription')).toContainText('Please correct the errors in the form!');
});
test('Network Boundary Correction', async ({ page }) => {
await page.goto('/');
await page.getByLabel('Network Address').click();
await page.getByLabel('Network Address').fill('123.45.67.89');
await page.getByLabel('Network Size').click();
await page.getByLabel('Network Size').fill('20');
await page.getByRole('button', { name: 'Go' }).click();
await expect(page.locator('#notifyModalLabel')).toContainText('Warning!');
await expect(page.locator('#notifyModalDescription')).toContainText('Your network input is not on a network boundary for this network size. It has been automatically changed:');
await expect(page.locator('#notifyModalDescription')).toContainText('123.45.67.89 -> 123.45.64.0');
await page.getByLabel('Warning!').getByLabel('Close').click();
await expect(page.getByLabel('Network Address')).toHaveValue('123.45.64.0');
await page.getByLabel('Network Size').click();
await expect(page.getByRole('cell', { name: '123.45.64.0/20 Subnet Address' })).toContainText('123.45.64.0/20');
await page.getByRole('cell', { name: '/20 Split' }).click();
await page.getByLabel('/20 Join').click();
await expect(page.getByLabel('123.45.64.0/20', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/20');
});
test('Subnet Too Small for AWS Mode', async ({ page }) => {
await page.goto('/');
await expect(page.locator('#useableHeader')).toContainText('Usable IPs');
await page.getByRole('button', { name: 'Tools' }).click();
await page.getByRole('link', { name: 'Mode - AWS' }).click();
await page.getByLabel('Network Size').click();
await page.getByLabel('Network Size').fill('29');
await page.getByRole('button', { name: 'Go' }).click();
await expect(page.locator('#notifyModalLabel')).toContainText('Warning!');
await expect(page.locator('#notifyModalDescription')).toContainText('Please correct the errors in the form!');
await expect(page.getByText('AWS Mode - Smallest size is /28')).toBeVisible();
});
test('Subnet Too Small for Azure Mode', async ({ page }) => {
await page.goto('/');
await expect(page.locator('#useableHeader')).toContainText('Usable IPs');
await page.getByRole('button', { name: 'Tools' }).click();
await page.getByRole('link', { name: 'Mode - Azure' }).click();
await page.getByLabel('Network Size').click();
await page.getByLabel('Network Size').fill('30');
await page.getByRole('button', { name: 'Go' }).click();
await expect(page.locator('#notifyModalLabel')).toContainText('Warning!');
await expect(page.locator('#notifyModalDescription')).toContainText('Please correct the errors in the form!');
await expect(page.getByText('Azure Mode - Smallest size is /29')).toBeVisible();
});

168
src/tests/ui-usage.spec.ts Normal file
View File

@@ -0,0 +1,168 @@
import { test, expect } from '@playwright/test';
test('CIDR Input Typing', async ({ page }) => {
await page.goto('/');
await page.getByLabel('Network Address').click();
await page.getByLabel('Network Address').press('End');
await page.getByLabel('Network Address').press('Shift+Home');
await page.getByLabel('Network Address').press('Delete');
await page.keyboard.type('192.168.0.0/24');
await page.getByRole('button', { name: 'Go' }).click();
await expect(page.getByRole('cell', { name: '192.168.0.0/24 Subnet Address' })).toContainText('192.168.0.0/24');
});
test('CIDR Input Paste', async ({ page }) => {
await page.goto('/');
await page.getByLabel('Network Address').click();
await page.getByLabel('Network Address').press('End');
await page.getByLabel('Network Address').press('Shift+Home');
// From: https://github.com/microsoft/playwright/issues/2511
await page.locator('#network').evaluate((formEl) => {
const data = `172.16.0.0/12`;
const clipboardData = new DataTransfer();
const dataType = 'text/plain';
clipboardData.setData(dataType, data);
const clipboardEvent = new ClipboardEvent('paste', {
clipboardData,
dataType,
data
});
formEl.dispatchEvent(clipboardEvent);
});
await page.getByRole('button', { name: 'Go' }).click();
await expect(page.getByRole('cell', { name: '172.16.0.0/12 Subnet Address' })).toContainText('172.16.0.0/12');
});
test('About Dialog', async ({ page }) => {
await page.goto('/');
await page.locator('#info_icon').click();
await expect(page.locator('#aboutModalLabel')).toContainText('About Visual Subnet Calculator');
await expect(page.getByLabel('About Visual Subnet Calculator')).toContainText('Design Tenets');
await expect(page.getByLabel('About Visual Subnet Calculator')).toContainText('Credits');
await expect(page.getByLabel('About Visual Subnet Calculator').getByText('Close')).toBeVisible();
});
test('GitHub Link', async ({ page }) => {
await page.goto('/');
const page1Promise = page.waitForEvent('popup');
await page.getByLabel('GitHub').click();
const page1 = await page1Promise;
await expect(page1.locator('#repository-container-header')).toContainText('ckabalan / visualsubnetcalc Public');
});
test('Table Header Standard Mode', async ({ page }) => {
await page.goto('/');
await expect(page.locator('#useableHeader')).toContainText('Usable IPs');
});
test('Table Header AWS Mode', async ({ page }) => {
await page.goto('/');
await expect(page.locator('#useableHeader')).toContainText('Usable IPs');
await page.getByRole('button', { name: 'Tools' }).click();
await page.getByRole('link', { name: 'Mode - AWS' }).click();
await expect(page.getByRole('cell', { name: 'Usable IPs', exact: true })).toContainText('Usable IPs (AWS)');
await page.getByRole('link', { name: 'AWS' }).hover()
await expect(page.getByText('AWS reserves 5 addresses in')).toBeVisible();
});
test('Table Header Azure Mode', async ({ page }) => {
await page.goto('/');
await expect(page.locator('#useableHeader')).toContainText('Usable IPs');
await page.getByRole('button', { name: 'Tools' }).click();
await page.getByRole('link', { name: 'Mode - Azure' }).click();
await expect(page.getByRole('cell', { name: 'Usable IPs', exact: true })).toContainText('Usable IPs (Azure)');
await page.getByRole('link', { name: 'Azure' }).hover()
await expect(page.getByText('Azure reserves 5 addresses in')).toBeVisible();
});
test('Table Header AWS then Standard', async ({ page }) => {
await page.goto('/');
await expect(page.locator('#useableHeader')).toContainText('Usable IPs');
await page.getByRole('button', { name: 'Tools' }).click();
await page.getByRole('link', { name: 'Mode - AWS' }).click();
await expect(page.getByRole('cell', { name: 'Usable IPs', exact: true })).toContainText('Usable IPs (AWS)');
await page.getByRole('button', { name: 'Tools' }).click();
await page.getByRole('link', { name: 'Mode - Standard' }).click();
await expect(page.getByRole('cell', { name: 'Usable IPs', exact: true })).toContainText('Usable IPs');
await expect(page.getByRole('cell', { name: 'Usable IPs', exact: true })).not.toContainText('(AWS)');
});
test('Color Palette', async ({ page }) => {
await page.goto('/');
await expect(page.getByLabel('Change Colors').locator('span')).toContainText('Change Colors »');
await page.getByText('Change Colors »').click();
await expect(page.getByLabel('Color 1', { exact: true })).toBeVisible();
await expect(page.getByLabel('Color 2', { exact: true })).toBeVisible();
await expect(page.getByLabel('Color 3', { exact: true })).toBeVisible();
await expect(page.getByLabel('Color 4', { exact: true })).toBeVisible();
await expect(page.getByLabel('Color 5', { exact: true })).toBeVisible();
await expect(page.getByLabel('Color 6', { exact: true })).toBeVisible();
await expect(page.getByLabel('Color 7', { exact: true })).toBeVisible();
await expect(page.getByLabel('Color 8', { exact: true })).toBeVisible();
await expect(page.getByLabel('Color 9', { exact: true })).toBeVisible();
await expect(page.getByLabel('Color 10', { exact: true })).toBeVisible();
await expect(page.getByLabel('Stop Changing Colors').locator('span')).toContainText('« Stop Changing Colors');
await page.getByText('« Stop Changing Colors').click();
await expect(page.getByLabel('Change Colors').locator('span')).toContainText('Change Colors »');
});
test('Test Default Colors', async ({ page }) => {
await page.goto('/index.html?c=1N4IgbiBcIEwgNCARlEBGADAOm7g9GgGwIgDOUoGA5hSBgBa0YCWTAVkwNYUC+ia3SMB590HIbEHDEAZikjRaVhJjjQAFnmIArPNEy1IQlpAB2PSP6MVyjYYAcJgJx6dhzCbQDelkDNtG7jCecj6Ipu6avPy6PgoiQA');
await page.getByText('Change Colors »').click();
// Set the top 10 rows to the default colors and check that they are correct
await page.getByLabel('Color 1', { exact: true }).click();
await page.getByRole('cell', { name: '10.0.0.0/20 Subnet Address' }).click();
await expect(page.getByRole('row', { name: '10.0.0.0/20' })).toHaveCSS('background-color', 'rgb(255, 173, 173)');
await page.getByLabel('Color 2', { exact: true }).click();
await page.getByRole('cell', { name: '10.0.16.0/20 Subnet Address' }).click();
await expect(page.getByRole('row', { name: '10.0.16.0/20' })).toHaveCSS('background-color', 'rgb(255, 214, 165)');
await page.getByLabel('Color 3', { exact: true }).click();
await page.getByRole('cell', { name: '10.0.32.0/20 Subnet Address' }).click();
await expect(page.getByRole('row', { name: '10.0.32.0/20' })).toHaveCSS('background-color', 'rgb(253, 255, 182)');
await page.getByLabel('Color 4', { exact: true }).click();
await page.getByRole('cell', { name: '10.0.48.0/20 Subnet Address' }).click();
await expect(page.getByRole('row', { name: '10.0.48.0/20' })).toHaveCSS('background-color', 'rgb(202, 255, 191)');
await page.getByLabel('Color 5', { exact: true }).click();
await page.getByRole('cell', { name: '10.0.64.0/20 Subnet Address' }).click();
await expect(page.getByRole('row', { name: '10.0.64.0/20' })).toHaveCSS('background-color', 'rgb(155, 246, 255)');
await page.getByLabel('Color 6', { exact: true }).click();
await page.getByRole('cell', { name: '10.0.80.0/20 Subnet Address' }).click();
await expect(page.getByRole('row', { name: '10.0.80.0/20' })).toHaveCSS('background-color', 'rgb(160, 196, 255)');
await page.getByLabel('Color 7', { exact: true }).click();
await page.getByRole('cell', { name: '10.0.96.0/20 Subnet Address' }).click();
await expect(page.getByRole('row', { name: '10.0.96.0/20' })).toHaveCSS('background-color', 'rgb(189, 178, 255)');
await page.getByLabel('Color 8', { exact: true }).click();
await page.getByRole('cell', { name: '10.0.112.0/20 Subnet Address' }).click();
await expect(page.getByRole('row', { name: '10.0.112.0/20' })).toHaveCSS('background-color', 'rgb(255, 198, 255)');
await page.getByLabel('Color 9', { exact: true }).click();
await page.getByRole('cell', { name: '10.0.128.0/20 Subnet Address' }).click();
await expect(page.getByRole('row', { name: '10.0.128.0/20' })).toHaveCSS('background-color', 'rgb(230, 230, 230)');
await page.getByLabel('Color 10', { exact: true }).click();
await page.getByRole('cell', { name: '10.0.144.0/20 Subnet Address' }).click();
await expect(page.getByRole('row', { name: '10.0.144.0/20' })).toHaveCSS('background-color', 'rgb(255, 255, 255)');
// Set rows 11 and 12 to Colors 1 and 2 respectively and check that they are correct
await page.getByLabel('Color 1', { exact: true }).click();
await page.getByRole('cell', { name: '10.0.160.0/20 Subnet Address' }).click();
await expect(page.getByRole('row', { name: '10.0.160.0/20' })).toHaveCSS('background-color', 'rgb(255, 173, 173)');
await page.getByLabel('Color 2', { exact: true }).click();
await page.getByRole('cell', { name: '10.0.176.0/20 Subnet Address' }).click();
await expect(page.getByRole('row', { name: '10.0.176.0/20' })).toHaveCSS('background-color', 'rgb(255, 214, 165)');
// Set rows 11 and 12 to Color 10 (white) to make sure you can change colors later
await page.getByLabel('Color 10', { exact: true }).click();
await page.getByRole('cell', { name: '10.0.160.0/20 Subnet Address' }).click();
await expect(page.getByRole('row', { name: '10.0.160.0/20' })).toHaveCSS('background-color', 'rgb(255, 255, 255)');
await page.getByRole('cell', { name: '10.0.176.0/20 Subnet Address' }).click();
await expect(page.getByRole('row', { name: '10.0.176.0/20' })).toHaveCSS('background-color', 'rgb(255, 255, 255)');
await page.getByText('« Stop Changing Colors').click();
// Make sure when you're not in color change mode you cannot change colors
await page.getByRole('cell', { name: '10.0.0.0/20 Subnet Address' }).click();
// Should still be the old color instead of white (the last palette color selected)
await expect(page.getByRole('row', { name: '10.0.0.0/20' })).toHaveCSS('background-color', 'rgb(255, 173, 173)');
});

View File

@@ -0,0 +1,134 @@
import { test, expect } from '@playwright/test';
async function getClipboardText(page) {
return page.evaluate(async () => {
return await navigator.clipboard.readText();
});
}
test('Default URL Share', async ({ page }) => {
await page.goto('/');
await page.getByText('Copy Shareable URL').click();
const clipboardUrl = await getClipboardText(page);
expect(clipboardUrl).toContain('/index.html?c=1N4IgbiBcIEwgNCARlEBGADAOm7g9GgGwIgDOUoGA5hQL71A');
});
test('Default URL Render', async ({ page }) => {
// This should match default-homepage.spec.ts
await page.goto('/index.html?c=1N4IgbiBcIEwgNCARlEBGADAOm7g9GgGwIgDOUoGA5hQL71A');
await expect(page).toHaveTitle(/Visual Subnet Calculator/);
await expect(page.getByRole('heading')).toContainText('Visual Subnet Calculator');
await expect(page.getByLabel('Network Address')).toHaveValue('10.0.0.0');
await expect(page.getByLabel('Network Size')).toHaveValue('16');
await expect(page.locator('#useableHeader')).toContainText('Usable IPs');
await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.0.0/16');
await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Range of Addresses')).toContainText('10.0.0.0 - 10.0.255.255');
await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Usable IPs')).toContainText('10.0.0.1 - 10.0.255.254');
await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Hosts')).toContainText('65534');
await expect(page.getByRole('textbox', { name: '10.0.0.0/16 Note' })).toBeEmpty();
await expect(page.getByLabel('10.0.0.0/16', { exact: true }).getByLabel('Split', { exact: true })).toContainText('/16');
// This "default no color" check could maybe be improved. May not be reliable cross-browser.
await expect(page.getByRole('row', { name: '10.0.0.0/16' })).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
await expect(page.getByLabel('Change Colors').locator('span')).toContainText('Change Colors »');
await expect(page.locator('#copy_url')).toContainText('Copy Shareable URL');
});
test('Reddit Example URL Render (URL v1 - Config v1)', async ({ page }) => {
// This is great to make sure older URLs still load and render properly
await page.goto('/index.html?c=1N4IgbiBcIIwgNCAzlUMAMA6LOD0AmdVWHbbAuSNUvffYjM2gZgZvPwBZiB9AOyggAIgEMALiIAEAYQCmfMbIBOkgLSSAagEslYgK4iANpIDKysMpSIeAY0EBiAJwAjAGYA2V65ABfRIywYDm4qEH5BUQkZeUUVdW1dA2MzJQslKzC7aCc3T28fPxIyfA5WUIDMEvQCENBw6EipOQVlNU0dfSNTc0sETIcXDy9ff1JmYN4BBvEmmNb1AAUACwBPJC0bLpS0jNsHEXQbTmGCworODnpy0gvq-DK6qZAAIT0tQwATLT4Ac0kAQTaAHUtK4tH09tkvB93CIAKwjIpYdylSaCV7vL6-AFtAAy-wAchCsiB7NDYQjTqMyAAODiUai0y5sJl3B5IzB0u61MJPDGfb5-QGLJTfWK7Elk1ww+GIiqOCaheovN4C7HCzQAew2smJDnJsoK1MCLDR0H5WL+z2BoPB1kl0q8zncvjOpBgVQIV0ZgU99zNKsxgsk1vU+KJ9v1HydLrdZBgtwI7IqCcVj3RqstIbaC1FLXSeqh0dczrl7rhad5GaD2NDWp1hdJjpLsdOpyAA');
await expect(page.getByLabel('Network Address')).toHaveValue('10.0.0.0');
await expect(page.getByLabel('Network Size')).toHaveValue('20');
await expect(page.getByLabel('10.0.0.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.0.0/24');
await expect(page.getByLabel('10.0.1.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.1.0/24');
await expect(page.getByLabel('10.0.2.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.2.0/24');
await expect(page.getByLabel('10.0.3.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.3.0/24');
await expect(page.getByLabel('10.0.4.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.4.0/23');
await expect(page.getByLabel('10.0.6.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.6.0/23');
await expect(page.getByLabel('10.0.8.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.8.0/24');
await expect(page.getByLabel('10.0.9.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.9.0/24');
await expect(page.getByLabel('10.0.10.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.10.0/23');
await expect(page.getByLabel('10.0.12.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.12.0/23');
await expect(page.getByLabel('10.0.14.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.14.0/24');
await expect(page.getByLabel('10.0.15.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.15.0/24');
await expect(page.getByRole('textbox', { name: '10.0.0.0/24 Note' })).toHaveValue('Data Center - Virtual Servers');
await expect(page.getByRole('textbox', { name: '10.0.1.0/24 Note' })).toHaveValue('Data Center - Virtual Servers');
await expect(page.getByRole('textbox', { name: '10.0.2.0/24 Note' })).toHaveValue('Data Center - Virtual Servers');
await expect(page.getByRole('textbox', { name: '10.0.3.0/24 Note' })).toHaveValue('Data Center - Physical Servers');
await expect(page.getByRole('textbox', { name: '10.0.4.0/23 Note' })).toHaveValue('Building A - Wifi');
await expect(page.getByRole('textbox', { name: '10.0.6.0/23 Note' })).toHaveValue('Building A - LAN');
await expect(page.getByRole('textbox', { name: '10.0.8.0/24 Note' })).toHaveValue('Building A - Printers');
await expect(page.getByRole('textbox', { name: '10.0.9.0/24 Note' })).toHaveValue('Building A - Voice');
await expect(page.getByRole('textbox', { name: '10.0.10.0/23 Note' })).toHaveValue('Building B - Wifi');
await expect(page.getByRole('textbox', { name: '10.0.12.0/23 Note' })).toHaveValue('Building B - LAN');
await expect(page.getByRole('textbox', { name: '10.0.14.0/24 Note' })).toHaveValue('Building B - Printers');
await expect(page.getByRole('textbox', { name: '10.0.15.0/24 Note' })).toHaveValue('Building B - Voice');
await expect(page.getByRole('row', { name: '10.0.0.0/24' })).toHaveCSS('background-color', 'rgb(155, 246, 255)');
await expect(page.getByRole('row', { name: '10.0.1.0/24' })).toHaveCSS('background-color', 'rgb(155, 246, 255)');
await expect(page.getByRole('row', { name: '10.0.2.0/24' })).toHaveCSS('background-color', 'rgb(155, 246, 255)');
await expect(page.getByRole('row', { name: '10.0.3.0/24' })).toHaveCSS('background-color', 'rgb(160, 196, 255)');
await expect(page.getByRole('row', { name: '10.0.4.0/23' })).toHaveCSS('background-color', 'rgb(255, 214, 165)');
await expect(page.getByRole('row', { name: '10.0.6.0/23' })).toHaveCSS('background-color', 'rgb(255, 214, 165)');
await expect(page.getByRole('row', { name: '10.0.8.0/24' })).toHaveCSS('background-color', 'rgb(255, 214, 165)');
await expect(page.getByRole('row', { name: '10.0.9.0/24' })).toHaveCSS('background-color', 'rgb(255, 214, 165)');
await expect(page.getByRole('row', { name: '10.0.10.0/23' })).toHaveCSS('background-color', 'rgb(253, 255, 182)');
await expect(page.getByRole('row', { name: '10.0.12.0/23' })).toHaveCSS('background-color', 'rgb(253, 255, 182)');
await expect(page.getByRole('row', { name: '10.0.14.0/24' })).toHaveCSS('background-color', 'rgb(253, 255, 182)');
await expect(page.getByRole('row', { name: '10.0.15.0/24' })).toHaveCSS('background-color', 'rgb(253, 255, 182)');
});
test('Reddit Example URL Conversion (URL v1 - Config v1 to v2)', async ({ page }) => {
// Basically if a user loads a URL (say v1), we load it, but then only produce the latest version URL (v2) when they copy the URL
await page.goto('/index.html?c=1N4IgbiBcIIwgNCAzlUMAMA6LOD0AmdVWHbbAuSNUvffYjM2gZgZvPwBZiB9AOyggAIgEMALiIAEAYQCmfMbIBOkgLSSAagEslYgK4iANpIDKysMpSIeAY0EBiAJwAjAGYA2V65ABfRIywYDm4qEH5BUQkZeUUVdW1dA2MzJQslKzC7aCc3T28fPxIyfA5WUIDMEvQCENBw6EipOQVlNU0dfSNTc0sETIcXDy9ff1JmYN4BBvEmmNb1AAUACwBPJC0bLpS0jNsHEXQbTmGCworODnpy0gvq-DK6qZAAIT0tQwATLT4Ac0kAQTaAHUtK4tH09tkvB93CIAKwjIpYdylSaCV7vL6-AFtAAy-wAchCsiB7NDYQjTqMyAAODiUai0y5sJl3B5IzB0u61MJPDGfb5-QGLJTfWK7Elk1ww+GIiqOCaheovN4C7HCzQAew2smJDnJsoK1MCLDR0H5WL+z2BoPB1kl0q8zncvjOpBgVQIV0ZgU99zNKsxgsk1vU+KJ9v1HydLrdZBgtwI7IqCcVj3RqstIbaC1FLXSeqh0dczrl7rhad5GaD2NDWp1hdJjpLsdOpyAA');
await page.getByText('Copy Shareable URL').click();
const clipboardUrl = await getClipboardText(page);
expect(clipboardUrl).toContain('/index.html?c=1N4IgbiBcIEwgNCARlEBGADAOm7g9DBgiAM5SgYDW5IGANjRgLaMB2jA9je9ACICGAF34ACAMIBTVoIkAnEQFoRANQCWswQFd+dEQGU5YOWUQBjVAGIAnEgBmANlu2QAX0RoukUDxADh4qRl5JTUNbV0DWSNZExBzaGs7R2cXN3QeUBhPb1Q-UUlpOUUVdS0dfUNjYniQRIcnV0QAZmyQHzyAwuCRAAUACwBPElVTcsjo2JqLfgxTABYG1LS0Fi9YDLbUACFNVToAE1VWAHMRAEFigHVVW1Vqyyd9+34AVkaQJo2fHb3Dk-PigAZM4AOXuCUezzeS3cDDWMFWoDmGwAHK1vrsDkdThclD1ZEcgpMHrYnq93lZ0dtMX8ccVlBwRhJwbVIeTUogXl9qb9sSItlcbnczA99k4kPZXGkmoiQPZudAflj-gKlMCwSKIWLbBL3gB2DZoOZUxU0vmq3oErrErXiyXLF4mkBK2n8+mM0zMzWs7W6pb+oA');
});
test('Reddit Example URL Render (URL v1 - Config v2)', async ({ page }) => {
// This is great to make sure older URLs still load and render properly
await page.goto('/index.html?c=1N4IgbiBcIEwgNCARlEBGADAOm7g9DBgiAM5SgYDW5IGANjRgLaMB2jA9je9ACICGAF34ACAMIBTVoIkAnEQFoRANQCWswQFd+dEQGU5YOWUQBjVAGIAnEgBmANlu2QAX0RoukUDxADh4qRl5JTUNbV0DWSNZExBzaGs7R2cXN3QeUBhPb1Q-UUlpOUUVdS0dfUNjYniQRIcnV0QAZmyQHzyAwuCRAAUACwBPElVTcsjo2JqLfgxTABYG1LS0Fi9YDLbUACFNVToAE1VWAHMRAEFigHVVW1Vqyyd9+34AVkaQJo2fHb3Dk-PigAZM4AOXuCUezzeS3cDDWMFWoDmGwAHK1vrsDkdThclD1ZEcgpMHrYnq93lZ0dtMX8ccVlBwRhJwbVIeTUogXl9qb9sSItlcbnczA99k4kPZXGkmoiQPZudAflj-gKlMCwSKIWLbBL3gB2DZoOZUxU0vmq3oErrErXiyXLF4mkBK2n8+mM0zMzWs7W6pb+oA');
await expect(page.getByLabel('Network Address')).toHaveValue('10.0.0.0');
await expect(page.getByLabel('Network Size')).toHaveValue('20');
await expect(page.getByLabel('10.0.0.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.0.0/24');
await expect(page.getByLabel('10.0.1.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.1.0/24');
await expect(page.getByLabel('10.0.2.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.2.0/24');
await expect(page.getByLabel('10.0.3.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.3.0/24');
await expect(page.getByLabel('10.0.4.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.4.0/23');
await expect(page.getByLabel('10.0.6.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.6.0/23');
await expect(page.getByLabel('10.0.8.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.8.0/24');
await expect(page.getByLabel('10.0.9.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.9.0/24');
await expect(page.getByLabel('10.0.10.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.10.0/23');
await expect(page.getByLabel('10.0.12.0/23', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.12.0/23');
await expect(page.getByLabel('10.0.14.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.14.0/24');
await expect(page.getByLabel('10.0.15.0/24', { exact: true }).getByLabel('Subnet Address')).toContainText('10.0.15.0/24');
await expect(page.getByRole('textbox', { name: '10.0.0.0/24 Note' })).toHaveValue('Data Center - Virtual Servers');
await expect(page.getByRole('textbox', { name: '10.0.1.0/24 Note' })).toHaveValue('Data Center - Virtual Servers');
await expect(page.getByRole('textbox', { name: '10.0.2.0/24 Note' })).toHaveValue('Data Center - Virtual Servers');
await expect(page.getByRole('textbox', { name: '10.0.3.0/24 Note' })).toHaveValue('Data Center - Physical Servers');
await expect(page.getByRole('textbox', { name: '10.0.4.0/23 Note' })).toHaveValue('Building A - Wifi');
await expect(page.getByRole('textbox', { name: '10.0.6.0/23 Note' })).toHaveValue('Building A - LAN');
await expect(page.getByRole('textbox', { name: '10.0.8.0/24 Note' })).toHaveValue('Building A - Printers');
await expect(page.getByRole('textbox', { name: '10.0.9.0/24 Note' })).toHaveValue('Building A - Voice');
await expect(page.getByRole('textbox', { name: '10.0.10.0/23 Note' })).toHaveValue('Building B - Wifi');
await expect(page.getByRole('textbox', { name: '10.0.12.0/23 Note' })).toHaveValue('Building B - LAN');
await expect(page.getByRole('textbox', { name: '10.0.14.0/24 Note' })).toHaveValue('Building B - Printers');
await expect(page.getByRole('textbox', { name: '10.0.15.0/24 Note' })).toHaveValue('Building B - Voice');
await expect(page.getByRole('row', { name: '10.0.0.0/24' })).toHaveCSS('background-color', 'rgb(155, 246, 255)');
await expect(page.getByRole('row', { name: '10.0.1.0/24' })).toHaveCSS('background-color', 'rgb(155, 246, 255)');
await expect(page.getByRole('row', { name: '10.0.2.0/24' })).toHaveCSS('background-color', 'rgb(155, 246, 255)');
await expect(page.getByRole('row', { name: '10.0.3.0/24' })).toHaveCSS('background-color', 'rgb(160, 196, 255)');
await expect(page.getByRole('row', { name: '10.0.4.0/23' })).toHaveCSS('background-color', 'rgb(255, 214, 165)');
await expect(page.getByRole('row', { name: '10.0.6.0/23' })).toHaveCSS('background-color', 'rgb(255, 214, 165)');
await expect(page.getByRole('row', { name: '10.0.8.0/24' })).toHaveCSS('background-color', 'rgb(255, 214, 165)');
await expect(page.getByRole('row', { name: '10.0.9.0/24' })).toHaveCSS('background-color', 'rgb(255, 214, 165)');
await expect(page.getByRole('row', { name: '10.0.10.0/23' })).toHaveCSS('background-color', 'rgb(253, 255, 182)');
await expect(page.getByRole('row', { name: '10.0.12.0/23' })).toHaveCSS('background-color', 'rgb(253, 255, 182)');
await expect(page.getByRole('row', { name: '10.0.14.0/24' })).toHaveCSS('background-color', 'rgb(253, 255, 182)');
await expect(page.getByRole('row', { name: '10.0.15.0/24' })).toHaveCSS('background-color', 'rgb(253, 255, 182)');
});
//test('Test', async ({ page }) => {
// await page.goto('/');
//});