First Upload
14
Dockerfile
Normal 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
@@ -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
@@ -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
@@ -0,0 +1,110 @@
|
||||
# Visual Subnet Calculator - [visualsubnetcalc.com](https://visualsubnetcalc.com)
|
||||
|
||||

|
||||
|
||||
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
1
dist/css/bootstrap.min.css.map
vendored
Normal file
272
dist/css/main.css
vendored
Normal 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
After Width: | Height: | Size: 15 KiB |
BIN
dist/icon/android-chrome-192x192.png
vendored
Normal file
After Width: | Height: | Size: 8.5 KiB |
BIN
dist/icon/android-chrome-512x512.png
vendored
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
dist/icon/apple-touch-icon.png
vendored
Normal file
After Width: | Height: | Size: 2.1 KiB |
9
dist/icon/browserconfig.xml
vendored
Normal 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
After Width: | Height: | Size: 1.6 KiB |
BIN
dist/icon/favicon-32x32.png
vendored
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
dist/icon/mstile-150x150.png
vendored
Normal file
After Width: | Height: | Size: 2.8 KiB |
32
dist/icon/safari-pinned-tab.svg
vendored
Normal 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
@@ -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
@@ -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 »</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>« 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
@@ -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
@@ -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
@@ -0,0 +1,2 @@
|
||||
User-Agent: *
|
||||
Allow: /
|
5
dist/sitemap.xml
vendored
Normal 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
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "visualsubnetcalc",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
19
repopack.config.json
Normal 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
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
215
src/cloudformation.yaml
Normal 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
After Width: | Height: | Size: 3.8 MiB |
675
src/package-lock.json
generated
Normal 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
@@ -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
@@ -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
@@ -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
After Width: | Height: | Size: 12 KiB |
2
src/tests/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# This file contains a private dataset used for testing real world use cases
|
||||
real-world-functional.spec.ts
|
192
src/tests/deep-functional.spec.ts
Normal 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('/');
|
||||
//});
|
22
src/tests/default-homepage.spec.ts
Normal 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');
|
||||
});
|
||||
|
||||
|
47
src/tests/import-export.spec.ts
Normal 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('/');
|
||||
//});
|
||||
|
BIN
src/tests/real-world-functional.spec.ts.gpg
Normal file
174
src/tests/subnet-basic.spec.ts
Normal 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('/');
|
||||
//});
|
76
src/tests/ui-error-handling.spec.ts
Normal 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
@@ -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)');
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
134
src/tests/url-sharing.spec.ts
Normal 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('/');
|
||||
//});
|
||||
|