Skip to content

Commit fb0dca5

Browse files
authored
feat: comprehensive User Management UI implementation
2 parents 5bc58b3 + f239420 commit fb0dca5

File tree

16 files changed

+1867
-10
lines changed

16 files changed

+1867
-10
lines changed

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"Bash(ping:*)",
1515
"Bash(findstr:*)",
1616
"Bash(npx eslint:*)",
17-
"Read(//c/ProgramData/ThingConnect.Pulse/logs/**)"
17+
"Read(//c/ProgramData/ThingConnect.Pulse/logs/**)",
18+
"WebSearch"
1819
],
1920
"deny": [],
2021
"ask": []

ThingConnect.Pulse.Server/Controllers/UserManagementController.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ public async Task<ActionResult<UserInfoDto>> CreateUserAsync([FromBody] CreateUs
187187
_logger.LogInformation("User created: {Username} (ID: {UserId}) by admin {AdminId}",
188188
user.UserName, user.Id, currentUser?.Id);
189189

190-
return CreatedAtAction(nameof(GetUserByIdAsync), new { id = user.Id }, new UserInfoDto
190+
var userDto = new UserInfoDto
191191
{
192192
Id = user.Id,
193193
Username = user.UserName,
@@ -196,7 +196,11 @@ public async Task<ActionResult<UserInfoDto>> CreateUserAsync([FromBody] CreateUs
196196
CreatedAt = user.CreatedAt,
197197
LastLoginAt = user.LastLoginAt,
198198
IsActive = user.IsActive
199-
});
199+
};
200+
201+
// Return Ok for now to avoid routing issues
202+
// TODO: Fix location header generation
203+
return Ok(userDto);
200204
}
201205
catch (Exception ex)
202206
{

thingconnect.pulse.client/obj/Debug/package.g.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<PackageJsonDependenciesNextThemes Condition="$(PackageJsonDependenciesNextThemes) == ''">^0.4.6</PackageJsonDependenciesNextThemes>
2828
<PackageJsonDependenciesReact Condition="$(PackageJsonDependenciesReact) == ''">^19.1.1</PackageJsonDependenciesReact>
2929
<PackageJsonDependenciesReactDom Condition="$(PackageJsonDependenciesReactDom) == ''">^19.1.1</PackageJsonDependenciesReactDom>
30+
<PackageJsonDependenciesReactHookForm Condition="$(PackageJsonDependenciesReactHookForm) == ''">^7.62.0</PackageJsonDependenciesReactHookForm>
3031
<PackageJsonDependenciesReactIcons Condition="$(PackageJsonDependenciesReactIcons) == ''">^5.5.0</PackageJsonDependenciesReactIcons>
3132
<PackageJsonDependenciesReactRouterDom Condition="$(PackageJsonDependenciesReactRouterDom) == ''">^7.8.1</PackageJsonDependenciesReactRouterDom>
3233
<PackageJsonDependenciesZod Condition="$(PackageJsonDependenciesZod) == ''">^4.0.17</PackageJsonDependenciesZod>

thingconnect.pulse.client/package-lock.json

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

thingconnect.pulse.client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"next-themes": "^0.4.6",
2929
"react": "^19.1.1",
3030
"react-dom": "^19.1.1",
31+
"react-hook-form": "^7.62.0",
3132
"react-icons": "^5.5.0",
3233
"react-router-dom": "^7.8.1",
3334
"zod": "^4.0.17"
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { apiClient } from '../client';
2+
import type {
3+
UserInfo,
4+
CreateUserRequest,
5+
UpdateUserRequest,
6+
ChangeRoleRequest,
7+
ResetPasswordRequest,
8+
UsersListParams,
9+
PagedResult,
10+
} from '../types';
11+
12+
export class UserManagementService {
13+
/**
14+
* Get paginated list of users with optional filtering
15+
*/
16+
static async getUsers(params: UsersListParams = {}): Promise<PagedResult<UserInfo>> {
17+
const searchParams = new URLSearchParams();
18+
19+
if (params.page) {
20+
searchParams.append('page', params.page.toString());
21+
}
22+
23+
if (params.pageSize) {
24+
searchParams.append('pageSize', params.pageSize.toString());
25+
}
26+
27+
if (params.search) {
28+
searchParams.append('search', params.search);
29+
}
30+
31+
if (params.role) {
32+
searchParams.append('role', params.role);
33+
}
34+
35+
if (params.isActive !== undefined) {
36+
searchParams.append('isActive', params.isActive.toString());
37+
}
38+
39+
const queryString = searchParams.toString();
40+
const url = `/api/UserManagement${queryString ? `?${queryString}` : ''}`;
41+
42+
return apiClient.get<PagedResult<UserInfo>>(url);
43+
}
44+
45+
/**
46+
* Get a single user by ID
47+
*/
48+
static async getUserById(id: string): Promise<UserInfo> {
49+
return apiClient.get<UserInfo>(`/api/UserManagement/${encodeURIComponent(id)}`);
50+
}
51+
52+
/**
53+
* Create a new user (admin only)
54+
*/
55+
static async createUser(request: CreateUserRequest): Promise<UserInfo> {
56+
return apiClient.post<UserInfo>('/api/UserManagement', request);
57+
}
58+
59+
/**
60+
* Update user details
61+
*/
62+
static async updateUser(id: string, request: UpdateUserRequest): Promise<UserInfo> {
63+
return apiClient.put<UserInfo>(`/api/UserManagement/${encodeURIComponent(id)}`, request);
64+
}
65+
66+
/**
67+
* Delete a user
68+
*/
69+
static async deleteUser(id: string): Promise<void> {
70+
return apiClient.delete<void>(`/api/UserManagement/${encodeURIComponent(id)}`);
71+
}
72+
73+
/**
74+
* Change user role
75+
*/
76+
static async changeUserRole(id: string, request: ChangeRoleRequest): Promise<UserInfo> {
77+
return apiClient.put<UserInfo>(`/api/UserManagement/${encodeURIComponent(id)}/role`, request);
78+
}
79+
80+
/**
81+
* Reset user password (admin only)
82+
*/
83+
static async resetUserPassword(id: string, request: ResetPasswordRequest): Promise<void> {
84+
return apiClient.post<void>(`/api/UserManagement/${encodeURIComponent(id)}/reset-password`, request);
85+
}
86+
87+
/**
88+
* Toggle user active status
89+
*/
90+
static async toggleUserStatus(id: string, isActive: boolean): Promise<UserInfo> {
91+
return apiClient.put<UserInfo>(`/api/UserManagement/${encodeURIComponent(id)}`, {
92+
isActive,
93+
});
94+
}
95+
}

thingconnect.pulse.client/src/api/types.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,51 @@ export interface ValidationErrorsDto {
182182
message: string;
183183
errors: ValidationError[];
184184
}
185+
186+
// User Management Types
187+
export interface UserInfo {
188+
id: string;
189+
username: string;
190+
email: string;
191+
role: 'User' | 'Administrator';
192+
createdAt: string;
193+
lastLoginAt?: string | null;
194+
isActive: boolean;
195+
}
196+
197+
export interface CreateUserRequest {
198+
username: string;
199+
email: string;
200+
password: string;
201+
role: 'User' | 'Administrator';
202+
}
203+
204+
export interface UpdateUserRequest {
205+
username?: string;
206+
email?: string;
207+
isActive?: boolean;
208+
}
209+
210+
export interface ChangeRoleRequest {
211+
role: 'User' | 'Administrator';
212+
}
213+
214+
export interface ResetPasswordRequest {
215+
newPassword: string;
216+
}
217+
218+
export interface UsersListParams {
219+
page?: number;
220+
pageSize?: number;
221+
search?: string;
222+
role?: string;
223+
isActive?: boolean;
224+
}
225+
226+
export interface PagedResult<T> {
227+
items: T[];
228+
page: number;
229+
pageSize: number;
230+
totalCount: number;
231+
totalPages: number;
232+
}

thingconnect.pulse.client/src/components/layout/Navigation.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Box, VStack, Text, Icon, Image, HStack, Badge, Button } from '@chakra-ui/react';
22
import { Link as RouterLink, useLocation } from 'react-router-dom';
3-
import { Wifi, Activity, LogOut } from 'lucide-react';
3+
import { Wifi, Activity, LogOut, Users } from 'lucide-react';
44
import thingConnectIcon from '@/assets/ThingConnectPulseLogo.svg';
55
import { Clock, Wrench, Settings, Info, Dashboard, Help } from '@/icons';
66
import { useAuth } from '@/features/auth/context/AuthContext';
@@ -11,15 +11,18 @@ interface NavigationProps {
1111
const navigationItems = [
1212
{ label: 'Dashboard', path: '/', icon: Dashboard },
1313
{ label: 'History', path: '/history', icon: Clock },
14-
{ label: 'Configuration', path: '/configuration', icon: Wrench },
15-
{ label: 'Settings', path: '/settings', icon: Settings },
14+
{ label: 'Configuration', path: '/configuration', icon: Wrench, adminOnly: true },
15+
{ label: 'User Management', path: '/users', icon: Users, adminOnly: true },
16+
{ label: 'Settings', path: '/settings', icon: Settings, adminOnly: true },
1617
{ label: 'Help', path: 'https://docs.thingconnect.io/pulse/', icon: Help, external: true },
1718
{ label: 'About', path: '/about', icon: Info },
1819
];
1920

2021
export function Navigation({ onItemClick }: NavigationProps) {
2122
const location = useLocation();
22-
const { logout } = useAuth();
23+
const { logout, user } = useAuth();
24+
25+
const isAdmin = user?.role === 'Administrator';
2326

2427
const isActiveRoute = (path: string) =>
2528
path === '/' ? location.pathname === '/' : location.pathname.startsWith(path);
@@ -51,7 +54,9 @@ export function Navigation({ onItemClick }: NavigationProps) {
5154
/>
5255
</Box>
5356
<VStack gap={2} p={4} flex='1' align='stretch' data-testid='navigation-items'>
54-
{navigationItems.map(item => {
57+
{navigationItems
58+
.filter(item => !item.adminOnly || isAdmin)
59+
.map(item => {
5560
const isActive = !item.external && isActiveRoute(item.path);
5661
const ItemContent = (
5762
<HStack

0 commit comments

Comments
 (0)