feat(admin-ui): minimal React+Vite+TanStack scaffolding

- Vite 7 + React 19 + TanStack Router (file-based) + TanStack Query
- Tailwind v4 (CSS-first config, OKLCH brand colors)
- i18next + react-i18next с RU/EN switcher в header
- Axios client с Accept-Language interceptor + JWT placeholder
- 2 pages: /dictionaries (list cards) + /dictionaries/$name (records table)
- Dockerfile (node build + nginx serving + API proxy)
- README + .gitignore

Backend:
- Новый GET /api/v1/dictionaries в read-api (DictionaryListController),
  фильтрует по доступному scope через ScopeContext
- DictionaryListItem DTO без heavy schema_json

routeTree.gen.ts — stub; реальное содержимое генерится Vite plugin'ом при
первом 'pnpm dev'.
This commit is contained in:
Александр Зимин
2026-05-04 00:55:04 +03:00
parent adc5315948
commit 1eba2ee59b
20 changed files with 609 additions and 0 deletions
@@ -0,0 +1,25 @@
package cloud.nstart.terravault.ordinis.readapi.dto;
import cloud.nstart.terravault.ordinis.domain.DataScope;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;
/**
* Облегчённое представление справочника для list-эндпоинта (без schema_json,
* чтобы не таскать тяжёлые JSON Schema на главную). Полная схема — отдельным
* GET /api/v1/dictionaries/{name}.
*/
public record DictionaryListItem(
UUID id,
String name,
String displayName,
String description,
DataScope scope,
String schemaVersion,
String bundle,
List<String> supportedLocales,
String defaultLocale,
OffsetDateTime createdAt,
OffsetDateTime updatedAt) {}
@@ -0,0 +1,59 @@
package cloud.nstart.terravault.ordinis.readapi.web;
import cloud.nstart.terravault.ordinis.auth.ScopeContext;
import cloud.nstart.terravault.ordinis.domain.DataScope;
import cloud.nstart.terravault.ordinis.domain.definition.DictionaryDefinition;
import cloud.nstart.terravault.ordinis.domain.definition.DictionaryDefinitionRepository;
import cloud.nstart.terravault.ordinis.readapi.dto.DictionaryListItem;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Set;
/**
* GET /api/v1/dictionaries — список доступных справочников. Фильтрация по
* scope такая же, как для records: возвращаются только справочники со scope ⊆
* текущим разрешённым scope клиенту.
*/
@RestController
@RequestMapping("/api/v1/dictionaries")
public class DictionaryListController {
private final DictionaryDefinitionRepository repository;
private final ScopeContext scopeContext;
public DictionaryListController(
DictionaryDefinitionRepository repository, ScopeContext scopeContext) {
this.repository = repository;
this.scopeContext = scopeContext;
}
@GetMapping
public List<DictionaryListItem> list(
@RequestParam(value = "as_scope", required = false) String asScope) {
Set<DataScope> scopes = scopeContext.resolveForRead(asScope);
return repository.findAll().stream()
.filter(d -> scopes.contains(d.getScope()))
.map(DictionaryListController::toItem)
.toList();
}
private static DictionaryListItem toItem(DictionaryDefinition d) {
return new DictionaryListItem(
d.getId(),
d.getName(),
d.getDisplayName(),
d.getDescription(),
d.getScope(),
d.getSchemaVersion(),
d.getBundle(),
d.getSupportedLocales() == null ? List.of() : List.of(d.getSupportedLocales()),
d.getDefaultLocale(),
d.getCreatedAt(),
d.getUpdatedAt());
}
}