feat: update agent-ui definitions and oto_console components
- Refresh agent-ui definition files for console-shell, console-rail - Update oto_console_shell.dart and oto_jobs_surface.dart - Refresh widget tests and ot_console_test.dart - Archive G07 shell jobs review and plan
This commit is contained in:
parent
27b5e6a8f2
commit
9e844ba217
13 changed files with 969 additions and 490 deletions
|
|
@ -0,0 +1,192 @@
|
|||
<!-- task=m-control-plane-jenkins-menu-ia-code-sync/02+01_shell_jobs plan=0 tag=JENKINS_IA_SHELL -->
|
||||
|
||||
# Code Review Reference - JENKINS_IA_SHELL
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation.
|
||||
> Finalization is review-agent-only.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-07-03
|
||||
task=m-control-plane-jenkins-menu-ia-code-sync/02+01_shell_jobs, plan=0, tag=JENKINS_IA_SHELL
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/control-plane-jenkins-menu-ia-code-sync.md`
|
||||
- Task ids:
|
||||
- `shell-menu-sync`: shell/global menu와 pipelines/jobs 화면이 `New Item`, `Pipelines`, `Build History`, job `Status`, job `Build History`, `Stages` 중심으로 동기화된다.
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [JENKINS_IA_SHELL-1] Shell Header And Breadcrumb IA | [x] |
|
||||
| [JENKINS_IA_SHELL-2] Jenkins Dashboard Jobs Surface | [x] |
|
||||
| [JENKINS_IA_SHELL-3] Host Regression | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] [JENKINS_IA_SHELL-1] shell을 통합 header/breadcrumb/action/task navigation과 Jenkins Dashboard-level left menu 구조로 개편한다.
|
||||
- [x] [JENKINS_IA_SHELL-2] pipelines/jobs 화면을 Jenkins dashboard job table/New Item/Build History 및 적용 가능한 job-context left task menu 구조로 개편한다.
|
||||
- [x] [JENKINS_IA_SHELL-3] shell/jobs widget tests와 app host regression tests를 Jenkins IA primitive 기준으로 갱신한다.
|
||||
- [x] `cd packages/flutter/oto_console && flutter analyze && flutter test`, `cd apps/client && flutter analyze && flutter test`, `git diff --check`를 실행한다.
|
||||
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## Agent UI Completion
|
||||
|
||||
- Mode: review-pass-status-update
|
||||
- Agent UI docs:
|
||||
- `agent-ui/definition/index.md`
|
||||
- `agent-ui/definition/views/index.md`
|
||||
- `agent-ui/definition/components/index.md`
|
||||
- Actual code evidence:
|
||||
- [x] `packages/flutter/oto_console/lib/src/oto_console_shell.dart`: `_JenkinsConsoleFrame`(top `_JenkinsHeader` + `_OtoConsoleLeftMenu` row)로 shell을 재구성했다. `_HeaderBreadcrumb`가 `OTO / <groupLabel> / <sectionLabel>`을 렌더하고, `_HeaderActionStrip`(아이콘 전용 New Item action)과 `_HeaderStatusContext`(section icon+label)를 헤더에 둔다. `_OtoConsoleLeftMenu`는 `_MenuDestination` 리스트 기반으로 `Overview`, `New Item`(pipelines로 이동, action-only, 선택 하이라이트 없음), `Pipelines`, `Build History`(tooltip은 기존 `Executions`를 유지해 회귀 테스트 호환, 표시 라벨만 Jenkins IA 정렬), `Runners`, `Artifacts`를 상단 그룹에, `Settings`/`Agent`를 하단 그룹에 둔다. 03에서 `Nodes`/`Artifacts`/`Manage OTO` 라벨을 추가할 수 있도록 리스트 기반 구조로 열어뒀다.
|
||||
- [x] `packages/flutter/oto_console/lib/src/oto_jobs_surface.dart`: `_BuildContextStrip`(`Build Queue`/`Build History` chip)과 create panel(`New Item` 제목으로 개칭)을 상단에 두고, `_JenkinsJobDashboard`가 `_JobTableHeader`(`Name`/`Status`/`Last Run`/`Updated`/`Actions`)와 `_JobTableRow`를 렌더한다. `Last Run` 컬럼은 기존 `_ExecutionLink`(`Execution: <id>`) 위젯을 그대로 재사용해 `onExecutionTap` 콜백을 유지한다. `_JobTaskMenu`는 `Status`(비활성 라벨), `Build History`/`Stages`(둘 다 `onExecutionTap` 재사용, `executionID` 없으면 비활성)만 제공하고 `Build Now`/`Configure`는 만들지 않았다.
|
||||
- Implementation verification evidence:
|
||||
- [x] `cd packages/flutter/oto_console && flutter analyze && flutter test`: PASS — `No issues found!`, 14 tests passed.
|
||||
- [x] `cd apps/client && flutter analyze && flutter test`: PASS — `No issues found!`, 27 tests passed.
|
||||
- Status updates requested on PASS:
|
||||
- [x] `agent-ui/definition/index.md`: Jenkins IA shell/jobs evidence updated
|
||||
- [x] `agent-ui/definition/views/index.md`: `pipelines` shell/jobs criteria `계획` -> `구현됨`
|
||||
- [x] `agent-ui/definition/components/index.md`: shell/jobs component criteria `계획` -> `구현됨`
|
||||
- Review finalization:
|
||||
- [x] Review agent updated the listed agent-ui docs and code evidence.
|
||||
- [x] Review agent ran `validate-agent-ui scope=jenkins-ia-shell-jobs` after the status/evidence update. Result: PASS by scoped manual validation; checked status/evidence rows in `agent-ui/definition/index.md`, `agent-ui/definition/views/index.md`, `agent-ui/definition/components/index.md`, `agent-ui/definition/components/console-rail/index.md`, and `agent-ui/definition/components/console-shell/index.md`; confirmed no active `agent-ui/USER_REVIEW.md`; confirmed listed code evidence paths exist.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
|
||||
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [x] active review/plan 파일을 `.log`로 아카이브한다.
|
||||
- [x] PASS이면 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/m-control-plane-jenkins-menu-ia-code-sync/02+01_shell_jobs/`로 이동한다.
|
||||
- [x] PASS split 작업이면 빈 active parent를 제거하거나 남은 sibling 때문에 유지했다고 확인한다. Parent `agent-task/m-control-plane-jenkins-menu-ia-code-sync/`는 sibling `03+01_executions_ops`가 남아 유지했다.
|
||||
- [ ] WARN/FAIL이면 후속 active plan/review 또는 정당한 `USER_REVIEW.md`를 작성한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- predecessor 확인: `agent-task/.../01_definition_guardrails/complete.log`는 계획에 적힌 active 경로에는 없지만, `agent-task/archive/2026/07/m-control-plane-jenkins-menu-ia-code-sync/01_definition_guardrails/complete.log`로 PASS 완료 후 이미 archive되어 있었다. archive complete.log의 `Roadmap Completion` 근거를 확인해 predecessor 충족으로 판단하고 구현을 진행했다. 최종 검증의 `test -f agent-task/.../01_definition_guardrails/complete.log`는 이 archive 경로 특성상 결과가 없음(파일 없음)으로 나오며, 이는 예상된 결과다.
|
||||
- 계획의 After 코드 스니펫은 `_JenkinsConsoleFrame(descriptor, selected, onSelect, child)`만 예시로 들었으나, 헤더(breadcrumb/action/status)와 좌측 메뉴를 각각 별도 위젯(`_JenkinsHeader`, `_OtoConsoleLeftMenu`)으로 분리 구현했다. 계획이 요구한 구조(통합 header + Jenkins Dashboard-level left menu)는 동일하게 충족한다.
|
||||
- Jenkins 메뉴 매핑 문서(`console-rail`)의 `Build History` 항목은 OTO의 기존 `executions` section을 가리키므로, 회귀 테스트 호환을 위해 `Tooltip.message`는 기존 `'Executions'`를 유지하고 화면에 보이는 라벨 텍스트만 `'Build History'`로 바꿨다. `apps/client/test/widget_test.dart`의 다수 `find.byTooltip('Executions')` 호출을 깨지 않기 위한 의도적 선택이다.
|
||||
- Header action strip에 `New Item` 텍스트 버튼을 두려던 최초 설계를 아이콘 전용 버튼(tooltip만 `New Item`)으로 변경했다. 좌측 메뉴에도 동일 텍스트가 항상 노출되므로, 헤더에도 텍스트를 두면 `apps/client/test/widget_test.dart`의 `find.textContaining('New Item')` 관련 기존 `findsOneWidget` 단정이 섹션 불문 항상 2건 이상으로 깨졌다. 좌측 메뉴를 유일한 텍스트 소스로 유지하고, Jobs surface의 create panel 제목만 `New Item`으로 개칭해 Pipelines 탭에서는 2건(좌측 메뉴 + create panel)이 되도록 정리했다. 관련 client 테스트 3곳의 단정을 `findsOneWidget` -> `findsNWidgets(2)`로 갱신했다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- `_MenuDestination`을 데이터 리스트로 만들어 `_OtoConsoleLeftMenu`가 리스트를 순회하며 렌더하도록 했다. 03에서 `Nodes`/`Artifacts`/`Manage OTO` 라벨이 추가될 때 리스트 항목만 추가하면 되도록 구조를 열어뒀다(계획의 "shell 구조를 열어둔다" 요구 반영).
|
||||
- `New Item`은 `isPrimaryAction: true`로 표시해 `Pipelines`와 동일한 section(`pipelines`)으로 이동하지만 좌측 메뉴의 `selected` 하이라이트 로직에서는 제외했다. Jenkins에서 `New Item`이 별도 액션이지 지속 선택 상태를 갖는 목적지가 아니라는 점을 반영한 결정이다.
|
||||
- Jobs 테이블의 `Last Run` 컬럼은 기존 `executionLink`(Icon + `Execution: <id>`) 위젯의 시각/문구를 그대로 `_ExecutionLink`로 옮겼다. 신규 문구를 만들지 않아 리뷰 대조와 회귀 테스트 위험을 최소화했다.
|
||||
- `_JobTaskMenu`의 `Status` 항목은 Core API/데이터 모델 확장이 이번 범위에서 제외되어 있어 비활성(탭 콜백 없는) 라벨로만 구현했다. `Build History`/`Stages`는 계획의 "selected job history/stage link"에 따라 기존 `onExecutionTap` 콜백을 재사용해 실행 상세로 연결한다.
|
||||
- Jobs surface 최상단의 `Center + ConstrainedBox(maxWidth:820)` 카드 레이아웃을 제거하고 dashboard 스타일 full-width 레이아웃으로 바꿨다. 계획의 After 스니펫이 `_JenkinsJobDashboard`를 직접 반환하도록 예시한 것과 일치한다.
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- Shell header가 breadcrumb/action/task context와 Jenkins Dashboard-level left menu를 1차 구조로 제공하는지 확인한다.
|
||||
- Pipelines가 Jenkins dashboard job table/New Item/Build History와 적용 가능한 job-context `Status`/`Build History`/`Stages` 문법을 갖는지 확인한다.
|
||||
- Existing navigation callbacks, create job validation, execution tap이 유지되는지 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### JENKINS_IA_SHELL-1 중간 검증
|
||||
```bash
|
||||
$ cd packages/flutter/oto_console && flutter test test/oto_console_test.dart
|
||||
00:00 +0: exports console contract models without shell dependency
|
||||
00:00 +1: renders embeddable OTO console shell
|
||||
00:00 +2: renders section surfaces with empty states
|
||||
...
|
||||
00:01 +14: All tests passed!
|
||||
```
|
||||
PASS. `renders embeddable OTO console shell`이 breadcrumb(`OTO`)와 좌측 메뉴 라벨(`New Item`, `Pipelines`, `Build History`)을 확인하고, 기존 `Agent` tooltip navigation도 통과했다.
|
||||
|
||||
### JENKINS_IA_SHELL-2 중간 검증
|
||||
```bash
|
||||
$ cd packages/flutter/oto_console && flutter test test/oto_console_test.dart
|
||||
00:01 +8: OtoJobsSurface renders loading/empty/error/data states
|
||||
...
|
||||
00:01 +13: OtoJobsSurface renders job create action states
|
||||
00:01 +14: All tests passed!
|
||||
```
|
||||
PASS. `OtoJobsSurface renders loading/empty/error/data states`가 table header(`Name`/`Status`/`Last Run`/`Updated`/`Actions`), `Build Queue`/`Build History` context strip, job-context `Status`/`Build History`/`Stages`, 기존 `onExecutionTap` 콜백을 모두 확인했다. `OtoJobsSurface renders job create action states`가 `New Item` create panel과 기존 validation/action-state 흐름을 확인했다.
|
||||
|
||||
### JENKINS_IA_SHELL-3 중간 검증
|
||||
```bash
|
||||
$ cd apps/client && flutter test test/widget_test.dart
|
||||
00:00 +0: OTO client hosts embeddable console widgets
|
||||
...
|
||||
00:03 +27: All tests passed!
|
||||
```
|
||||
PASS. 27개 테스트 전부 통과. `find.byTooltip('Pipelines'|'Executions'|'Runners'|'Artifacts'|'Agent')` 기반 기존 navigation은 tooltip 문구를 유지했으므로 변경 없이 통과했고, `Create New Job` -> `New Item` 라벨 변경에 맞춰 3곳의 단정을 갱신했다(2곳은 헤더 좌측 메뉴 `New Item`과 create panel `New Item`이 동시에 존재해 `findsNWidgets(2)`로 조정).
|
||||
|
||||
### 최종 검증
|
||||
```bash
|
||||
$ test -f agent-task/m-control-plane-jenkins-menu-ia-code-sync/01_definition_guardrails/complete.log
|
||||
(파일 없음, exit=1)
|
||||
```
|
||||
predecessor `01_definition_guardrails`는 PASS로 완료되어 `agent-task/archive/2026/07/m-control-plane-jenkins-menu-ia-code-sync/01_definition_guardrails/complete.log`로 이미 archive되었다(계획 작성 시점의 active 경로는 archive 이동으로 더 이상 존재하지 않음). archive complete.log의 `Roadmap Completion`에서 `definition-guardrail` task가 evidence와 함께 PASS로 기록된 것을 확인했다.
|
||||
|
||||
```bash
|
||||
$ cd packages/flutter/oto_console && flutter analyze && flutter test
|
||||
Analyzing oto_console...
|
||||
No issues found! (ran in 3.8s)
|
||||
00:01 +14: All tests passed!
|
||||
```
|
||||
PASS.
|
||||
|
||||
```bash
|
||||
$ cd apps/client && flutter analyze && flutter test
|
||||
Analyzing client...
|
||||
No issues found! (ran in 2.6s)
|
||||
00:03 +27: All tests passed!
|
||||
```
|
||||
PASS.
|
||||
|
||||
```bash
|
||||
$ git diff --check
|
||||
(출력 없음, exit=0)
|
||||
```
|
||||
PASS.
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: PASS
|
||||
- 차원별 평가:
|
||||
- correctness: Pass
|
||||
- completeness: Pass
|
||||
- test coverage: Pass
|
||||
- API contract: Pass
|
||||
- code quality: Pass
|
||||
- implementation deviation: Pass
|
||||
- verification trust: Pass
|
||||
- agent-ui completion: Pass
|
||||
- 발견된 문제: 없음
|
||||
- 다음 단계: PASS 종결 절차를 진행한다. active `CODE_REVIEW-cloud-G07.md`와 `PLAN-cloud-G07.md`를 `.log`로 아카이브하고, `complete.log` 작성 후 task directory를 `agent-task/archive/2026/07/m-control-plane-jenkins-menu-ia-code-sync/02+01_shell_jobs/`로 이동한다.
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
# Complete - m-control-plane-jenkins-menu-ia-code-sync/02+01_shell_jobs
|
||||
|
||||
## 완료 일시
|
||||
|
||||
2026-07-03
|
||||
|
||||
## 요약
|
||||
|
||||
Jenkins IA shell/jobs 동기화 1회차 리뷰에서 최종 판정 PASS로 종결했다.
|
||||
|
||||
## 루프 이력
|
||||
|
||||
| Plan | Review | Verdict | 메모 |
|
||||
|------|--------|---------|------|
|
||||
| `plan_cloud_G07_0.log` | `code_review_cloud_G07_0.log` | PASS | Shell header/breadcrumb/action, Dashboard-level `New Item`/`Build History` menu, pipelines job table, job context `Status`/`Build History`/`Stages` primitive와 검증 증거가 충족되었다. |
|
||||
|
||||
## 구현/정리 내용
|
||||
|
||||
- `OtoConsoleShell`을 통합 header/breadcrumb/action slot과 Jenkins Dashboard-level left menu 구조로 재구성했다.
|
||||
- `OtoJobsSurface`를 `New Item`, `Build Queue`/`Build History`, dense job table, job context `Status`/`Build History`/`Stages` 중심으로 재구성했다.
|
||||
- console package와 client widget tests를 Jenkins IA shell/jobs primitive 기준으로 갱신했다.
|
||||
- PASS 조건에 따라 agent-ui shell/jobs 정의서 상태와 evidence를 갱신했다.
|
||||
|
||||
## 최종 검증
|
||||
|
||||
- `test -f agent-task/archive/2026/07/m-control-plane-jenkins-menu-ia-code-sync/01_definition_guardrails/complete.log` - PASS; predecessor `definition-guardrail` 완료 근거를 확인했다.
|
||||
- `cd packages/flutter/oto_console && flutter analyze` - PASS; `No issues found!`.
|
||||
- `cd packages/flutter/oto_console && flutter test` - PASS; 14 tests passed.
|
||||
- `cd apps/client && flutter analyze` - PASS; `No issues found!`.
|
||||
- `cd apps/client && flutter test` - PASS; 27 tests passed.
|
||||
- `git diff --check` - PASS; 출력 없음.
|
||||
- `validate-agent-ui scope=jenkins-ia-shell-jobs` - PASS; scoped manual validation으로 status/evidence rows, stale shell symbol references, code evidence paths, active `agent-ui/USER_REVIEW.md` 부재를 확인했다.
|
||||
|
||||
## Roadmap Completion
|
||||
|
||||
- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/control-plane-jenkins-menu-ia-code-sync.md`
|
||||
- Completed task ids:
|
||||
- `shell-menu-sync`: PASS; evidence=`agent-task/archive/2026/07/m-control-plane-jenkins-menu-ia-code-sync/02+01_shell_jobs/plan_cloud_G07_0.log`, `agent-task/archive/2026/07/m-control-plane-jenkins-menu-ia-code-sync/02+01_shell_jobs/code_review_cloud_G07_0.log`; verification=`cd packages/flutter/oto_console && flutter analyze && flutter test`, `cd apps/client && flutter analyze && flutter test`
|
||||
- Not completed task ids: 없음
|
||||
|
||||
## Agent UI Completion
|
||||
|
||||
- Agent UI docs:
|
||||
- `agent-ui/definition/index.md`: shell/jobs Jenkins IA evidence updated; code evidence=`packages/flutter/oto_console/lib/src/oto_console_shell.dart`, `packages/flutter/oto_console/lib/src/oto_jobs_surface.dart`; implementation verification=`flutter analyze/test for oto_console and apps/client`; validation=`validate-agent-ui scope=jenkins-ia-shell-jobs PASS`.
|
||||
- `agent-ui/definition/views/index.md`: `pipelines` shell/jobs criteria `구현됨`; code evidence=`packages/flutter/oto_console/lib/src/oto_jobs_surface.dart`; implementation verification=`cd packages/flutter/oto_console && flutter test`, `cd apps/client && flutter test`; validation=`validate-agent-ui scope=jenkins-ia-shell-jobs PASS`.
|
||||
- `agent-ui/definition/components/index.md`: shell/jobs component criteria reflected for `console-shell` and `record-list`; 03 ops criteria remains in the active executions/ops task; code evidence=`packages/flutter/oto_console/lib/src/oto_console_shell.dart`, `packages/flutter/oto_console/lib/src/oto_jobs_surface.dart`; validation=`validate-agent-ui scope=jenkins-ia-shell-jobs PASS`.
|
||||
- Status updates: 완료
|
||||
- Remaining agent-ui issues: 없음
|
||||
|
||||
## 잔여 Nit
|
||||
|
||||
- 없음
|
||||
|
||||
## 후속 작업
|
||||
|
||||
- 없음
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
<!-- task=m-control-plane-jenkins-menu-ia-code-sync/02+01_shell_jobs plan=0 tag=JENKINS_IA_SHELL -->
|
||||
|
||||
# Code Review Reference - JENKINS_IA_SHELL
|
||||
|
||||
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
|
||||
> The task is NOT complete until every implementation-owned section below is filled in.
|
||||
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
|
||||
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
|
||||
> If implementation is blocked by a selected Milestone `구현 잠금 > 결정 필요` item, fill `사용자 리뷰 요청` with linked evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation.
|
||||
> Finalization is review-agent-only.
|
||||
|
||||
## 개요
|
||||
|
||||
date=2026-07-03
|
||||
task=m-control-plane-jenkins-menu-ia-code-sync/02+01_shell_jobs, plan=0, tag=JENKINS_IA_SHELL
|
||||
|
||||
## Roadmap Targets
|
||||
|
||||
- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/control-plane-jenkins-menu-ia-code-sync.md`
|
||||
- Task ids:
|
||||
- `shell-menu-sync`: shell/global menu와 pipelines/jobs 화면이 `New Item`, `Pipelines`, `Build History`, job `Status`, job `Build History`, `Stages` 중심으로 동기화된다.
|
||||
- Completion mode: check-on-pass
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다.
|
||||
|
||||
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [JENKINS_IA_SHELL-1] Shell Header And Breadcrumb IA | [ ] |
|
||||
| [JENKINS_IA_SHELL-2] Jenkins Dashboard Jobs Surface | [ ] |
|
||||
| [JENKINS_IA_SHELL-3] Host Regression | [ ] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] [JENKINS_IA_SHELL-1] shell을 통합 header/breadcrumb/action/task navigation과 Jenkins Dashboard-level left menu 구조로 개편한다.
|
||||
- [ ] [JENKINS_IA_SHELL-2] pipelines/jobs 화면을 Jenkins dashboard job table/New Item/Build History 및 적용 가능한 job-context left task menu 구조로 개편한다.
|
||||
- [ ] [JENKINS_IA_SHELL-3] shell/jobs widget tests와 app host regression tests를 Jenkins IA primitive 기준으로 갱신한다.
|
||||
- [ ] `cd packages/flutter/oto_console && flutter analyze && flutter test`, `cd apps/client && flutter analyze && flutter test`, `git diff --check`를 실행한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
## Agent UI Completion
|
||||
|
||||
- Mode: review-pass-status-update
|
||||
- Agent UI docs:
|
||||
- `agent-ui/definition/index.md`
|
||||
- `agent-ui/definition/views/index.md`
|
||||
- `agent-ui/definition/components/index.md`
|
||||
- Actual code evidence:
|
||||
- [ ] `packages/flutter/oto_console/lib/src/oto_console_shell.dart`: <implemented evidence, including New Item/Pipelines/Build History menu entries>
|
||||
- [ ] `packages/flutter/oto_console/lib/src/oto_jobs_surface.dart`: <implemented evidence>
|
||||
- Implementation verification evidence:
|
||||
- [ ] `cd packages/flutter/oto_console && flutter analyze && flutter test`: <actual result or output path>
|
||||
- [ ] `cd apps/client && flutter analyze && flutter test`: <actual result or output path>
|
||||
- Status updates requested on PASS:
|
||||
- [ ] `agent-ui/definition/index.md`: Jenkins IA shell/jobs evidence updated
|
||||
- [ ] `agent-ui/definition/views/index.md`: `pipelines` shell/jobs criteria `계획` -> `구현됨`
|
||||
- [ ] `agent-ui/definition/components/index.md`: shell/jobs component criteria `계획` -> `구현됨`
|
||||
- Review finalization:
|
||||
- [ ] Review agent updated the listed agent-ui docs and code evidence.
|
||||
- [ ] Review agent ran `validate-agent-ui scope=jenkins-ia-shell-jobs` after the status/evidence update.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
|
||||
|
||||
- [ ] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [ ] active review/plan 파일을 `.log`로 아카이브한다.
|
||||
- [ ] PASS이면 `complete.log` 작성 후 `agent-task/archive/YYYY/MM/m-control-plane-jenkins-menu-ia-code-sync/02+01_shell_jobs/`로 이동한다.
|
||||
- [ ] PASS split 작업이면 빈 active parent를 제거하거나 남은 sibling 때문에 유지했다고 확인한다.
|
||||
- [ ] WARN/FAIL이면 후속 active plan/review 또는 정당한 `USER_REVIEW.md`를 작성한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
_기본값은 `없음`이다. 구현 중 새 결정이 필요해 보여도 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 이 섹션은 선택된 Milestone `구현 잠금 > 결정 필요` 항목이 실구현을 차단할 때만 채운다. 외부 환경/secret/서비스 준비, 검증 증거 공백, 반복 실패, 일반 범위 조정은 사용자 리뷰 요청이 아니며 `검증 결과`, `계획 대비 변경 사항`, 또는 code-review의 일반 follow-up plan으로 처리한다._
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 연결 대상: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- Shell header가 breadcrumb/action/task context와 Jenkins Dashboard-level left menu를 1차 구조로 제공하는지 확인한다.
|
||||
- Pipelines가 Jenkins dashboard job table/New Item/Build History와 적용 가능한 job-context `Status`/`Build History`/`Stages` 문법을 갖는지 확인한다.
|
||||
- Existing navigation callbacks, create job validation, execution tap이 유지되는지 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### JENKINS_IA_SHELL-1 중간 검증
|
||||
```bash
|
||||
$ cd packages/flutter/oto_console && flutter test test/oto_console_test.dart
|
||||
(output)
|
||||
```
|
||||
|
||||
### JENKINS_IA_SHELL-2 중간 검증
|
||||
```bash
|
||||
$ cd packages/flutter/oto_console && flutter test test/oto_console_test.dart
|
||||
(output)
|
||||
```
|
||||
|
||||
### JENKINS_IA_SHELL-3 중간 검증
|
||||
```bash
|
||||
$ cd apps/client && flutter test test/widget_test.dart
|
||||
(output)
|
||||
```
|
||||
|
||||
### 최종 검증
|
||||
```bash
|
||||
$ test -f agent-task/m-control-plane-jenkins-menu-ia-code-sync/01_definition_guardrails/complete.log
|
||||
(output)
|
||||
$ cd packages/flutter/oto_console && flutter analyze && flutter test
|
||||
(output)
|
||||
$ cd apps/client && flutter analyze && flutter test
|
||||
(output)
|
||||
$ git diff --check
|
||||
(output)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
|
||||
|
|
@ -5,7 +5,7 @@ status: 계획
|
|||
source_evidence:
|
||||
- type: code
|
||||
path: packages/flutter/oto_console/lib/src/oto_console_shell.dart
|
||||
notes: "_OtoConsoleRail, _RailButton 구현"
|
||||
notes: "_OtoConsoleLeftMenu, _LeftMenuButton 구현"
|
||||
- type: docs
|
||||
path: apps/client/README.md
|
||||
notes: "oto_console embeddable UI surface"
|
||||
|
|
@ -24,7 +24,7 @@ Status: `계획` (Jenkins IA gate 미충족: global/context menu 모델 분리
|
|||
|
||||
| Type | Path | Notes |
|
||||
|------|------|-------|
|
||||
| code | `packages/flutter/oto_console/lib/src/oto_console_shell.dart` | `_OtoConsoleRail`, `_RailButton` 구현 |
|
||||
| code | `packages/flutter/oto_console/lib/src/oto_console_shell.dart` | `_OtoConsoleLeftMenu`, `_LeftMenuButton` 구현 |
|
||||
| docs | `apps/client/README.md` | embeddable console surface |
|
||||
| user | 없음 | agent-ui 생성 요청 |
|
||||
|
||||
|
|
@ -93,12 +93,12 @@ Status: `계획` (Jenkins IA gate 미충족: global/context menu 모델 분리
|
|||
- rail은 현재 60px icon-only 구조에서 Jenkins Dashboard-level left menu를 알아볼 수 있는 메뉴로 재정렬한다.
|
||||
- Jenkins job/run left menu parity는 rail이 아니라 view-local contextual task menu로 정의하고 구현한다.
|
||||
- selected state와 tooltip은 유지하고, section ordering은 overview, new item, pipelines, build history, runners, artifacts, manage, agent 순서를 기준으로 검토한다.
|
||||
- 현재 rail 구현은 global destination 축약(current)만 존재하고, 위 1차 적용 Global/Context menu(target)는 Jenkins IA gate 미충족 상태다.
|
||||
- 후속 구현(02/03)의 widget 테스트는 global menu label `New Item`, `Build History`, `Nodes`, `Artifacts`, `Manage OTO`와 job/run context menu(`Status`, `Stages`, `Pipeline Overview`, `Console Output`) 분리를 고정 label로 확인한다.
|
||||
- 02 shell/jobs 구현에서 `New Item`, `Pipelines`, `Build History`, `Artifacts` global label과 job context `Status`, `Build History`, `Stages` primitive를 코드와 widget 테스트로 확인했다.
|
||||
- 03 executions/ops 구현 전까지 `Nodes`, `Manage OTO`, run context menu(`Pipeline Overview`, `Console Output`) primitive는 Jenkins IA gate 미충족 상태로 둔다.
|
||||
|
||||
## Variants
|
||||
|
||||
- default: 60px rail width, 56x48 button hit area
|
||||
- default: 208px left menu width, labeled menu button hit area
|
||||
- selected: primary color icon and tinted background
|
||||
|
||||
## States
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ status: 구현됨
|
|||
source_evidence:
|
||||
- type: code
|
||||
path: packages/flutter/oto_console/lib/src/oto_console_shell.dart
|
||||
notes: "OtoConsoleShell Scaffold, rail, selected section content 구현"
|
||||
notes: "OtoConsoleShell Scaffold, header/breadcrumb/action slot, left menu, selected section content 구현"
|
||||
- type: code
|
||||
path: apps/client/lib/src/app/oto_client_app.dart
|
||||
notes: "OtoClientApp이 OtoConsoleShell을 host MaterialApp home으로 mount"
|
||||
|
|
@ -24,7 +24,7 @@ Status: `구현됨`
|
|||
|
||||
| Type | Path | Notes |
|
||||
|------|------|-------|
|
||||
| code | `packages/flutter/oto_console/lib/src/oto_console_shell.dart` | `OtoConsoleShell` Scaffold, rail, selected section content 구현 |
|
||||
| code | `packages/flutter/oto_console/lib/src/oto_console_shell.dart` | `OtoConsoleShell` Scaffold, header/breadcrumb/action slot, left menu, selected section content 구현 |
|
||||
| code | `apps/client/lib/src/app/oto_client_app.dart` | host `MaterialApp`이 `OtoConsoleShell`을 mount |
|
||||
| docs | `apps/client/README.md` | embeddable shell 기준 |
|
||||
|
||||
|
|
@ -46,22 +46,23 @@ Control Plane 콘솔의 전체 layout shell을 제공하고, rail navigation과
|
|||
|
||||
- `Scaffold`
|
||||
- `SafeArea`
|
||||
- header/breadcrumb/action slot
|
||||
- `console-rail`
|
||||
- selected section content area
|
||||
- optional fallback `section-surface`
|
||||
|
||||
## Jenkins Reference Alignment
|
||||
|
||||
- Jenkins `2.516+`의 통합 header/breadcrumb/action 모델은 future shell header 후보로 둔다.
|
||||
- 현재 구현은 rail 중심 shell이므로 Jenkins classic side task list와 Blue Ocean common navigation을 절충한 OTO 운영 콘솔 shell로 본다.
|
||||
- view header와 action은 각 surface 내부에 있고, shell은 navigation과 content mounting 책임만 가진다.
|
||||
- Jenkins `2.516+`의 통합 header/breadcrumb/action 모델을 shell 상단 영역으로 반영했다.
|
||||
- 현재 구현은 header + Dashboard-level left menu + content 구조로 OTO 운영 콘솔 shell을 제공한다.
|
||||
- view-local action은 각 surface가 유지하되, shell은 global `New Item` action slot과 navigation/content mounting 책임을 가진다.
|
||||
- global search, command palette, account/theme picker는 이번 정의 범위에서 제외한다.
|
||||
|
||||
## Target Layout Delta
|
||||
|
||||
- shell은 rail + content만이 아니라 page header/action slot을 담을 수 있는 구조를 목표로 둔다.
|
||||
- breadcrumb는 이번 범위에서 제외하지만, Jenkins refreshed header처럼 current view context와 primary action을 같은 상단 영역에 둘 수 있어야 한다.
|
||||
- 현재 `OtoConsoleShell` 구현은 목표 shell layout 반영이 완료되어 `구현됨` 상태다.
|
||||
- shell은 page header/action slot과 current view breadcrumb를 포함한다.
|
||||
- Jenkins refreshed header처럼 current view context와 primary action을 같은 상단 영역에 둔다.
|
||||
- 현재 `OtoConsoleShell` 구현은 shell/jobs 범위의 목표 shell layout 반영이 완료되어 `구현됨` 상태다.
|
||||
|
||||
## Variants
|
||||
|
||||
|
|
|
|||
|
|
@ -27,15 +27,15 @@ components:
|
|||
|
||||
| Component ID | Status | Evidence | Description |
|
||||
|--------------|--------|----------|-------------|
|
||||
| `console-shell` | 계획 | `oto_console_shell.dart` + Jenkins IA gate | overall console scaffold with rail and selected section content. 필수 primitive: 통합 header, breadcrumb, action 영역. Jenkins IA gate 미충족, 후속 구현 대기. |
|
||||
| `console-rail` | 계획 | `oto_console_shell.dart` + Jenkins IA gate | section navigation rail. 필수 primitive: `New Item`, `Build History`, `Nodes`, `Artifacts`, `Manage OTO` global menu와 job/run context menu 분리. Jenkins IA gate 미충족, 후속 구현 대기. |
|
||||
| `console-shell` | 구현됨 | `oto_console_shell.dart` + Jenkins IA shell/jobs gate | overall console scaffold with integrated header, breadcrumb, action slot, left menu, and selected section content. |
|
||||
| `console-rail` | 계획 | `oto_console_shell.dart` + Jenkins IA gate | section navigation rail. `New Item`/`Build History`와 job context menu는 구현됨. `Nodes`, `Manage OTO`, run context menu는 03 후속 구현 대기. |
|
||||
| `section-surface` | 계획 | `oto_console_shell.dart` + Jenkins IA gate | generic titled empty/detail section shell. 필수 primitive: `Manage OTO` 관리 category grouping. Jenkins IA gate 미충족, 후속 구현 대기. |
|
||||
| `state-panel` | 구현됨 | surface files + Jenkins reference baseline | loading, empty, error state blocks. Jenkins monitor/banner-style state 반영 |
|
||||
| `status-badge` | 구현됨 | overview/runners/jobs/executions surface files + Jenkins reference baseline | compact status label badge. Jenkins result/severity vocabulary 정렬 반영 |
|
||||
| `endpoint-tile` | 구현됨 | `oto_console_overview.dart` | endpoint or config value tile |
|
||||
| `connection-status-grid` | 구현됨 | `oto_console_overview.dart` | health/readiness status grid |
|
||||
| `auth-token-reference` | 구현됨 | `oto_console_overview.dart` | display-only auth token reference |
|
||||
| `record-list` | 계획 | runners/jobs/executions/artifacts surface files + Jenkins IA gate | repeated entity list rows. 필수 primitive: Jenkins Dashboard job table / Build History / `Nodes` density의 dense row. Jenkins IA gate 미충족, 후속 구현 대기. |
|
||||
| `record-list` | 구현됨 | jobs/runners/executions/artifacts surface files + Jenkins IA shell/jobs gate | repeated entity list rows. Pipelines job table은 Jenkins Dashboard density로 구현됨. Build History/Nodes density 보강은 03에서 이어간다. |
|
||||
| `action-feedback` | 구현됨 | runners/jobs/executions surface files + Jenkins reference baseline | success/deferred/failed action message. Jenkins action outcome 정렬 반영 |
|
||||
| `runner-self-update-form` | 구현됨 | `oto_runners_surface.dart` + Jenkins reference baseline | runner self-update draft form. Jenkins update action density 정렬 반영 |
|
||||
| `job-create-form` | 구현됨 | `oto_jobs_surface.dart` + Jenkins reference baseline | job create draft form. Jenkins New Item flow 정렬 반영 |
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@ OTO Control Plane Flutter 콘솔의 운영 화면, 반복 component, 상태 표
|
|||
|
||||
## Target Layout Status
|
||||
|
||||
- 현재 Flutter 구현은 운영 밀도 일부만 반영했으며 Jenkins IA gate는 미충족이다. Jenkins 정렬 대상 view/component는 `계획` 상태로 두고 gate 미충족 설명을 함께 둔다.
|
||||
- Shell/jobs 범위는 통합 header/breadcrumb/action, Dashboard-level `New Item`/`Build History` menu, pipelines dashboard job table, job context `Status`/`Build History`/`Stages` primitive가 코드와 widget 테스트로 확인되어 구현 근거를 갖는다.
|
||||
- Executions/runners/settings 범위의 run stage graph, `Console Output`, Nodes/Agents, `Manage OTO` 구조는 후속 03 구현 전까지 `계획` 상태로 둔다.
|
||||
- 목표는 pixel parity가 아니라 header/action/status/list/detail/log/artifact의 정보 배치와 운영 밀도를 Jenkins 최신 UI 흐름에 맞추는 것이다.
|
||||
- 보안/감사 source of truth처럼 문서 계획만 있는 항목은 계속 `계획` 상태로 두며, Jenkins IA gate 미충족 항목과 섞지 않는다.
|
||||
|
||||
|
|
@ -96,3 +97,4 @@ Jenkins 정렬 대상 view/component는 아래 structural primitive가 코드와
|
|||
- 2026-07-02: Jenkins 최신 weekly/LTS와 `2.516+` refreshed UI를 러프 레이아웃 참조 기준선으로 추가했다.
|
||||
- 2026-07-03: Jenkins 정렬 레이아웃 패스 구현과 검증 근거를 바탕으로 대상 view/component status를 `구현됨`으로 갱신했다.
|
||||
- 2026-07-03: Jenkins reference를 러프 정렬에서 `Jenkins IA gate` structural acceptance로 강화하고, gate 미충족 대상 view/component status를 `계획`으로 정정했다. 후속 구현(02/03)이 primitive를 테스트로 확인하면 `구현됨`으로 승격한다.
|
||||
- 2026-07-03: 02 shell/jobs 구현에서 통합 header/breadcrumb/action, `New Item`/`Build History` menu, pipelines dashboard job table과 job context menu primitive를 코드와 widget 테스트로 확인했다. Executions/runners/settings primitive는 03 범위로 남겼다.
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ views:
|
|||
|---------|--------|----------|-------------|
|
||||
| `overview` | 계획 | `OtoConsoleOverview` + Jenkins IA gate | Control Plane endpoint, connection, authentication reference overview. 필수: 통합 header/breadcrumb/action(`Dashboard` root)이 shell/header에 존재한다. Jenkins IA gate 미충족, 후속 구현 대기. |
|
||||
| `runners` | 계획 | `OtoRunnersSurface` + Jenkins IA gate | runner registry state and self-update action surface. 필수: Jenkins Nodes/Agents 목록 구조로 node 상태/현재 작업/연결 메시지를 스캔한다. Jenkins IA gate 미충족, 후속 구현 대기. |
|
||||
| `pipelines` | 계획 | `OtoJobsSurface` + Jenkins IA gate | pipeline/job queue list and create job action surface. 필수: Dashboard job table density와 `New Item` 생성 entry가 존재한다. Jenkins IA gate 미충족, 후속 구현 대기. |
|
||||
| `pipelines` | 구현됨 | `OtoJobsSurface` + Jenkins IA shell/jobs gate | pipeline/job queue list and create job action surface. Dashboard job table density, `New Item` 생성 entry, job context `Status`/`Build History`/`Stages` primitive가 widget 테스트로 확인됨. |
|
||||
| `executions` | 계획 | `OtoExecutionsSurface` + Jenkins IA gate | execution list, detail expansion, write actions, log/artifact preview. 필수: Pipeline Graph View 스타일 `Pipeline Overview`/`Stage Graph`, `Console Output`, `Build History`가 run detail에 존재한다. Jenkins IA gate 미충족, 후속 구현 대기. |
|
||||
| `artifacts` | 구현됨 | `OtoArtifactsSurface` + Jenkins reference baseline | artifact event list. Jenkins artifact browser-style list 반영 |
|
||||
| `agent` | 구현됨 | `OtoAgentPanel` | embeddable agent-shell panel for OTO capabilities |
|
||||
|
|
|
|||
|
|
@ -1428,7 +1428,7 @@ void _registerJobCreateTests() {
|
|||
// Verify Pipelines tab renders the jobs surface
|
||||
await tester.tap(find.byTooltip('Pipelines'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('Create New Job'), findsOneWidget);
|
||||
expect(find.textContaining('New Item'), findsNWidgets(2));
|
||||
expect(find.byType(TextField), findsNWidgets(2));
|
||||
expect(find.text('Create'), findsOneWidget);
|
||||
|
||||
|
|
@ -1600,7 +1600,7 @@ void _registerJobCreateTests() {
|
|||
await tester.tap(find.byTooltip('Pipelines'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('Create New Job'), findsOneWidget);
|
||||
expect(find.textContaining('New Item'), findsNWidgets(2));
|
||||
expect(find.byType(TextField), findsNWidgets(2));
|
||||
});
|
||||
|
||||
|
|
@ -1658,7 +1658,7 @@ void _registerJobCreateTests() {
|
|||
await tester.tap(find.byTooltip('Pipelines'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('Create New Job'), findsOneWidget);
|
||||
expect(find.textContaining('New Item'), findsNWidgets(2));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,22 +55,12 @@ class _OtoConsoleShellState extends State<OtoConsoleShell> {
|
|||
return Scaffold(
|
||||
backgroundColor: theme.backgroundColor,
|
||||
body: SafeArea(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_OtoConsoleRail(
|
||||
selected: _section,
|
||||
onSelect: _select,
|
||||
theme: theme,
|
||||
),
|
||||
Expanded(
|
||||
child: _ConsolePageFrame(
|
||||
descriptor: descriptor,
|
||||
theme: theme,
|
||||
child: _buildContent(theme),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: _JenkinsConsoleFrame(
|
||||
descriptor: descriptor,
|
||||
selected: _section,
|
||||
onSelect: _select,
|
||||
theme: theme,
|
||||
child: _buildContent(theme),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -163,61 +153,83 @@ class _OtoConsoleShellState extends State<OtoConsoleShell> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Descriptor for the currently selected section: label, breadcrumb group,
|
||||
/// and the icon shared by the breadcrumb and the left menu.
|
||||
class _SectionDescriptor {
|
||||
final OtoConsoleSection section;
|
||||
final String label;
|
||||
final String group;
|
||||
final String groupLabel;
|
||||
final IconData icon;
|
||||
|
||||
const _SectionDescriptor({
|
||||
required this.section,
|
||||
required this.label,
|
||||
required this.group,
|
||||
required this.groupLabel,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
static _SectionDescriptor forSection(OtoConsoleSection section) {
|
||||
return switch (section) {
|
||||
OtoConsoleSection.overview => _SectionDescriptor(
|
||||
section: section,
|
||||
OtoConsoleSection.overview => const _SectionDescriptor(
|
||||
section: OtoConsoleSection.overview,
|
||||
label: 'Overview',
|
||||
group: 'overview',
|
||||
groupLabel: 'Dashboard',
|
||||
icon: Icons.dashboard_outlined,
|
||||
),
|
||||
OtoConsoleSection.pipelines || OtoConsoleSection.executions =>
|
||||
_SectionDescriptor(
|
||||
section: section,
|
||||
label: section == OtoConsoleSection.pipelines
|
||||
? 'Pipelines'
|
||||
: 'Executions',
|
||||
group: 'workflow',
|
||||
OtoConsoleSection.pipelines => const _SectionDescriptor(
|
||||
section: OtoConsoleSection.pipelines,
|
||||
label: 'Pipelines',
|
||||
groupLabel: 'Workflow',
|
||||
icon: Icons.account_tree_outlined,
|
||||
),
|
||||
OtoConsoleSection.runners || OtoConsoleSection.artifacts =>
|
||||
_SectionDescriptor(
|
||||
section: section,
|
||||
label: section == OtoConsoleSection.runners
|
||||
? 'Runners'
|
||||
: 'Artifacts',
|
||||
group: 'operations',
|
||||
OtoConsoleSection.executions => const _SectionDescriptor(
|
||||
section: OtoConsoleSection.executions,
|
||||
label: 'Executions',
|
||||
groupLabel: 'Workflow',
|
||||
icon: Icons.play_circle_outline,
|
||||
),
|
||||
OtoConsoleSection.agent => _SectionDescriptor(
|
||||
section: section,
|
||||
OtoConsoleSection.runners => const _SectionDescriptor(
|
||||
section: OtoConsoleSection.runners,
|
||||
label: 'Runners',
|
||||
groupLabel: 'Operations',
|
||||
icon: Icons.precision_manufacturing_outlined,
|
||||
),
|
||||
OtoConsoleSection.artifacts => const _SectionDescriptor(
|
||||
section: OtoConsoleSection.artifacts,
|
||||
label: 'Artifacts',
|
||||
groupLabel: 'Operations',
|
||||
icon: Icons.inventory_2_outlined,
|
||||
),
|
||||
OtoConsoleSection.agent => const _SectionDescriptor(
|
||||
section: OtoConsoleSection.agent,
|
||||
label: 'Agent',
|
||||
group: 'admin',
|
||||
groupLabel: 'Admin',
|
||||
icon: Icons.smart_toy_outlined,
|
||||
),
|
||||
OtoConsoleSection.settings => _SectionDescriptor(
|
||||
section: section,
|
||||
OtoConsoleSection.settings => const _SectionDescriptor(
|
||||
section: OtoConsoleSection.settings,
|
||||
label: 'Settings',
|
||||
group: 'admin',
|
||||
groupLabel: 'Admin',
|
||||
icon: Icons.tune,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class _ConsolePageFrame extends StatelessWidget {
|
||||
/// Jenkins Dashboard-aligned shell frame: a top header (breadcrumb, action
|
||||
/// strip, status/context) above a left menu + content Row. Replaces the
|
||||
/// former 60px icon-only rail + page-local header layout.
|
||||
class _JenkinsConsoleFrame extends StatelessWidget {
|
||||
final _SectionDescriptor descriptor;
|
||||
final OtoConsoleSection selected;
|
||||
final ValueChanged<OtoConsoleSection> onSelect;
|
||||
final OtoConsoleThemeAdapter theme;
|
||||
final Widget child;
|
||||
|
||||
const _ConsolePageFrame({
|
||||
const _JenkinsConsoleFrame({
|
||||
required this.descriptor,
|
||||
required this.selected,
|
||||
required this.onSelect,
|
||||
required this.theme,
|
||||
required this.child,
|
||||
});
|
||||
|
|
@ -227,67 +239,163 @@ class _ConsolePageFrame extends StatelessWidget {
|
|||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_PageHeader(descriptor: descriptor, theme: theme),
|
||||
Expanded(child: child),
|
||||
_JenkinsHeader(
|
||||
descriptor: descriptor,
|
||||
theme: theme,
|
||||
onNewItem: () => onSelect(OtoConsoleSection.pipelines),
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_OtoConsoleLeftMenu(
|
||||
selected: selected,
|
||||
onSelect: onSelect,
|
||||
theme: theme,
|
||||
),
|
||||
Expanded(child: child),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PageHeader extends StatelessWidget {
|
||||
class _JenkinsHeader extends StatelessWidget {
|
||||
final _SectionDescriptor descriptor;
|
||||
final OtoConsoleThemeAdapter theme;
|
||||
final VoidCallback onNewItem;
|
||||
|
||||
const _PageHeader({
|
||||
const _JenkinsHeader({
|
||||
required this.descriptor,
|
||||
required this.theme,
|
||||
required this.onNewItem,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
color: theme.railBackgroundColor,
|
||||
child: Row(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.railBackgroundColor,
|
||||
border: Border(bottom: BorderSide(color: theme.borderColor)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(descriptor.section == OtoConsoleSection.overview
|
||||
? Icons.dashboard_outlined
|
||||
: descriptor.section == OtoConsoleSection.runners
|
||||
? Icons.precision_manufacturing_outlined
|
||||
: descriptor.section == OtoConsoleSection.pipelines
|
||||
? Icons.account_tree_outlined
|
||||
: descriptor.section == OtoConsoleSection.executions
|
||||
? Icons.play_circle_outline
|
||||
: descriptor.section == OtoConsoleSection.artifacts
|
||||
? Icons.inventory_2_outlined
|
||||
: descriptor.section == OtoConsoleSection.agent
|
||||
? Icons.smart_toy_outlined
|
||||
: Icons.tune,
|
||||
color: theme.primaryColor,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
descriptor.label,
|
||||
style: TextStyle(
|
||||
color: theme.textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _HeaderBreadcrumb(descriptor: descriptor, theme: theme),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
_HeaderActionStrip(theme: theme, onNewItem: onNewItem),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_HeaderStatusContext(descriptor: descriptor, theme: theme),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HeaderBreadcrumb extends StatelessWidget {
|
||||
final _SectionDescriptor descriptor;
|
||||
final OtoConsoleThemeAdapter theme;
|
||||
|
||||
const _HeaderBreadcrumb({required this.descriptor, required this.theme});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final crumbStyle = TextStyle(color: theme.textSecondaryColor, fontSize: 13);
|
||||
final activeStyle = TextStyle(
|
||||
color: theme.textColor,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w800,
|
||||
);
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
'OTO',
|
||||
style: TextStyle(
|
||||
color: theme.primaryColor,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
Text(' / ', style: crumbStyle),
|
||||
Text(descriptor.groupLabel, style: crumbStyle),
|
||||
Text(' / ', style: crumbStyle),
|
||||
Flexible(
|
||||
child: Text(
|
||||
descriptor.label,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: activeStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Header-level action slot. The visible `New Item` label lives in the left
|
||||
/// menu (single source of truth for that primitive), so this strip stays
|
||||
/// icon-only to avoid a duplicate text match; it remains an extensible slot
|
||||
/// for 03 to add more contextual page actions.
|
||||
class _HeaderActionStrip extends StatelessWidget {
|
||||
final OtoConsoleThemeAdapter theme;
|
||||
final VoidCallback onNewItem;
|
||||
|
||||
const _HeaderActionStrip({required this.theme, required this.onNewItem});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: 'New Item',
|
||||
waitDuration: const Duration(milliseconds: 500),
|
||||
child: IconButton(
|
||||
onPressed: onNewItem,
|
||||
icon: const Icon(Icons.add),
|
||||
style: IconButton.styleFrom(
|
||||
foregroundColor: theme.primaryColor,
|
||||
backgroundColor: theme.primaryColor.withValues(alpha: 0.1),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HeaderStatusContext extends StatelessWidget {
|
||||
final _SectionDescriptor descriptor;
|
||||
final OtoConsoleThemeAdapter theme;
|
||||
|
||||
const _HeaderStatusContext({required this.descriptor, required this.theme});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(descriptor.icon, color: theme.primaryColor, size: 16),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'${descriptor.label} section',
|
||||
style: TextStyle(color: theme.textSecondaryColor, fontSize: 12),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RailDivider extends StatelessWidget {
|
||||
const _RailDivider();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
child: Container(
|
||||
height: 1,
|
||||
color: Colors.white38,
|
||||
|
|
@ -296,21 +404,100 @@ class _RailDivider extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _OtoConsoleRail extends StatelessWidget {
|
||||
/// A single left-menu destination. `section` is the navigation target;
|
||||
/// `isPrimaryAction` marks entries (like New Item) that trigger navigation
|
||||
/// without participating in the persistent "selected" highlight.
|
||||
class _MenuDestination {
|
||||
final String label;
|
||||
final String tooltip;
|
||||
final IconData icon;
|
||||
final OtoConsoleSection section;
|
||||
final bool isPrimaryAction;
|
||||
|
||||
const _MenuDestination({
|
||||
required this.label,
|
||||
required this.tooltip,
|
||||
required this.icon,
|
||||
required this.section,
|
||||
this.isPrimaryAction = false,
|
||||
});
|
||||
}
|
||||
|
||||
/// Jenkins Dashboard-level left menu. Replaces the former icon-only rail
|
||||
/// with a labeled menu; `New Item`, `Pipelines`, and `Build History` are
|
||||
/// exposed as readable destinations per the Jenkins IA menu mapping.
|
||||
/// Structure is intentionally list-driven so 03 can add Nodes/Artifacts/
|
||||
/// Manage OTO labels without reshaping the frame.
|
||||
class _OtoConsoleLeftMenu extends StatelessWidget {
|
||||
final OtoConsoleSection selected;
|
||||
final ValueChanged<OtoConsoleSection> onSelect;
|
||||
final OtoConsoleThemeAdapter theme;
|
||||
|
||||
const _OtoConsoleRail({
|
||||
const _OtoConsoleLeftMenu({
|
||||
required this.selected,
|
||||
required this.onSelect,
|
||||
required this.theme,
|
||||
});
|
||||
|
||||
static const _primaryDestinations = [
|
||||
_MenuDestination(
|
||||
label: 'Overview',
|
||||
tooltip: 'Overview',
|
||||
icon: Icons.dashboard_outlined,
|
||||
section: OtoConsoleSection.overview,
|
||||
),
|
||||
_MenuDestination(
|
||||
label: 'New Item',
|
||||
tooltip: 'New Item',
|
||||
icon: Icons.add_circle_outline,
|
||||
section: OtoConsoleSection.pipelines,
|
||||
isPrimaryAction: true,
|
||||
),
|
||||
_MenuDestination(
|
||||
label: 'Pipelines',
|
||||
tooltip: 'Pipelines',
|
||||
icon: Icons.account_tree_outlined,
|
||||
section: OtoConsoleSection.pipelines,
|
||||
),
|
||||
_MenuDestination(
|
||||
label: 'Build History',
|
||||
tooltip: 'Executions',
|
||||
icon: Icons.play_circle_outline,
|
||||
section: OtoConsoleSection.executions,
|
||||
),
|
||||
_MenuDestination(
|
||||
label: 'Runners',
|
||||
tooltip: 'Runners',
|
||||
icon: Icons.precision_manufacturing_outlined,
|
||||
section: OtoConsoleSection.runners,
|
||||
),
|
||||
_MenuDestination(
|
||||
label: 'Artifacts',
|
||||
tooltip: 'Artifacts',
|
||||
icon: Icons.inventory_2_outlined,
|
||||
section: OtoConsoleSection.artifacts,
|
||||
),
|
||||
];
|
||||
|
||||
static const _adminDestinations = [
|
||||
_MenuDestination(
|
||||
label: 'Settings',
|
||||
tooltip: 'Settings',
|
||||
icon: Icons.tune,
|
||||
section: OtoConsoleSection.settings,
|
||||
),
|
||||
_MenuDestination(
|
||||
label: 'Agent',
|
||||
tooltip: 'Agent',
|
||||
icon: Icons.smart_toy_outlined,
|
||||
section: OtoConsoleSection.agent,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 60,
|
||||
width: 208,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.railBackgroundColor,
|
||||
border: Border(right: BorderSide(color: theme.borderColor)),
|
||||
|
|
@ -318,58 +505,22 @@ class _OtoConsoleRail extends StatelessWidget {
|
|||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
_RailButton(
|
||||
tooltip: 'Overview',
|
||||
icon: Icons.dashboard_outlined,
|
||||
selected: selected == OtoConsoleSection.overview,
|
||||
onPressed: () => onSelect(OtoConsoleSection.overview),
|
||||
theme: theme,
|
||||
),
|
||||
const _RailDivider(),
|
||||
_RailButton(
|
||||
tooltip: 'Pipelines',
|
||||
icon: Icons.account_tree_outlined,
|
||||
selected: selected == OtoConsoleSection.pipelines,
|
||||
onPressed: () => onSelect(OtoConsoleSection.pipelines),
|
||||
theme: theme,
|
||||
),
|
||||
_RailButton(
|
||||
tooltip: 'Executions',
|
||||
icon: Icons.play_circle_outline,
|
||||
selected: selected == OtoConsoleSection.executions,
|
||||
onPressed: () => onSelect(OtoConsoleSection.executions),
|
||||
theme: theme,
|
||||
),
|
||||
const _RailDivider(),
|
||||
_RailButton(
|
||||
tooltip: 'Runners',
|
||||
icon: Icons.precision_manufacturing_outlined,
|
||||
selected: selected == OtoConsoleSection.runners,
|
||||
onPressed: () => onSelect(OtoConsoleSection.runners),
|
||||
theme: theme,
|
||||
),
|
||||
_RailButton(
|
||||
tooltip: 'Artifacts',
|
||||
icon: Icons.inventory_2_outlined,
|
||||
selected: selected == OtoConsoleSection.artifacts,
|
||||
onPressed: () => onSelect(OtoConsoleSection.artifacts),
|
||||
theme: theme,
|
||||
),
|
||||
for (final destination in _primaryDestinations)
|
||||
_LeftMenuButton(
|
||||
destination: destination,
|
||||
selected: !destination.isPrimaryAction && selected == destination.section,
|
||||
onPressed: () => onSelect(destination.section),
|
||||
theme: theme,
|
||||
),
|
||||
const Spacer(),
|
||||
_RailButton(
|
||||
tooltip: 'Settings',
|
||||
icon: Icons.tune,
|
||||
selected: selected == OtoConsoleSection.settings,
|
||||
onPressed: () => onSelect(OtoConsoleSection.settings),
|
||||
theme: theme,
|
||||
),
|
||||
_RailButton(
|
||||
tooltip: 'Agent',
|
||||
icon: Icons.smart_toy_outlined,
|
||||
selected: selected == OtoConsoleSection.agent,
|
||||
onPressed: () => onSelect(OtoConsoleSection.agent),
|
||||
theme: theme,
|
||||
),
|
||||
const _RailDivider(),
|
||||
for (final destination in _adminDestinations)
|
||||
_LeftMenuButton(
|
||||
destination: destination,
|
||||
selected: selected == destination.section,
|
||||
onPressed: () => onSelect(destination.section),
|
||||
theme: theme,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
|
|
@ -377,16 +528,14 @@ class _OtoConsoleRail extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _RailButton extends StatelessWidget {
|
||||
final String tooltip;
|
||||
final IconData icon;
|
||||
class _LeftMenuButton extends StatelessWidget {
|
||||
final _MenuDestination destination;
|
||||
final bool selected;
|
||||
final VoidCallback onPressed;
|
||||
final OtoConsoleThemeAdapter theme;
|
||||
|
||||
const _RailButton({
|
||||
required this.tooltip,
|
||||
required this.icon,
|
||||
const _LeftMenuButton({
|
||||
required this.destination,
|
||||
required this.selected,
|
||||
required this.onPressed,
|
||||
required this.theme,
|
||||
|
|
@ -394,22 +543,43 @@ class _RailButton extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = selected ? theme.primaryColor : theme.textSecondaryColor;
|
||||
final color = selected || destination.isPrimaryAction
|
||||
? theme.primaryColor
|
||||
: theme.textSecondaryColor;
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
message: destination.tooltip,
|
||||
waitDuration: const Duration(milliseconds: 500),
|
||||
child: SizedBox(
|
||||
width: 56,
|
||||
height: 48,
|
||||
child: IconButton(
|
||||
onPressed: onPressed,
|
||||
icon: Icon(icon, color: color),
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: selected
|
||||
? theme.primaryColor.withValues(alpha: 0.1)
|
||||
: Colors.transparent,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
child: Material(
|
||||
color: selected
|
||||
? theme.primaryColor.withValues(alpha: 0.1)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: onPressed,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(destination.icon, color: color, size: 18),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
destination.label,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 13,
|
||||
fontWeight: selected || destination.isPrimaryAction
|
||||
? FontWeight.w700
|
||||
: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -45,47 +45,45 @@ class _OtoJobsSurfaceState extends State<OtoJobsSurface> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = widget.themeAdapter ?? const OtoConsoleThemeAdapter();
|
||||
final jobCount = widget.snapshot.data?.length ?? 0;
|
||||
return Container(
|
||||
color: theme.backgroundColor,
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 820),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.account_tree_outlined,
|
||||
color: theme.primaryColor,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Pipelines / Jobs',
|
||||
style: TextStyle(
|
||||
color: theme.textColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
Icon(
|
||||
Icons.account_tree_outlined,
|
||||
color: theme.primaryColor,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
if (widget.jobCreateState != null) ...[
|
||||
_CreatePanel(
|
||||
theme: theme,
|
||||
child: _buildJobCreateForm(context, theme),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Pipelines / Jobs',
|
||||
style: TextStyle(
|
||||
color: theme.textColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
Expanded(child: _buildContent(context, theme)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_BuildContextStrip(theme: theme, jobCount: jobCount),
|
||||
const SizedBox(height: 20),
|
||||
if (widget.jobCreateState != null) ...[
|
||||
_CreatePanel(
|
||||
theme: theme,
|
||||
child: _buildJobCreateForm(context, theme),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
Expanded(child: _buildContent(context, theme)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -101,7 +99,7 @@ class _OtoJobsSurfaceState extends State<OtoJobsSurface> {
|
|||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'Create New Job',
|
||||
'New Item',
|
||||
style: TextStyle(
|
||||
color: theme.primaryColor,
|
||||
fontSize: 16,
|
||||
|
|
@ -199,7 +197,11 @@ class _OtoJobsSurfaceState extends State<OtoJobsSurface> {
|
|||
if (jobs.isEmpty) {
|
||||
return _buildEmptyState(theme);
|
||||
}
|
||||
return _buildJobsList(context, theme, jobs);
|
||||
return _JenkinsJobDashboard(
|
||||
jobs: jobs,
|
||||
onExecutionTap: widget.onExecutionTap,
|
||||
theme: theme,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -290,69 +292,85 @@ class _OtoJobsSurfaceState extends State<OtoJobsSurface> {
|
|||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildJobsList(
|
||||
BuildContext context,
|
||||
OtoConsoleThemeAdapter theme,
|
||||
List<OtoJobViewModel> jobs,
|
||||
) {
|
||||
return ListView.separated(
|
||||
itemCount: jobs.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final job = jobs[index];
|
||||
/// Build Queue / Build History context strip shown above the job table.
|
||||
/// Queue depth has no OTO Core data source yet (out of scope for this
|
||||
/// pass), so it renders a structural placeholder count.
|
||||
class _BuildContextStrip extends StatelessWidget {
|
||||
final OtoConsoleThemeAdapter theme;
|
||||
final int jobCount;
|
||||
|
||||
final executionLink = job.executionID.isNotEmpty
|
||||
? InkWell(
|
||||
onTap: () => widget.onExecutionTap?.call(job.executionID),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.primaryColor.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: theme.primaryColor.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.play_circle_outline,
|
||||
color: theme.primaryColor,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Execution: ${job.executionID}',
|
||||
style: TextStyle(
|
||||
color: theme.primaryColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
const _BuildContextStrip({required this.theme, required this.jobCount});
|
||||
|
||||
return _RecordRow(
|
||||
title: job.name,
|
||||
subtitle: 'Job ID: ${job.jobID}',
|
||||
status: _StatusBadge(state: job.state, theme: theme),
|
||||
metadata: _JobMetadata(
|
||||
job: job,
|
||||
executionLink: executionLink,
|
||||
theme: theme,
|
||||
),
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
_ContextChip(
|
||||
icon: Icons.queue_outlined,
|
||||
label: 'Build Queue',
|
||||
value: '0',
|
||||
theme: theme,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_ContextChip(
|
||||
icon: Icons.history,
|
||||
label: 'Build History',
|
||||
value: '$jobCount',
|
||||
theme: theme,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ContextChip extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String value;
|
||||
final OtoConsoleThemeAdapter theme;
|
||||
|
||||
const _ContextChip({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.theme,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.railBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: theme.borderColor),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, color: theme.textSecondaryColor, size: 14),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: theme.textSecondaryColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: theme.textColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -377,111 +395,274 @@ class _CreatePanel extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
class _RecordRow extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final Widget status;
|
||||
final Widget? metadata;
|
||||
/// Jenkins Dashboard-style job table: header row + dense job rows. Replaces
|
||||
/// the former card-style ListView of `_RecordRow`.
|
||||
class _JenkinsJobDashboard extends StatelessWidget {
|
||||
final List<OtoJobViewModel> jobs;
|
||||
final ValueChanged<String>? onExecutionTap;
|
||||
final OtoConsoleThemeAdapter theme;
|
||||
|
||||
const _RecordRow({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.status,
|
||||
this.metadata,
|
||||
required this.theme,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.railBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: theme.borderColor),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: theme.textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
status,
|
||||
],
|
||||
),
|
||||
if (subtitle.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(color: theme.textSecondaryColor, fontSize: 13),
|
||||
),
|
||||
],
|
||||
if (metadata != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
metadata!,
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _JobMetadata extends StatelessWidget {
|
||||
final OtoJobViewModel job;
|
||||
final Widget? executionLink;
|
||||
final OtoConsoleThemeAdapter theme;
|
||||
|
||||
const _JobMetadata({
|
||||
required this.job,
|
||||
this.executionLink,
|
||||
const _JenkinsJobDashboard({
|
||||
required this.jobs,
|
||||
this.onExecutionTap,
|
||||
required this.theme,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Created: ${job.createdAt}',
|
||||
style: TextStyle(color: theme.textSecondaryColor, fontSize: 12),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
_JobTableHeader(theme: theme),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: jobs.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
Divider(height: 1, color: theme.borderColor),
|
||||
itemBuilder: (context, index) => _JobTableRow(
|
||||
job: jobs[index],
|
||||
onExecutionTap: onExecutionTap,
|
||||
theme: theme,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _JobTableHeader extends StatelessWidget {
|
||||
final OtoConsoleThemeAdapter theme;
|
||||
|
||||
const _JobTableHeader({required this.theme});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = TextStyle(
|
||||
color: theme.textSecondaryColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.railBackgroundColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
|
||||
border: Border.all(color: theme.borderColor),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(flex: 3, child: Text('Name', style: style)),
|
||||
Expanded(flex: 2, child: Text('Status', style: style)),
|
||||
Expanded(flex: 2, child: Text('Last Run', style: style)),
|
||||
Expanded(flex: 2, child: Text('Updated', style: style)),
|
||||
Expanded(flex: 3, child: Text('Actions', style: style)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _JobTableRow extends StatelessWidget {
|
||||
final OtoJobViewModel job;
|
||||
final ValueChanged<String>? onExecutionTap;
|
||||
final OtoConsoleThemeAdapter theme;
|
||||
|
||||
const _JobTableRow({
|
||||
required this.job,
|
||||
this.onExecutionTap,
|
||||
required this.theme,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.railBackgroundColor,
|
||||
border: Border(
|
||||
left: BorderSide(color: theme.borderColor),
|
||||
right: BorderSide(color: theme.borderColor),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
job.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: theme.textColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Job ID: ${job.jobID}',
|
||||
style: TextStyle(color: theme.textSecondaryColor, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(flex: 2, child: _StatusBadge(state: job.state, theme: theme)),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: job.executionID.isNotEmpty
|
||||
? _ExecutionLink(
|
||||
executionID: job.executionID,
|
||||
onTap: onExecutionTap,
|
||||
theme: theme,
|
||||
)
|
||||
: Text(
|
||||
'-',
|
||||
style: TextStyle(color: theme.textSecondaryColor, fontSize: 12),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
job.updatedAt,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: theme.textSecondaryColor, fontSize: 12),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: _JobTaskMenu(
|
||||
job: job,
|
||||
onExecutionTap: onExecutionTap,
|
||||
theme: theme,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExecutionLink extends StatelessWidget {
|
||||
final String executionID;
|
||||
final ValueChanged<String>? onTap;
|
||||
final OtoConsoleThemeAdapter theme;
|
||||
|
||||
const _ExecutionLink({
|
||||
required this.executionID,
|
||||
this.onTap,
|
||||
required this.theme,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () => onTap?.call(executionID),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.primaryColor.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: theme.primaryColor.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.play_circle_outline, color: theme.primaryColor, size: 14),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Updated: ${job.updatedAt}',
|
||||
style: TextStyle(color: theme.textSecondaryColor, fontSize: 12),
|
||||
'Execution: $executionID',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: theme.primaryColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (executionLink != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
executionLink!,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Job-context task menu limited to the primitives directly applicable to
|
||||
/// OTO today: `Status`, `Build History`, `Stages`. `Build Now`/`Configure`
|
||||
/// are intentionally not implemented (no job dispatch/config contract yet).
|
||||
class _JobTaskMenu extends StatelessWidget {
|
||||
final OtoJobViewModel job;
|
||||
final ValueChanged<String>? onExecutionTap;
|
||||
final OtoConsoleThemeAdapter theme;
|
||||
|
||||
const _JobTaskMenu({
|
||||
required this.job,
|
||||
this.onExecutionTap,
|
||||
required this.theme,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasExecution = job.executionID.isNotEmpty;
|
||||
return Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 2,
|
||||
children: [
|
||||
_TaskMenuChip(label: 'Status', enabled: true, onTap: null, theme: theme),
|
||||
_TaskMenuChip(
|
||||
label: 'Build History',
|
||||
enabled: hasExecution,
|
||||
onTap: hasExecution ? () => onExecutionTap?.call(job.executionID) : null,
|
||||
theme: theme,
|
||||
),
|
||||
_TaskMenuChip(
|
||||
label: 'Stages',
|
||||
enabled: hasExecution,
|
||||
onTap: hasExecution ? () => onExecutionTap?.call(job.executionID) : null,
|
||||
theme: theme,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TaskMenuChip extends StatelessWidget {
|
||||
final String label;
|
||||
final bool enabled;
|
||||
final VoidCallback? onTap;
|
||||
final OtoConsoleThemeAdapter theme;
|
||||
|
||||
const _TaskMenuChip({
|
||||
required this.label,
|
||||
required this.enabled,
|
||||
this.onTap,
|
||||
required this.theme,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = enabled
|
||||
? theme.primaryColor
|
||||
: theme.textSecondaryColor.withValues(alpha: 0.5);
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(color: color, fontSize: 12, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusBadge extends StatelessWidget {
|
||||
final String state;
|
||||
final OtoConsoleThemeAdapter theme;
|
||||
|
|
|
|||
|
|
@ -81,6 +81,12 @@ void main() {
|
|||
expect(find.text('Readiness'), findsOneWidget);
|
||||
expect(find.byTooltip('Agent'), findsOneWidget);
|
||||
|
||||
// Jenkins IA shell: breadcrumb and Dashboard-level left menu labels.
|
||||
expect(find.text('OTO'), findsOneWidget);
|
||||
expect(find.text('New Item'), findsOneWidget);
|
||||
expect(find.text('Pipelines'), findsOneWidget);
|
||||
expect(find.text('Build History'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byTooltip('Agent'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
|
|
@ -411,6 +417,18 @@ void main() {
|
|||
expect(find.text('Job ID: job-1'), findsOneWidget);
|
||||
expect(find.textContaining('Execution: exec-1'), findsOneWidget);
|
||||
|
||||
// Jenkins dashboard job table: header columns and job-context task menu.
|
||||
expect(find.text('Name'), findsOneWidget);
|
||||
expect(find.text('Last Run'), findsOneWidget);
|
||||
expect(find.text('Updated'), findsOneWidget);
|
||||
expect(find.text('Actions'), findsOneWidget);
|
||||
// 'Status' appears as both a table header and a job-context task menu
|
||||
// chip; 'Build History' appears in the context strip and the task menu.
|
||||
expect(find.text('Status'), findsNWidgets(2));
|
||||
expect(find.text('Build History'), findsNWidgets(2));
|
||||
expect(find.text('Stages'), findsOneWidget);
|
||||
expect(find.text('Build Queue'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.textContaining('Execution: exec-1'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tappedExecution, 'exec-1');
|
||||
|
|
@ -749,7 +767,7 @@ void main() {
|
|||
),
|
||||
),
|
||||
);
|
||||
expect(find.textContaining('Create New Job'), findsOneWidget);
|
||||
expect(find.textContaining('New Item'), findsOneWidget);
|
||||
expect(find.byType(TextField), findsNWidgets(2));
|
||||
final createButton = find.ancestor(
|
||||
of: find.text('Create'),
|
||||
|
|
|
|||
Loading…
Reference in a new issue