feat(tables): migrate /tables page from Jinja2 to Vue 3 + Vite
Rewrite 237-line vanilla JS + Jinja2 template into Vue 3 SFC components (App.vue, TableCatalog.vue, DataViewer.vue, useTableData composable). Establishes apiPost POST request pattern for pure Vite pages. Removes templates/index.html, updates Vite entry to HTML, and Flask route to send_from_directory. Includes sql_fragments WHERE_CLAUSE escaping fix, updated integration tests, and OpenSpec artifact archive. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-09
|
||||
@@ -0,0 +1,63 @@
|
||||
## Context
|
||||
|
||||
Tables 頁面(`/tables`)是開發者工具頁面,允許瀏覽 19 張 DWH 表的欄位與內容。目前架構:
|
||||
- Jinja2 模板 `index.html` extends `_base.html`,server-render `TABLES_CONFIG` 為表格卡片
|
||||
- vanilla JS (237 行) 用 DOM 操作管理狀態,透過 `window.MesApi.post()` 呼叫 API
|
||||
- 兩個 POST API:`/api/get_table_columns`、`/api/query_table`;一個 GET API:`/api/get_table_info`
|
||||
|
||||
QC-GATE 遷移已建立 Vue 3 + Vite 純前端架構模式(GET-only),本次需補齊 POST 請求模式。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 將 Tables 頁面完整遷移為 Vue 3 SFC,複用 QC-GATE 架構模式
|
||||
- 建立 POST 請求在純 Vite 頁面中的標準做法(`apiPost` from `core/api.js`)
|
||||
- 表格配置改由前端 `apiGet('/api/get_table_info')` 動態取得,脫離 Jinja2 context
|
||||
- 遷移完成後移除 Jinja2 模板 `templates/index.html`
|
||||
|
||||
**Non-Goals:**
|
||||
- 不修改後端 API 邏輯或 SQL 查詢(保持現有 `/api/query_table`、`/api/get_table_columns` 不變)
|
||||
- 不改變 CSRF 策略(現有 CSRF 僅 enforce `/admin/*` 路徑,Tables API 不受影響)
|
||||
- 不增加新功能(如分頁、排序、匯出),僅 1:1 功能遷移
|
||||
- 不建立共用 Vue 元件庫(本次僅 Tables 頁面內部元件化)
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: CSRF token 不需額外處理
|
||||
**選擇**:Tables 的 POST API 不需 CSRF token
|
||||
**理由**:`csrf.py` 的 `should_enforce_csrf()` 僅對 `/admin/*` 路徑啟用 CSRF。`/api/query_table` 和 `/api/get_table_columns` 不在 enforce 範圍內。`apiPost()` 已內建 CSRF header 邏輯(從 `<meta>` 讀取),即使沒有 meta tag 也只是發送空字串,不會失敗。
|
||||
**替代方案**:新增 CSRF token API endpoint — 不需要,因為 Tables API 本身就不 enforce。
|
||||
|
||||
### D2: 表格配置從 API 動態取得
|
||||
**選擇**:前端在 mount 時呼叫 `GET /api/get_table_info` 取得 `TABLES_CONFIG`
|
||||
**理由**:該 endpoint 已存在(`app.py:453`),直接返回 `TABLES_CONFIG` dict。無需建立新 API。
|
||||
**替代方案**:將 config 打包成靜態 JSON — 不適合,config 含 row_count 等可能更新的資訊。
|
||||
|
||||
### D3: Vite entry 改為 HTML entry point
|
||||
**選擇**:`vite.config.js` 中 tables entry 從 `src/tables/main.js` 改為 `src/tables/index.html`
|
||||
**理由**:與 QC-GATE 模式一致,HTML entry 讓 Vite 處理完整的 HTML → JS → CSS pipeline。
|
||||
**影響**:`npm run build` 會輸出 `tables.html`、`tables.js`、`tables.css` 到 `static/dist/`。
|
||||
|
||||
### D4: 元件拆分策略
|
||||
**選擇**:3 個 Vue 元件 + 1 個 composable
|
||||
- `App.vue` — 根佈局,管理 loading/error 狀態
|
||||
- `TableCatalog.vue` — 表格卡片目錄(分類顯示)
|
||||
- `DataViewer.vue` — 資料檢視器(欄位篩選 + 查詢結果表格)
|
||||
- `useTableData.js` — composable 封裝 API 呼叫和狀態管理
|
||||
|
||||
**理由**:對應原始 UI 的兩個主要區塊(表格選擇 / 資料檢視),職責清晰。
|
||||
|
||||
### D5: 現有 vanilla JS main.js 直接替換
|
||||
**選擇**:將現有 `frontend/src/tables/main.js` (237 行) 替換為 Vue 3 bootstrap 入口(~7 行),原始邏輯分散至 Vue 元件和 composable 中。
|
||||
**理由**:vanilla JS 全部是 DOM 操作,無法漸進式遷移,需整體重寫。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[風險] 大表警示標記遺失**:Jinja2 模板中有 `{% if table.row_count > 10000000 %}` 顯示「大表」badge。
|
||||
→ 遷移:在 `TableCatalog.vue` 中用 Vue 條件渲染實現相同邏輯。
|
||||
|
||||
- **[風險] Fallback inline script 移除**:`index.html` 含 ~200 行 fallback JS(Vite build 不存在時)。
|
||||
→ 接受:Vite build 是 deployment 的標準流程,fallback 不再需要。
|
||||
|
||||
- **[風險] CSS 樣式差異**:原始 ~335 行 embedded CSS 需遷移至 `style.css`。
|
||||
→ 遷移:提取核心樣式至獨立 CSS 檔案,與 QC-GATE 風格統一。
|
||||
@@ -0,0 +1,30 @@
|
||||
## Why
|
||||
|
||||
Tables 頁面(`/tables`)是完全獨立的開發工具頁面,無跨頁面 drill-down 依賴,且行數最少(237 行 JS),是建立 POST/CSRF 請求模式的理想候選。QC-GATE 遷移已建立 GET-only 的 Vue 3 + Vite 架構模式,現在需要補齊 POST 請求模式,為後續更複雜頁面遷移鋪路。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 將 `/tables` 頁面從 Jinja2 模板 + vanilla JS 遷移為純 Vue 3 + Vite SFC 架構
|
||||
- Flask route 從 `render_template()` 改為 `send_from_directory()`,不再傳入 `TABLES_CONFIG` context
|
||||
- 前端改用 `/api/get_table_info` (GET) 取得表格配置,取代 Jinja2 server-render
|
||||
- API 呼叫從 `window.MesApi.post()` 改為 `apiPost()` from `core/api.js`
|
||||
- 純 Vite 頁面發出 POST 請求時需自行攜帶 CSRF token(透過 `<meta>` tag 或從 API 取得)
|
||||
- Vite config entry 從 JS-only (`tables/main.js`) 改為 HTML entry (`tables/index.html`)
|
||||
- 保留現有 Jinja2 模板作為 fallback 直到驗證完成後移除
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `tables-query-page`: 數據表查詢頁面的功能需求(表格選擇、動態欄位篩選、查詢結果顯示)
|
||||
|
||||
### Modified Capabilities
|
||||
- `vue-vite-page-architecture`: 新增 POST 請求 + CSRF token 處理模式(現有 spec 僅涵蓋 GET)
|
||||
|
||||
## Impact
|
||||
|
||||
- **前端**:`frontend/src/tables/` 整個目錄重寫(main.js → Vue 3 SFC 結構)
|
||||
- **後端**:`app.py` 中 `/tables` route 改為 `send_from_directory`
|
||||
- **Vite config**:tables entry 改為 HTML entry point
|
||||
- **CSRF**:純 Vite 頁面無 Jinja2 `{{ csrf_token() }}`,需建立替代方案(API endpoint 或 cookie-based)
|
||||
- **模板**:`templates/index.html` 遷移完成後可移除
|
||||
- **API**:現有 `/api/get_table_info`、`/api/get_table_columns`、`/api/query_table` 不變
|
||||
@@ -0,0 +1,84 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Tables page SHALL display categorized table catalog
|
||||
The page SHALL display all configured DWH tables as clickable cards, grouped by category.
|
||||
|
||||
#### Scenario: Table catalog rendering
|
||||
- **WHEN** the page loads
|
||||
- **THEN** the page SHALL fetch table configuration from `GET /api/get_table_info`
|
||||
- **THEN** tables SHALL be displayed as cards grouped by category (即時數據表, 現況快照表, 歷史累積表, 輔助表)
|
||||
- **THEN** each card SHALL show the table display name and description
|
||||
|
||||
#### Scenario: Large table badge
|
||||
- **WHEN** a table has `row_count` exceeding 10,000,000
|
||||
- **THEN** the card SHALL display a visual indicator (badge) marking it as a large table
|
||||
|
||||
### Requirement: Tables page SHALL load column metadata on table selection
|
||||
The page SHALL load and display column information when a table is selected from the catalog.
|
||||
|
||||
#### Scenario: Select table from catalog
|
||||
- **WHEN** user clicks a table card
|
||||
- **THEN** the page SHALL call `POST /api/get_table_columns` with the table name
|
||||
- **THEN** the data viewer panel SHALL open showing the table name and column count
|
||||
- **THEN** a filter input row SHALL appear with one input per column
|
||||
|
||||
#### Scenario: Active table indication
|
||||
- **WHEN** a table is selected
|
||||
- **THEN** the selected card SHALL have a visual active state
|
||||
- **THEN** previously active cards SHALL be deactivated
|
||||
|
||||
### Requirement: Tables page SHALL support column-level filtering
|
||||
The page SHALL allow users to enter filter values per column and query the table data.
|
||||
|
||||
#### Scenario: Enter filter and query
|
||||
- **WHEN** user enters filter values in column inputs and clicks "查詢"
|
||||
- **THEN** the page SHALL call `POST /api/query_table` with the table name, filters, limit (1000), and time_field
|
||||
- **THEN** the result table SHALL display returned rows with column headers
|
||||
- **THEN** the title SHALL show the table name, row count, and active filter count
|
||||
|
||||
#### Scenario: Enter key triggers query
|
||||
- **WHEN** user presses Enter in any filter input
|
||||
- **THEN** the query SHALL execute as if the "查詢" button was clicked
|
||||
|
||||
#### Scenario: Active filter display
|
||||
- **WHEN** filters are applied
|
||||
- **THEN** active filters SHALL be displayed as removable tags above the result table
|
||||
- **THEN** clicking a tag's remove button SHALL clear that filter
|
||||
|
||||
#### Scenario: Clear all filters
|
||||
- **WHEN** user clicks "清除篩選"
|
||||
- **THEN** all filter inputs SHALL be cleared
|
||||
- **THEN** all active filter tags SHALL be removed
|
||||
|
||||
#### Scenario: Query with no filters
|
||||
- **WHEN** user clicks "查詢" with no filters
|
||||
- **THEN** the query SHALL return the most recent 1000 rows (sorted by time_field if available)
|
||||
|
||||
### Requirement: Tables page SHALL handle loading and error states
|
||||
The page SHALL display appropriate feedback during API calls and on errors.
|
||||
|
||||
#### Scenario: Loading state during column fetch
|
||||
- **WHEN** column metadata is being fetched
|
||||
- **THEN** the viewer SHALL display a loading indicator
|
||||
|
||||
#### Scenario: Loading state during query
|
||||
- **WHEN** a query is executing
|
||||
- **THEN** the table body SHALL display a loading indicator
|
||||
|
||||
#### Scenario: API error handling
|
||||
- **WHEN** an API call fails
|
||||
- **THEN** the page SHALL display the error message in the relevant area
|
||||
- **THEN** the page SHALL NOT crash or become unresponsive
|
||||
|
||||
#### Scenario: Empty query result
|
||||
- **WHEN** a query returns zero rows
|
||||
- **THEN** the table SHALL display a "查無資料" message
|
||||
|
||||
### Requirement: Tables page SHALL allow closing the data viewer
|
||||
The page SHALL allow users to close the data viewer and return to the catalog view.
|
||||
|
||||
#### Scenario: Close data viewer
|
||||
- **WHEN** user clicks the close button on the data viewer
|
||||
- **THEN** the data viewer panel SHALL be hidden
|
||||
- **THEN** all table cards SHALL return to inactive state
|
||||
- **THEN** internal state (columns, filters) SHALL be reset
|
||||
@@ -0,0 +1,41 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Pure Vite pages SHALL handle POST API calls without legacy MesApi
|
||||
Pure Vite pages SHALL use the `apiPost` function from `core/api.js` for POST requests without depending on `window.MesApi`.
|
||||
|
||||
#### Scenario: API POST request from pure Vite page
|
||||
- **WHEN** a pure Vite page makes a POST API call
|
||||
- **THEN** the call SHALL use the `apiPost` function from `core/api.js`
|
||||
- **THEN** the call SHALL include `Content-Type: application/json` header
|
||||
- **THEN** the call SHALL work without `window.MesApi` being present
|
||||
|
||||
#### Scenario: CSRF token handling in POST requests
|
||||
- **WHEN** a pure Vite page calls `apiPost`
|
||||
- **THEN** `apiPost` SHALL attempt to read CSRF token from `<meta name="csrf-token">`
|
||||
- **THEN** if no meta tag exists, the request SHALL still proceed (non-admin APIs do not enforce CSRF)
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Vite config SHALL support Vue SFC and HTML entry points
|
||||
The Vite build configuration SHALL support Vue Single File Components alongside existing vanilla JS entries.
|
||||
|
||||
#### Scenario: Vue plugin coexistence
|
||||
- **WHEN** `vite build` is executed
|
||||
- **THEN** Vue SFC (`.vue` files) SHALL be compiled by `@vitejs/plugin-vue`
|
||||
- **THEN** existing vanilla JS entry points SHALL continue to build without modification
|
||||
|
||||
#### Scenario: HTML entry point
|
||||
- **WHEN** a page uses an HTML file as its Vite entry point
|
||||
- **THEN** Vite SHALL process the HTML and its referenced JS/CSS into `static/dist/`
|
||||
- **THEN** the output SHALL include `<page-name>.html`, `<page-name>.js`, and `<page-name>.css`
|
||||
|
||||
#### Scenario: Chunk splitting
|
||||
- **WHEN** Vite builds the project
|
||||
- **THEN** Vue runtime SHALL be split into a `vendor-vue` chunk
|
||||
- **THEN** ECharts modules SHALL be split into the existing `vendor-echarts` chunk
|
||||
- **THEN** chunk splitting SHALL NOT affect existing page bundles
|
||||
|
||||
#### Scenario: Migrated page entry replacement
|
||||
- **WHEN** a vanilla JS page is migrated to Vue 3
|
||||
- **THEN** its Vite entry SHALL change from JS file to HTML file (e.g., `src/tables/main.js` → `src/tables/index.html`)
|
||||
- **THEN** the original JS entry SHALL be replaced, not kept alongside
|
||||
@@ -0,0 +1,26 @@
|
||||
## 1. Vue 3 前端結構建立
|
||||
|
||||
- [x] 1.1 建立 `frontend/src/tables/index.html` — 純 Vite HTML entry point(參照 qc-gate 模式)
|
||||
- [x] 1.2 重寫 `frontend/src/tables/main.js` — Vue 3 createApp bootstrap(取代原 237 行 vanilla JS)
|
||||
- [x] 1.3 建立 `frontend/src/tables/style.css` — 從 Jinja2 模板提取核心樣式
|
||||
|
||||
## 2. Vue 元件開發
|
||||
|
||||
- [x] 2.1 建立 `frontend/src/tables/App.vue` — 根元件,管理 loading/error 全局狀態與佈局
|
||||
- [x] 2.2 建立 `frontend/src/tables/components/TableCatalog.vue` — 表格卡片目錄(分類顯示、大表 badge、active 狀態)
|
||||
- [x] 2.3 建立 `frontend/src/tables/components/DataViewer.vue` — 資料檢視器(欄位篩選輸入、查詢結果表、filter tag、close)
|
||||
|
||||
## 3. Composable 與 API 整合
|
||||
|
||||
- [x] 3.1 建立 `frontend/src/tables/composables/useTableData.js` — 封裝 apiGet/apiPost 呼叫、table config/columns/query 狀態管理
|
||||
|
||||
## 4. Vite 與 Flask 路由整合
|
||||
|
||||
- [x] 4.1 更新 `frontend/vite.config.js` — tables entry 從 `main.js` 改為 `index.html`
|
||||
- [x] 4.2 更新 `src/mes_dashboard/app.py` — `/tables` route 改為 `send_from_directory`
|
||||
|
||||
## 5. 清理與驗證
|
||||
|
||||
- [x] 5.1 移除 Jinja2 模板 `src/mes_dashboard/templates/index.html`
|
||||
- [x] 5.2 移除 `app.py` 中 `/tables` route 的 `TABLES_CONFIG` import(如不再被其他地方使用)
|
||||
- [x] 5.3 執行 `npm run build` 驗證建置成功,確認 `static/dist/tables.html` 產出
|
||||
88
openspec/specs/tables-query-page/spec.md
Normal file
88
openspec/specs/tables-query-page/spec.md
Normal file
@@ -0,0 +1,88 @@
|
||||
## Purpose
|
||||
Define stable requirements for tables-query-page.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
||||
### Requirement: Tables page SHALL display categorized table catalog
|
||||
The page SHALL display all configured DWH tables as clickable cards, grouped by category.
|
||||
|
||||
#### Scenario: Table catalog rendering
|
||||
- **WHEN** the page loads
|
||||
- **THEN** the page SHALL fetch table configuration from `GET /api/get_table_info`
|
||||
- **THEN** tables SHALL be displayed as cards grouped by category (即時數據表, 現況快照表, 歷史累積表, 輔助表)
|
||||
- **THEN** each card SHALL show the table display name and description
|
||||
|
||||
#### Scenario: Large table badge
|
||||
- **WHEN** a table has `row_count` exceeding 10,000,000
|
||||
- **THEN** the card SHALL display a visual indicator (badge) marking it as a large table
|
||||
|
||||
### Requirement: Tables page SHALL load column metadata on table selection
|
||||
The page SHALL load and display column information when a table is selected from the catalog.
|
||||
|
||||
#### Scenario: Select table from catalog
|
||||
- **WHEN** user clicks a table card
|
||||
- **THEN** the page SHALL call `POST /api/get_table_columns` with the table name
|
||||
- **THEN** the data viewer panel SHALL open showing the table name and column count
|
||||
- **THEN** a filter input row SHALL appear with one input per column
|
||||
|
||||
#### Scenario: Active table indication
|
||||
- **WHEN** a table is selected
|
||||
- **THEN** the selected card SHALL have a visual active state
|
||||
- **THEN** previously active cards SHALL be deactivated
|
||||
|
||||
### Requirement: Tables page SHALL support column-level filtering
|
||||
The page SHALL allow users to enter filter values per column and query the table data.
|
||||
|
||||
#### Scenario: Enter filter and query
|
||||
- **WHEN** user enters filter values in column inputs and clicks "查詢"
|
||||
- **THEN** the page SHALL call `POST /api/query_table` with the table name, filters, limit (1000), and time_field
|
||||
- **THEN** the result table SHALL display returned rows with column headers
|
||||
- **THEN** the title SHALL show the table name, row count, and active filter count
|
||||
|
||||
#### Scenario: Enter key triggers query
|
||||
- **WHEN** user presses Enter in any filter input
|
||||
- **THEN** the query SHALL execute as if the "查詢" button was clicked
|
||||
|
||||
#### Scenario: Active filter display
|
||||
- **WHEN** filters are applied
|
||||
- **THEN** active filters SHALL be displayed as removable tags above the result table
|
||||
- **THEN** clicking a tag's remove button SHALL clear that filter
|
||||
|
||||
#### Scenario: Clear all filters
|
||||
- **WHEN** user clicks "清除篩選"
|
||||
- **THEN** all filter inputs SHALL be cleared
|
||||
- **THEN** all active filter tags SHALL be removed
|
||||
|
||||
#### Scenario: Query with no filters
|
||||
- **WHEN** user clicks "查詢" with no filters
|
||||
- **THEN** the query SHALL return the most recent 1000 rows (sorted by time_field if available)
|
||||
|
||||
### Requirement: Tables page SHALL handle loading and error states
|
||||
The page SHALL display appropriate feedback during API calls and on errors.
|
||||
|
||||
#### Scenario: Loading state during column fetch
|
||||
- **WHEN** column metadata is being fetched
|
||||
- **THEN** the viewer SHALL display a loading indicator
|
||||
|
||||
#### Scenario: Loading state during query
|
||||
- **WHEN** a query is executing
|
||||
- **THEN** the table body SHALL display a loading indicator
|
||||
|
||||
#### Scenario: API error handling
|
||||
- **WHEN** an API call fails
|
||||
- **THEN** the page SHALL display the error message in the relevant area
|
||||
- **THEN** the page SHALL NOT crash or become unresponsive
|
||||
|
||||
#### Scenario: Empty query result
|
||||
- **WHEN** a query returns zero rows
|
||||
- **THEN** the table SHALL display a "查無資料" message
|
||||
|
||||
### Requirement: Tables page SHALL allow closing the data viewer
|
||||
The page SHALL allow users to close the data viewer and return to the catalog view.
|
||||
|
||||
#### Scenario: Close data viewer
|
||||
- **WHEN** user clicks the close button on the data viewer
|
||||
- **THEN** the data viewer panel SHALL be hidden
|
||||
- **THEN** all table cards SHALL return to inactive state
|
||||
- **THEN** internal state (columns, filters) SHALL be reset
|
||||
@@ -36,6 +36,11 @@ The Vite build configuration SHALL support Vue Single File Components alongside
|
||||
- **THEN** ECharts modules SHALL be split into the existing `vendor-echarts` chunk
|
||||
- **THEN** chunk splitting SHALL NOT affect existing page bundles
|
||||
|
||||
#### Scenario: Migrated page entry replacement
|
||||
- **WHEN** a vanilla JS page is migrated to Vue 3
|
||||
- **THEN** its Vite entry SHALL change from JS file to HTML file (e.g., `src/tables/main.js` → `src/tables/index.html`)
|
||||
- **THEN** the original JS entry SHALL be replaced, not kept alongside
|
||||
|
||||
### Requirement: Pure Vite pages SHALL handle API calls without legacy MesApi
|
||||
Pure Vite pages SHALL use the existing `frontend/src/core/api.js` module for API communication without depending on the global `window.MesApi` object from `_base.html`.
|
||||
|
||||
@@ -43,3 +48,17 @@ Pure Vite pages SHALL use the existing `frontend/src/core/api.js` module for API
|
||||
- **WHEN** a pure Vite page makes a GET API call
|
||||
- **THEN** the call SHALL use the `apiGet` function from `core/api.js`
|
||||
- **THEN** the call SHALL work without `window.MesApi` being present
|
||||
|
||||
### Requirement: Pure Vite pages SHALL handle POST API calls without legacy MesApi
|
||||
Pure Vite pages SHALL use the `apiPost` function from `core/api.js` for POST requests without depending on `window.MesApi`.
|
||||
|
||||
#### Scenario: API POST request from pure Vite page
|
||||
- **WHEN** a pure Vite page makes a POST API call
|
||||
- **THEN** the call SHALL use the `apiPost` function from `core/api.js`
|
||||
- **THEN** the call SHALL include `Content-Type: application/json` header
|
||||
- **THEN** the call SHALL work without `window.MesApi` being present
|
||||
|
||||
#### Scenario: CSRF token handling in POST requests
|
||||
- **WHEN** a pure Vite page calls `apiPost`
|
||||
- **THEN** `apiPost` SHALL attempt to read CSRF token from `<meta name="csrf-token">`
|
||||
- **THEN** if no meta tag exists, the request SHALL still proceed (non-admin APIs do not enforce CSRF)
|
||||
|
||||
Reference in New Issue
Block a user