Skip to content

Commit 6359efa

Browse files
committed
feat: sprint 79 — UX: padronização de header em ClustersView e UserManagementView
Programado em par com: Claude IA
1 parent 20357fb commit 6359efa

8 files changed

Lines changed: 131 additions & 97 deletions

File tree

.scratch/sprint-68/issues/01-registry-menu-and-repository-listing.md renamed to .scratch/archive/sprint-68/issues/01-registry-menu-and-repository-listing.md

File renamed without changes.

.scratch/sprint-68/issues/02-repository-tags-view.md renamed to .scratch/archive/sprint-68/issues/02-repository-tags-view.md

File renamed without changes.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
title: ClustersView — padronizar header com buildSectionHeader
3+
status: done
4+
sprint: 79
5+
---
6+
7+
## Problem
8+
`ClustersView` usa um `buildToolbar()` próprio com `H2` + botão "Add Cluster" com `LUMO_PRIMARY` sem `LUMO_SMALL`. Não tem botão Refresh nem Help. As ações de Test Connection e Delete ficam como colunas inline no grid, fora do padrão de `SelectionAction` das demais views.
9+
10+
## Expected
11+
- Substituir `buildToolbar()` por `UiConstants.buildSectionHeader(...)` com H3
12+
- "Add Cluster" como `extraLeadingButton` com `LUMO_PRIMARY + LUMO_SMALL`
13+
- Test Connection e Delete como `SelectionAction` na barra (habilitados por seleção)
14+
- Refresh (recarrega a grid) e Help adicionados automaticamente pelo header
15+
- Coluna de ações inline removida do grid
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
title: UserManagementView — padronizar header com buildSectionHeader
3+
status: done
4+
sprint: 79
5+
---
6+
7+
## Problem
8+
`UserManagementView` usa um `buildToolbar()` próprio com `H2` + botão "Add User" com `LUMO_PRIMARY` sem `LUMO_SMALL`. Não tem botão Refresh nem Help. As ações de Deactivate e Edit Permissions ficam como colunas inline no grid.
9+
10+
## Expected
11+
- Substituir `buildToolbar()` por `UiConstants.buildSectionHeader(...)` com H3
12+
- "Add User" como `extraLeadingButton` com `LUMO_PRIMARY + LUMO_SMALL`
13+
- Deactivate como `SelectionAction` destrutivo na barra
14+
- Edit Permissions como `SelectionAction` na barra
15+
- Refresh e Help adicionados automaticamente pelo header
16+
- Coluna de ações inline removida do grid

