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:
Zimin A.N.
2026-05-25 16:48:41 +03:00
parent e72aa37e84
commit 3f05741ed1
5 changed files with 228 additions and 4 deletions
@@ -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,
@@ -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,