feat(new-tool): IPv4 subnet calculator
This commit is contained in:
		
							parent
							
								
									35a58df4cd
								
							
						
					
					
						commit
						27a4a08570
					
				
							
								
								
									
										18
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								package.json
									
									
									
									
									
								
							| @ -47,7 +47,7 @@ | ||||
|     "colord": "^2.9.3", | ||||
|     "composerize": "^1.2.0", | ||||
|     "cron-validator": "^1.3.1", | ||||
|     "cronstrue": "^2.23.0", | ||||
|     "cronstrue": "^2.24.0", | ||||
|     "crypto-js": "^4.1.1", | ||||
|     "date-fns": "^2.29.3", | ||||
|     "figue": "^1.2.0", | ||||
| @ -59,13 +59,14 @@ | ||||
|     "mathjs": "^10.6.4", | ||||
|     "mime-types": "^2.1.35", | ||||
|     "naive-ui": "^2.34.3", | ||||
|     "netmask": "^2.0.2", | ||||
|     "node-forge": "^1.3.1", | ||||
|     "pinia": "^2.0.33", | ||||
|     "plausible-tracker": "^0.3.8", | ||||
|     "qrcode": "^1.5.1", | ||||
|     "randombytes": "^2.1.0", | ||||
|     "sql-formatter": "^8.2.0", | ||||
|     "ts-pattern": "^4.2.1", | ||||
|     "ts-pattern": "^4.2.2", | ||||
|     "uuid": "^8.3.2", | ||||
|     "vue": "^3.2.47", | ||||
|     "vue-router": "^4.1.6" | ||||
| @ -75,30 +76,31 @@ | ||||
|     "@types/bcryptjs": "^2.4.2", | ||||
|     "@types/crypto-js": "^4.1.1", | ||||
|     "@types/jsdom": "^16.2.15", | ||||
|     "@types/lodash": "^4.14.191", | ||||
|     "@types/lodash": "^4.14.192", | ||||
|     "@types/mime-types": "^2.1.1", | ||||
|     "@types/node": "^16.18.16", | ||||
|     "@types/netmask": "^2.0.0", | ||||
|     "@types/node": "^16.18.22", | ||||
|     "@types/node-forge": "^1.3.1", | ||||
|     "@types/prettier": "^2.7.2", | ||||
|     "@types/qrcode": "^1.5.0", | ||||
|     "@types/randombytes": "^2.0.0", | ||||
|     "@types/uuid": "^8.3.4", | ||||
|     "@typescript-eslint/parser": "^5.55.0", | ||||
|     "@typescript-eslint/parser": "^5.57.0", | ||||
|     "@vitejs/plugin-vue": "^2.3.4", | ||||
|     "@vitejs/plugin-vue-jsx": "^1.3.10", | ||||
|     "@vue/eslint-config-prettier": "^7.1.0", | ||||
|     "@vue/eslint-config-typescript": "^10.0.0", | ||||
|     "@vue/test-utils": "^2.3.1", | ||||
|     "@vue/test-utils": "^2.3.2", | ||||
|     "@vue/tsconfig": "^0.1.3", | ||||
|     "c8": "^7.13.0", | ||||
|     "eslint": "^8.36.0", | ||||
|     "eslint-config-prettier": "^8.7.0", | ||||
|     "eslint-config-prettier": "^8.8.0", | ||||
|     "eslint-import-resolver-typescript": "^3.5.3", | ||||
|     "eslint-plugin-import": "^2.27.5", | ||||
|     "eslint-plugin-vue": "^8.7.1", | ||||
|     "jsdom": "^19.0.0", | ||||
|     "less": "^4.1.3", | ||||
|     "prettier": "^2.8.4", | ||||
|     "prettier": "^2.8.7", | ||||
|     "standard-version": "^9.5.0", | ||||
|     "start-server-and-test": "^1.15.4", | ||||
|     "typescript": "~4.5.5", | ||||
|  | ||||
							
								
								
									
										3219
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3219
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,6 +1,7 @@ | ||||
| import { tool as base64FileConverter } from './base64-file-converter'; | ||||
| import { tool as base64StringConverter } from './base64-string-converter'; | ||||
| import { tool as basicAuthGenerator } from './basic-auth-generator'; | ||||
| import { tool as ipv4SubnetCalculator } from './ipv4-subnet-calculator'; | ||||
| import { tool as dockerRunToDockerComposeConverter } from './docker-run-to-docker-compose-converter'; | ||||
| import { tool as htmlWysiwygEditor } from './html-wysiwyg-editor'; | ||||
| import { tool as rsaKeyPairGenerator } from './rsa-key-pair-generator'; | ||||
| @ -96,6 +97,10 @@ export const toolsByCategory: ToolCategory[] = [ | ||||
|       dockerRunToDockerComposeConverter, | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     name: 'Network', | ||||
|     components: [ipv4SubnetCalculator], | ||||
|   }, | ||||
|   { | ||||
|     name: 'Math', | ||||
|     components: [mathEvaluator, etaCalculator], | ||||
|  | ||||
							
								
								
									
										35
									
								
								src/tools/ipv4-subnet-calculator/copyable-ip-like.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/tools/ipv4-subnet-calculator/copyable-ip-like.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| <template> | ||||
|   <n-tooltip trigger="hover"> | ||||
|     <template #trigger> | ||||
|       <span class="ip" @click="handleClick">{{ ip }}</span> | ||||
|     </template> | ||||
|     {{ tooltipText }} | ||||
|   </n-tooltip> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { useClipboard } from '@vueuse/core'; | ||||
| import { ref, toRefs } from 'vue'; | ||||
| 
 | ||||
| const props = withDefaults(defineProps<{ ip?: string }>(), { ip: '' }); | ||||
| const { ip } = toRefs(props); | ||||
| 
 | ||||
| const initialText = 'Copy to clipboard'; | ||||
| const tooltipText = ref(initialText); | ||||
| 
 | ||||
| const { copy } = useClipboard({ source: ip }); | ||||
| 
 | ||||
| function handleClick() { | ||||
|   copy(); | ||||
|   tooltipText.value = 'Copied!'; | ||||
| 
 | ||||
|   setTimeout(() => (tooltipText.value = initialText), 1000); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped lang="less"> | ||||
| .ip { | ||||
|   font-family: monospace; | ||||
|   cursor: pointer; | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										11
									
								
								src/tools/ipv4-subnet-calculator/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/tools/ipv4-subnet-calculator/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| import { RouterOutlined } from '@vicons/material'; | ||||
| import { defineTool } from '../tool'; | ||||
| 
 | ||||
| export const tool = defineTool({ | ||||
|   name: 'IPv4 subnet calculator', | ||||
|   path: '/ipv4-subnet-calculator', | ||||
|   description: 'Parse your IPv4 CIDR blocks and get all the info you need about your sub network.', | ||||
|   keywords: ['ipv4', 'subnet', 'calculator', 'mask', 'network', 'cidr', 'netmask', 'bitmask', 'broadcast', 'address'], | ||||
|   component: () => import('./ipv4-subnet-calculator.vue'), | ||||
|   icon: RouterOutlined, | ||||
| }); | ||||
| @ -0,0 +1,23 @@ | ||||
| export { getIPClass }; | ||||
| 
 | ||||
| function getIPClass({ ip }: { ip: string }) { | ||||
|   const [firstOctet] = ip.split('.').map(Number); | ||||
| 
 | ||||
|   if (firstOctet < 128) { | ||||
|     return 'A'; | ||||
|   } | ||||
|   if (firstOctet > 127 && firstOctet < 192) { | ||||
|     return 'B'; | ||||
|   } | ||||
|   if (firstOctet > 191 && firstOctet < 224) { | ||||
|     return 'C'; | ||||
|   } | ||||
|   if (firstOctet > 223 && firstOctet < 240) { | ||||
|     return 'D'; | ||||
|   } | ||||
|   if (firstOctet > 239 && firstOctet < 256) { | ||||
|     return 'E'; | ||||
|   } | ||||
| 
 | ||||
|   return undefined; | ||||
| } | ||||
							
								
								
									
										125
									
								
								src/tools/ipv4-subnet-calculator/ipv4-subnet-calculator.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/tools/ipv4-subnet-calculator/ipv4-subnet-calculator.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,125 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <n-form-item label="An IPv4 address with or without mask" v-bind="validationAttrs"> | ||||
|       <n-input v-model:value="ip" /> | ||||
|     </n-form-item> | ||||
| 
 | ||||
|     <div v-if="networkInfo"> | ||||
|       <n-table> | ||||
|         <tbody> | ||||
|           <tr v-for="{ getValue, label, undefinedFallback } in sections" :key="label"> | ||||
|             <td> | ||||
|               <n-text strong>{{ label }}</n-text> | ||||
|             </td> | ||||
|             <td> | ||||
|               <copyable-ip-like v-if="getValue(networkInfo)" :ip="getValue(networkInfo)"></copyable-ip-like> | ||||
|               <n-text v-else depth="3">{{ undefinedFallback }}</n-text> | ||||
|             </td> | ||||
|           </tr> | ||||
|         </tbody> | ||||
|       </n-table> | ||||
| 
 | ||||
|       <n-space style="margin-top: 14px" justify="space-between"> | ||||
|         <n-button tertiary @click="switchToBlock({ count: -1 })"> | ||||
|           <n-icon :component="ArrowLeft" /> | ||||
|           Previous block | ||||
|         </n-button> | ||||
|         <n-button tertiary @click="switchToBlock({ count: 1 })"> | ||||
|           Next block | ||||
|           <n-icon :component="ArrowRight" /> | ||||
|         </n-button> | ||||
|       </n-space> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { computed } from 'vue'; | ||||
| import { Netmask } from 'netmask'; | ||||
| import { withDefaultOnError } from '@/utils/defaults'; | ||||
| import { useValidation } from '@/composable/validation'; | ||||
| import { isNotThrowing } from '@/utils/boolean'; | ||||
| import { useStorage } from '@vueuse/core'; | ||||
| import { ArrowLeft, ArrowRight } from '@vicons/tabler'; | ||||
| import { getIPClass } from './ipv4-subnet-calculator.models'; | ||||
| import CopyableIpLike from './copyable-ip-like.vue'; | ||||
| 
 | ||||
| const ip = useStorage('ipv4-subnet-calculator:ip', '192.168.0.1/24'); | ||||
| 
 | ||||
| const getNetworkInfo = (address: string) => new Netmask(address.trim()); | ||||
| 
 | ||||
| const networkInfo = computed(() => withDefaultOnError(() => getNetworkInfo(ip.value), undefined)); | ||||
| 
 | ||||
| const { attrs: validationAttrs } = useValidation({ | ||||
|   source: ip, | ||||
|   rules: [ | ||||
|     { | ||||
|       message: 'We cannot parse this address, check the format', | ||||
|       validator: (value) => isNotThrowing(() => getNetworkInfo(value.trim())), | ||||
|     }, | ||||
|   ], | ||||
| }); | ||||
| 
 | ||||
| const sections: { | ||||
|   label: string; | ||||
|   getValue: (blocks: Netmask) => string | undefined; | ||||
|   undefinedFallback?: string; | ||||
| }[] = [ | ||||
|   { | ||||
|     label: 'Netmask', | ||||
|     getValue: (block) => block.toString(), | ||||
|   }, | ||||
|   { | ||||
|     label: 'Network address', | ||||
|     getValue: ({ base }) => base, | ||||
|   }, | ||||
|   { | ||||
|     label: 'Network mask', | ||||
|     getValue: ({ mask }) => mask, | ||||
|   }, | ||||
|   { | ||||
|     label: 'Network mask in binary', | ||||
|     getValue: ({ bitmask }) => ('1'.repeat(bitmask) + '0'.repeat(32 - bitmask)).match(/.{8}/g)?.join('.') ?? '', | ||||
|   }, | ||||
|   { | ||||
|     label: 'CIDR notation', | ||||
|     getValue: ({ bitmask }) => `/${bitmask}`, | ||||
|   }, | ||||
|   { | ||||
|     label: 'Wildcard mask', | ||||
|     getValue: ({ hostmask }) => hostmask, | ||||
|   }, | ||||
|   { | ||||
|     label: 'Network size', | ||||
|     getValue: ({ size }) => String(size), | ||||
|   }, | ||||
|   { | ||||
|     label: 'First address', | ||||
|     getValue: ({ first }) => first, | ||||
|   }, | ||||
|   { | ||||
|     label: 'Last address', | ||||
|     getValue: ({ last }) => last, | ||||
|   }, | ||||
|   { | ||||
|     label: 'Broadcast address', | ||||
|     getValue: ({ broadcast }) => broadcast, | ||||
|     undefinedFallback: 'No broadcast address with this mask', | ||||
|   }, | ||||
|   { | ||||
|     label: 'IP class', | ||||
|     getValue: ({ base: ip }) => getIPClass({ ip }), | ||||
|     undefinedFallback: 'Unknown class type', | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| function switchToBlock({ count = 1 }: { count?: number }) { | ||||
|   const next = networkInfo.value?.next(count); | ||||
| 
 | ||||
|   if (next) { | ||||
|     ip.value = next.toString(); | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style lang="less" scoped></style> | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user