Skip to content

Commit 1a3a017

Browse files
committed
feat: sprint 80 — Add Cluster dialog: Minikube (Docker), aviso OpenShift e comando kubectl copiável
Programado em par com: Claude IA
1 parent 24bf7c3 commit 1a3a017

8 files changed

Lines changed: 131 additions & 17 deletions

File tree

.scratch/sprint-69/issues/01-fix-registry-permission-treeview-view-tags.md renamed to .scratch/archive/sprint-69/issues/01-fix-registry-permission-treeview-view-tags.md

File renamed without changes.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
title: Add Cluster dialog — UX improvements (provider list, OpenShift warning, copyable command)
3+
status: done
4+
sprint: 80
5+
---
6+
7+
## Problem
8+
The Add Cluster dialog has three UX gaps:
9+
1. `ClusterProvider.Kubernetes` is semantically wrong — the only supported local provider is Minikube (Docker driver). The enum value misleads users into thinking any generic Kubernetes cluster is the target.
10+
2. Selecting OpenShift gives no feedback that it is not yet supported.
11+
3. The kubeconfig placeholder mentions `kubectl config view --flatten --minify` but it is not copyable — users have to type it manually.
12+
13+
## Expected
14+
15+
### 1 — ClusterProvider rename
16+
- Rename enum value `Kubernetes``MinikubeDocker`, displayed as `"Minikube (Docker)"` in the Select via `ItemLabelGenerator`.
17+
- Flyway migration `V{n}__rename_provider_kubernetes_to_minikube_docker.sql`:
18+
- Drop CHECK constraint on `clusters.provider`
19+
- UPDATE existing rows: `'Kubernetes'``'MinikubeDocker'`
20+
- Re-add CHECK constraint with new values `('MinikubeDocker', 'OpenShift')`
21+
- Default value in the dialog Select updated to `ClusterProvider.MinikubeDocker`.
22+
23+
### 2 — OpenShift "coming soon"
24+
- When the user selects `OpenShift` in the provider Select, show an inline warning message below the field: `"OpenShift support is coming in a future release."`.
25+
- The Save button is disabled while OpenShift is selected.
26+
- Selecting any other provider hides the warning and re-enables Save.
27+
28+
### 3 — Copyable kubectl command
29+
- Below the kubeconfig TextArea, add a styled code block (dark background) displaying `kubectl config view --flatten --minify`.
30+
- The block has a copy icon button on the right that copies the command to the clipboard via `UI.getCurrent().getPage().executeJs(...)`.
31+
- The TextArea placeholder is simplified to reference the block below rather than repeating the command inline.

CONTEXT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ A write operation on a CronJob that pauses new Job creation by patching `spec.su
8282
_Avoid_: Pause, stop, disable
8383

8484
**ClusterProvider**:
85-
Contextual metadata describing the Kubernetes distribution behind a Cluster (OKD, OpenShift, Kubernetes, Rancher). Does not alter GreenCap's behavior — used for display and identification only.
85+
Contextual metadata describing the Kubernetes distribution behind a Cluster. Current values: `MinikubeDocker` (displayed as "Minikube (Docker)") and `OpenShift` (planned, not yet supported). Does not alter GreenCap's behavior — used for display and identification only.
8686
_Avoid_: Type, flavor, vendor
8787

8888
**Kubeconfig**:

docs/sprints-archive.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,3 +651,9 @@
651651
- `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
652652
- 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
653653
- Issues: `.scratch/archive/sprint-68/issues/01-registry-menu-and-repository-listing.md`, `02-repository-tags-view.md`
654+
655+
### Sprint 69 ✅ — Fix: Container Registry — item ausente na treeview de permissões + ação View Tags na grid
656+
657+
- `UserManagementView.buildGlobalGroups()`: novo grupo "Container Registry" (`GLOBAL_REGISTRY_VIEW`) — permission introduzida na sprint 68 que não havia sido exposta na treeview de permissões (GLOBAL), mesmo padrão de grupo único do "Infrastructure"
658+
- `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")
659+
- Issue: `.scratch/archive/sprint-69/issues/01-fix-registry-permission-treeview-view-tags.md`

docs/sprints.md

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

99
| Sprint | Tema | Status |
1010
|--------|------|--------|
11-
| 69 | Fix — Container Registry: item ausente na treeview de permissões + View Tags na grid | ✅ Concluído |
1211
| 70 | Platform Settings — auto-refresh: nova opção "3 seconds" e novo default | ✅ Concluído |
1312
| 71 | Infraestrutura de Demo — PVC para persistir o Container Registry interno | ✅ Concluído |
1413
| 73 | Container Registry — Build & push de imagem via Kaniko a partir de Git Repository público | ✅ Concluído |
@@ -18,6 +17,7 @@
1817
| 77 | Topologia: nó Ingress + botão "Go to resource" + pré-filtro ?name= nas views | ✅ Concluído |
1918
| 78 | Topologia: correções de layout (randomize), tap em group nodes e botão Reset Positions | ✅ Concluído |
2019
| 79 | UX — Padronização de header: ClustersView e UserManagementView com buildSectionHeader | ✅ Concluído |
20+
| 80 | Add Cluster dialog — provider Minikube (Docker), aviso OpenShift e comando kubectl copiável | ✅ Concluído |
2121

2222
---
2323

@@ -83,6 +83,15 @@
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 80 ✅ — Add Cluster dialog: provider Minikube (Docker), aviso OpenShift e comando kubectl copiável
87+
88+
- `ClusterProvider`: enum renomeado de `Kubernetes``MinikubeDocker`; método `displayName()` retorna `"Minikube (Docker)"` / `"OpenShift"`
89+
- `V27__rename_provider_kubernetes_to_minikube_docker.sql`: DROP/re-ADD CHECK constraint; UPDATE `'Kubernetes'``'MinikubeDocker'` em linhas existentes
90+
- `ClustersView` grid: coluna Provider usa `displayName()` em vez de `.name()`
91+
- `ClustersView` dialog: Select usa `ItemLabelGenerator` com `displayName()`; default alterado para `MinikubeDocker`; ao selecionar OpenShift, exibe aviso inline `"OpenShift support is coming in a future release."` e desabilita o botão Save; code block escuro com `kubectl config view --flatten --minify` e botão de cópia via `navigator.clipboard.writeText`
92+
- `CONTEXT.md`: entrada `ClusterProvider` atualizada com os valores reais (`MinikubeDocker`, `OpenShift`)
93+
- Issue: `.scratch/sprint-80/issues/01-add-cluster-dialog-ux.md`
94+
8695
### Sprint 79 ✅ — UX: padronização de header em ClustersView e UserManagementView
8796

8897
- `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
@@ -176,12 +185,6 @@
176185
- `MainLayout`: default do field `currentRefreshInterval` e fallback em `onAttach()` passam de `NONE` para `THREE_SECONDS` — auto-refresh a 3s ativo desde o login para quem nunca configurou; usuários que já salvaram explicitamente "No auto refresh" (0) ou outro valor continuam inalterados; aplicado uniformemente a todas as views `Refreshable`, sem migration Flyway (mesmo padrão do fallback de tema `"DARK"`)
177186
- Issue: `.scratch/sprint-70/issues/01-auto-refresh-3-seconds-default.md`
178187

