Teams: Refactor TeamGroupSync UI to functional component and hooks (#115223)
* Refactor TeamGroupSync to use func component and hooks * Fix tests, lint * Lint prune suppressions * Address feedback * Address feedback - tests
This commit is contained in:
@@ -3107,11 +3107,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/teams/TeamGroupSync.tsx": {
|
||||
"react-prefer-function-component/react-prefer-function-component": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/templating/fieldAccessorCache.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 1
|
||||
|
||||
@@ -3440,16 +3440,16 @@ export type RemoveTeamGroupApiQueryApiResponse =
|
||||
/** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody;
|
||||
export type RemoveTeamGroupApiQueryApiArg = {
|
||||
groupId?: string;
|
||||
teamId: number;
|
||||
teamId: string;
|
||||
};
|
||||
export type GetTeamGroupsApiApiResponse = /** status 200 (empty) */ TeamGroupDto[];
|
||||
export type GetTeamGroupsApiApiArg = {
|
||||
teamId: number;
|
||||
teamId: string;
|
||||
};
|
||||
export type AddTeamGroupApiApiResponse =
|
||||
/** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody;
|
||||
export type AddTeamGroupApiApiArg = {
|
||||
teamId: number;
|
||||
teamId: string;
|
||||
teamGroupMapping: TeamGroupMapping;
|
||||
};
|
||||
export type SearchTeamGroupsApiResponse = /** status 200 (empty) */ SearchTeamGroupsQueryResult;
|
||||
|
||||
@@ -24,13 +24,15 @@ export const MOCK_TEAMS = [
|
||||
},
|
||||
];
|
||||
|
||||
export const MOCK_TEAM_GROUPS = [{ groupId: 'cn=users,ou=groups,dc=grafana,dc=org' }, { groupId: 'another-group' }];
|
||||
|
||||
export const setupMockTeams = () => {
|
||||
mockTeamsMap.clear();
|
||||
MOCK_TEAMS.forEach((team) => {
|
||||
mockTeamsMap.set(team.metadata.name, { team, groups: [] });
|
||||
mockTeamsMap.set(team.metadata.name, { team, groups: [...MOCK_TEAM_GROUPS] });
|
||||
});
|
||||
};
|
||||
|
||||
export const mockTeamsMap = new Map<string, { team: (typeof MOCK_TEAMS)[number]; groups: Array<{ groupId: string }> }>(
|
||||
MOCK_TEAMS.map((team) => [team.metadata.name, { team, groups: [] }])
|
||||
MOCK_TEAMS.map((team) => [team.metadata.name, { team, groups: [...MOCK_TEAM_GROUPS] }])
|
||||
);
|
||||
|
||||
@@ -79,7 +79,7 @@ const teamsGroupsHandler = () =>
|
||||
return HttpResponse.json(team.groups);
|
||||
});
|
||||
|
||||
const teamsUpdateGroupsHandler = () =>
|
||||
const teamsAddGroupHandler = () =>
|
||||
http.post<{ uid: string }, { groupId: string }>('/api/teams/:uid/groups', async ({ params, request }) => {
|
||||
const teamData = mockTeamsMap.get(params.uid);
|
||||
const body = await request.json();
|
||||
@@ -95,6 +95,29 @@ const teamsUpdateGroupsHandler = () =>
|
||||
return HttpResponse.json({ message: 'Group added to Team' });
|
||||
});
|
||||
|
||||
const teamsRemoveGroupHandler = () =>
|
||||
http.delete<{ uid: string }>('/api/teams/:uid/groups', async ({ params, request }) => {
|
||||
const teamData = mockTeamsMap.get(params.uid);
|
||||
const url = new URL(request.url);
|
||||
const groupId = url.searchParams.get('groupId');
|
||||
|
||||
if (!teamData) {
|
||||
return HttpResponse.json({ message: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!groupId) {
|
||||
return HttpResponse.json({ message: 'Missing groupId' }, { status: 400 });
|
||||
}
|
||||
|
||||
const updatedTeam = {
|
||||
...teamData,
|
||||
groups: teamData.groups.filter((g) => g.groupId !== groupId),
|
||||
};
|
||||
mockTeamsMap.set(params.uid, updatedTeam);
|
||||
|
||||
return HttpResponse.json({ message: 'Group removed from Team' });
|
||||
});
|
||||
|
||||
const searchTeamsHandler = () =>
|
||||
http.get('/api/teams/search', async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
@@ -150,7 +173,8 @@ const updateTeamHandler = () =>
|
||||
const handlers = [
|
||||
teamsPreferencesHandler(),
|
||||
teamsGroupsHandler(),
|
||||
teamsUpdateGroupsHandler(),
|
||||
teamsAddGroupHandler(),
|
||||
teamsRemoveGroupHandler(),
|
||||
searchTeamsHandler(),
|
||||
getTeamHandler(),
|
||||
deleteTeamHandler(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { wellFormedTree } from './fixtures/folders';
|
||||
|
||||
export const getFolderFixtures = wellFormedTree;
|
||||
export { MOCK_TEAMS } from './fixtures/teams';
|
||||
export { MOCK_TEAMS, MOCK_TEAM_GROUPS } from './fixtures/teams';
|
||||
|
||||
Generated
+3
-6
@@ -2263,8 +2263,7 @@
|
||||
"operationId": "getTeamGroupsApi",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"type": "string",
|
||||
"name": "teamId",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@@ -2308,8 +2307,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"type": "string",
|
||||
"name": "teamId",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@@ -2350,8 +2348,7 @@
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"type": "string",
|
||||
"name": "teamId",
|
||||
"in": "path",
|
||||
"required": true
|
||||
|
||||
Generated
+3
-6
@@ -9927,8 +9927,7 @@
|
||||
"operationId": "getTeamGroupsApi",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"type": "string",
|
||||
"name": "teamId",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@@ -9972,8 +9971,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"type": "string",
|
||||
"name": "teamId",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@@ -10014,8 +10012,7 @@
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"type": "string",
|
||||
"name": "teamId",
|
||||
"in": "path",
|
||||
"required": true
|
||||
|
||||
@@ -23,7 +23,6 @@ import { reducer as pluginsReducer } from 'app/features/plugins/admin/state/redu
|
||||
import userReducers from 'app/features/profile/state/reducers';
|
||||
import serviceAccountsReducer from 'app/features/serviceaccounts/state/reducers';
|
||||
import supportBundlesReducer from 'app/features/support-bundles/state/reducers';
|
||||
import teamsReducers from 'app/features/teams/state/reducers';
|
||||
import usersReducers from 'app/features/users/state/reducers';
|
||||
import templatingReducers from 'app/features/variables/state/keyedVariablesReducer';
|
||||
|
||||
@@ -33,7 +32,6 @@ import { cleanUpAction } from '../actions/cleanUp';
|
||||
const rootReducers = {
|
||||
...sharedReducers,
|
||||
...alertingReducers,
|
||||
...teamsReducers,
|
||||
...dashboardReducers,
|
||||
...exploreReducers,
|
||||
...dataSourcesReducers,
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
import { render, screen } from 'test/test-utils';
|
||||
import { render, screen, waitFor } from 'test/test-utils';
|
||||
|
||||
import { setBackendSrv } from '@grafana/runtime';
|
||||
import { setupMockServer } from '@grafana/test-utils/server';
|
||||
import { MOCK_TEAMS } from '@grafana/test-utils/unstable';
|
||||
import { MOCK_TEAMS, MOCK_TEAM_GROUPS } from '@grafana/test-utils/unstable';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { TeamGroup, TeamState } from 'app/types/teams';
|
||||
|
||||
import TeamGroupSync from './TeamGroupSync';
|
||||
import { getMockTeamGroups } from './mocks/teamMocks';
|
||||
|
||||
setBackendSrv(backendSrv);
|
||||
setupMockServer();
|
||||
|
||||
const setup = (preloadedTeamState?: Partial<TeamState>) => {
|
||||
return render(<TeamGroupSync teamUid={MOCK_TEAMS[0].metadata.name} isReadOnly={false} />, {
|
||||
preloadedState: {
|
||||
team: {
|
||||
groups: [],
|
||||
...preloadedTeamState,
|
||||
},
|
||||
},
|
||||
});
|
||||
const setup = () => {
|
||||
return render(<TeamGroupSync teamUid={MOCK_TEAMS[0].metadata.name} isReadOnly={false} />);
|
||||
};
|
||||
|
||||
describe('TeamGroupSync', () => {
|
||||
@@ -29,28 +20,37 @@ describe('TeamGroupSync', () => {
|
||||
expect(screen.getByRole('heading', { name: /External group sync/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render groups table', () => {
|
||||
setup({ groups: getMockTeamGroups(3) });
|
||||
expect(screen.getAllByRole('row')).toHaveLength(4); // 3 items plus table header
|
||||
it('should render groups table', async () => {
|
||||
setup();
|
||||
expect(await screen.findAllByRole('row')).toHaveLength(MOCK_TEAM_GROUPS.length + 1); // items plus table header
|
||||
});
|
||||
|
||||
it('should call add group', async () => {
|
||||
const { user } = setup();
|
||||
// Empty List CTA "Add group" button is second in the DOM order
|
||||
await user.click(screen.getAllByRole('button', { name: /add group/i })[1]);
|
||||
// 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]);
|
||||
expect(screen.getByRole('textbox', { name: /add external group/i })).toBeVisible();
|
||||
|
||||
await user.type(screen.getByRole('textbox', { name: /add external group/i }), 'test/group');
|
||||
await user.click(screen.getAllByRole('button', { name: /add group/i })[0]);
|
||||
await user.click(screen.getAllByRole('button', { name: /add group/i })[1]);
|
||||
|
||||
expect(screen.getByRole('row', { name: /test\/group/i })).toBeInTheDocument();
|
||||
expect(await screen.findByRole('row', { name: /test\/group/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should remove group', async () => {
|
||||
const mockGroup: TeamGroup = { teamId: 1, groupId: 'someGroup' };
|
||||
const { user } = setup({ groups: [mockGroup] });
|
||||
await user.click(screen.getByRole('button', { name: 'Remove group someGroup' }));
|
||||
const { user } = setup();
|
||||
const groupToRemove = MOCK_TEAM_GROUPS[0].groupId;
|
||||
|
||||
expect(screen.queryByRole('row', { name: /test\/group/i })).not.toBeInTheDocument();
|
||||
// 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()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,85 +1,63 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { FormEventHandler, PureComponent } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { FormEventHandler, useState } from 'react';
|
||||
|
||||
import {
|
||||
TeamGroupDto,
|
||||
useAddTeamGroupApiMutation,
|
||||
useGetTeamGroupsApiQuery,
|
||||
useRemoveTeamGroupApiQueryMutation,
|
||||
} from '@grafana/api-clients/rtkq/legacy';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Input, Tooltip, Icon, Button, useTheme2, InlineField, InlineFieldRow } from '@grafana/ui';
|
||||
import { Input, Tooltip, Icon, Button, useTheme2, InlineField, InlineFieldRow, useStyles2 } from '@grafana/ui';
|
||||
import { SlideDown } from 'app/core/components/Animations/SlideDown';
|
||||
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
|
||||
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 { StoreState } from 'app/types/store';
|
||||
import { TeamGroup } from 'app/types/teams';
|
||||
|
||||
import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions';
|
||||
import { getTeamGroups } from './state/selectors';
|
||||
|
||||
function mapStateToProps(state: StoreState) {
|
||||
return {
|
||||
groups: getTeamGroups(state.team),
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadTeamGroups,
|
||||
addTeamGroup,
|
||||
removeTeamGroup,
|
||||
};
|
||||
|
||||
interface OwnProps {
|
||||
interface Props {
|
||||
isReadOnly: boolean;
|
||||
teamUid: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isAdding: boolean;
|
||||
newGroupId: string;
|
||||
}
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
export type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||
|
||||
const headerTooltip = `Sync LDAP, OAuth or SAML groups with your Grafana teams.`;
|
||||
|
||||
export class TeamGroupSync extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { isAdding: false, newGroupId: '' };
|
||||
}
|
||||
export const TeamGroupSync = ({ isReadOnly, teamUid }: Props) => {
|
||||
const [isAddBoxVisible, setIsAddBoxVisible] = useState(false);
|
||||
const [newGroupId, setNewGroupId] = useState('');
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchTeamGroups();
|
||||
}
|
||||
const { data: groups = [] } = useGetTeamGroupsApiQuery({ teamId: teamUid });
|
||||
const [addTeamGroup] = useAddTeamGroupApiMutation();
|
||||
const [removeTeamGroup] = useRemoveTeamGroupApiQueryMutation();
|
||||
|
||||
async fetchTeamGroups() {
|
||||
this.props.loadTeamGroups(this.props.teamUid);
|
||||
}
|
||||
|
||||
onToggleAdding = () => {
|
||||
this.setState({ isAdding: !this.state.isAdding });
|
||||
const onToggleAdding = () => {
|
||||
setIsAddBoxVisible(!isAddBoxVisible);
|
||||
};
|
||||
|
||||
onNewGroupIdChanged: FormEventHandler<HTMLInputElement> = (event) => {
|
||||
this.setState({ newGroupId: event.currentTarget.value });
|
||||
const onNewGroupIdChanged: FormEventHandler<HTMLInputElement> = (event) => {
|
||||
setNewGroupId(event.currentTarget.value);
|
||||
};
|
||||
|
||||
onAddGroup: FormEventHandler<HTMLFormElement> = (event) => {
|
||||
const onAddGroup: FormEventHandler<HTMLFormElement> = async (event) => {
|
||||
event.preventDefault();
|
||||
this.props.addTeamGroup(this.props.teamUid, this.state.newGroupId);
|
||||
this.setState({ isAdding: false, newGroupId: '' });
|
||||
await addTeamGroup({ teamId: teamUid, teamGroupMapping: { groupId: newGroupId } });
|
||||
setIsAddBoxVisible(false);
|
||||
setNewGroupId('');
|
||||
};
|
||||
|
||||
onRemoveGroup = (group: TeamGroup) => {
|
||||
this.props.removeTeamGroup(this.props.teamUid, group.groupId);
|
||||
const onRemoveGroup = async (groupId: string | undefined) => {
|
||||
if (!groupId) {
|
||||
return;
|
||||
}
|
||||
await removeTeamGroup({ teamId: teamUid, groupId });
|
||||
};
|
||||
|
||||
isNewGroupValid() {
|
||||
return this.state.newGroupId.length > 1;
|
||||
}
|
||||
const isNewGroupValid = () => {
|
||||
return newGroupId.length > 1;
|
||||
};
|
||||
|
||||
renderGroup(group: TeamGroup) {
|
||||
const { isReadOnly } = this.props;
|
||||
const renderGroup = (group: TeamGroupDto) => {
|
||||
return (
|
||||
<tr key={group.groupId}>
|
||||
<td>{group.groupId}</td>
|
||||
@@ -87,7 +65,7 @@ export class TeamGroupSync extends PureComponent<Props, State> {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => this.onRemoveGroup(group)}
|
||||
onClick={() => onRemoveGroup(group.groupId)}
|
||||
disabled={isReadOnly}
|
||||
aria-label={t('teams.team-group-sync.aria-label-remove', 'Remove group {{groupName}}', {
|
||||
groupName: group.groupId,
|
||||
@@ -98,114 +76,106 @@ export class TeamGroupSync extends PureComponent<Props, State> {
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isAdding, newGroupId } = this.state;
|
||||
const { groups, isReadOnly } = this.props;
|
||||
const styles = getStyles();
|
||||
return (
|
||||
<div>
|
||||
{highlightTrial() && (
|
||||
<UpgradeBox
|
||||
featureId={'team-sync'}
|
||||
eventVariant={'trial'}
|
||||
featureName={'team sync'}
|
||||
text={t(
|
||||
'teams.team-group-sync.team-sync-upgrade',
|
||||
'Add a group to enable team sync for free during your trial of Grafana Pro'
|
||||
)}
|
||||
/>
|
||||
return (
|
||||
<div>
|
||||
{highlightTrial() && (
|
||||
<UpgradeBox
|
||||
featureId={'team-sync'}
|
||||
eventVariant={'trial'}
|
||||
featureName={'team sync'}
|
||||
text={t(
|
||||
'teams.team-group-sync.team-sync-upgrade',
|
||||
'Add a group to enable team sync for free during your trial of Grafana Pro'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div className="page-action-bar">
|
||||
{(!highlightTrial() || groups.length > 0) && (
|
||||
<>
|
||||
<h3 className="page-sub-heading">
|
||||
<Trans i18nKey="teams.team-group-sync.external-group-sync">External group sync</Trans>
|
||||
</h3>
|
||||
<Tooltip placement="auto" content={headerTooltip}>
|
||||
<Icon className={cx(styles.icon, 'page-sub-heading-icon')} name="question-circle" />
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
<div className="page-action-bar">
|
||||
{(!highlightTrial() || groups.length > 0) && (
|
||||
<>
|
||||
<h3 className="page-sub-heading">
|
||||
<Trans i18nKey="teams.team-group-sync.external-group-sync">External group sync</Trans>
|
||||
</h3>
|
||||
<Tooltip placement="auto" content={headerTooltip}>
|
||||
<Icon className={cx(styles.icon, 'page-sub-heading-icon')} name="question-circle" />
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
<div className="page-action-bar__spacer" />
|
||||
{groups.length > 0 && (
|
||||
<Button onClick={this.onToggleAdding} icon="plus" disabled={isReadOnly}>
|
||||
<Trans i18nKey="teams.team-group-sync.add-group-button">Add group</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SlideDown in={isAdding}>
|
||||
<div className="cta-form">
|
||||
<CloseButton onClick={this.onToggleAdding} />
|
||||
<form onSubmit={this.onAddGroup}>
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label={t('teams.team-group-sync.label-add-external-group', 'Add external group')}
|
||||
tooltip={t('teams.team-group-sync.tooltip-add-external-group', 'LDAP group example: {{example}}', {
|
||||
example: 'cn=users,ou=groups,dc=grafana,dc=org',
|
||||
})}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
id={'add-external-group'}
|
||||
placeholder=""
|
||||
value={newGroupId}
|
||||
onChange={this.onNewGroupIdChanged}
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
</InlineField>
|
||||
<Button type="submit" disabled={isReadOnly || !this.isNewGroupValid()} style={{ marginLeft: 4 }}>
|
||||
<Trans i18nKey="teams.team-group-sync.add-group">Add group</Trans>
|
||||
</Button>
|
||||
</InlineFieldRow>
|
||||
</form>
|
||||
</div>
|
||||
</SlideDown>
|
||||
|
||||
{groups.length === 0 &&
|
||||
!isAdding &&
|
||||
(highlightTrial() ? (
|
||||
<TeamSyncUpgradeContent
|
||||
action={{ onClick: this.onToggleAdding, text: t('teams.team-group-sync.text.add-group', 'Add group') }}
|
||||
/>
|
||||
) : (
|
||||
<EmptyListCTA
|
||||
onClick={this.onToggleAdding}
|
||||
buttonIcon="users-alt"
|
||||
title={t(
|
||||
'teams.team-group-sync.title-there-external-groups',
|
||||
'There are no external groups to sync with'
|
||||
)}
|
||||
buttonTitle="Add group"
|
||||
proTip={headerTooltip}
|
||||
proTipLinkTitle="Learn more"
|
||||
proTipLink="https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-team-sync/"
|
||||
proTipTarget="_blank"
|
||||
buttonDisabled={isReadOnly}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className="page-action-bar__spacer" />
|
||||
{groups.length > 0 && (
|
||||
<div className="admin-list-table">
|
||||
<table className="filter-table filter-table--hover form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<Trans i18nKey="teams.team-group-sync.external-group-id">External Group ID</Trans>
|
||||
</th>
|
||||
<th style={{ width: '1%' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{groups.map((group) => this.renderGroup(group))}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Button onClick={onToggleAdding} icon="plus" disabled={isReadOnly}>
|
||||
<Trans i18nKey="teams.team-group-sync.add-group-button">Add group</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
<SlideDown in={isAddBoxVisible}>
|
||||
<div className="cta-form">
|
||||
<CloseButton onClick={onToggleAdding} />
|
||||
<form onSubmit={onAddGroup}>
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label={t('teams.team-group-sync.label-add-external-group', 'Add external group')}
|
||||
tooltip={t('teams.team-group-sync.tooltip-add-external-group', 'LDAP group example: {{example}}', {
|
||||
example: 'cn=users,ou=groups,dc=grafana,dc=org',
|
||||
})}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
id={'add-external-group'}
|
||||
placeholder=""
|
||||
value={newGroupId}
|
||||
onChange={onNewGroupIdChanged}
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
</InlineField>
|
||||
<Button type="submit" disabled={isReadOnly || !isNewGroupValid()} style={{ marginLeft: 4 }}>
|
||||
<Trans i18nKey="teams.team-group-sync.add-group">Add group</Trans>
|
||||
</Button>
|
||||
</InlineFieldRow>
|
||||
</form>
|
||||
</div>
|
||||
</SlideDown>
|
||||
|
||||
{groups.length === 0 &&
|
||||
!isAddBoxVisible &&
|
||||
(highlightTrial() ? (
|
||||
<TeamSyncUpgradeContent
|
||||
action={{ onClick: onToggleAdding, text: t('teams.team-group-sync.text.add-group', 'Add group') }}
|
||||
/>
|
||||
) : (
|
||||
<EmptyListCTA
|
||||
onClick={onToggleAdding}
|
||||
buttonIcon="users-alt"
|
||||
title={t('teams.team-group-sync.title-there-external-groups', 'There are no external groups to sync with')}
|
||||
buttonTitle="Add group"
|
||||
proTip={headerTooltip}
|
||||
proTipLinkTitle="Learn more"
|
||||
proTipLink="https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-team-sync/"
|
||||
proTipTarget="_blank"
|
||||
buttonDisabled={isReadOnly}
|
||||
/>
|
||||
))}
|
||||
|
||||
{groups.length > 0 && (
|
||||
<div className="admin-list-table">
|
||||
<table className="filter-table filter-table--hover form-inline">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<Trans i18nKey="teams.team-group-sync.external-group-id">External Group ID</Trans>
|
||||
</th>
|
||||
<th style={{ width: '1%' }} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{groups.map((group) => renderGroup(group))}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const TeamSyncUpgradeContent = ({ action }: { action?: UpgradeContentProps['action'] }) => {
|
||||
const theme = useTheme2();
|
||||
@@ -226,7 +196,7 @@ export const TeamSyncUpgradeContent = ({ action }: { action?: UpgradeContentProp
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TeamGroupSync);
|
||||
export default TeamGroupSync;
|
||||
|
||||
const getStyles = () => ({
|
||||
icon: css({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
import { TeamPermissionLevel } from 'app/types/acl';
|
||||
import { Team, TeamMember, TeamGroup } from 'app/types/teams';
|
||||
import { Team, TeamMember } from 'app/types/teams';
|
||||
|
||||
function generateShortUid(): string {
|
||||
return randomBytes(3).toString('hex'); // Generate a short UID
|
||||
@@ -44,16 +44,3 @@ export const getMockTeamMember = (): TeamMember => {
|
||||
permission: TeamPermissionLevel.Member,
|
||||
};
|
||||
};
|
||||
|
||||
export const getMockTeamGroups = (amount: number): TeamGroup[] => {
|
||||
const groups: TeamGroup[] = [];
|
||||
|
||||
for (let i = 1; i <= amount; i++) {
|
||||
groups.push({
|
||||
groupId: `group-${i}`,
|
||||
teamId: 1,
|
||||
});
|
||||
}
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { ThunkResult } from 'app/types/store';
|
||||
|
||||
import { teamGroupsLoaded } from './reducers';
|
||||
|
||||
export function loadTeamGroups(teamUid: string): ThunkResult<void> {
|
||||
return async (dispatch) => {
|
||||
const response = await getBackendSrv().get(`/api/teams/${teamUid}/groups`);
|
||||
dispatch(teamGroupsLoaded(response));
|
||||
};
|
||||
}
|
||||
|
||||
export function addTeamGroup(teamUid: string, groupId: string): ThunkResult<void> {
|
||||
return async (dispatch) => {
|
||||
await getBackendSrv().post(`/api/teams/${teamUid}/groups`, { groupId: groupId });
|
||||
dispatch(loadTeamGroups(teamUid));
|
||||
};
|
||||
}
|
||||
|
||||
export function removeTeamGroup(teamUid: string, groupId: string): ThunkResult<void> {
|
||||
return async (dispatch) => {
|
||||
// need to use query parameter due to escaped characters in the request
|
||||
await getBackendSrv().delete(`/api/teams/${teamUid}/groups?groupId=${encodeURIComponent(groupId)}`);
|
||||
dispatch(loadTeamGroups(teamUid));
|
||||
};
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { TeamState, TeamGroup } from 'app/types/teams';
|
||||
|
||||
export const initialTeamState: TeamState = {
|
||||
groups: [],
|
||||
};
|
||||
|
||||
const teamSlice = createSlice({
|
||||
name: 'team',
|
||||
initialState: initialTeamState,
|
||||
reducers: {
|
||||
teamGroupsLoaded: (state, action: PayloadAction<TeamGroup[]>): TeamState => {
|
||||
return { ...state, groups: action.payload };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { teamGroupsLoaded } = teamSlice.actions;
|
||||
|
||||
export const teamReducer = teamSlice.reducer;
|
||||
|
||||
export default {
|
||||
team: teamReducer,
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
import { TeamState } from 'app/types/teams';
|
||||
|
||||
export const getTeamGroups = (state: TeamState) => state.groups;
|
||||
@@ -37,7 +37,3 @@ export interface TeamGroup {
|
||||
groupId: string;
|
||||
teamId: number;
|
||||
}
|
||||
|
||||
export interface TeamState {
|
||||
groups: TeamGroup[];
|
||||
}
|
||||
|
||||
Generated
+3
-6
@@ -24691,8 +24691,7 @@
|
||||
"name": "teamId",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -24730,8 +24729,7 @@
|
||||
"name": "teamId",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -24769,8 +24767,7 @@
|
||||
"name": "teamId",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user