docs/sprints-archive.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,3 +639,15 @@
639639
- `PodsView`: novo `Checkbox` "Hide completed Job pods" (marcado por padrão); novo predicado `isCompletedJobPod` (`jobName` não vazio + `phase == "Succeeded"`) combinado ao filtro existente do `ListDataProvider`, junto com Name/Status/Node e o filtro de Job — Pods `Failed` de Jobs permanecem sempre visíveis, independente do toggle
640640
- Ao abrir via `?job=<nome>` (botão "View Pods" de `JobsView`/`CronJobsView`), o checkbox inicia desmarcado — evita grid vazia ao ver os pods de um Job já `Complete`; volta a marcado ao limpar o filtro de Job pelo `jobFilterBanner`
641641
- Issue: `.scratch/sprint-67/issues/01-hide-completed-job-pods.md`
642+
643+
### Sprint 68 ✅ — Container Registry: menu Global, listagem de Repositories e Tags
644+
645+
- `CONTEXT.md`: novos termos `Registry` (capacidade derivada do `Cluster`, alcançada via port-forward da API do Kubernetes para o `Service` `registry` no Namespace `kube-system` — sem entidade persistida, sem credenciais novas), `Repository` (coleção nomeada de versões de imagem) e `Tag` (referência nomeada a uma versão específica, com digest/size/created); `docs/adr/0006-registry-via-port-forward.md` documenta a decisão de alcançar o Registry via port-forward em vez de uma entidade/configuração própria
646+
- `RepositoryInfo`/`TagInfo` (novos DTOs): `RepositoryInfo(name, tagCount)`, `TagInfo(name, digest, size, createdAt)`
647+
- `RegistryService` (novo, `io.greencap.k8s.kubernetes`): `listRepositories(Cluster)` — port-forward para o `Service` `registry`/`kube-system` (porta `5000`, porta do container — Fabric8 `ServiceResource#portForward` encaminha direto para a porta do Pod, não resolve `targetPort`), `GET /v2/_catalog` + `/v2/<repo>/tags/list` via `java.net.http.HttpClient`; `listTags(Cluster, repository)` — para cada tag, `GET /v2/<repo>/manifests/<tag>` (digest via header `Docker-Content-Digest`, size = `config.size` + soma de `layers[].size`) e `GET /v2/<repo>/blobs/<configDigest>` (campo `created`, formatado via `NamespaceService.age(...)`); qualquer exceção (Service ausente, port-forward falha, catálogo vazio) → `log.warn` + `List.of()`, sem `KubernetesOperationException` — ausência do Registry é estado esperado, não falha de cluster
648+
- `Permission.GLOBAL_REGISTRY_VIEW` (novo, grupo Global): incluído em `operatorPermissions()`/`viewerPermissions()`; `V22__add_registry_permission.sql` concede a todos os usuários com `GLOBAL_INFRASTRUCTURE_VIEW`
649+
- `RegistryView` (nova): rota `registry`, item "Container Registry" no drawer GLOBAL (`MainLayout.buildRegistryNavItem()`, ícone `VaadinIcon.ARCHIVE`); grid de Repositories (Repository/Tags, filtro por nome), ação "View Tags" navega para `registry/<repository>`; estado vazio único ("No repositories found. Make sure the Service \"registry\" in the \"kube-system\" namespace is available on this Cluster.") sem distinguir Service ausente/port-forward falho/catálogo vazio
650+
- `RegistryTagsView` (nova): rota `registry/:repository*` (wildcard para repositories com `/` no nome, ex. `greencap-demo/backend`); cabeçalho com nome do repository + botão Back para `RegistryView`; grid de Tags (Tag/Digest/Size/Created) — coluna Digest com `overflow:hidden`/`text-overflow:ellipsis`/`title` (tooltip) em vez de truncamento fixo
651+
- `samples/greencap-demo/cluster-provision.sh`: addon `registry` habilitado junto de `metrics-server`/`ingress`; `create-demo.sh` refatorado — addons (antes espalhados entre os dois scripts) agora centralizados em `cluster-provision.sh`, `create-demo.sh` passa a só aplicar os manifests do demo; `README.md` atualizado
652+
- Validado ponta a ponta no `greencap-demo`: addon `registry` habilitado, imagens de teste (`greencap-demo/hello` com tags `v1`/`v2`/`latest`, `greencap-demo/backend` com tag `v1`) buildadas e enviadas via port-forward + `docker push`; menu "Container Registry" lista os repositories com contagem de tags e "View Tags" exibe nome/digest/size/created corretamente
653+
- Issues: `.scratch/archive/sprint-68/issues/01-registry-menu-and-repository-listing.md`, `02-repository-tags-view.md`

docs/sprints.md

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
| Sprint | Tema | Status |
1010
|--------|------|--------|
11-
| 68 | Container Registry — menu Global, listagem de Repositories e Tags | ✅ Concluído |
1211
| 69 | Fix — Container Registry: item ausente na treeview de permissões + View Tags na grid | ✅ Concluído |
1312
| 70 | Platform Settings — auto-refresh: nova opção "3 seconds" e novo default | ✅ Concluído |
1413
| 71 | Infraestrutura de Demo — PVC para persistir o Container Registry interno | ✅ Concluído |
@@ -18,6 +17,7 @@
1817
| 76 | Namespaces View — Global: listagem com contagens de recursos, Create e Delete Namespace | ✅ Concluído |
1918
| 77 | Topologia: nó Ingress + botão "Go to resource" + pré-filtro ?name= nas views | ✅ Concluído |
2019
| 78 | Topologia: correções de layout (randomize), tap em group nodes e botão Reset Positions | ✅ Concluído |
20+
| 79 | UX — Padronização de header: ClustersView e UserManagementView com buildSectionHeader | ✅ Concluído |
2121

2222
---
2323

@@ -83,6 +83,12 @@
8383

