feat(ipv4-range-expander): expands a given IPv4 start and end address to a valid IPv4 subnet
This commit is contained in:
		
							parent
							
								
									c68a1fd713
								
							
						
					
					
						commit
						0b7ba4fbfe
					
				
							
								
								
									
										1
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -67,6 +67,7 @@ declare module '@vue/runtime-core' { | ||||
|     RouterView: typeof import('vue-router')['RouterView'] | ||||
|     SearchBar: typeof import('./src/components/SearchBar.vue')['default'] | ||||
|     SearchBarItem: typeof import('./src/components/SearchBarItem.vue')['default'] | ||||
|     SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default'] | ||||
|     TextareaCopyable: typeof import('./src/components/TextareaCopyable.vue')['default'] | ||||
|     ToolCard: typeof import('./src/components/ToolCard.vue')['default'] | ||||
|   } | ||||
|  | ||||
							
								
								
									
										35
									
								
								src/components/SpanCopyable.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/components/SpanCopyable.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| <template> | ||||
|   <n-tooltip trigger="hover"> | ||||
|     <template #trigger> | ||||
|       <span class="value" @click="handleClick">{{ value }}</span> | ||||
|     </template> | ||||
|     {{ tooltipText }} | ||||
|   </n-tooltip> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { useClipboard } from '@vueuse/core'; | ||||
| import { ref, toRefs } from 'vue'; | ||||
| 
 | ||||
| const props = withDefaults(defineProps<{ value?: string }>(), { value: '' }); | ||||
| const { value } = toRefs(props); | ||||
| 
 | ||||
| const initialText = 'Copy to clipboard'; | ||||
| const tooltipText = ref(initialText); | ||||
| 
 | ||||
| const { copy } = useClipboard({ source: value }); | ||||
| 
 | ||||
| function handleClick() { | ||||
|   copy(); | ||||
|   tooltipText.value = 'Copied!'; | ||||
| 
 | ||||
|   setTimeout(() => (tooltipText.value = initialText), 1000); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped lang="less"> | ||||
| .value { | ||||
|   cursor: pointer; | ||||
|   font-family: monospace; | ||||
| } | ||||
| </style> | ||||
| @ -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 ipv4RangeExpander } from './ipv4-range-expander'; | ||||
| import { tool as httpStatusCodes } from './http-status-codes'; | ||||
| import { tool as yamlToJson } from './yaml-to-json-converter'; | ||||
| import { tool as jsonToYaml } from './json-to-yaml-converter'; | ||||
| @ -111,7 +112,7 @@ export const toolsByCategory: ToolCategory[] = [ | ||||
|   }, | ||||
|   { | ||||
|     name: 'Network', | ||||
|     components: [ipv4SubnetCalculator, ipv4AddressConverter, macAddressLookup, ipv6UlaGenerator], | ||||
|     components: [ipv4SubnetCalculator, ipv4AddressConverter, ipv4RangeExpander, macAddressLookup, ipv6UlaGenerator], | ||||
|   }, | ||||
|   { | ||||
|     name: 'Math', | ||||
|  | ||||
							
								
								
									
										13
									
								
								src/tools/ipv4-range-expander/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/tools/ipv4-range-expander/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| import { UnfoldMoreOutlined } from '@vicons/material'; | ||||
| import { defineTool } from '../tool'; | ||||
| 
 | ||||
| export const tool = defineTool({ | ||||
|   name: 'IPv4 range expander', | ||||
|   path: '/ipv4-range-expander', | ||||
|   description: | ||||
|     'Given a start and an end IPv4 address this tool calculates a valid IPv4 network with its CIDR notation.', | ||||
|   keywords: ['ipv4', 'range', 'expander', 'subnet', 'creator', 'cidr'], | ||||
|   component: () => import('./ipv4-range-expander.vue'), | ||||
|   icon: UnfoldMoreOutlined, | ||||
|   createdAt: new Date('2023-04-19'), | ||||
| }); | ||||
| @ -0,0 +1,32 @@ | ||||
| import { test, expect } from '@playwright/test'; | ||||
| 
 | ||||
| test.describe('Tool - IPv4 range expander', () => { | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await page.goto('/ipv4-range-expander'); | ||||
|   }); | ||||
| 
 | ||||
|   test('Has correct title', async ({ page }) => { | ||||
|     await expect(page).toHaveTitle('IPv4 range expander - IT Tools'); | ||||
|   }); | ||||
| 
 | ||||
|   test('Calculates correct for valid input', async ({ page }) => { | ||||
|     await page.getByPlaceholder('Start IPv4 address...').fill('192.168.1.1'); | ||||
|     await page.getByPlaceholder('End IPv4 address...').fill('192.168.7.255'); | ||||
| 
 | ||||
|     expect(await page.getByTestId('start-address.old').textContent()).toEqual('192.168.1.1'); | ||||
|     expect(await page.getByTestId('start-address.new').textContent()).toEqual('192.168.0.0'); | ||||
|     expect(await page.getByTestId('end-address.old').textContent()).toEqual('192.168.7.255'); | ||||
|     expect(await page.getByTestId('end-address.new').textContent()).toEqual('192.168.7.255'); | ||||
|     expect(await page.getByTestId('addresses-in-range.old').textContent()).toEqual('1,791'); | ||||
|     expect(await page.getByTestId('addresses-in-range.new').textContent()).toEqual('2,048'); | ||||
|     expect(await page.getByTestId('cidr.old').textContent()).toEqual(''); | ||||
|     expect(await page.getByTestId('cidr.new').textContent()).toEqual('192.168.0.0/21'); | ||||
|   }); | ||||
| 
 | ||||
|   test('Hides result for invalid input', async ({ page }) => { | ||||
|     await page.getByPlaceholder('Start IPv4 address...').fill('192.168.1.1'); | ||||
|     await page.getByPlaceholder('End IPv4 address...').fill('192.168.0.255'); | ||||
| 
 | ||||
|     await expect(page.getByTestId('result')).not.toBeVisible(); | ||||
|   }); | ||||
| }); | ||||
| @ -0,0 +1,21 @@ | ||||
| import { expect, describe, it } from 'vitest'; | ||||
| import { calculateCidr } from './ipv4-range-expander.service'; | ||||
| 
 | ||||
