diff --git a/changelog/9892.fixed.md b/changelog/9892.fixed.md new file mode 100644 index 00000000000..3330a8bd45e --- /dev/null +++ b/changelog/9892.fixed.md @@ -0,0 +1 @@ +Following an IP prefix or address link from the relationship tabs of an IPAM namespace now keeps you in that namespace, instead of redirecting to the default namespace. diff --git a/frontend/app/src/entities/nodes/object/ui/object-table/cells/table-identifier-cell.tsx b/frontend/app/src/entities/nodes/object/ui/object-table/cells/table-identifier-cell.tsx index 16bb898b91a..8dec38d8f25 100644 --- a/frontend/app/src/entities/nodes/object/ui/object-table/cells/table-identifier-cell.tsx +++ b/frontend/app/src/entities/nodes/object/ui/object-table/cells/table-identifier-cell.tsx @@ -1,6 +1,7 @@ import { LinkButton } from "@infrahub/ui"; import type { PressEvent } from "react-aria-components"; +import type { overrideQueryParams } from "@/shared/api/rest/fetch"; import { Checkbox } from "@/shared/components/aria/checkbox"; import { useAuth } from "@/entities/authentication/ui/useAuth"; @@ -13,6 +14,7 @@ export interface TableIdentifierCellProps { label: React.ReactNode; isSelected?: boolean; onClickCheckbox?: (e: PressEvent) => void; + overrideParams?: overrideQueryParams[]; } export function TableIdentifierCell({ @@ -21,6 +23,7 @@ export function TableIdentifierCell({ label, isSelected, onClickCheckbox, + overrideParams, }: TableIdentifierCellProps) { const { isAuthenticated } = useAuth(); return ( @@ -36,7 +39,7 @@ export function TableIdentifierCell({ {label} diff --git a/frontend/app/src/entities/nodes/object/ui/object-table/utils/get-object-table-columns.tsx b/frontend/app/src/entities/nodes/object/ui/object-table/utils/get-object-table-columns.tsx index 8fff8bbeff5..0a6b5a0d638 100644 --- a/frontend/app/src/entities/nodes/object/ui/object-table/utils/get-object-table-columns.tsx +++ b/frontend/app/src/entities/nodes/object/ui/object-table/utils/get-object-table-columns.tsx @@ -1,6 +1,7 @@ import type { PopoverTriggerProps } from "@radix-ui/react-popover"; import { type ColumnDef, createColumnHelper } from "@tanstack/react-table"; +import type { overrideQueryParams } from "@/shared/api/rest/fetch"; import { FROM_RESOURCE_POOL_SUFFIX } from "@/shared/components/form/constants"; import { TableCell } from "@/shared/components/table/table-cell"; import { sortByOrderWeight } from "@/shared/utils/common"; @@ -34,7 +35,8 @@ import { isGenericSchema } from "@/entities/schema/utils/is-generic-schema"; const columnHelper = createColumnHelper(); export function getObjectIdentifierColumns( - schema: ModelSchema + schema: ModelSchema, + identifierOverrideParams?: overrideQueryParams[] ): Array> { return [ columnHelper.accessor((node) => getNodeLabel(node), { @@ -59,6 +61,7 @@ export function getObjectIdentifierColumns( label={label} isSelected={row.getIsSelected()} onClickCheckbox={getToggleSelectedRowHandler({ row, table })} + overrideParams={identifierOverrideParams} /> ); }, @@ -145,10 +148,11 @@ export function getObjectFieldsColumns( export const getObjectTableColumns = ( schema: ModelSchema, - headerProps?: PopoverTriggerProps + headerProps?: PopoverTriggerProps, + identifierOverrideParams?: overrideQueryParams[] ): Array> => { return [ - ...getObjectIdentifierColumns(schema), + ...getObjectIdentifierColumns(schema, identifierOverrideParams), ...getObjectGenericColumns(schema), ...getObjectFieldsColumns(schema, headerProps), ] as Array>; diff --git a/frontend/app/src/entities/nodes/relationships/ui/relationship-table/relationship-table.tsx b/frontend/app/src/entities/nodes/relationships/ui/relationship-table/relationship-table.tsx index b02f58c8910..d9e96138528 100644 --- a/frontend/app/src/entities/nodes/relationships/ui/relationship-table/relationship-table.tsx +++ b/frontend/app/src/entities/nodes/relationships/ui/relationship-table/relationship-table.tsx @@ -3,6 +3,7 @@ import { DataTable } from "@/shared/components/table/data-table"; import { InfiniteScroll } from "@/shared/components/utils/infinite-scroll"; import useFilters from "@/shared/hooks/useFilters"; +import { IP_NAMESPACE_GENERIC, IPAM_QSP } from "@/entities/ipam/constants"; import { ObjectTableEmpty } from "@/entities/nodes/object/ui/object-table/object-table-empty"; import { getObjectTableColumns } from "@/entities/nodes/object/ui/object-table/utils/get-object-table-columns"; import { @@ -15,6 +16,7 @@ import { ToolbarDissociateAction } from "@/entities/nodes/relationships/ui/relat import { canDissociateRelationship } from "@/entities/nodes/relationships/utils/can-dissociate-relationship"; import { useGetObjectPermissions } from "@/entities/permission/ui/queries/get-object-permissions.query"; import { useSchema } from "@/entities/schema/ui/hooks/useSchema"; +import { isOfKind } from "@/entities/schema/utils/is-of-kind"; export interface RelationshipTableProps extends UseObjectRelationshipsParams {} @@ -49,8 +51,18 @@ export function RelationshipTable({ } const flatData = data?.pages?.flat() ?? []; + + // On an IP namespace's relationship tabs (e.g. its prefixes/addresses), the active + // namespace lives in the route path, not the `namespace` query param that IPAM links + // rely on. Forward it explicitly so child IP links stay in this namespace instead of + // falling back to the default one. + const identifierOverrideParams = + parentSchema && isOfKind(IP_NAMESPACE_GENERIC, parentSchema) + ? [{ name: IPAM_QSP.NAMESPACE, value: parentId }] + : undefined; + const columns = [ - ...getObjectTableColumns(relationshipSchema, { disabled: true }), + ...getObjectTableColumns(relationshipSchema, { disabled: true }, identifierOverrideParams), getRelationshipActionsColumn({ parentId, parentKind, diff --git a/frontend/app/src/entities/nodes/utils.test.ts b/frontend/app/src/entities/nodes/utils.test.ts new file mode 100644 index 00000000000..037ce8d3c79 --- /dev/null +++ b/frontend/app/src/entities/nodes/utils.test.ts @@ -0,0 +1,49 @@ +import { afterEach, describe, expect, it } from "vitest"; + +import { store } from "@/shared/stores"; + +import { IP_PREFIX_GENERIC, IPAM_QSP } from "@/entities/ipam/constants"; +import { getObjectDetailsUrl } from "@/entities/nodes/utils"; +import { + genericSchemasAtom, + nodeSchemasAtom, + profileSchemasAtom, + templateSchemasAtom, +} from "@/entities/schema/stores/schema.atom"; + +import { generateNodeSchema } from "../../../tests/fake/schema"; + +const ipPrefixSchema = generateNodeSchema({ + kind: "BuiltinIPPrefix", + name: "IPPrefix", + namespace: "Builtin", + inherit_from: [IP_PREFIX_GENERIC], +}); + +describe("getObjectDetailsUrl", () => { + afterEach(() => { + store.set(nodeSchemasAtom, []); + store.set(genericSchemasAtom, []); + store.set(profileSchemasAtom, []); + store.set(templateSchemasAtom, []); + }); + + it("keeps the namespace context for an IP prefix link when passed as an override param", () => { + store.set(nodeSchemasAtom, [ipPrefixSchema]); + + const url = getObjectDetailsUrl("BuiltinIPPrefix", "prefix-1", [ + { name: IPAM_QSP.NAMESPACE, value: "namespace-1" }, + ]); + + expect(url).toContain("/ipam/BuiltinIPPrefix/prefix-1"); + expect(url).toContain(`${IPAM_QSP.NAMESPACE}=namespace-1`); + }); + + it("does not add a namespace param to an IP prefix link when none is provided", () => { + store.set(nodeSchemasAtom, [ipPrefixSchema]); + + const url = getObjectDetailsUrl("BuiltinIPPrefix", "prefix-1"); + + expect(url).toBe("/ipam/BuiltinIPPrefix/prefix-1"); + }); +});