Skip to content

Commit 0b7efcb

Browse files
committed
test ui
1 parent 9ca4515 commit 0b7efcb

File tree

6 files changed

+346
-14
lines changed

6 files changed

+346
-14
lines changed

.github/workflows/deploy-dev.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ jobs:
8585
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
8686
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
8787
projectName: management-ui-dev
88-
directory: dist/ui/
88+
directory: dist_ui/
8989
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
9090
branch: main
9191
test:

.github/workflows/deploy-prod.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ jobs:
8585
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
8686
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
8787
projectName: management-ui-dev
88-
directory: dist/ui/
88+
directory: dist_ui/
8989
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
9090
test:
9191
runs-on: ubuntu-latest
@@ -103,7 +103,7 @@ jobs:
103103
node-version: 20.x
104104
- uses: actions/checkout@v4
105105
env:
106-
HUSKY: "0"
106+
HUSKY: "0"
107107
- name: Set up Python 3.11 for testing
108108
uses: actions/setup-python@v5
109109
with:
@@ -126,7 +126,7 @@ jobs:
126126
node-version: 20.x
127127
- uses: actions/checkout@v4
128128
env:
129-
HUSKY: "0"
129+
HUSKY: "0"
130130
- uses: aws-actions/setup-sam@v2
131131
with:
132132
use-installer: true
@@ -171,7 +171,7 @@ jobs:
171171
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
172172
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
173173
projectName: management-ui-prod
174-
directory: dist/ui/
174+
directory: dist_ui/
175175
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
176176
branch: main
177177
health-check-prod:
@@ -189,6 +189,6 @@ jobs:
189189
node-version: 20.x
190190
- uses: actions/checkout@v4
191191
env:
192-
HUSKY: "0"
192+
HUSKY: "0"
193193
- name: Call the health check script
194194
run: make prod_health_check
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
import React, { useState, useEffect } from 'react';
2+
import {
3+
Box,
4+
Button,
5+
Text,
6+
TextInput,
7+
Group,
8+
Modal,
9+
List,
10+
ListItem,
11+
Alert,
12+
ActionIcon,
13+
ScrollArea,
14+
Badge,
15+
} from '@mantine/core';
16+
import { IconTrash, IconCheck, IconX } from '@tabler/icons-react';
17+
import { notifications } from '@mantine/notifications';
18+
import { GroupMemberGetResponse, EntraActionResponse } from '@common/types/iam';
19+
20+
interface GroupMemberManagementProps {
21+
fetchMembers: () => Promise<GroupMemberGetResponse>;
22+
updateMembers: (toAdd: string[], toRemove: string[]) => Promise<EntraActionResponse>;
23+
}
24+
25+
export const GroupMemberManagement: React.FC<GroupMemberManagementProps> = ({
26+
fetchMembers,
27+
updateMembers,
28+
}) => {
29+
const [members, setMembers] = useState<GroupMemberGetResponse>([]);
30+
const [toAdd, setToAdd] = useState<string[]>([]);
31+
const [toRemove, setToRemove] = useState<string[]>([]);
32+
const [results, setResults] = useState<
33+
{ email: string; status: 'success' | 'failure'; message?: string }[]
34+
>([]);
35+
const [email, setEmail] = useState<string>('');
36+
const [isLoading, setIsLoading] = useState<boolean>(false);
37+
const [confirmationModal, setConfirmationModal] = useState<boolean>(false);
38+
const [errorModal, setErrorModal] = useState<{ open: boolean; email: string; message: string }>({
39+
open: false,
40+
email: '',
41+
message: '',
42+
});
43+
44+
useEffect(() => {
45+
const loadMembers = async () => {
46+
try {
47+
const memberList = await fetchMembers();
48+
setMembers(memberList);
49+
} catch (error) {
50+
notifications.show({
51+
title: 'Error',
52+
message: 'Failed to retrieve members.',
53+
color: 'red',
54+
});
55+
}
56+
};
57+
loadMembers();
58+
}, [fetchMembers]);
59+
60+
const handleAddMember = () => {
61+
if (email && !members.some((member) => member.email === email) && !toAdd.includes(email)) {
62+
setToAdd((prev) => [...prev, email]);
63+
setEmail('');
64+
}
65+
};
66+
67+
const handleRemoveMember = (email: string) => {
68+
if (!toRemove.includes(email)) {
69+
setToRemove((prev) => [...prev, email]);
70+
}
71+
};
72+
73+
const handleSaveChanges = async () => {
74+
setIsLoading(true);
75+
const newResults: { email: string; status: 'success' | 'failure'; message?: string }[] = [];
76+
77+
try {
78+
const response = await updateMembers(toAdd, toRemove);
79+
response.success?.forEach(({ email }) => {
80+
newResults.push({ email, status: 'success' });
81+
});
82+
response.failure?.forEach(({ email, message }) => {
83+
newResults.push({ email, status: 'failure', message });
84+
});
85+
setResults(newResults);
86+
setToAdd([]);
87+
setToRemove([]);
88+
} catch (error) {
89+
notifications.show({
90+
title: 'Error',
91+
message: 'An error occurred while saving changes.',
92+
color: 'red',
93+
});
94+
} finally {
95+
setIsLoading(false);
96+
}
97+
};
98+
99+
const handleViewErrorDetails = (email: string, message: string) => {
100+
setErrorModal({ open: true, email, message });
101+
};
102+
103+
return (
104+
<Box p="md">
105+
<Text fw={500} mb={4}>
106+
Group Member Management
107+
</Text>
108+
109+
{/* Member List */}
110+
<Box mb="md">
111+
<Text size="sm" fw={500} mb="xs">
112+
Current Members
113+
</Text>
114+
<ScrollArea style={{ height: 200 }}>
115+
<List>
116+
{members.map((member) => (
117+
<ListItem key={member.email}>
118+
<Group position="apart">
119+
<Text size="sm">{member.email}</Text>
120+
<ActionIcon
121+
color="red"
122+
variant="light"
123+
onClick={() => handleRemoveMember(member.email)}
124+
>
125+
<IconTrash size={16} />
126+
</ActionIcon>
127+
</Group>
128+
</ListItem>
129+
))}
130+
</List>
131+
</ScrollArea>
132+
</Box>
133+
134+
{/* Add Member */}
135+
<Box mb="md">
136+
<TextInput
137+
value={email}
138+
onChange={(event) => setEmail(event.currentTarget.value)}
139+
placeholder="Enter email to add"
140+
disabled={isLoading}
141+
/>
142+
<Button mt="sm" onClick={handleAddMember} disabled={!email.trim() || isLoading}>
143+
Add Member
144+
</Button>
145+
</Box>
146+
147+
{/* Save Changes Button */}
148+
<Button
149+
fullWidth
150+
color="blue"
151+
onClick={() => setConfirmationModal(true)}
152+
disabled={!toAdd.length && !toRemove.length}
153+
loading={isLoading}
154+
>
155+
Save Changes
156+
</Button>
157+
158+
{/* Confirmation Modal */}
159+
<Modal
160+
opened={confirmationModal}
161+
onClose={() => setConfirmationModal(false)}
162+
title="Confirm Changes"
163+
size="md"
164+
>
165+
<Box>
166+
{toAdd.length > 0 && (
167+
<Box mb="md">
168+
<Text fw={500} size="sm">
169+
Members to Add:
170+
</Text>
171+
<ScrollArea style={{ height: 100 }}>
172+
<List>
173+
{toAdd.map((email) => (
174+
<ListItem key={email}>
175+
<Text size="sm">{email}</Text>
176+
</ListItem>
177+
))}
178+
</List>
179+
</ScrollArea>
180+
</Box>
181+
)}
182+
{toRemove.length > 0 && (
183+
<Box mb="md">
184+
<Text fw={500} size="sm">
185+
Members to Remove:
186+
</Text>
187+
<ScrollArea style={{ height: 100 }}>
188+
<List>
189+
{toRemove.map((email) => (
190+
<ListItem key={email}>
191+
<Text size="sm">{email}</Text>
192+
</ListItem>
193+
))}
194+
</List>
195+
</ScrollArea>
196+
</Box>
197+
)}
198+
<Group position="center" mt="lg">
199+
<Button onClick={handleSaveChanges} loading={isLoading} color="blue">
200+
Confirm and Save
201+
</Button>
202+
<Button
203+
variant="outline"
204+
onClick={() => setConfirmationModal(false)}
205+
disabled={isLoading}
206+
>
207+
Cancel
208+
</Button>
209+
</Group>
210+
</Box>
211+
</Modal>
212+
213+
{/* Results */}
214+
{results.length > 0 && (
215+
<Box mt="md">
216+
<Text fw={500} size="sm" mb="xs">
217+
Results
218+
</Text>
219+
<List>
220+
{results.map(({ email, status, message }) => (
221+
<ListItem key={email}>
222+
<Group position="apart">
223+
<Text size="sm">{email}</Text>
224+
<Group>
225+
<Badge color={status === 'success' ? 'green' : 'red'}>
226+
{status === 'success' ? 'Success' : 'Failure'}
227+
</Badge>
228+
{status === 'failure' && (
229+
<Button
230+
variant="subtle"
231+
size="xs"
232+
onClick={() => handleViewErrorDetails(email, message || 'Unknown error')}
233+
>
234+
View Details
235+
</Button>
236+
)}
237+
</Group>
238+
</Group>
239+
</ListItem>
240+
))}
241+
</List>
242+
</Box>
243+
)}
244+
245+
{/* Error Modal */}
246+
<Modal
247+
opened={errorModal.open}
248+
onClose={() => setErrorModal({ open: false, email: '', message: '' })}
249+
title="Error Details"
250+
>
251+
<Box>
252+
<Text fw={500} size="sm" mb={2}>
253+
Email:
254+
</Text>
255+
<Text size="sm" mb="md">
256+
{errorModal.email}
257+
</Text>
258+
<Text fw={500} size="sm" mb={2}>
259+
Error Message:
260+
</Text>
261+
<Text size="sm" mb="md">
262+
{errorModal.message}
263+
</Text>
264+
<Button fullWidth onClick={() => setErrorModal({ open: false, email: '', message: '' })}>
265+
Close
266+
</Button>
267+
</Box>
268+
</Modal>
269+
270+
{/* Notifications for Feedback */}
271+
{isLoading && (
272+
<Alert color="blue" title="Processing Changes" mt="md">
273+
Please wait while the changes are being processed.
274+
</Alert>
275+
)}
276+
</Box>
277+
);
278+
};
279+
280+
export default GroupMemberManagement;