| describe('ipv4RangeExpander', () => { | ||||
|   describe('when there are two valid ipv4 addresses given', () => { | ||||
|     it('should calculate valid cidr for given addresses', () => { | ||||
|       const result = calculateCidr({ startIp: '192.168.1.1', endIp: '192.168.7.255' }); | ||||
| 
 | ||||
|       expect(result).toBeDefined(); | ||||
|       expect(result?.oldSize).toEqual(1791); | ||||
|       expect(result?.newSize).toEqual(2048); | ||||
|       expect(result?.newStart).toEqual('192.168.0.0'); | ||||
|       expect(result?.newEnd).toEqual('192.168.7.255'); | ||||
|       expect(result?.newCidr).toEqual('192.168.0.0/21'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return empty result for invalid input', () => { | ||||
|       expect(calculateCidr({ startIp: '192.168.7.1', endIp: '192.168.6.255' })).not.toBeDefined(); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										70
									
								
								src/tools/ipv4-range-expander/ipv4-range-expander.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/tools/ipv4-range-expander/ipv4-range-expander.service.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,70 @@ | ||||
| import { convertBase } from '../integer-base-converter/integer-base-converter.model'; | ||||
| import { ipv4ToInt } from '../ipv4-address-converter/ipv4-address-converter.service'; | ||||
| class Ipv4RangeExpanderResult { | ||||
|   oldSize?: number; | ||||
|   newStart?: string; | ||||
|   newEnd?: string; | ||||
|   newCidr?: string; | ||||
|   newSize?: number; | ||||
| } | ||||
| 
 | ||||
| export { calculateCidr, Ipv4RangeExpanderResult }; | ||||
| 
 | ||||
| function bits2ip(ipInt: number) { | ||||
|   return (ipInt >>> 24) + '.' + ((ipInt >> 16) & 255) + '.' + ((ipInt >> 8) & 255) + '.' + (ipInt & 255); | ||||
| } | ||||
| 
 | ||||
| function getRangesize(start: string, end: string) { | ||||
|   if (start == null || end == null) return -1; | ||||
| 
 | ||||
|   return 1 + parseInt(end, 2) - parseInt(start, 2); | ||||
| } | ||||
| 
 | ||||
| function getCidr(start: string, end: string) { | ||||
|   if (start == null || end == null) return null; | ||||
| 
 | ||||
|   const range = getRangesize(start, end); | ||||
|   if (range < 1) { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   let mask = 32; | ||||
|   for (let i = 0; i < 32; i++) { | ||||
|     if (start[i] !== end[i]) { | ||||
|       mask = i; | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const newStart = start.substring(0, mask) + '0'.repeat(32 - mask); | ||||
|   const newEnd = end.substring(0, mask) + '1'.repeat(32 - mask); | ||||
| 
 | ||||
|   return { start: newStart, end: newEnd, mask: mask }; | ||||
| } | ||||
| 
 | ||||
| function calculateCidr({ startIp, endIp }: { startIp: string; endIp: string }) { | ||||
|   const start = convertBase({ | ||||
|     value: ipv4ToInt({ ip: startIp }).toString(), | ||||
|     fromBase: 10, | ||||
|     toBase: 2, | ||||
|   }); | ||||
|   const end = convertBase({ | ||||
|     value: ipv4ToInt({ ip: endIp }).toString(), | ||||
|     fromBase: 10, | ||||
|     toBase: 2, | ||||
|   }); | ||||
| 
 | ||||
|   const cidr = getCidr(start, end); | ||||
|   if (cidr != null) { | ||||
|     const result = new Ipv4RangeExpanderResult(); | ||||
|     result.newEnd = bits2ip(parseInt(cidr.end, 2)); | ||||
|     result.newStart = bits2ip(parseInt(cidr.start, 2)); | ||||
|     result.newCidr = result.newStart + '/' + cidr.mask; | ||||
|     result.newSize = getRangesize(cidr.start, cidr.end); | ||||
| 
 | ||||
|     result.oldSize = getRangesize(start, end); | ||||
|     return result; | ||||
|   } | ||||
| 
 | ||||
|   return undefined; | ||||
| } | ||||
							
								
								
									
										92
									
								
								src/tools/ipv4-range-expander/ipv4-range-expander.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/tools/ipv4-range-expander/ipv4-range-expander.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,92 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <n-space item-style="flex:1 1 0"> | ||||
|       <div> | ||||
|         <n-space item-style="flex:1 1 0"> | ||||
|           <n-form-item label="Start address" v-bind="validationAttrsStart"> | ||||
|             <n-input v-model:value="rawStartAddress" placeholder="Start IPv4 address..." /> | ||||
|           </n-form-item> | ||||
|           <n-form-item label="End address" v-bind="validationAttrsEnd"> | ||||
|             <n-input v-model:value="rawEndAddress" placeholder="End IPv4 address..." /> | ||||
|           </n-form-item> | ||||
|         </n-space> | ||||
| 
 | ||||
|         <n-table v-if="showResult" data-test-id="result"> | ||||
|           <thead> | ||||
|             <tr> | ||||
|               <th> </th> | ||||
|               <th>old value</th> | ||||
|               <th>new value</th> | ||||
|             </tr> | ||||
|           </thead> | ||||
|           <tbody> | ||||
|             <result-row | ||||
|               v-for="{ label, getOldValue, getNewValue } in calculatedValues" | ||||
|               :key="label" | ||||
|               :label="label" | ||||
|               :old-value="getOldValue(result)" | ||||
|               :new-value="getNewValue(result)" | ||||
|             /> | ||||
|           </tbody> | ||||
|         </n-table> | ||||
|       </div> | ||||
|     </n-space> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import { useValidation } from '@/composable/validation'; | ||||
| import { isValidIpv4 } from '../ipv4-address-converter/ipv4-address-converter.service'; | ||||
| import { calculateCidr, Ipv4RangeExpanderResult } from './ipv4-range-expander.service'; | ||||
| import ResultRow from './result-row.vue'; | ||||
| 
 | ||||
| const rawStartAddress = useStorage('ipv4-range-expander:startAddress', '192.168.1.1'); | ||||
| const rawEndAddress = useStorage('ipv4-range-expander:endAddress', '192.168.6.255'); | ||||
| 
 | ||||
| const result = computed(() => calculateCidr({ startIp: rawStartAddress.value, endIp: rawEndAddress.value })); | ||||
| 
 | ||||
| const calculatedValues: { | ||||
|   label: string; | ||||
|   getOldValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined; | ||||
|   getNewValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined; | ||||
| }[] = [ | ||||
|   { | ||||
|     label: 'Start address', | ||||
|     getOldValue: () => rawStartAddress.value, | ||||
|     getNewValue: (result) => result?.newStart, | ||||
|   }, | ||||
|   { | ||||
|     label: 'End address', | ||||
|     getOldValue: () => rawEndAddress.value, | ||||
|     getNewValue: (result) => result?.newEnd, | ||||
|   }, | ||||
|   { | ||||
|     label: 'Addresses in range', | ||||
|     getOldValue: (result) => result?.oldSize?.toLocaleString(), | ||||
|     getNewValue: (result) => result?.newSize?.toLocaleString(), | ||||
|   }, | ||||
|   { | ||||
|     label: 'CIDR', | ||||
|     getOldValue: () => '', | ||||
|     getNewValue: (result) => result?.newCidr, | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| const showResult = computed( | ||||
|   () => | ||||
|     validationAttrsStart.validationStatus !== 'error' && | ||||
|     validationAttrsEnd.validationStatus !== 'error' && | ||||
|     result.value !== undefined, | ||||
| ); | ||||
| const { attrs: validationAttrsStart } = useValidation({ | ||||
|   source: rawStartAddress, | ||||
|   rules: [{ message: 'Invalid ipv4 address', validator: (ip) => isValidIpv4({ ip }) }], | ||||
| }); | ||||
| 
 | ||||
| const { attrs: validationAttrsEnd } = useValidation({ | ||||
|   source: rawEndAddress, | ||||
|   rules: [{ message: 'Invalid ipv4 address', validator: (ip) => isValidIpv4({ ip }) }], | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="less" scoped></style> | ||||
							
								
								
									
										27
									
								
								src/tools/ipv4-range-expander/result-row.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/tools/ipv4-range-expander/result-row.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| <template> | ||||
|   <tr> | ||||
|     <td> | ||||
|       <n-text strong>{{ label }}</n-text> | ||||
|     </td> | ||||
|     <td :data-test-id="testId + '.old'"><span-copyable :value="oldValue" class="monospace" /></td> | ||||
|     <td :data-test-id="testId + '.new'"> | ||||
|       <span-copyable :value="newValue"></span-copyable> | ||||
|     </td> | ||||
|   </tr> | ||||
| </template> | ||||
| 
 | ||||
| <script setup lang="ts"> | ||||
| import SpanCopyable from '@/components/SpanCopyable.vue'; | ||||
| import { paramCase } from 'change-case'; | ||||
| 
 | ||||
| const props = withDefaults(defineProps<{ label: string; oldValue?: string; newValue?: string }>(), { | ||||
|   label: '', | ||||
|   oldValue: '', | ||||
|   newValue: '', | ||||
| }); | ||||
| const { label, oldValue, newValue } = toRefs(props); | ||||
| 
 | ||||
| const testId = computed(() => paramCase(label.value)); | ||||
| </script> | ||||
| 
 | ||||
| <style scoped lang="less"></style> | ||||
| @ -12,7 +12,7 @@ | ||||
|               <n-text strong>{{ label }}</n-text> | ||||
|             </td> | ||||
|             <td> | ||||
|               <copyable-ip-like v-if="getValue(networkInfo)" :ip="getValue(networkInfo)"></copyable-ip-like> | ||||
|               <span-copyable v-if="getValue(networkInfo)" :value="getValue(networkInfo)"></span-copyable> | ||||
|               <n-text v-else depth="3">{{ undefinedFallback }}</n-text> | ||||
|             </td> | ||||
|           </tr> | ||||
| @ -41,8 +41,8 @@ import { useValidation } from '@/composable/validation'; | ||||
| import { isNotThrowing } from '@/utils/boolean'; | ||||
| import { useStorage } from '@vueuse/core'; | ||||
| import { ArrowLeft, ArrowRight } from '@vicons/tabler'; | ||||
| import SpanCopyable from '@/components/SpanCopyable.vue'; | ||||
| 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'); | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user