8484
> Mostra apenas as últimas 10 sprints. Histórico completo em `docs/sprints-archive.md` (ver `docs/agents/sprint-archiving.md`).
8585
86+
### Sprint 79 ✅ — UX: padronização de header em ClustersView e UserManagementView
87+
88+
- `ClustersView`: `buildToolbar()` com `H2` removido; substituído por `UiConstants.buildSectionHeader` com H3 + botão "Add Cluster" (`LUMO_PRIMARY + LUMO_SMALL`) como extra leading button; ações "Test Connection" (`VaadinIcon.CONNECT`) e "Remove" (`VaadinIcon.TRASH`, destrutivo) movidas para `SelectionAction` no header (habilitadas pela seleção de linha); coluna de ações inline removida do grid; `GridSelectionMemory` injetado com `configureSingleSelection`; `refreshGrid()` retorna `boolean` para uso como `BooleanSupplier`; imports `H2` e `HorizontalLayout` removidos
89+
- `UserManagementView`: mesmo padrão — botão "Add User" (`LUMO_PRIMARY + LUMO_SMALL`); ações "Edit Permissions" (`VaadinIcon.EDIT`) e "Deactivate" (`VaadinIcon.BAN`, destrutivo) como `SelectionAction`; proteções antes feitas via botão desabilitado por linha passaram para early-exit com toast em `openEditPermissionsDialog` (admin padrão bloqueado) e `confirmDeactivate` (auto-desativação e usuário já inativo bloqueados); imports `H2` e `H4` removidos
90+
- Issues: `.scratch/sprint-79/issues/01-clusters-view-header-padrao.md`, `02-user-management-view-header-padrao.md`
91+
8692
### Sprint 78 ✅ — Topologia: correções de layout (randomize), tap em group nodes e botão Reset Positions
8793

8894
- `topology-graph.ts`: `randomize` dinâmico — `true` quando posições salvas ausentes (fix para nós empilhados na primeira renderização), `false` quando presentes (mantém layout salvo); guard `if (node.data('isGroup')) return` no tap handler (fix para painel lateral não abrir ao clicar em group nodes)
@@ -176,18 +182,6 @@
176182
- `RegistryView`: ação "View Tags" sai da barra de título (selection action) e passa para uma coluna de ações na própria grid (`UiConstants.addActionsColumn`, botão por linha), mesmo padrão de `JobsView` ("View Pods")
177183
- Issue: `.scratch/sprint-69/issues/01-fix-registry-permission-treeview-view-tags.md`
178184

179-
### Sprint 68 ✅ — Container Registry: menu Global, listagem de Repositories e Tags
180-
181-
- `CONTEXT.md`: novos termos `Registry` (capacidade derivada do `Cluster`, alcançada via port-forward da API do Kubernetes para o `Service` `registry` no Namespace `kube-system` — sem entidade persistida, sem credenciais novas), `Repository` (coleção nomeada de versões de imagem) e `Tag` (referência nomeada a uma versão específica, com digest/size/created); `docs/adr/0006-registry-via-port-forward.md` documenta a decisão de alcançar o Registry via port-forward em vez de uma entidade/configuração própria
182-
- `RepositoryInfo`/`TagInfo` (novos DTOs): `RepositoryInfo(name, tagCount)`, `TagInfo(name, digest, size, createdAt)`
183-
- `RegistryService` (novo, `io.greencap.k8s.kubernetes`): `listRepositories(Cluster)` — port-forward para o `Service` `registry`/`kube-system` (porta `5000`, porta do container — Fabric8 `ServiceResource#portForward` encaminha direto para a porta do Pod, não resolve `targetPort`), `GET /v2/_catalog` + `/v2/<repo>/tags/list` via `java.net.http.HttpClient`; `listTags(Cluster, repository)` — para cada tag, `GET /v2/<repo>/manifests/<tag>` (digest via header `Docker-Content-Digest`, size = `config.size` + soma de `layers[].size`) e `GET /v2/<repo>/blobs/<configDigest>` (campo `created`, formatado via `NamespaceService.age(...)`); qualquer exceção (Service ausente, port-forward falha, catálogo vazio) → `log.warn` + `List.of()`, sem `KubernetesOperationException` — ausência do Registry é estado esperado, não falha de cluster
184-
- `Permission.GLOBAL_REGISTRY_VIEW` (novo, grupo Global): incluído em `operatorPermissions()`/`viewerPermissions()`; `V22__add_registry_permission.sql` concede a todos os usuários com `GLOBAL_INFRASTRUCTURE_VIEW`
185-
- `RegistryView` (nova): rota `registry`, item "Container Registry" no drawer GLOBAL (`MainLayout.buildRegistryNavItem()`, ícone `VaadinIcon.ARCHIVE`); grid de Repositories (Repository/Tags, filtro por nome), ação "View Tags" navega para `registry/<repository>`; estado vazio único ("No repositories found. Make sure the Service \"registry\" in the \"kube-system\" namespace is available on this Cluster.") sem distinguir Service ausente/port-forward falho/catálogo vazio
186-
- `RegistryTagsView` (nova): rota `registry/:repository*` (wildcard para repositories com `/` no nome, ex. `greencap-demo/backend`); cabeçalho com nome do repository + botão Back para `RegistryView`; grid de Tags (Tag/Digest/Size/Created) — coluna Digest com `overflow:hidden`/`text-overflow:ellipsis`/`title` (tooltip) em vez de truncamento fixo
187-
- `samples/greencap-demo/cluster-provision.sh`: addon `registry` habilitado junto de `metrics-server`/`ingress`; `create-demo.sh` refatorado — addons (antes espalhados entre os dois scripts) agora centralizados em `cluster-provision.sh`, `create-demo.sh` passa a só aplicar os manifests do demo; `README.md` atualizado
188-
- Validado ponta a ponta no `greencap-demo`: addon `registry` habilitado, imagens de teste (`greencap-demo/hello` com tags `v1`/`v2`/`latest`, `greencap-demo/backend` com tag `v1`) buildadas e enviadas via port-forward + `docker push`; menu "Container Registry" lista os repositories com contagem de tags e "View Tags" exibe nome/digest/size/created corretamente
189-
- Issues: `.scratch/sprint-68/issues/01-registry-menu-and-repository-listing.md`, `02-repository-tags-view.md`
190-
191185

