Compare commits

...

11 Commits

Author SHA1 Message Date
Mihaly Gyongyosi 9becd8e634 Merge remote-tracking branch 'origin/main' into mgyongyosi/teamsync-use-app-ptf-apis 2026-01-07 17:39:45 +01:00
Tom Ratcliffe 88746cf96c Refactor to avoid rule of hooks violations
(cherry picked from commit e2d459afdd)
2026-01-07 17:36:27 +01:00
Mihaly Gyongyosi 27c464c833 Address feedback 2026-01-07 15:44:14 +01:00
Mihaly Gyongyosi e1efcae8f3 Revert "Use the search endpoint for listing"
This reverts commit 65cee8e6b4.
2026-01-07 15:32:24 +01:00
Mihaly Gyongyosi 1f32a31c2b Merge remote-tracking branch 'origin/main' into mgyongyosi/teamsync-use-app-ptf-apis 2026-01-07 10:58:20 +01:00
Mihaly Gyongyosi 65cee8e6b4 Use the search endpoint for listing 2026-01-06 12:37:48 +01:00
Mihaly Gyongyosi 6da8c5d1bb Fix typecheck 2026-01-05 16:58:07 +01:00
Mihaly Gyongyosi 87f09ffa2d Merge remote-tracking branch 'origin/main' into mgyongyosi/teamsync-use-app-ptf-apis 2026-01-05 16:08:43 +01:00
Mihaly Gyongyosi e0b76f7916 Add tests 2026-01-05 14:55:52 +01:00
Mihaly Gyongyosi 87b32fe05c Merge remote-tracking branch 'origin/main' into mgyongyosi/teamsync-use-app-ptf-apis 2026-01-05 12:21:04 +01:00
Mihaly Gyongyosi 649fabb2e6 Use the new APIs if the ft is enabled 2025-12-19 17:20:29 +01:00
4 changed files with 214 additions and 19 deletions
@@ -1,5 +1,7 @@
import { HttpResponse, http } from 'msw';
import { mockTeamsMap } from '../../../../fixtures/teams';
const getDisplayMapping = () =>
http.get<{ namespace: string }>('/apis/iam.grafana.app/v0alpha1/namespaces/:namespace/display', ({ request }) => {
const url = new URL(request.url);
@@ -26,4 +28,76 @@ const getDisplayMapping = () =>
});
});
export default [getDisplayMapping()];
const listExternalGroupMappings = () =>
http.get<{ namespace: string }>('/apis/iam.grafana.app/v0alpha1/namespaces/:namespace/externalgroupmappings', () => {
const items = [];
for (const [teamName, data] of mockTeamsMap.entries()) {
for (const group of data.groups) {
items.push({
apiVersion: 'iam.grafana.app/v0alpha1',
kind: 'ExternalGroupMapping',
metadata: {
name: `mapping-${teamName}-${group.groupId}`,
creationTimestamp: new Date().toISOString(),
},
spec: {
externalGroupId: group.groupId,
teamRef: {
name: teamName,
},
},
});
}
}
return HttpResponse.json({ items });
});
const createExternalGroupMapping = () =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
http.post<{ namespace: string }, any>(
'/apis/iam.grafana.app/v0alpha1/namespaces/:namespace/externalgroupmappings',
async ({ request }) => {
const body = await request.json();
const teamName = body.spec.teamRef.name;
const groupId = body.spec.externalGroupId;
const teamData = mockTeamsMap.get(teamName);
if (teamData) {
teamData.groups.push({ groupId });
}
return HttpResponse.json({
...body,
metadata: {
name: `mapping-${teamName}-${groupId}`,
creationTimestamp: new Date().toISOString(),
...body.metadata,
},
});
}
);
const deleteExternalGroupMapping = () =>
http.delete<{ namespace: string; name: string }>(
'/apis/iam.grafana.app/v0alpha1/namespaces/:namespace/externalgroupmappings/:name',
({ params }) => {
const { name } = params;
for (const [teamName, data] of mockTeamsMap.entries()) {
const groupIndex = data.groups.findIndex((g) => `mapping-${teamName}-${g.groupId}` === name);
if (groupIndex !== -1) {
data.groups.splice(groupIndex, 1);
return HttpResponse.json({ status: 'Success' });
}
}
return HttpResponse.json({ status: 'Failure', message: 'Not found' }, { status: 404 });
}
);
export default [
getDisplayMapping(),
listExternalGroupMappings(),
createExternalGroupMapping(),
deleteExternalGroupMapping(),
];
@@ -1,6 +1,6 @@
import { render, screen, waitFor } from 'test/test-utils';
import { setBackendSrv } from '@grafana/runtime';
import { setBackendSrv, config } from '@grafana/runtime';
import { setupMockServer } from '@grafana/test-utils/server';
import { MOCK_TEAMS, MOCK_TEAM_GROUPS } from '@grafana/test-utils/unstable';
import { backendSrv } from 'app/core/services/backend_srv';
@@ -25,9 +25,8 @@ describe('TeamGroupSync', () => {
expect(await screen.findAllByRole('row')).toHaveLength(MOCK_TEAM_GROUPS.length + 1); // items plus table header
});
it('should call add group', async () => {
it('should add group', async () => {
const { user } = setup();
// Wait for the groups to load so the "Add group" button appears
await screen.findAllByRole('row');
await user.click(screen.getAllByRole('button', { name: /add group/i })[0]);
@@ -43,10 +42,54 @@ describe('TeamGroupSync', () => {
const { user } = setup();
const groupToRemove = MOCK_TEAM_GROUPS[0].groupId;
// Wait for group to be rendered
await screen.findByRole('row', { name: new RegExp(groupToRemove, 'i') });
// Remove group
await user.click(screen.getByRole('button', { name: `Remove group ${groupToRemove}` }));
await waitFor(() =>
expect(screen.queryByRole('row', { name: new RegExp(groupToRemove, 'i') })).not.toBeInTheDocument()
);
});
});
describe('TeamGroupSync with kubernetesExternalGroupMapping enabled', () => {
const originalFeatureToggles = { ...config.featureToggles };
const originalNamespace = config.namespace;
beforeAll(() => {
config.featureToggles.kubernetesExternalGroupMapping = true;
config.namespace = 'default';
});
afterAll(() => {
config.featureToggles = originalFeatureToggles;
config.namespace = originalNamespace;
});
it('should render groups table', async () => {
setup();
expect(await screen.findAllByRole('row')).toHaveLength(MOCK_TEAM_GROUPS.length + 1); // items plus table header
});
it('should add group', async () => {
const { user } = setup();
await screen.findAllByRole('row');
await user.click(screen.getAllByRole('button', { name: /add group/i })[0]);
expect(screen.getByRole('textbox', { name: /add external group/i })).toBeVisible();
await user.type(screen.getByRole('textbox', { name: /add external group/i }), 'new-group');
await user.click(screen.getAllByRole('button', { name: /add group/i })[1]);
expect(await screen.findByRole('row', { name: /new-group/i })).toBeInTheDocument();
});
it('should remove group', async () => {
const { user } = setup();
const groupToRemove = MOCK_TEAM_GROUPS[0].groupId;
await screen.findByRole('row', { name: new RegExp(groupToRemove, 'i') });
await user.click(screen.getByRole('button', { name: `Remove group ${groupToRemove}` }));
await waitFor(() =>
+11 -13
View File
@@ -1,12 +1,7 @@
import { css, cx } from '@emotion/css';
import { FormEventHandler, useState } from 'react';
import {
TeamGroupDto,
useAddTeamGroupApiMutation,
useGetTeamGroupsApiQuery,
useRemoveTeamGroupApiQueryMutation,
} from '@grafana/api-clients/rtkq/legacy';
import { TeamGroupDto } from '@grafana/api-clients/rtkq/legacy';
import { Trans, t } from '@grafana/i18n';
import { Input, Tooltip, Icon, Button, useTheme2, InlineField, InlineFieldRow, useStyles2 } from '@grafana/ui';
import { SlideDown } from 'app/core/components/Animations/SlideDown';
@@ -15,6 +10,8 @@ import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { UpgradeBox, UpgradeContent, UpgradeContentProps } from 'app/core/components/Upgrade/UpgradeBox';
import { highlightTrial } from 'app/features/admin/utils';
import { useAddExternalGroupMapping, useGetExternalGroupMappings, useRemoveExternalGroupMapping } from './hooks';
interface Props {
isReadOnly: boolean;
teamUid: string;
@@ -27,9 +24,9 @@ export const TeamGroupSync = ({ isReadOnly, teamUid }: Props) => {
const [newGroupId, setNewGroupId] = useState('');
const styles = useStyles2(getStyles);
const { data: groups = [] } = useGetTeamGroupsApiQuery({ teamId: teamUid });
const [addTeamGroup] = useAddTeamGroupApiMutation();
const [removeTeamGroup] = useRemoveTeamGroupApiQueryMutation();
const { data: groups = [] } = useGetExternalGroupMappings({ teamId: teamUid });
const [addTeamGroup] = useAddExternalGroupMapping();
const [removeTeamGroup] = useRemoveExternalGroupMapping();
const onToggleAdding = () => {
setIsAddBoxVisible(!isAddBoxVisible);
@@ -46,11 +43,12 @@ export const TeamGroupSync = ({ isReadOnly, teamUid }: Props) => {
setNewGroupId('');
};
const onRemoveGroup = async (groupId: string | undefined) => {
if (!groupId) {
const onRemoveGroup = async (group: TeamGroupDto) => {
if (!group.groupId) {
return;
}
await removeTeamGroup({ teamId: teamUid, groupId });
// group.uid is always defined here because it comes from the API
await removeTeamGroup({ teamId: teamUid, groupId: group.groupId, uid: group.uid! });
};
const isNewGroupValid = () => {
@@ -65,7 +63,7 @@ export const TeamGroupSync = ({ isReadOnly, teamUid }: Props) => {
<Button
size="sm"
variant="destructive"
onClick={() => onRemoveGroup(group.groupId)}
onClick={() => onRemoveGroup(group)}
disabled={isReadOnly}
aria-label={t('teams.team-group-sync.aria-label-remove', 'Remove group {{groupName}}', {
groupName: group.groupId,
+80
View File
@@ -1,6 +1,18 @@
import { skipToken } from '@reduxjs/toolkit/query';
import { useEffect, useMemo } from 'react';
import {
useCreateExternalGroupMappingMutation,
useListExternalGroupMappingQuery,
useDeleteExternalGroupMappingMutation,
} from '@grafana/api-clients/rtkq/iam/v0alpha1';
import {
useAddTeamGroupApiMutation,
useGetTeamGroupsApiQuery,
useRemoveTeamGroupApiQueryMutation,
TeamGroupDto,
} from '@grafana/api-clients/rtkq/legacy';
import { config } from '@grafana/runtime';
import {
useSearchTeamsQuery as useLegacySearchTeamsQuery,
useCreateTeamMutation,
@@ -152,3 +164,71 @@ export const useCreateTeam = () => {
return [trigger, response] as const;
};
export const useGetExternalGroupMappings = (args: { teamId: string }) => {
const shouldUseAppPlatform = Boolean(config.featureToggles.kubernetesExternalGroupMapping);
const legacyResult = useGetTeamGroupsApiQuery(args, { skip: shouldUseAppPlatform });
const { data: newApiData, ...newApiRest } = useListExternalGroupMappingQuery({}, { skip: !shouldUseAppPlatform });
const groups: TeamGroupDto[] = useMemo(() => {
// FIXME: Consider using the search API which has sorting support
return (newApiData?.items || [])
.filter((item) => item.spec.teamRef.name === args.teamId)
.map((item) => ({
groupId: item.spec.externalGroupId,
uid: item.metadata.name,
}));
}, [newApiData, args.teamId]);
if (shouldUseAppPlatform) {
return {
...newApiRest,
data: groups,
};
}
return legacyResult;
};
export const useAddExternalGroupMapping = () => {
const legacyMutation = useAddTeamGroupApiMutation();
const [addNew, newResult] = useCreateExternalGroupMappingMutation();
const add = async (args: { teamId: string; teamGroupMapping: { groupId: string } }) => {
return addNew({
externalGroupMapping: {
metadata: {
generateName: 'external-group-mapping-',
},
spec: {
externalGroupId: args.teamGroupMapping.groupId,
teamRef: {
name: args.teamId,
},
},
},
});
};
if (!config.featureToggles.kubernetesExternalGroupMapping) {
return legacyMutation;
}
return [add, newResult] as const;
};
export const useRemoveExternalGroupMapping = () => {
const legacyMutation = useRemoveTeamGroupApiQueryMutation();
const [deleteMapping, deleteResult] = useDeleteExternalGroupMappingMutation();
const remove = async (args: { teamId: string; groupId: string; uid: string }) => {
return deleteMapping({ name: args.uid });
};
if (!config.featureToggles.kubernetesExternalGroupMapping) {
return legacyMutation;
}
return [remove, deleteResult] as const;
};