Skip to content

Commit

Permalink
add ip range counting
Browse files Browse the repository at this point in the history
  • Loading branch information
ericwang401 committed Sep 2, 2023
1 parent 92f5df3 commit f64afe2
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 59 deletions.
5 changes: 2 additions & 3 deletions lang/en_US/admin/addressPools/addresses.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
'create_address' => 'Create Address',
'create_modal' => [
'title' => 'Create Address',
'bulkActionUnchecked' => 'Bulk Import',
'bulkActionChecked_one' => 'Bulk Import (:count address)',
'bulkActionChecked_other' => 'Bulk Import (:count addresses)',
'starting_address' => 'Starting Address',
'ending_address' => 'Ending Address',
],
'assigned_server' => 'Assigned Server',
'edit_modal' => [
Expand Down
3 changes: 2 additions & 1 deletion lang/en_US/strings.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
'suspended' => 'Suspended',
'network' => 'Network',
'ip' => 'IP Address',
'address' => 'Address',
'address_one' => 'Address',
'address_other' => 'Addresses',
'gateway' => 'Gateway',
Expand Down Expand Up @@ -104,4 +103,6 @@
'cidr' => 'CIDR',
'ipv4' => 'IPv4',
'ipv6' => 'IPv6',
'single' => 'Single',
'multiple' => 'Multiple',
];
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
import { AddressInclude } from '@/api/admin/nodes/addresses/getAddresses'
import { AddressType, rawDataToAddress } from '@/api/server/getServer'
import http from '@/api/http'
import { z } from 'zod'
import { ipAddress, macAddress } from '@/util/validation'