192186
---
193187

src/main/java/io/greencap/k8s/ui/ClustersView.java

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@
66
import com.vaadin.flow.component.formlayout.FormLayout;
77
import com.vaadin.flow.component.grid.Grid;
88
import com.vaadin.flow.component.html.Div;
9-
import com.vaadin.flow.component.html.H2;
109
import com.vaadin.flow.component.html.Span;
1110
import com.vaadin.flow.component.icon.VaadinIcon;
1211
import com.vaadin.flow.component.notification.Notification;
1312
import com.vaadin.flow.component.notification.NotificationVariant;
14-
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
1513
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
1614
import com.vaadin.flow.component.select.Select;
1715
import com.vaadin.flow.component.textfield.TextArea;
@@ -38,27 +36,58 @@
3836

3937
import java.io.IOException;
4038
import java.nio.charset.StandardCharsets;
39+
import java.util.ArrayList;
4140
import java.util.List;
4241

4342
@Route(value = "clusters", layout = MainLayout.class)
4443
@PageTitle("Clusters — GreenCap K8s")
4544
@PermitAll
4645
public class ClustersView extends VerticalLayout implements BeforeEnterObserver {
4746

47+
private static final String HELP_TITLE = "Clusters";
48+
private static final String HELP_TEXT = "A Cluster is a registered Kubernetes environment (kubeconfig). " +
49+
"Select the active cluster using the radio button — all workload views will reflect its resources. " +
50+
"Select a row and use \"Test Connection\" to verify reachability, or \"Remove\" to delete the registration " +
51+
"(the cluster itself is not affected).";
52+
4853
private final ClusterService clusterService;
4954
private final KubeconfigValidator kubeconfigValidator;
5055
private final ClusterContext clusterContext;
5156
private final UserService userService;
5257
private final Grid<Cluster> grid = new Grid<>(Cluster.class, false);
5358

5459
public ClustersView(ClusterService clusterService, KubeconfigValidator kubeconfigValidator,
55-
ClusterContext clusterContext, UserService userService) {
60+
ClusterContext clusterContext, UserService userService,
61+
GridSelectionMemory selectionMemory) {
5662
this.clusterService = clusterService;
5763
this.kubeconfigValidator = kubeconfigValidator;
5864
this.clusterContext = clusterContext;
5965
this.userService = userService;
66+
6067
setSizeFull();
61-
add(buildToolbar(), buildGrid());
68+
setPadding(true);
69+
70+
buildGrid();
71+
UiConstants.configureSingleSelection(grid, selectionMemory, getClass().getSimpleName(), Cluster::getName);
72+
73+
boolean canWrite = SecurityUtils.hasPermission(Permission.GLOBAL_CLUSTERS_WRITE);
74+
75+
List<UiConstants.SelectionAction<Cluster>> selectionActions = List.of(
76+
UiConstants.SelectionAction.of(VaadinIcon.CONNECT, "Test Connection", this::testConnection),
77+
UiConstants.SelectionAction.destructive(VaadinIcon.TRASH, "Remove", canWrite, this::confirmDelete)
78+
);
79+
80+
List<Button> extraButtons = new ArrayList<>();
81+
if (canWrite) {
82+
Button addBtn = new Button("Add Cluster", VaadinIcon.PLUS.create());
83+
addBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY, ButtonVariant.LUMO_SMALL);
84+
addBtn.addClickListener(e -> openAddDialog());
85+
extraButtons.add(addBtn);
86+
}
87+
88+
add(UiConstants.buildSectionHeader("Clusters", this::refreshGrid, HELP_TITLE, HELP_TEXT,
89+
grid, selectionActions, extraButtons),
90+
grid);
6291
}
6392

