chore: sync up with group mappers

This commit is contained in:
Gaurav Tewari
2026-07-09 12:06:38 +05:30
parent 466b575aa5
commit a1497d8a8b
4 changed files with 27 additions and 55 deletions

View File

@@ -41,8 +41,6 @@ async function expandGroup(
await user.click(screen.getByTestId(`group-expand-${groupId}`));
}
// DropdownMenuSimple drops the trigger's testId, so the kebab is reached via
// its accessible name instead.
async function openGroupActionsMenu(
user: ReturnType<typeof userEvent.setup>,
groupId: string,
@@ -53,18 +51,18 @@ async function openGroupActionsMenu(
await user.click(within(row).getByRole('button', { name: 'Group actions' }));
}
interface HarnessProps {
interface AttributeMappingsTabWithStoreProps {
onEditGroup?: (group: DraftGroup) => void;
onAddGroup?: () => void;
}
// The tab is a presentational view over a store owned by the container, so the
// harness creates the store (via the hook, backed by the mocked API) and wires
// The tab is a presentational view over a store owned by the container, so this
// wrapper creates the store (via the hook, backed by the mocked API) and wires
// the edit/add callbacks the tab needs.
function AttributeMappingHarness({
function AttributeMappingsTabWithStore({
onEditGroup,
onAddGroup,
}: HarnessProps): JSX.Element {
}: AttributeMappingsTabWithStoreProps): JSX.Element {
const store = useAttributeMappingStore();
return (
<AttributeMappingsTab
@@ -87,7 +85,7 @@ describe('AttributeMappingsTab (integration)', () => {
it('renders no error banner on a successful load', async () => {
setupGroups();
render(<AttributeMappingHarness />);
render(<AttributeMappingsTabWithStore />);
await waitFor(() =>
expect(screen.getByTestId('group-name-group-1')).toBeInTheDocument(),
@@ -99,7 +97,7 @@ describe('AttributeMappingsTab (integration)', () => {
server.use(
rest.get(GROUPS_ENDPOINT, (_req, res, ctx) => res(ctx.status(500))),
);
render(<AttributeMappingHarness />);
render(<AttributeMappingsTabWithStore />);
await expect(screen.findByRole('alert')).resolves.toHaveTextContent(
'Failed to load mapping groups. Please try again.',
@@ -108,7 +106,7 @@ describe('AttributeMappingsTab (integration)', () => {
it('shows the empty state when there are no groups', async () => {
setupGroups([]);
render(<AttributeMappingHarness />);
render(<AttributeMappingsTabWithStore />);
await expect(
screen.findByTestId('mapper-groups-empty'),
@@ -117,7 +115,7 @@ describe('AttributeMappingsTab (integration)', () => {
it('renders each group header row with its name, condition count and status', async () => {
setupGroups();
render(<AttributeMappingHarness />);
render(<AttributeMappingsTabWithStore />);
// Condition filters are no longer shown inline as clauses — the header
// carries a count instead (the keys surface in the group drawer).
@@ -150,7 +148,7 @@ describe('AttributeMappingsTab (integration)', () => {
it("stages a toggle of the group's enabled state via the header switch", async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
setupGroups();
render(<AttributeMappingHarness />);
render(<AttributeMappingsTabWithStore />);
const toggle = await screen.findByTestId('group-enabled-group-1');
expect(toggle).toBeChecked();
@@ -164,7 +162,7 @@ describe('AttributeMappingsTab (integration)', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const onAddGroup = jest.fn();
setupGroups();
render(<AttributeMappingHarness onAddGroup={onAddGroup} />);
render(<AttributeMappingsTabWithStore onAddGroup={onAddGroup} />);
await screen.findByTestId('group-name-group-1');
await user.click(screen.getByTestId('add-group-row'));
@@ -176,7 +174,7 @@ describe('AttributeMappingsTab (integration)', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const onEditGroup = jest.fn();
setupGroups();
render(<AttributeMappingHarness onEditGroup={onEditGroup} />);
render(<AttributeMappingsTabWithStore onEditGroup={onEditGroup} />);
await screen.findByTestId('group-name-group-1');
await openGroupActionsMenu(user, 'group-1');
@@ -192,7 +190,7 @@ describe('AttributeMappingsTab (integration)', () => {
it('stages a group removal when Delete is chosen from the actions menu', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
setupGroups();
render(<AttributeMappingHarness />);
render(<AttributeMappingsTabWithStore />);
await screen.findByTestId('group-name-group-1');
await openGroupActionsMenu(user, 'group-1');
@@ -210,7 +208,7 @@ describe('AttributeMappingsTab (integration)', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
setupGroups();
setupMappers([makeMapper({ id: 'mapper-1' })]);
render(<AttributeMappingHarness />);
render(<AttributeMappingsTabWithStore />);
await screen.findByTestId('group-name-group-1');
// The toggle is the antd Collapse header, which owns the expanded state.
@@ -239,7 +237,7 @@ describe('AttributeMappingsTab (integration)', () => {
setupMappers([
makeMapper({ id: 'mapper-1', name: 'gen_ai.request.model', enabled: true }),
]);
render(<AttributeMappingHarness />);
render(<AttributeMappingsTabWithStore />);
await screen.findByTestId('group-name-group-1');
// Mappers are not fetched until the row is expanded.
@@ -267,7 +265,7 @@ describe('AttributeMappingsTab (integration)', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
setupGroups();
setupMappers([makeMapper({ id: 'mapper-1', enabled: true })]);
render(<AttributeMappingHarness />);
render(<AttributeMappingsTabWithStore />);
await screen.findByTestId('group-name-group-1');
await expandGroup(user);
@@ -287,7 +285,7 @@ describe('AttributeMappingsTab (integration)', () => {
res(ctx.status(500)),
),
);
render(<AttributeMappingHarness />);
render(<AttributeMappingsTabWithStore />);
await screen.findByTestId('group-name-group-1');
await expandGroup(user);
@@ -301,7 +299,7 @@ describe('AttributeMappingsTab (integration)', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
setupGroups();
setupMappers([]);
render(<AttributeMappingHarness />);
render(<AttributeMappingsTabWithStore />);
await screen.findByTestId('group-name-group-1');
await expandGroup(user);
@@ -327,7 +325,7 @@ describe('AttributeMappingsTab (integration)', () => {
},
}),
]);
render(<AttributeMappingHarness />);
render(<AttributeMappingsTabWithStore />);
await screen.findByTestId('group-name-group-1');
await expandGroup(user);
@@ -339,7 +337,7 @@ describe('AttributeMappingsTab (integration)', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
setupGroups();
setupMappers([makeMapper({ id: 'mapper-1', config: { sources: [] } })]);
render(<AttributeMappingHarness />);
render(<AttributeMappingsTabWithStore />);
await screen.findByTestId('group-name-group-1');
await expandGroup(user);

View File

@@ -11,10 +11,6 @@ interface GroupHeaderActionsProps {
onRemove: (localId: string) => void;
}
// The enable/disable toggle stays inline in the header (it's the primary,
// high-frequency action); the lower-frequency Edit and Delete actions live
// behind the kebab to keep the row compact. The wrapper swallows clicks so
// they don't also toggle the Collapse panel.
function GroupHeaderActions({
group,
onToggle,

View File

@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import type { SpantypesSpanMapperDTO } from 'api/generated/services/sigNoz.schemas';
import { useListSpanMappers } from 'api/generated/services/spanmapper';
import { motion, useReducedMotion } from 'motion/react';
import { motion } from 'motion/react';
import {
DraftGroup,
@@ -15,25 +15,19 @@ import styles from './GroupMappers.module.scss';
const MAPPER_SKELETON_ROWS = 2;
// Fade shared by the non-row states (error / empty) so they reveal in step
// with the mapper rows while the antd Collapse runs its height animation on
// expand.
const STATE_ROW_MOTION = {
initial: { opacity: 0 },
animate: { opacity: 1 },
transition: { duration: 0.18, ease: 'easeOut' },
} as const;
type StateRowMotion = typeof STATE_ROW_MOTION | { initial: false };
interface StateRowProps {
groupId: string;
rowMotion: StateRowMotion;
}
function ErrorRow({ groupId, rowMotion }: StateRowProps): JSX.Element {
function ErrorRow({ groupId }: StateRowProps): JSX.Element {
return (
<motion.tr className={styles.mapperStateRow} {...rowMotion}>
<motion.tr className={styles.mapperStateRow} {...STATE_ROW_MOTION}>
<td
colSpan={COLUMN_COUNT}
className={styles.stateCell}
@@ -45,9 +39,9 @@ function ErrorRow({ groupId, rowMotion }: StateRowProps): JSX.Element {
);
}
function EmptyRow({ groupId, rowMotion }: StateRowProps): JSX.Element {
function EmptyRow({ groupId }: StateRowProps): JSX.Element {
return (
<motion.tr className={styles.mapperStateRow} {...rowMotion}>
<motion.tr className={styles.mapperStateRow} {...STATE_ROW_MOTION}>
<td
colSpan={COLUMN_COUNT}
className={styles.stateCell}
@@ -64,11 +58,6 @@ interface GroupMappersProps {
}
function GroupMappers({ group }: GroupMappersProps): JSX.Element {
const prefersReducedMotion = useReducedMotion();
const stateRowMotion: StateRowMotion = prefersReducedMotion
? { initial: false as const }
: STATE_ROW_MOTION;
// A not-yet-saved group has no serverId, so there is nothing to fetch —
// the query stays disabled and the panel falls through to the empty row.
const hasServerId = group.serverId !== null;
@@ -92,18 +81,14 @@ function GroupMappers({ group }: GroupMappersProps): JSX.Element {
let rows: JSX.Element[];
if (isError) {
rows = [
<ErrorRow key="error" groupId={group.localId} rowMotion={stateRowMotion} />,
];
rows = [<ErrorRow key="error" groupId={group.localId} />];
} else if (isLoading) {
rows = Array.from({ length: MAPPER_SKELETON_ROWS }).map((_, index) => (
// eslint-disable-next-line react/no-array-index-key
<MapperRowSkeleton key={`mapper-skeleton-${index}`} />
));
} else if (mappers.length === 0) {
rows = [
<EmptyRow key="empty" groupId={group.localId} rowMotion={stateRowMotion} />,
];
rows = [<EmptyRow key="empty" groupId={group.localId} />];
} else {
rows = mappers.map((mapper, index) => (
<MapperRow key={mapper.id} mapper={mapper} index={index} />

View File

@@ -84,16 +84,12 @@ export function buildPostableGroup(
};
}
// A full group payload is also a valid partial-update payload (all updatable
// fields are present), so we reuse the postable builder.
export function buildUpdatableGroup(
draft: GroupDraft,
): SpantypesUpdatableSpanMapperGroupDTO {
return buildPostableGroup(draft);
}
// ---- working-copy (draft list) helpers ----
export function buildDraftGroup(group: MapperGroup): DraftGroup {
return {
localId: group.id,
@@ -105,7 +101,6 @@ export function buildDraftGroup(group: MapperGroup): DraftGroup {
};
}
// DraftGroup -> editable form state (id carries the localId).
export function groupDraftFromNode(group: DraftGroup): GroupDraft {
return {
id: group.localId,
@@ -116,8 +111,6 @@ export function groupDraftFromNode(group: DraftGroup): GroupDraft {
};
}
// Form state -> working-copy node. Reuses cleanKeys so the staged list already
// holds normalized values.
export function nodeFromGroupDraft(
draft: GroupDraft,
existing?: DraftGroup,