From 4d08de5642a04b849b80bfe2e282039fa7e39796 Mon Sep 17 00:00:00 2001 From: Eric Wang <37554696+ericwang401@users.noreply.github.com> Date: Sat, 11 Nov 2023 06:39:33 +0000 Subject: [PATCH] add IPAM node allocation - NEEDS WORK! --- .../AddressPools/AddressPoolController.php | 3 +- .../AddressPools/UpdateAddressPoolRequest.php | 12 +- public/vendor/horizon/img/sprite.svg | 2 +- .../{ => addresses}/getAddresses.ts | 0 .../admin/addressPools/getNodesAllocatedTo.ts | 35 +++ .../admin/addressPools/updateAddressPool.ts | 10 +- .../addressPools/useAddressPoolNodesSWR.ts | 24 ++ .../api/admin/addressPools/useAddressesSWR.ts | 5 +- .../components/admin/ipam/EditPoolModal.tsx | 22 +- .../admin/ipam/NodesMultiSelectForm.tsx | 4 +- .../admin/ipam/addresses/EditAddressModal.tsx | 5 +- .../nodes/addresses/CreateAddressModal.tsx | 218 ------------------ .../addresses/NodeAddressesContainer.tsx | 16 +- .../elements/forms/MultiSelectForm.tsx | 7 +- resources/scripts/routers/AdminIpamRouter.tsx | 15 +- 15 files changed, 119 insertions(+), 259 deletions(-) rename resources/scripts/api/admin/addressPools/{ => addresses}/getAddresses.ts (100%) create mode 100644 resources/scripts/api/admin/addressPools/getNodesAllocatedTo.ts create mode 100644 resources/scripts/api/admin/addressPools/useAddressPoolNodesSWR.ts delete mode 100644 resources/scripts/components/admin/nodes/addresses/CreateAddressModal.tsx diff --git a/app/Http/Controllers/Admin/AddressPools/AddressPoolController.php b/app/Http/Controllers/Admin/AddressPools/AddressPoolController.php index b70267b2e0b..ad2594f7d63 100644 --- a/app/Http/Controllers/Admin/AddressPools/AddressPoolController.php +++ b/app/Http/Controllers/Admin/AddressPools/AddressPoolController.php @@ -56,7 +56,8 @@ public function store(StoreAddressPoolRequest $request) public function update(UpdateAddressPoolRequest $request, AddressPool $addressPool) { - $addressPool->update($request->validated()); + $addressPool->update($request->safe()->except('node_ids')); + $addressPool->nodes()->sync($request->node_ids); $addressPool->loadCount(['addresses', 'nodes']); return fractal($addressPool, new AddressPoolTransformer)->respond(); diff --git a/app/Http/Requests/Admin/AddressPools/UpdateAddressPoolRequest.php b/app/Http/Requests/Admin/AddressPools/UpdateAddressPoolRequest.php index 747dde64386..072bb10dc8d 100644 --- a/app/Http/Requests/Admin/AddressPools/UpdateAddressPoolRequest.php +++ b/app/Http/Requests/Admin/AddressPools/UpdateAddressPoolRequest.php @@ -11,6 +11,16 @@ public function rules(): array { $addressPool = $this->parameter('address_pool', AddressPool::class); - return AddressPool::getRulesForUpdate($addressPool); + return [ + ...AddressPool::getRulesForUpdate($addressPool), + 'node_ids' => 'sometimes|array', + 'node_ids.*' => 'exists:nodes,id|integer', + ]; + } + + public function after(): array + { + // TODO: if nodes are removed, check whether their servers are using any of IPs from this pool before unmounting + return []; } } diff --git a/public/vendor/horizon/img/sprite.svg b/public/vendor/horizon/img/sprite.svg index 12960779db6..725949feaee 100644 --- a/public/vendor/horizon/img/sprite.svg +++ b/public/vendor/horizon/img/sprite.svg @@ -1,4 +1,4 @@ - + diff --git a/resources/scripts/api/admin/addressPools/getAddresses.ts b/resources/scripts/api/admin/addressPools/addresses/getAddresses.ts similarity index 100% rename from resources/scripts/api/admin/addressPools/getAddresses.ts rename to resources/scripts/api/admin/addressPools/addresses/getAddresses.ts diff --git a/resources/scripts/api/admin/addressPools/getNodesAllocatedTo.ts b/resources/scripts/api/admin/addressPools/getNodesAllocatedTo.ts new file mode 100644 index 00000000000..ecac6843713 --- /dev/null +++ b/resources/scripts/api/admin/addressPools/getNodesAllocatedTo.ts @@ -0,0 +1,35 @@ +import { NodeResponse, rawDataToNode } from '@/api/admin/nodes/getNodes' +import http, { getPaginationSet } from '@/api/http' + +export interface QueryParams { + query?: string | null + fqdn?: string | null + locationId?: number | null + page?: number | null + perPage?: number | null +} + +const getNodesAllocatedTo = async ( + addressPoolId: number, + { query, fqdn, locationId, page, perPage = 50 }: QueryParams +): Promise => { + const { data } = await http.get( + `/api/admin/address-pools/${addressPoolId}/nodes`, + { + params: { + query, + fqdn, + location_id: locationId, + page, + per_page: perPage, + }, + } + ) + + return { + items: data.data.map(rawDataToNode), + pagination: getPaginationSet(data.meta.pagination), + } +} + +export default getNodesAllocatedTo diff --git a/resources/scripts/api/admin/addressPools/updateAddressPool.ts b/resources/scripts/api/admin/addressPools/updateAddressPool.ts index b29b6bc4462..50a42a059ff 100644 --- a/resources/scripts/api/admin/addressPools/updateAddressPool.ts +++ b/resources/scripts/api/admin/addressPools/updateAddressPool.ts @@ -3,17 +3,21 @@ import http from '@/api/http' interface UpdateAddressPoolParameters { name: string + nodeIds?: number[] | null } const updateAddressPool = async ( id: number, - payload: UpdateAddressPoolParameters + { name, nodeIds }: UpdateAddressPoolParameters ) => { const { data: { data }, - } = await http.put(`/api/admin/address-pools/${id}`, payload) + } = await http.put(`/api/admin/address-pools/${id}`, { + name, + node_ids: nodeIds, + }) return rawDataToAddressPool(data) } -export default updateAddressPool \ No newline at end of file +export default updateAddressPool diff --git a/resources/scripts/api/admin/addressPools/useAddressPoolNodesSWR.ts b/resources/scripts/api/admin/addressPools/useAddressPoolNodesSWR.ts new file mode 100644 index 00000000000..a1cb8dda2dc --- /dev/null +++ b/resources/scripts/api/admin/addressPools/useAddressPoolNodesSWR.ts @@ -0,0 +1,24 @@ +import { useParams } from 'react-router-dom' +import useSWR from 'swr' + +import getNodesAllocatedTo, { + QueryParams, +} from '@/api/admin/addressPools/getNodesAllocatedTo' +import { NodeResponse } from '@/api/admin/nodes/getNodes' + +const useAddressPoolNodesSWR = ( + poolId: number, + { page, query, ...params }: QueryParams +) => { + return useSWR( + ['admin.address-pools.nodes', poolId, page, query], + () => + getNodesAllocatedTo(poolId, { + page, + query, + ...params, + }) + ) +} + +export default useAddressPoolNodesSWR diff --git a/resources/scripts/api/admin/addressPools/useAddressesSWR.ts b/resources/scripts/api/admin/addressPools/useAddressesSWR.ts index 51a87765b73..1fb8f6d977a 100644 --- a/resources/scripts/api/admin/addressPools/useAddressesSWR.ts +++ b/resources/scripts/api/admin/addressPools/useAddressesSWR.ts @@ -4,10 +4,9 @@ import useSWR, { Key, SWRResponse } from 'swr' import getAddresses, { QueryParams, -} from '@/api/admin/addressPools/getAddresses' +} from '@/api/admin/addressPools/addresses/getAddresses' import { AddressResponse } from '@/api/admin/nodes/addresses/getAddresses' - export const getKey = (id: number, page?: number, query?: string): Key => [ 'admin.address-pools.addresses', id, @@ -33,4 +32,4 @@ const useAddressesSWR = ({ page, query, ...params }: QueryParams) => { ) as Optimistic> } -export default useAddressesSWR \ No newline at end of file +export default useAddressesSWR diff --git a/resources/scripts/components/admin/ipam/EditPoolModal.tsx b/resources/scripts/components/admin/ipam/EditPoolModal.tsx index c2c69704049..94b5b3a80f6 100644 --- a/resources/scripts/components/admin/ipam/EditPoolModal.tsx +++ b/resources/scripts/components/admin/ipam/EditPoolModal.tsx @@ -1,5 +1,6 @@ import { useFlashKey } from '@/util/useFlash' import { zodResolver } from '@hookform/resolvers/zod' +import { data } from 'autoprefixer' import { useEffect } from 'react' import { FormProvider, useForm } from 'react-hook-form' import { useTranslation } from 'react-i18next' @@ -11,11 +12,13 @@ import { AddressPoolResponse, } from '@/api/admin/addressPools/getAddressPools' import updateAddressPool from '@/api/admin/addressPools/updateAddressPool' +import useAddressPoolNodesSWR from '@/api/admin/addressPools/useAddressPoolNodesSWR' import FlashMessageRender from '@/components/elements/FlashMessageRenderer' import Modal from '@/components/elements/Modal' import TextInputForm from '@/components/elements/forms/TextInputForm' +import NodesMultiSelectForm from '@/components/admin/ipam/NodesMultiSelectForm' interface Props { pool: AddressPool | null @@ -29,21 +32,31 @@ const EditPoolModal = ({ pool, onClose, mutate }: Props) => { const { clearFlashes, clearAndAddHttpError } = useFlashKey( `admin.addressPools.${pool?.id}.update` ) + const { data: nodes } = useAddressPoolNodesSWR(pool?.id ?? -1, {}) const schema = z.object({ - name: z.string().nonempty().max(191), + name: z.string().min(1).max(191), + nodeIds: z.array(z.coerce.number()), }) const form = useForm({ resolver: zodResolver(schema), defaultValues: { name: '', + nodeIds: [] as string[], }, }) + useEffect(() => { + form.reset({ + nodeIds: nodes?.items.map(node => node.id.toString()) ?? [], + }) + }, [nodes]) + useEffect(() => { form.reset({ name: pool?.name ?? '', + nodeIds: nodes?.items.map(node => node.id.toString()) ?? [], }) }, [pool]) @@ -52,7 +65,9 @@ const EditPoolModal = ({ pool, onClose, mutate }: Props) => { onClose() } - const submit = async (data: z.infer) => { + const submit = async (_data: any) => { + const data = _data as z.infer + clearFlashes() try { const updatedPool = await updateAddressPool(pool!.id, data) @@ -90,6 +105,7 @@ const EditPoolModal = ({ pool, onClose, mutate }: Props) => { byKey={`admin.addressPools.${pool?.id}.update`} /> + @@ -109,4 +125,4 @@ const EditPoolModal = ({ pool, onClose, mutate }: Props) => { ) } -export default EditPoolModal \ No newline at end of file +export default EditPoolModal diff --git a/resources/scripts/components/admin/ipam/NodesMultiSelectForm.tsx b/resources/scripts/components/admin/ipam/NodesMultiSelectForm.tsx index c1425437a60..b9f840f4b3a 100644 --- a/resources/scripts/components/admin/ipam/NodesMultiSelectForm.tsx +++ b/resources/scripts/components/admin/ipam/NodesMultiSelectForm.tsx @@ -8,7 +8,6 @@ import useNodesSWR from '@/api/admin/nodes/useNodesSWR' import DescriptiveItemComponent from '@/components/elements/DescriptiveItemComponent' import MultiSelectForm from '@/components/elements/forms/MultiSelectForm' - interface Props { disabled?: boolean } @@ -70,8 +69,9 @@ const NodesMultiSelectForm = ({ disabled }: Props) => { nothingFound={t('nodes_nothing_found')} name={'nodeIds'} disabled={disabled} + value={nodeIds} /> ) } -export default NodesMultiSelectForm \ No newline at end of file +export default NodesMultiSelectForm diff --git a/resources/scripts/components/admin/ipam/addresses/EditAddressModal.tsx b/resources/scripts/components/admin/ipam/addresses/EditAddressModal.tsx index b3f26904d61..26b703d0030 100644 --- a/resources/scripts/components/admin/ipam/addresses/EditAddressModal.tsx +++ b/resources/scripts/components/admin/ipam/addresses/EditAddressModal.tsx @@ -19,14 +19,13 @@ import Radio from '@/components/elements/inputs/Radio' import ServersSelectForm from '@/components/admin/ipam/addresses/ServersSelectForm' - interface Props { address: Address | null onClose: () => void mutate: KeyedMutator } -const CreateAddressModal = ({ address, onClose, mutate }: Props) => { +const EditAddressModal = ({ address, onClose, mutate }: Props) => { const { t: tStrings } = useTranslation('strings') const { t } = useTranslation('admin.addressPools.addresses') const { clearFlashes, clearAndAddHttpError } = useFlashKey( @@ -172,4 +171,4 @@ const CreateAddressModal = ({ address, onClose, mutate }: Props) => { ) } -export default CreateAddressModal \ No newline at end of file +export default EditAddressModal diff --git a/resources/scripts/components/admin/nodes/addresses/CreateAddressModal.tsx b/resources/scripts/components/admin/nodes/addresses/CreateAddressModal.tsx deleted file mode 100644 index d4f9d6db04f..00000000000 --- a/resources/scripts/components/admin/nodes/addresses/CreateAddressModal.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import { countIPsInRange } from '@/util/helpers' -import { useFlashKey } from '@/util/useFlash' -import { zodResolver } from '@hookform/resolvers/zod' -import { useMemo } from 'react' -import { FormProvider, useForm } from 'react-hook-form' -import { Trans, useTranslation } from 'react-i18next' -import { KeyedMutator } from 'swr' -import { z } from 'zod' - -import createAddress, { - schema, -} from '@/api/admin/addressPools/addresses/createAddress' -import { AddressResponse } from '@/api/admin/nodes/addresses/getAddresses' -import useNodeSWR from '@/api/admin/nodes/useNodeSWR' -import { AddressType } from '@/api/server/getServer' - -import FlashMessageRender from '@/components/elements/FlashMessageRenderer' -import Modal from '@/components/elements/Modal' -import SegmentedControl from '@/components/elements/SegmentedControl' -import RadioGroupForm from '@/components/elements/forms/RadioGroupForm' -import TextInputForm from '@/components/elements/forms/TextInputForm' -import Radio from '@/components/elements/inputs/Radio' - -import ServersSelectForm from '@/components/admin/ipam/addresses/ServersSelectForm' - - -interface Props { - open: boolean - onClose: () => void - mutate: KeyedMutator -} - -const CreateAddressModal = ({ open, onClose, mutate }: Props) => { - const { t: tStrings } = useTranslation('strings') - const { t } = useTranslation('admin.addressPools.addresses') - const { data: node } = useNodeSWR() - const { clearFlashes, clearAndAddHttpError } = useFlashKey( - `admin.nodes.${node.id}` - ) - - const form = useForm({ - resolver: zodResolver(schema), - defaultValues: { - isBulkAction: false, - type: 'ipv4', - startingAddress: '', - endingAddress: '', - address: '', - cidr: '', - gateway: '', - macAddress: '', - serverId: '', - }, - }) - - const watchIsBulkAction = form.watch('isBulkAction') - 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() - onClose() - } - - const submit = async (_data: any) => { - const { macAddress, serverId, ...data } = _data as z.infer< - typeof schema - > - - clearFlashes() - try { - // TODO: add pool id selector for create address - const address = await createAddress(0, { - macAddress: - macAddress && macAddress.length > 0 ? macAddress : null, - serverId: serverId !== '' ? serverId : null, - include: ['server'], - ...data, - }) - - if (address) { - mutate(data => { - if (!data) return data - if (data.pagination.currentPage !== 1) return data - - return { - ...data, - items: [address, ...data.items], - } - }, false) - } else { - mutate() - } - - handleClose() - } catch (e) { - clearAndAddHttpError(e as Error) - } - } - - return ( - - - {t('create_address')} - - - -
- - - - form.setValue( - 'isBulkAction', - val === 'multiple' - ) - } - data={[ - { value: 'single', label: tStrings('single') }, - { - value: 'multiple', - label: tStrings('multiple'), - }, - ]} - /> - - - - - {watchIsBulkAction ? ( - <> - - -

- - {{ addressCount }} address - -

- - ) : ( - - )} - - - - - - - - - {tStrings('cancel')} - - - {tStrings('create')} - - - - - - ) -} - -export default CreateAddressModal \ No newline at end of file diff --git a/resources/scripts/components/admin/nodes/addresses/NodeAddressesContainer.tsx b/resources/scripts/components/admin/nodes/addresses/NodeAddressesContainer.tsx index 0b248bb4e9b..3201b8756a6 100644 --- a/resources/scripts/components/admin/nodes/addresses/NodeAddressesContainer.tsx +++ b/resources/scripts/components/admin/nodes/addresses/NodeAddressesContainer.tsx @@ -6,7 +6,6 @@ import useAddressesSWR from '@/api/admin/nodes/addresses/useAddressesSWR' import useNodeSWR from '@/api/admin/nodes/useNodeSWR' import { Address } from '@/api/server/getServer' -import Button from '@/components/elements/Button' import Menu from '@/components/elements/Menu' import Pagination from '@/components/elements/Pagination' import Spinner from '@/components/elements/Spinner' @@ -20,7 +19,6 @@ import NodeContentBlock from '@/components/admin/nodes/NodeContentBlock' import DeleteAddressModal from '@/components/admin/nodes/addresses/DeleteAddressModal' import EditAddressModal from '@/components/admin/nodes/addresses/EditAddressModal' - const columns: ColumnArray
= [ { accessor: 'address', @@ -64,7 +62,6 @@ const NodeAddressesContainer = () => { page, include: ['server'], }) - const [open, setOpen] = useState(false) const rowActions = ({ row }: RowActionsProps
) => { const [showEditModal, setShowEditModal] = useState(false) @@ -100,16 +97,7 @@ const NodeAddressesContainer = () => { return (
- - setOpen(false)} /> -
- -
+ {!data ? ( ) : ( @@ -128,4 +116,4 @@ const NodeAddressesContainer = () => { ) } -export default NodeAddressesContainer \ No newline at end of file +export default NodeAddressesContainer diff --git a/resources/scripts/components/elements/forms/MultiSelectForm.tsx b/resources/scripts/components/elements/forms/MultiSelectForm.tsx index f86094ff283..21825b42352 100644 --- a/resources/scripts/components/elements/forms/MultiSelectForm.tsx +++ b/resources/scripts/components/elements/forms/MultiSelectForm.tsx @@ -5,7 +5,10 @@ import MultiSelect, { } from '@/components/elements/inputs/MultiSelect' interface Props - extends Omit { + extends Omit< + MultiSelectProps, + 'error' | keyof Omit + > { control?: Control name: string } @@ -22,4 +25,4 @@ const MultiSelectForm = ({ control, ...props }: Props) => { return } -export default MultiSelectForm \ No newline at end of file +export default MultiSelectForm diff --git a/resources/scripts/routers/AdminIpamRouter.tsx b/resources/scripts/routers/AdminIpamRouter.tsx index d52c8e71e69..9378e3f7068 100644 --- a/resources/scripts/routers/AdminIpamRouter.tsx +++ b/resources/scripts/routers/AdminIpamRouter.tsx @@ -4,14 +4,13 @@ import { lazy, useContext, useEffect } from 'react' import { Translation, useTranslation } from 'react-i18next' import { Navigate, Outlet } from 'react-router-dom' +import getAddresses from '@/api/admin/addressPools/addresses/getAddresses' import getAddressPool from '@/api/admin/addressPools/getAddressPool' -import getAddresses from '@/api/admin/addressPools/getAddresses' import { getKey as getPoolKey } from '@/api/admin/addressPools/useAddressPoolSWR' import { getKey as getAddressesKey } from '@/api/admin/addressPools/useAddressesSWR' import { NavigationBarContext } from '@/components/elements/navigation/NavigationBar' - export const routes: Route[] = [ { path: 'ipam', @@ -31,15 +30,15 @@ export const routes: Route[] = [ ), }, { - path: ':id', + path: ':poolId', loader: ({ params }) => - query(getPoolKey(parseInt(params.id!)), () => - getAddressPool(parseInt(params.id!)) + query(getPoolKey(parseInt(params.poolId!)), () => + getAddressPool(parseInt(params.poolId!)) ), element: lazyLoad(lazy(() => import('./AdminIpamRouter'))), handle: { crumb: data => ({ - to: `/admin/ipam/${data.id}`, + to: `/admin/ipam/${data.poolId}`, element: data.name, }), }, @@ -51,7 +50,7 @@ export const routes: Route[] = [ { path: 'addresses', loader: ({ params }) => { - const id = parseInt(params.id!) + const id = parseInt(params.poolId!) const page = params.page ? parseInt(params.page) : 1 return query(getAddressesKey(id, page, ''), () => @@ -120,4 +119,4 @@ const AdminIpamRouter = () => { return } -export default AdminIpamRouter \ No newline at end of file +export default AdminIpamRouter