src/ui/pages/iam/ManageIam.page.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ import { AuthGuard } from '@ui/components/AuthGuard';
44
import { useApi } from '@ui/util/api';
55
import { AppRoles } from '@common/roles';
66
import UserInvitePanel from './UserInvitePanel';
7+
import GroupMemberManagement from './GroupMemberManagement';
8+
import { execCouncilGroupId } from '@common/config';
9+
import {
10+
EntraActionResponse,
11+
GroupMemberGetResponse,
12+
GroupModificationPatchRequest,
13+
} from '@common/types/iam';
714

815
export const ManageIamPage = () => {
916
const api = useApi('core');
@@ -26,13 +33,44 @@ export const ManageIamPage = () => {
2633
}
2734
};
2835

36+
const getExecMembers = async () => {
37+
try {
38+
const response = await api.get(`/api/v1/iam/groups/${execCouncilGroupId}`);
39+
return response.data as GroupMemberGetResponse;
40+
} catch (error: any) {
41+
console.error('Failed to get users:', error);
42+
return [];
43+
}
44+
};
45+
46+
const updateExecMembers = async (toAdd: string[], toRemove: string[]) => {
47+
const allMembers = toAdd.concat(toRemove);
48+
try {
49+
const response = await api.patch(`/api/v1/iam/groups/${execCouncilGroupId}`, {
50+
remove: toRemove,
51+
add: toAdd,
52+
} as GroupModificationPatchRequest);
53+
return response.data as EntraActionResponse;
54+
} catch (error: any) {
55+
console.error('Failed to get users:', error);
56+
return {
57+
success: [],
58+
failure: allMembers.map((email) => ({
59+
email,
60+
message: error.message || 'Failed to modify group member',
61+
})),
62+
};
63+
}
64+
};
65+
2966
return (
3067
<AuthGuard
3168
resourceDef={{ service: 'core', validRoles: [AppRoles.IAM_ADMIN, AppRoles.IAM_INVITE_ONLY] }}
3269
>
3370
<Title order={2}>Manage Authentication</Title>
3471
<SimpleGrid cols={2}>
3572
<UserInvitePanel onSubmit={handleInviteSubmit} />
73+
<GroupMemberManagement fetchMembers={getExecMembers} updateMembers={updateExecMembers} />
3674
{/* For future panels, make sure to add an auth guard if not every IAM role can see it. */}
3775
</SimpleGrid>
3876
</AuthGuard>

0 commit comments

Comments
 (0)