179-
### Sprint 69 ✅ — Fix: Container Registry — item ausente na treeview de permissões + ação View Tags na grid
180-
181-
- `UserManagementView.buildGlobalGroups()`: novo grupo "Container Registry" (`GLOBAL_REGISTRY_VIEW`) — permission introduzida na sprint 68 que não havia sido exposta na treeview de permissões (GLOBAL), mesmo padrão de grupo único do "Infrastructure"
182-
- `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")
183-
- Issue: `.scratch/sprint-69/issues/01-fix-registry-permission-treeview-view-tags.md`
184-
185188

186189
---
187190

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
package io.greencap.k8s.domain.cluster;
22

33
public enum ClusterProvider {
4-
Kubernetes, OpenShift
4+
MinikubeDocker,
5+
OpenShift;
6+
7+
public String displayName() {
8+
return switch (this) {
9+
case MinikubeDocker -> "Minikube (Docker)";
10+
case OpenShift -> "OpenShift";
11+
};
12+
}
513
}

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

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
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.Paragraph;
910
import com.vaadin.flow.component.html.Span;
11+
import com.vaadin.flow.component.icon.Icon;
1012
import com.vaadin.flow.component.icon.VaadinIcon;
1113
import com.vaadin.flow.component.notification.Notification;
1214
import com.vaadin.flow.component.notification.NotificationVariant;
15+
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
1316
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
1417
import com.vaadin.flow.component.select.Select;
1518
import com.vaadin.flow.component.textfield.TextArea;
@@ -102,7 +105,7 @@ public void beforeEnter(BeforeEnterEvent event) {
102105
private void buildGrid() {
103106
grid.addComponentColumn(this::buildRadioCell).setHeader("Active").setWidth("90px").setFlexGrow(0).setResizable(true);
104107
grid.addColumn(Cluster::getName).setHeader("Name").setSortable(true).setFlexGrow(1).setResizable(true);
105-
grid.addColumn(c -> c.getProvider().name()).setHeader("Provider").setWidth("120px").setResizable(true);
108+
grid.addColumn(c -> c.getProvider().displayName()).setHeader("Provider").setWidth("140px").setResizable(true);
106109
grid.addComponentColumn(c -> statusBadge(c.getConnectionStatus()))
107110
.setHeader("Status").setWidth("140px").setResizable(true);
108111
grid.setSizeFull();
@@ -212,17 +215,25 @@ private void openAddDialog() {
212215
Select<ClusterProvider> providerSelect = new Select<>();
213216
providerSelect.setLabel("Provider");
214217
providerSelect.setItems(ClusterProvider.values());
215-
providerSelect.setValue(ClusterProvider.Kubernetes);
218+
providerSelect.setItemLabelGenerator(ClusterProvider::displayName);
219+
providerSelect.setValue(ClusterProvider.MinikubeDocker);
216220
providerSelect.setWidthFull();
217221

222+
Paragraph openShiftWarning = new Paragraph("OpenShift support is coming in a future release.");
223+
openShiftWarning.getStyle()
224+
.set("color", "var(--lumo-secondary-text-color)")
225+
.set("font-size", "var(--lumo-font-size-s)")
226+
.set("margin", "0");
227+
openShiftWarning.setVisible(false);
228+
218229
TextArea kubeconfigArea = new TextArea("Kubeconfig YAML");
219230
kubeconfigArea.setWidthFull();
220231
kubeconfigArea.setMinHeight("200px");
221232
kubeconfigArea.setPlaceholder(
222233
"Paste the kubeconfig content or upload the file.\n\n" +
223-
"⚠️ The kubeconfig must be portable and self-contained: generate it with\n" +
224-
"kubectl config view --flatten --minify\n" +
225-
"to embed certificates and export only the required context.");
234+
"⚠️ The kubeconfig must be portable and self-contained — use the command below to generate it.");
235+
236+
Div commandBlock = buildCopyableCommand("kubectl config view --flatten --minify");
226237

227238
MemoryBuffer buffer = new MemoryBuffer();
228239
Upload upload = new Upload(buffer);
@@ -241,15 +252,24 @@ private void openAddDialog() {
241252
}
242253
});
243254

255+
Button saveBtn = new Button("Save");
256+
saveBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
257+
258+
providerSelect.addValueChangeListener(e -> {
259+
boolean isOpenShift = e.getValue() == ClusterProvider.OpenShift;
260+
openShiftWarning.setVisible(isOpenShift);
261+
saveBtn.setEnabled(!isOpenShift);
262+
});
263+
244264
FormLayout form = new FormLayout(nameField, providerSelect);
245265
form.setResponsiveSteps(new FormLayout.ResponsiveStep("0", 1));
246266

247-
VerticalLayout content = new VerticalLayout(form, upload, kubeconfigArea);
267+
VerticalLayout content = new VerticalLayout(form, openShiftWarning, commandBlock, upload, kubeconfigArea);
248268
content.setPadding(false);
249269
content.setSpacing(true);
250270
dialog.add(content);
251271

252-
Button saveBtn = new Button("Save", e -> {
272+
saveBtn.addClickListener(e -> {
253273
if (nameField.isEmpty()) {
254274
nameField.setErrorMessage("Name is required");
255275
nameField.setInvalid(true);
@@ -284,7 +304,6 @@ private void openAddDialog() {
284304
? NotificationVariant.LUMO_SUCCESS
285305
: NotificationVariant.LUMO_WARNING);
286306
});
287-
saveBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
288307

289308
Button cancelBtn = new Button("Cancel", e -> dialog.close());
290309

@@ -293,6 +312,47 @@ private void openAddDialog() {
293312
nameField.focus();
294313
}
295314

315+
private Div buildCopyableCommand(String command) {
316+
Span commandText = new Span(command);
317+
commandText.getStyle()
318+
.set("font-family", "monospace")
319+
.set("font-size", "var(--lumo-font-size-s)")
320+
.set("color", "#e2e8f0")
321+
.set("flex", "1");
322+
323+
Icon copyIcon = VaadinIcon.COPY_O.create();
324+
copyIcon.setSize("20px");
325+
copyIcon.getStyle().set("color", "#94a3b8").set("flex-shrink", "0");
326+
327+
Button copyBtn = new Button(copyIcon);
328+
copyBtn.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE, ButtonVariant.LUMO_SMALL);
329+
copyBtn.getStyle()
330+
.set("color", "#94a3b8")
331+
.set("padding", "0")
332+
.set("cursor", "pointer");
333+
copyBtn.addClickListener(e -> {
334+
getUI().ifPresent(ui -> ui.getPage().executeJs(
335+
"navigator.clipboard.writeText($0)", command));
336+
notify("Command copied to clipboard", NotificationVariant.LUMO_SUCCESS);
337+
});
338+
339+
HorizontalLayout row = new HorizontalLayout(commandText, copyBtn);
340+
row.setAlignItems(com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment.CENTER);
341+
row.setWidthFull();
342+
row.setPadding(false);
343+
row.setSpacing(false);
344+
row.getStyle().set("gap", "8px");
345+
346+
Div block = new Div(row);
347+
block.setWidthFull();
348+
block.getStyle()
349+
.set("background", "#1e293b")
350+
.set("border-radius", "6px")
351+
.set("padding", "10px 12px")
352+
.set("box-sizing", "border-box");
353+
return block;
354+
}
355+
296356
private java.util.Optional<MainLayout> getMainLayout() {
297357
return getUI().flatMap(ui -> ui.getChildren()
298358
.filter(c -> c instanceof MainLayout)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
ALTER TABLE clusters DROP CONSTRAINT clusters_provider_check;
2+
3+
UPDATE clusters SET provider = 'MinikubeDocker' WHERE provider = 'Kubernetes';
4+
5+
ALTER TABLE clusters ADD CONSTRAINT clusters_provider_check
6+
CHECK (provider IN ('MinikubeDocker', 'OpenShift'));

0 commit comments

Comments
 (0)