interface CreateAddressParameters {
address: string
type: AddressType
cidr: number
isBulkAction: boolean
gateway: string
macAddress: string | null
serverId: number | null
const baseSchema = z.object({
type: z.enum(['ipv4', 'ipv6']),
cidr: z.preprocess(Number, z.number().int().min(1).max(128)),
gateway: ipAddress().nonempty().max(191),
macAddress: macAddress().max(191).nullable().or(z.literal('')),
serverId: z.literal('').or(z.preprocess(Number, z.number())).nullable(),
})

const singleAddressSchema = z.object({
isBulkAction: z.literal(false),
address: ipAddress().nonempty().max(191),
})

const multipleAddressesSchema = z.object({
isBulkAction: z.literal(true),
startingAddress: ipAddress().nonempty().max(191),
endingAddress: ipAddress().nonempty().max(191),
})

export const schema = z
.discriminatedUnion('isBulkAction', [singleAddressSchema, multipleAddressesSchema])
.and(baseSchema)

type CreateAddressParameters = z.infer<typeof schema> & {
include?: AddressInclude[]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const AddressesContainer = () => {

const columns: ColumnArray<Address> = [
{
header: tStrings('address'),
header: tStrings('address_one'),
accessor: 'address',
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ import { useFlashKey } from '@/util/useFlash'
import { ipAddress, macAddress } from '@/util/validation'
import { zodResolver } from '@hookform/resolvers/zod'
import { FormProvider, useForm } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
import { z } from 'zod'
import ServersSelectForm from '@/components/admin/ipam/addresses/ServersSelectForm'
import createAddress from '@/api/admin/addressPools/addresses/createAddress'
import createAddress, { schema } from '@/api/admin/addressPools/addresses/createAddress'
import { KeyedMutator, mutate } from 'swr'
import { AddressResponse } from '@/api/admin/nodes/addresses/getAddresses'
import CheckboxForm from '@/components/elements/forms/CheckboxForm'
import { useMemo } from 'react'
import { Address } from '@/api/server/getServer'
import { Address, AddressType } from '@/api/server/getServer'
import SegmentedControl from '@/components/elements/SegmentedControl'
import { countIPsInRange } from '@/util/helpers'

interface Props {
open: boolean
Expand Down Expand Up @@ -49,56 +51,31 @@ const CreateAddressModal = ({ open, onClose, mutate }: Props) => {
const { data: pool } = useAddressPoolSWR()
const { clearFlashes, clearAndAddHttpError } = useFlashKey(`admin.addressPools.${pool.id}.addresses.create`)

const schema = z.object({
address: ipAddress().nonempty().max(191),
type: z.enum(['ipv4', 'ipv6']),
cidr: z.preprocess(Number, z.number().int().min(1).max(128)),
isBulkAction: z.boolean(),
gateway: ipAddress().nonempty().max(191),
macAddress: macAddress().max(191).optional().or(z.literal('')),
serverId: z.literal('').or(z.preprocess(Number, z.number())),
})

const form = useForm({
resolver: zodResolver(schema),
defaultValues: {
address: '',
isBulkAction: false,
type: 'ipv4',
startingAddress: '',
endingAddress: '',
address: '',
cidr: '',
isBulkAction: false,
gateway: '',
macAddress: '',
serverId: '',
},
})

const watchAddress = form.watch('address')
const watchIsBulkAction = form.watch('isBulkAction')
const watchCidr = form.watch('cidr')

const isBulkActionLabel = useMemo(() => {
if (watchIsBulkAction) {
const cidr = parseInt(watchCidr)
if (!isNaN(cidr)) {
const addressCount = calculateAddressBlockSize(watchAddress, cidr)
if (addressCount === 0 || addressCount > 1) {
return t('create_modal.bulkActionChecked_other', {
count: addressCount,
})
} else {
return t('create_modal.bulkActionChecked_one', {
count: addressCount,
})
}
} else {
return t('create_modal.bulkActionChecked_other', {
count: 0,
})
}
} else {
return t('create_modal.bulkActionUnchecked')
}
}, [watchAddress, watchIsBulkAction, watchCidr])
const watchType = form.watch('type') as AddressType
const watchStartingAddress = form.watch('startingAddress')
const watchEndingAddress = form.watch('endingAddress')

const addressCount = useMemo(() => {
if (!watchIsBulkAction) return 0

return countIPsInRange(watchType, watchStartingAddress, watchEndingAddress)
}, [watchIsBulkAction, watchType, watchStartingAddress, watchEndingAddress])

const handleClose = () => {
form.reset()
Expand Down Expand Up @@ -151,13 +128,32 @@ const CreateAddressModal = ({ open, onClose, mutate }: Props) => {
<form onSubmit={form.handleSubmit(submit)}>
<Modal.Body>
<FlashMessageRender className='mb-5' byKey={`admin.addressPools.${pool.id}.addresses.create`} />
<TextInputForm name='address' label={tStrings('address')} />
<SegmentedControl
className='!w-full'
disabled={form.formState.isSubmitting}
value={watchIsBulkAction ? 'multiple' : 'single'}
onChange={val => form.setValue('isBulkAction', val === 'multiple')}
data={[
{ value: 'single', label: tStrings('single') },
{ value: 'multiple', label: tStrings('multiple') },
]}
/>
<RadioGroupForm name='type' orientation='vertical' spacing={6}>
<Radio name='type' value='ipv4' label={tStrings('ipv4')} />
<Radio name='type' value='ipv6' label={tStrings('ipv6')} />
</RadioGroupForm>
{watchIsBulkAction ? (
<>
<TextInputForm name='startingAddress' label={t('create_modal.starting_address')} />
<TextInputForm name='endingAddress' label={t('create_modal.ending_address')} />
<p className={'description-small pt-2 pb-4'}>
{addressCount} <Trans t={tStrings} i18nKey={'address'} count={addressCount} />
</p>
</>
) : (
<TextInputForm name='address' label={tStrings('address_one')} />
)}
<TextInputForm name='cidr' label={tStrings('cidr')} placeholder='24' />
<CheckboxForm className='mt-3' name={'isBulkAction'} label={isBulkActionLabel} />
<TextInputForm name='gateway' label={tStrings('gateway')} />
<TextInputForm name='macAddress' label={tStrings('mac_address')} />
<ServersSelectForm />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ const CreateAddressModal = ({ address, onClose, mutate }: Props) => {
className='mb-5'
byKey={`admin.addressPools.${address?.addressPoolId}.addresses.${address?.id}.edit`}
/>
<TextInputForm name='address' label={tStrings('address')} />
<TextInputForm name='address' label={tStrings('address_one')} />
<RadioGroupForm name='type' orientation='vertical' spacing={6}>
<Radio name='type' value='ipv4' label={tStrings('ipv4')} />
<Radio name='type' value='ipv6' label={tStrings('ipv6')} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const ServerNetworkBlock = () => {
{addresses.map(ip => (
<Display.Row key={ip.id} className='grid-cols-1 md:grid-cols-3 text-sm'>
<div className='overflow-hidden'>
<p className='description-small !text-xs'>{tStrings('address')}</p>
<p className='description-small !text-xs'>{tStrings('address_one')}</p>
<p className='font-semibold truncate text-foreground overflow-hidden overflow-ellipsis'>
{ip.address}/{ip.cidr}
</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const HardwareDetailsCard = () => {
{addresses.map(ip => (
<Display.Row key={ip.id} className='grid-cols-1 md:grid-cols-3 text-sm'>
<div>
<p className='description-small !text-xs'>{tStrings('address')}</p>
<p className='description-small !text-xs'>{tStrings('address_one')}</p>
<p className='font-semibold text-foreground'>
{ip.address}/{ip.cidr}
</p>
Expand Down
32 changes: 32 additions & 0 deletions resources/scripts/util/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Params } from 'react-router-dom'
import { AddressType } from '@/api/server/getServer'

export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] }

Expand Down Expand Up @@ -105,3 +106,34 @@ export const bindUrlParams = (url: string, params: Params<string>) => {

return url
}

export const countIPsInRange = (ipType: 'ipv4' | 'ipv6', startIP: string, endIP: string): number => {
if (startIP === '' || endIP === '') return 0

try {
const ipToNumber = (ip: string): bigint => {
const parts = ipType === 'ipv4' ? ip.split('.').map(Number) : ip.split(':')
let number = BigInt(0)
for (let i = 0; i < parts.length; i++) {
if (ipType === 'ipv4') {
number = (number << BigInt(8)) + BigInt(parts[i])
} else {
// @ts-expect-error
number = (number << BigInt(16)) + BigInt(parseInt(parts[i], 16))
}
}
return number
}

const startNumber = ipToNumber(startIP)
const endNumber = ipToNumber(endIP)

if (startNumber > endNumber) {
return 0
}

return Number(endNumber - startNumber + BigInt(1))
} catch {
return 0
}
}

0 comments on commit f64afe2

Please sign in to comment.