feat: /audit поле «Пользователь» — typeahead picker из user-cache
Backend: GET /api/v1/admin/users (INTERNAL+ scope) — list cached юзеров из user_display_cache. UserDisplayService.listAll(limit) сортирует по preferred_username, hard-cap 1000. Контроллер: UserDisplayController.list(). Frontend: - queries.ts → useAllUsers() / allUsersQuery (5min stale, 403/404 → []). - components/audit/UserPicker.tsx (новый, ~140 строк) — cmdk-powered combobox: trigger показывает username (или UUID prefix fallback), popover c поиском по preferredUsername/email/UUID. X сбрасывает selection. - routes/audit.tsx — TextInput → UserPicker. UX: раньше юзер вручную копипастил UUID из audit-row'а; теперь поиск по имени/email типа `solov` → находит `solovyev.da`.
This commit is contained in:
+19
@@ -10,6 +10,7 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@@ -125,6 +126,24 @@ public class UserDisplayService {
|
||||
return hotCache.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Список всех закэшированных юзеров — для admin-UI пикеров (audit
|
||||
* filter «Пользователь», review reviewer selector и т.п.). Сортируется
|
||||
* по preferred_username для предсказуемой выдачи; hard-capped лимит
|
||||
* чтобы не вытащить всю таблицу когда cache разрастётся до 10k+
|
||||
* (в этот момент перейдём на server-side search; пока 1000 хватит).
|
||||
*/
|
||||
public List<UserDisplayInfo> listAll(int limit) {
|
||||
int cap = Math.min(Math.max(limit, 1), 1000);
|
||||
return repository.findAll().stream()
|
||||
.map(UserDisplayService::toInfo)
|
||||
.sorted(java.util.Comparator.comparing(
|
||||
(UserDisplayInfo i) -> i.preferredUsername() == null ? "" : i.preferredUsername(),
|
||||
String.CASE_INSENSITIVE_ORDER))
|
||||
.limit(cap)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// --- internals ---
|
||||
|
||||
private UserDisplayInfo upsert(String sub, String preferredUsername, String name,
|
||||
|
||||
+16
@@ -7,9 +7,11 @@ import cloud.nstart.terravault.ordinis.restapi.service.UserDisplayService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Resolve UUID (JWT sub) → human-readable display info. Frontend UserCell
|
||||
@@ -57,6 +59,20 @@ public class UserDisplayController {
|
||||
"authenticated request на этом backend'е чтобы попасть в кэш."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Список всех закэшированных юзеров — для admin-UI пикеров (audit
|
||||
* filter «Пользователь», reviewer selectors и т.п.). INTERNAL+ scope,
|
||||
* hard-cap limit 1000.
|
||||
*/
|
||||
@GetMapping("")
|
||||
public List<UserDisplayResponse> list(
|
||||
@RequestParam(defaultValue = "500") int limit) {
|
||||
requireInternal();
|
||||
return service.listAll(limit).stream()
|
||||
.map(UserDisplayResponse::from)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public record UserDisplayResponse(
|
||||
String sub,
|
||||
String preferredUsername,
|
||||
|
||||
Reference in New Issue
Block a user