6493
@Override
@@ -70,28 +99,13 @@ public void beforeEnter(BeforeEnterEvent event) {
7099
refreshGrid();
71100
}
72101

73-
private HorizontalLayout buildToolbar() {
74-
Button addBtn = new Button("Add Cluster", VaadinIcon.PLUS.create(),
75-
e -> openAddDialog());
76-
addBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
77-
addBtn.setEnabled(SecurityUtils.hasPermission(Permission.GLOBAL_CLUSTERS_WRITE));
78-
79-
HorizontalLayout toolbar = new HorizontalLayout(new H2("Clusters"), addBtn);
80-
toolbar.setDefaultVerticalComponentAlignment(Alignment.CENTER);
81-
toolbar.expand(new H2("Clusters"));
82-
toolbar.setWidthFull();
83-
return toolbar;
84-
}
85-
86-
private Grid<Cluster> buildGrid() {
102+
private void buildGrid() {
87103
grid.addComponentColumn(this::buildRadioCell).setHeader("Active").setWidth("90px").setFlexGrow(0).setResizable(true);
88104
grid.addColumn(Cluster::getName).setHeader("Name").setSortable(true).setFlexGrow(1).setResizable(true);
89105
grid.addColumn(c -> c.getProvider().name()).setHeader("Provider").setWidth("120px").setResizable(true);
90106
grid.addComponentColumn(c -> statusBadge(c.getConnectionStatus()))
91107
.setHeader("Status").setWidth("140px").setResizable(true);
92-
UiConstants.addActionsColumn(grid, 2, this::buildActions);
93108
grid.setSizeFull();
94-
return grid;
95109
}
96110

97111
private Div buildRadioCell(Cluster cluster) {
@@ -141,23 +155,6 @@ private Span statusBadge(ConnectionStatus status) {
141155
return badge;
142156
}
143157

144-
private List<Button> buildActions(Cluster cluster) {
145-
var testIcon = VaadinIcon.CONNECT.create();
146-
testIcon.setSize(UiConstants.ICON_SIZE);
147-
Button testBtn = new Button(testIcon, e -> testConnection(cluster));
148-
testBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ICON);
149-
testBtn.getElement().setAttribute("title", "Test connection");
150-
151-
var deleteIcon = VaadinIcon.TRASH.create();
152-
deleteIcon.setSize(UiConstants.ICON_SIZE);
153-
Button deleteBtn = new Button(deleteIcon, e -> confirmDelete(cluster));
154-
deleteBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY, ButtonVariant.LUMO_ICON, ButtonVariant.LUMO_ERROR);
155-
deleteBtn.getElement().setAttribute("title", "Remove cluster");
156-
deleteBtn.setEnabled(SecurityUtils.hasPermission(Permission.GLOBAL_CLUSTERS_WRITE));
157-
158-
return List.of(testBtn, deleteBtn);
159-
}
160-
161158
private void confirmDelete(Cluster cluster) {
162159
Dialog dialog = new Dialog();
163160
dialog.setHeaderTitle("Remove cluster");
@@ -303,8 +300,9 @@ private java.util.Optional<MainLayout> getMainLayout() {
303300
.findFirst());
304301
}
305302

306-
private void refreshGrid() {
303+
private boolean refreshGrid() {
307304
grid.setItems(clusterService.findAll());
305+
return true;
308306
}
309307

310308
private void notify(String message, NotificationVariant variant) {

0 commit comments

Comments
 (0)