From baa4fac16ea005babefbc9625ead694f27d77860 Mon Sep 17 00:00:00 2001 From: dogfootman Date: Tue, 11 Nov 2025 09:14:39 +0900 Subject: [PATCH 1/9] [feat]: Add cluster status validation for PMK NodeGroup addition - Add validation to check cluster status before adding NodeGroup - Only allow NodeGroup addition when cluster status is Active - Display error message for Creating/Deleting status - Implement error handling for API failures - Add documentation in doc/fix folder --- doc/fix/pmk-nodegroup-status-validation.md | 123 ++++++++++++++++++ .../operation/manage/clustercreate.js | 48 ++++++- 2 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 doc/fix/pmk-nodegroup-status-validation.md diff --git a/doc/fix/pmk-nodegroup-status-validation.md b/doc/fix/pmk-nodegroup-status-validation.md new file mode 100644 index 00000000..6e3982bf --- /dev/null +++ b/doc/fix/pmk-nodegroup-status-validation.md @@ -0,0 +1,123 @@ +# PMK NodeGroup 추가 상태 검증 기능 + +## 개요 +PMK Cluster의 NodeGroup을 추가할 때, Cluster의 상태를 확인하여 Active 상태일 때만 NodeGroup 추가가 가능하도록 검증 로직을 추가한 기능입니다. + +## 문제점 +기존에는 PMK Cluster의 상태와 관계없이 NodeGroup 추가를 시도할 수 있었습니다. 이로 인해: +- Creating 상태의 Cluster에 NodeGroup을 추가하려고 시도하면 실패 +- Deleting 상태의 Cluster에 NodeGroup을 추가하려고 시도하면 실패 +- 사용자에게 명확한 피드백이 제공되지 않음 + +## 해결 방법 +`addNewNodeGroup()` 함수에 Cluster 상태 검증 로직을 추가하여, Active 상태일 때만 NodeGroup 추가 폼이 표시되도록 구현했습니다. + +## 구현 내용 + +### 1. Cluster 선택 여부 확인 +- `selectedPmkObj`가 존재하는지 확인 +- 선택되지 않은 경우 에러 메시지 표시 + +### 2. Cluster 상태 조회 +- **API**: `GetK8sCluster` (operationId) +- **함수**: `webconsolejs["common/api/services/pmk_api"].getCluster(nsId, clusterId)` +- **Response Path**: `response.data.responseData.spiderViewK8sClusterDetail.Status` + +### 3. 상태 검증 로직 +- **허용되는 상태**: `Active` +- **거부되는 상태**: `Creating`, `Deleting` +- 거부 시 메시지: "NodeGroup can only be added when the cluster is in Active status. Current status is: {status}" + +### 4. 에러 처리 +- API 호출 실패 시 에러 메시지 표시 +- try-catch 블록으로 예외 상황 처리 +- 모든 에러 상황에서 사용자에게 명확한 피드백 제공 + +## 변경된 파일 + +### 1. front/assets/js/partials/operation/manage/clustercreate.js +**함수**: `addNewNodeGroup()` +**변경 내용**: +```javascript +// 추가된 검증 로직 +1. Cluster 선택 여부 확인 +2. GetK8sCluster API 호출하여 현재 상태 조회 +3. Status 값이 'Active'인 경우만 기존 로직 실행 +4. Active가 아닌 경우 commonShowDefaultModal로 에러 메시지 표시 +``` + +**위치**: 394번째 줄 ~ 459번째 줄 + +## API 정보 + +### GetK8sCluster +- **Method**: GET +- **Resource Path**: `/ns/{nsId}/k8sCluster/{k8sClusterId}` +- **Description**: K8s Cluster 정보를 조회합니다 +- **Parameters**: + - `nsId`: Namespace ID + - `k8sClusterId`: K8s Cluster ID +- **Response**: + ```json + { + "status": 200, + "data": { + "responseData": { + "spiderViewK8sClusterDetail": { + "Status": "Active" | "Creating" | "Deleting" + } + } + } + } + ``` + +## 테스트 시나리오 + +### Case 1: Cluster가 선택되지 않은 경우 +1. PMK 목록 페이지에서 Cluster를 선택하지 않음 +2. "Add NodeGroup" 버튼 클릭 +3. **예상 결과**: "Cluster Selection Required" 모달 표시 + +### Case 2: Cluster 상태가 Creating인 경우 +1. Creating 상태의 Cluster 선택 +2. "Add NodeGroup" 버튼 클릭 +3. **예상 결과**: "NodeGroup can only be added when the cluster is in Active status. Current status is: Creating" 모달 표시 +4. NodeGroup 추가 폼이 표시되지 않음 + +### Case 3: Cluster 상태가 Deleting인 경우 +1. Deleting 상태의 Cluster 선택 +2. "Add NodeGroup" 버튼 클릭 +3. **예상 결과**: "NodeGroup can only be added when the cluster is in Active status. Current status is: Deleting" 모달 표시 +4. NodeGroup 추가 폼이 표시되지 않음 + +### Case 4: Cluster 상태가 Active인 경우 +1. Active 상태의 Cluster 선택 +2. "Add NodeGroup" 버튼 클릭 +3. **예상 결과**: NodeGroup 추가 폼이 정상적으로 표시됨 +4. 기존 기능과 동일하게 동작 + +### Case 5: API 호출 실패 +1. 네트워크 문제 또는 API 오류 발생 +2. "Add NodeGroup" 버튼 클릭 +3. **예상 결과**: "Failed to retrieve cluster status. Please try again." 또는 "An error occurred while checking cluster status. Please try again." 모달 표시 + +## 사용자 영향 +- **긍정적 영향**: + - Cluster 상태에 따른 명확한 피드백 제공 + - 불필요한 API 호출 실패 방지 + - 사용자 경험 개선 +- **부정적 영향**: + - NodeGroup 추가 시 약간의 지연 발생 (API 호출로 인한) + - 하지만 사용성 개선이 더 큰 이점 + +## 브랜치 정보 +- **Base Branch**: develop +- **Feature Branch**: feature/pmk-nodegroup-status-validation + +## 관련 이슈 +- PMK NodeGroup 추가 시 상태 검증 필요 + +## 작성자 +- Date: 2025-11-11 +- Feature: PMK NodeGroup Status Validation + diff --git a/front/assets/js/partials/operation/manage/clustercreate.js b/front/assets/js/partials/operation/manage/clustercreate.js index 0114981c..bd36b228 100644 --- a/front/assets/js/partials/operation/manage/clustercreate.js +++ b/front/assets/js/partials/operation/manage/clustercreate.js @@ -411,7 +411,53 @@ export async function addNewNodeGroup() { Create_Cluster_Config_Arr = new Array(); Create_Node_Config_Arr = new Array(); - var selectedCluster = webconsolejs["pages/operation/manage/pmk"].selectedPmkObj + var selectedCluster = webconsolejs["pages/operation/manage/pmk"].selectedPmkObj; + + // Validation: Check if cluster is selected + if (!selectedCluster || selectedCluster.length === 0) { + webconsolejs['partials/layout/modal'].commonShowDefaultModal( + 'Cluster Selection Required', + 'Please select a cluster first before adding a NodeGroup.' + ); + return; + } + + // Get workspace and cluster information + var selectedWorkspaceProject = await webconsolejs["partials/layout/navbar"].workspaceProjectInit(); + var selectedNsId = selectedWorkspaceProject.nsId; + var clusterId = selectedCluster[0].id; + + // Validation: Check cluster status via API + try { + var clusterResponse = await webconsolejs["common/api/services/pmk_api"].getCluster(selectedNsId, clusterId); + + if (clusterResponse.status !== 200) { + webconsolejs['partials/layout/modal'].commonShowDefaultModal( + 'Cluster Status Check Failed', + 'Failed to retrieve cluster status. Please try again.' + ); + return; + } + + var clusterStatus = clusterResponse.data.responseData.spiderViewK8sClusterDetail?.Status; + + // Check if cluster is in Active status + if (clusterStatus !== 'Active') { + webconsolejs['partials/layout/modal'].commonShowDefaultModal( + 'Cluster Status Check', + 'NodeGroup can only be added when the cluster is in Active status. Current status is: ' + clusterStatus + ); + return; + } + } catch (error) { + console.error('Error checking cluster status:', error); + webconsolejs['partials/layout/modal'].commonShowDefaultModal( + 'Error', + 'An error occurred while checking cluster status. Please try again.' + ); + return; + } + var cluster_name = selectedCluster[0].name var cluster_desc = selectedCluster[0].description var cluster_connection = selectedCluster[0].provider// 임시 From 9d93d36016b9cca84797b439e45231f7c0cdc408 Mon Sep 17 00:00:00 2001 From: dogfootman Date: Tue, 11 Nov 2025 09:33:18 +0900 Subject: [PATCH 2/9] [fix]: Improve error handling for PMK cluster data retrieval - Add null check for cluster response in getSelectedPmkData() - Replace alert with modal for better UX in getCluster() - Add toast notifications for error scenarios - Return error objects instead of undefined - Add try-catch blocks for exception handling - Update documentation with known issues and fixes --- doc/fix/pmk-nodegroup-status-validation.md | 183 ++++++++++++++++++ .../assets/js/common/api/services/pmk_api.js | 54 ++++-- front/assets/js/pages/operation/manage/pmk.js | 53 +++-- 3 files changed, 261 insertions(+), 29 deletions(-) diff --git a/doc/fix/pmk-nodegroup-status-validation.md b/doc/fix/pmk-nodegroup-status-validation.md index 6e3982bf..6c211cdc 100644 --- a/doc/fix/pmk-nodegroup-status-validation.md +++ b/doc/fix/pmk-nodegroup-status-validation.md @@ -121,3 +121,186 @@ PMK Cluster의 NodeGroup을 추가할 때, Cluster의 상태를 확인하여 Act - Date: 2025-11-11 - Feature: PMK NodeGroup Status Validation +--- + +## Known Issues and Fixes + +### Issue 1: TypeError when clicking on Cluster +**Problem**: +- Cluster 목록에서 Cluster를 클릭하면 `TypeError: Cannot read properties of undefined (reading 'status')` 에러 발생 +- 특히 Creating 상태의 Cluster를 클릭할 때 발생 +- `getCluster()` API가 `undefined`를 반환하는 경우 처리되지 않음 + +**Root Cause**: +1. `getCluster()` 함수에서 validation 실패 시 `return;`만 실행하여 `undefined` 반환 +2. `getSelectedPmkData()`에서 `pmkResp.status` 접근 시 null check 없음 +3. API 호출 실패 시 예외 처리 누락 +4. alert 사용으로 사용자 경험 저하 + +**Solution (2025-11-11)**: + +#### 1. getSelectedPmkData() 함수 개선 +**File**: `front/assets/js/pages/operation/manage/pmk.js` + +**Changes**: +- `pmkResp` 존재 여부 체크 추가 +- try-catch 블록으로 예외 처리 +- Toast 알림으로 사용자 피드백 개선 + +```javascript +export async function getSelectedPmkData() { + if (currentPmkId != undefined && currentPmkId != "") { + var selectedNsId = selectedWorkspaceProject.nsId; + + try { + var pmkResp = await webconsolejs["common/api/services/pmk_api"].getCluster(selectedNsId, currentPmkId); + + // Check if pmkResp exists + if (!pmkResp) { + webconsolejs["common/util"].showToast( + 'Failed to retrieve cluster information. The cluster may not exist or the API is not responding.', + 'error', + 5000 + ); + return; + } + + // Check response status + if (pmkResp.status != 200) { + webconsolejs["common/util"].showToast( + 'Failed to load cluster information. Status: ' + (pmkResp.status || 'Unknown'), + 'error', + 5000 + ); + return; + } + + // SET PMK Info page + setPmkInfoData(pmkResp.data); + // ... 나머지 코드 + } catch (error) { + console.error('Error in getSelectedPmkData:', error); + webconsolejs["common/util"].showToast( + 'An error occurred while loading cluster information. Please try again.', + 'error', + 5000 + ); + } + } +} +``` + +#### 2. getCluster() 함수 개선 +**File**: `front/assets/js/common/api/services/pmk_api.js` + +**Changes**: +- alert → Modal 변경으로 UX 개선 +- 에러 객체 반환 (undefined 대신) +- try-catch로 API 호출 실패 처리 +- 일관된 에러 응답 형식 + +```javascript +export async function getCluster(nsId, clusterId) { + // Validation: Check nsId + if (!nsId || nsId === "") { + webconsolejs['partials/layout/modal'].commonShowDefaultModal( + 'Project Selection Required', + 'Please select a project first before viewing cluster details.' + ); + return { status: 400, error: 'No project selected' }; + } + + // Validation: Check clusterId + if (!clusterId || clusterId === "") { + webconsolejs['partials/layout/modal'].commonShowDefaultModal( + 'Cluster Selection Required', + 'Please select a cluster first.' + ); + return { status: 400, error: 'No cluster selected' }; + } + + // API call with error handling + try { + const data = { + pathParams: { + nsId: nsId, + k8sClusterId: clusterId + } + }; + + var controller = "/api/" + "mc-infra-manager/" + "Getk8scluster"; + const response = await webconsolejs["common/api/http"].commonAPIPost( + controller, + data + ); + + return response; + } catch (error) { + console.error('Error in getCluster API call:', error); + return { + status: 500, + error: 'API call failed: ' + (error.message || 'Unknown error') + }; + } +} +``` + +### Error Handling Improvements + +| Scenario | Before | After | +|----------|--------|-------| +| Project not selected | alert → undefined → TypeError | Modal → Error object → Toast notification | +| Cluster not selected | alert → undefined → TypeError | Modal → Error object → Toast notification | +| API call fails | undefined → TypeError | Error object → Toast notification | +| Creating status Cluster | TypeError occurs | Normal handling + Status display | + +### Additional Test Scenarios + +#### Case 6: Project가 선택되지 않은 상태에서 Cluster 클릭 +1. Project 선택하지 않음 +2. Cluster 클릭 +3. **예상 결과**: "Project Selection Required" 모달 표시 +4. Toast로 "Failed to load cluster information" 표시 + +#### Case 7: Creating 상태 Cluster 클릭 +1. GetAllK8sCluster로 목록 조회 +2. Creating 상태의 Cluster 클릭 +3. **예상 결과**: + - 에러 없이 정상 처리 + - Cluster 정보 표시 (Status: Creating) + - 데이터가 없으면 Toast로 에러 알림 + +#### Case 8: API 호출 중 네트워크 에러 +1. Cluster 클릭 +2. 네트워크 연결 끊김 또는 API 서버 응답 없음 +3. **예상 결과**: + - Toast로 "An error occurred while loading cluster information" 표시 + - 콘솔에 에러 로그 출력 + +## User Feedback Methods + +### Toast Notifications +- **Function**: `webconsolejs["common/util"].showToast(message, type, duration)` +- **Types**: 'info', 'success', 'error', 'warning' +- **Duration**: 5000ms (5 seconds) +- **Position**: Top-right corner +- **Usage**: For non-blocking error notifications + +### Modal Dialogs +- **Function**: `webconsolejs['partials/layout/modal'].commonShowDefaultModal(title, content)` +- **Usage**: For critical errors requiring user acknowledgment +- **Actions**: OK button to dismiss + +## Change History + +### 2025-11-11 - Initial Implementation +- Added cluster status validation for NodeGroup addition +- Only Active clusters allow NodeGroup addition + +### 2025-11-11 - Error Handling Improvements +- Fixed TypeError when clicking clusters +- Replaced alert with modal for better UX +- Added toast notifications for error scenarios +- Improved API error handling with try-catch blocks +- Return error objects instead of undefined + diff --git a/front/assets/js/common/api/services/pmk_api.js b/front/assets/js/common/api/services/pmk_api.js index 109c5bec..07478acb 100644 --- a/front/assets/js/common/api/services/pmk_api.js +++ b/front/assets/js/common/api/services/pmk_api.js @@ -25,25 +25,49 @@ export async function getClusterList(nsId) { } export async function getCluster(nsId, clusterId) { - if (nsId == "" || nsId == undefined || clusterId == undefined || clusterId == "") { - alert(" undefined nsId: " + nsId + " clusterId " + clusterId); - return; + // Validation: Check nsId + if (!nsId || nsId === "") { + webconsolejs['partials/layout/modal'].commonShowDefaultModal( + 'Project Selection Required', + 'Please select a project first before viewing cluster details.' + ); + return { status: 400, error: 'No project selected' }; } - const data = { - pathParams: { - nsId: nsId, - k8sClusterId: clusterId - } + + // Validation: Check clusterId + if (!clusterId || clusterId === "") { + webconsolejs['partials/layout/modal'].commonShowDefaultModal( + 'Cluster Selection Required', + 'Please select a cluster first.' + ); + return { status: 400, error: 'No cluster selected' }; } - var controller = "/api/" + "mc-infra-manager/" + "Getk8scluster"; - const response = await webconsolejs["common/api/http"].commonAPIPost( - controller, - data - ); + // API call with error handling + try { + const data = { + pathParams: { + nsId: nsId, + k8sClusterId: clusterId + } + }; - // error check를 위해 response를 return - return response + var controller = "/api/" + "mc-infra-manager/" + "Getk8scluster"; + const response = await webconsolejs["common/api/http"].commonAPIPost( + controller, + data + ); + + // error check를 위해 response를 return + return response; + } catch (error) { + console.error('Error in getCluster API call:', error); + // API 호출 실패 시에도 에러 객체 반환 + return { + status: 500, + error: 'API call failed: ' + (error.message || 'Unknown error') + }; + } } export async function CreateCluster(clusterName, selectedConnection, clusterVersion, selectedVpc, selectedSubnet, selectedSecurityGroup, Create_Cluster_Config_Arr, selectedNsId) { diff --git a/front/assets/js/pages/operation/manage/pmk.js b/front/assets/js/pages/operation/manage/pmk.js index 1c276d1e..a60f800d 100644 --- a/front/assets/js/pages/operation/manage/pmk.js +++ b/front/assets/js/pages/operation/manage/pmk.js @@ -171,21 +171,46 @@ export async function getSelectedPmkData() { if (currentPmkId != undefined && currentPmkId != "") { var selectedNsId = selectedWorkspaceProject.nsId; - var pmkResp = await webconsolejs["common/api/services/pmk_api"].getCluster(selectedNsId, currentPmkId) + try { + var pmkResp = await webconsolejs["common/api/services/pmk_api"].getCluster(selectedNsId, currentPmkId); + + // Check if pmkResp exists + if (!pmkResp) { + webconsolejs["common/util"].showToast( + 'Failed to retrieve cluster information. The cluster may not exist or the API is not responding.', + 'error', + 5000 + ); + return; + } - if (pmkResp.status != 200) { - // failed. // TODO : Error Popup 처리 - return; - } - // SET PMK Info page - setPmkInfoData(pmkResp.data) - - // Toggle PMK Info - var div = document.getElementById("cluster_info"); - const hasActiveClass = div.classList.contains("active"); - if (!hasActiveClass) { - // cluster_info 가 active면 toggle 필요 없음 - webconsolejs["partials/layout/navigatePages"].toggleElement(div) + // Check response status + if (pmkResp.status != 200) { + webconsolejs["common/util"].showToast( + 'Failed to load cluster information. Status: ' + (pmkResp.status || 'Unknown'), + 'error', + 5000 + ); + return; + } + + // SET PMK Info page + setPmkInfoData(pmkResp.data); + + // Toggle PMK Info + var div = document.getElementById("cluster_info"); + const hasActiveClass = div.classList.contains("active"); + if (!hasActiveClass) { + // cluster_info 가 active면 toggle 필요 없음 + webconsolejs["partials/layout/navigatePages"].toggleElement(div); + } + } catch (error) { + console.error('Error in getSelectedPmkData:', error); + webconsolejs["common/util"].showToast( + 'An error occurred while loading cluster information. Please try again.', + 'error', + 5000 + ); } } } From 64287357a4f9af27233d6584ea3d68c32077adf5 Mon Sep 17 00:00:00 2001 From: dogfootman Date: Tue, 11 Nov 2025 09:48:57 +0900 Subject: [PATCH 3/9] [refactor]: Improve Add NodeGroup button state management - Add updateAddNodeGroupButtonState() function following existing pattern - Enable/disable button based on cluster status (Active only) - Remove status check logic from addNewNodeGroup() - Update HTML to remove href and set initial disabled state - Add tooltip to show why button is disabled - Improve UX by preventing form flash before validation - Update documentation with UI/UX improvements --- doc/fix/pmk-nodegroup-status-validation.md | 118 ++++++++++++++++++ front/assets/js/pages/operation/manage/pmk.js | 33 ++++- .../operation/manage/clustercreate.js | 38 +----- .../manage/_nodegrouplist_status.html | 5 +- 4 files changed, 156 insertions(+), 38 deletions(-) diff --git a/doc/fix/pmk-nodegroup-status-validation.md b/doc/fix/pmk-nodegroup-status-validation.md index 6c211cdc..9a49ee44 100644 --- a/doc/fix/pmk-nodegroup-status-validation.md +++ b/doc/fix/pmk-nodegroup-status-validation.md @@ -304,3 +304,121 @@ export async function getCluster(nsId, clusterId) { - Improved API error handling with try-catch blocks - Return error objects instead of undefined +### 2025-11-11 - UI/UX Improvements (Button State Management) +- Changed Add NodeGroup button behavior to prevent form flash +- Implemented button enable/disable based on cluster status +- Button only enabled when cluster is Active +- Added tooltip to show current cluster status +- Removed href to prevent automatic form display +- Simplified addNewNodeGroup() function by removing redundant status checks +- Following existing pattern: updateClusterRemoteCmdButtonState() + +## UI/UX Improvements + +### Problem +이전에는 Add NodeGroup 버튼을 클릭하면: +1. `href="#addnode"`로 인해 URL이 변경되고 폼이 즉시 표시됨 +2. 그 후 상태 체크가 실행됨 +3. 상태가 Active가 아니면 모달이 표시되고 폼이 사라짐 +4. 사용자 혼란 발생 (폼이 잠깐 보였다가 사라짐) + +### Solution: Button State Management +**패턴**: Cluster Terminal 버튼과 동일한 방식 사용 + +#### 구현 내용 + +1. **updateAddNodeGroupButtonState() 함수 추가** + - **File**: `front/assets/js/pages/operation/manage/pmk.js` + - **Location**: Line 1789-1811 + - Cluster 상태에 따라 버튼 활성화/비활성화 + - Active 상태일 때만 버튼 활성화 + - Tooltip으로 현재 상태 표시 + +```javascript +function updateAddNodeGroupButtonState(clusterStatus) { + const addNodeGroupBtns = document.querySelectorAll('a[onclick*="addNewNodeGroup"]'); + + addNodeGroupBtns.forEach(btn => { + if (!currentPmkId) { + btn.classList.add('disabled'); + btn.style.pointerEvents = 'none'; + btn.title = 'Please select a cluster first'; + } else if (clusterStatus === 'Active') { + btn.classList.remove('disabled'); + btn.style.pointerEvents = 'auto'; + btn.title = 'Add NodeGroup to this cluster'; + } else { + btn.classList.add('disabled'); + btn.style.pointerEvents = 'none'; + btn.title = 'NodeGroup can only be added when cluster is Active. Current status: ' + clusterStatus; + } + }); +} +``` + +2. **setPmkInfoData() 함수 수정** + - **File**: `front/assets/js/pages/operation/manage/pmk.js` + - **Location**: Line 343 + - Cluster 정보 표시 후 버튼 상태 업데이트 + +```javascript +// Add NodeGroup 버튼 상태 업데이트 +updateAddNodeGroupButtonState(pmkStatus); +``` + +3. **addNewNodeGroup() 함수 단순화** + - **File**: `front/assets/js/partials/operation/manage/clustercreate.js` + - **Removed**: Lines 425-459 (상태 체크 로직 전체 제거) + - Cluster 선택 여부만 확인 + - 버튼이 이미 비활성화되어 있으므로 상태 체크 불필요 + +4. **HTML 수정** + - **File**: `front/templates/partials/operation/manage/_nodegrouplist_status.html` + - **Changes**: + - `href="#addnode"` 제거 → 폼 자동 표시 방지 + - `class="btn btn-outline-primary disabled"` 추가 + - `style="pointer-events: none;"` 추가 + - `title="Please select an Active cluster first"` 추가 + +```html + + + + + +``` + +### User Experience Comparison + +| 시나리오 | 이전 동작 | 개선 후 동작 | +|---------|----------|-------------| +| Cluster 미선택 | 버튼 활성화 → 클릭 → 모달 표시 | 버튼 비활성화 (tooltip: "Please select a cluster first") | +| Creating 상태 | 버튼 활성화 → 클릭 → 폼 표시 → 모달 → 폼 사라짐 | 버튼 비활성화 (tooltip: "Current status: Creating") | +| Deleting 상태 | 버튼 활성화 → 클릭 → 폼 표시 → 모달 → 폼 사라짐 | 버튼 비활성화 (tooltip: "Current status: Deleting") | +| Active 상태 | 버튼 활성화 → 클릭 → API 호출 → 상태 체크 → 폼 표시 | 버튼 활성화 → 클릭 → 바로 폼 표시 | + +### Benefits + +1. **예방적 접근**: 불가능한 상태에서 시도를 미연에 방지 +2. **명확한 피드백**: Tooltip으로 버튼이 비활성화된 이유 즉시 확인 가능 +3. **성능 개선**: 불필요한 API 호출 및 validation 제거 +4. **일관성**: 기존 Terminal 버튼들과 동일한 패턴 사용 +5. **혼란 제거**: 폼이 잠깐 나타났다가 사라지는 현상 완전히 제거 + +### Pattern Consistency + +이 구현은 기존 코드와 일관성을 유지합니다: +- `updateClusterRemoteCmdButtonState()` - Cluster Terminal 버튼 +- `updateMciRemoteCmdButtonState()` - MCI Terminal 버튼 +- `updateSubGroupRemoteCmdButtonState()` - SubGroup Terminal 버튼 +- `updateAddNodeGroupButtonState()` - Add NodeGroup 버튼 (신규) + diff --git a/front/assets/js/pages/operation/manage/pmk.js b/front/assets/js/pages/operation/manage/pmk.js index a60f800d..7e5dee6a 100644 --- a/front/assets/js/pages/operation/manage/pmk.js +++ b/front/assets/js/pages/operation/manage/pmk.js @@ -285,12 +285,16 @@ function setPmkInfoData(pmkData) { var pmkNetwork = clusterDetailData?.Network || {}; var clusterProvider = clusterData.connectionConfig.providerName currentProvider = clusterProvider + + // pmkStatus를 try 블록 밖에서 선언 + var pmkStatus = "N/A"; + try { var pmkName = clusterData.name; var pmkID = clusterData.id var pmkVersion = clusterDetailData?.Version || "N/A"; - var pmkStatus = clusterDetailData?.Status || "N/A"; + pmkStatus = clusterDetailData?.Status || "N/A"; // 네트워크 정보 var pmkVpc = (pmkNetwork.VpcIID && pmkNetwork.VpcIID.SystemId) || "N/A"; @@ -334,6 +338,9 @@ function setPmkInfoData(pmkData) { if (Array.isArray(nodeGroupList) && nodeGroupList.length > 0) { displayNodeGroupStatusList(pmkID, clusterProvider, clusterData); } + + // Add NodeGroup 버튼 상태 업데이트 + updateAddNodeGroupButtonState(pmkStatus); } // pmk life cycle 변경 @@ -1784,4 +1791,28 @@ function updateClusterRemoteCmdButtonState() { clusterRemoteCmdBtn.title = 'Please select a Cluster first'; } } +} + +// Add NodeGroup 버튼 상태 업데이트 +function updateAddNodeGroupButtonState(clusterStatus) { + const addNodeGroupBtns = document.querySelectorAll('a[onclick*="addNewNodeGroup"]'); + + addNodeGroupBtns.forEach(btn => { + if (!currentPmkId) { + // Cluster가 선택되지 않은 경우 + btn.classList.add('disabled'); + btn.style.pointerEvents = 'none'; + btn.title = 'Please select a cluster first'; + } else if (clusterStatus === 'Active') { + // Active 상태인 경우 활성화 + btn.classList.remove('disabled'); + btn.style.pointerEvents = 'auto'; + btn.title = 'Add NodeGroup to this cluster'; + } else { + // Active가 아닌 경우 비활성화 + btn.classList.add('disabled'); + btn.style.pointerEvents = 'none'; + btn.title = 'NodeGroup can only be added when cluster is Active. Current status: ' + clusterStatus; + } + }); } \ No newline at end of file diff --git a/front/assets/js/partials/operation/manage/clustercreate.js b/front/assets/js/partials/operation/manage/clustercreate.js index bd36b228..d2c24f74 100644 --- a/front/assets/js/partials/operation/manage/clustercreate.js +++ b/front/assets/js/partials/operation/manage/clustercreate.js @@ -422,41 +422,9 @@ export async function addNewNodeGroup() { return; } - // Get workspace and cluster information - var selectedWorkspaceProject = await webconsolejs["partials/layout/navbar"].workspaceProjectInit(); - var selectedNsId = selectedWorkspaceProject.nsId; - var clusterId = selectedCluster[0].id; - - // Validation: Check cluster status via API - try { - var clusterResponse = await webconsolejs["common/api/services/pmk_api"].getCluster(selectedNsId, clusterId); - - if (clusterResponse.status !== 200) { - webconsolejs['partials/layout/modal'].commonShowDefaultModal( - 'Cluster Status Check Failed', - 'Failed to retrieve cluster status. Please try again.' - ); - return; - } - - var clusterStatus = clusterResponse.data.responseData.spiderViewK8sClusterDetail?.Status; - - // Check if cluster is in Active status - if (clusterStatus !== 'Active') { - webconsolejs['partials/layout/modal'].commonShowDefaultModal( - 'Cluster Status Check', - 'NodeGroup can only be added when the cluster is in Active status. Current status is: ' + clusterStatus - ); - return; - } - } catch (error) { - console.error('Error checking cluster status:', error); - webconsolejs['partials/layout/modal'].commonShowDefaultModal( - 'Error', - 'An error occurred while checking cluster status. Please try again.' - ); - return; - } + // Note: Cluster status check is handled by button state management + // The button is only enabled when cluster status is Active + // See updateAddNodeGroupButtonState() in pmk.js var cluster_name = selectedCluster[0].name var cluster_desc = selectedCluster[0].description diff --git a/front/templates/partials/operation/manage/_nodegrouplist_status.html b/front/templates/partials/operation/manage/_nodegrouplist_status.html index 91b5ef70..cb797ab1 100644 --- a/front/templates/partials/operation/manage/_nodegrouplist_status.html +++ b/front/templates/partials/operation/manage/_nodegrouplist_status.html @@ -332,8 +332,9 @@

Date: Tue, 11 Nov 2025 09:51:43 +0900 Subject: [PATCH 4/9] [fix]: Add missing form display logic in addNewNodeGroup() - Add toggleSubElement call to display NodeGroup form - Previously removed href caused no action on button click - Now form properly displays when button is clicked --- front/assets/js/partials/operation/manage/clustercreate.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/front/assets/js/partials/operation/manage/clustercreate.js b/front/assets/js/partials/operation/manage/clustercreate.js index d2c24f74..7b164a46 100644 --- a/front/assets/js/partials/operation/manage/clustercreate.js +++ b/front/assets/js/partials/operation/manage/clustercreate.js @@ -444,6 +444,11 @@ export async function addNewNodeGroup() { $("#node_cluster_sg").html(''); $("#node_cluster_version").html(''); + // Display the NodeGroup form + var addNodeDiv = document.getElementById("addnode"); + if (addNodeDiv && !addNodeDiv.classList.contains('active')) { + webconsolejs["partials/layout/navigatePages"].toggleSubElement(addNodeDiv); + } isNodeGroup = true } From fa82f773ff9a8ec9f26cc33789c3453fec976c78 Mon Sep 17 00:00:00 2001 From: dogfootman Date: Tue, 11 Nov 2025 09:57:33 +0900 Subject: [PATCH 5/9] [fix]: Use hash navigation for Add NodeGroup section switching - Replace toggleSubElement with window.location.hash - Follow existing pattern used by other section navigations - Fix issue where previous sections remained visible - Enable browser back button support --- front/assets/js/partials/operation/manage/clustercreate.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/front/assets/js/partials/operation/manage/clustercreate.js b/front/assets/js/partials/operation/manage/clustercreate.js index 7b164a46..5f14c098 100644 --- a/front/assets/js/partials/operation/manage/clustercreate.js +++ b/front/assets/js/partials/operation/manage/clustercreate.js @@ -444,11 +444,8 @@ export async function addNewNodeGroup() { $("#node_cluster_sg").html(''); $("#node_cluster_version").html(''); - // Display the NodeGroup form - var addNodeDiv = document.getElementById("addnode"); - if (addNodeDiv && !addNodeDiv.classList.contains('active')) { - webconsolejs["partials/layout/navigatePages"].toggleSubElement(addNodeDiv); - } + // Navigate to Add NodeGroup section (following existing pattern) + window.location.hash = "#addnode"; isNodeGroup = true } From 88c9e0cef0e62406e82eb170666cb3cf3bb7424a Mon Sep 17 00:00:00 2001 From: dogfootman Date: Tue, 11 Nov 2025 10:10:20 +0900 Subject: [PATCH 6/9] [feat]: Add Provider and Region fields to Add NodeGroup form - Add connectionName and description to mappingTablePmkData - Add extractRegionFromConnection helper function - Add loadRegionListForNodeGroup to load region list from API - Add updateNodeClusterConnectionsByRegion for region change handling - Set Provider as fixed (disabled) with current cluster value - Load and display Region dropdown with API data (changeable) - Extract cluster provider and connectionName from selectedCluster - Update form to properly populate Provider and Region fields --- front/assets/js/pages/operation/manage/pmk.js | 2 + .../operation/manage/clustercreate.js | 175 ++++++++++++++---- 2 files changed, 146 insertions(+), 31 deletions(-) diff --git a/front/assets/js/pages/operation/manage/pmk.js b/front/assets/js/pages/operation/manage/pmk.js index 7e5dee6a..1becbac5 100644 --- a/front/assets/js/pages/operation/manage/pmk.js +++ b/front/assets/js/pages/operation/manage/pmk.js @@ -149,6 +149,8 @@ function mappingTablePmkData(totalPmkListObj) { return { name: item.name, id: item.id, + description: item.description || "", + connectionName: item.connectionName || "N/A", resourceType: item.resourceType, systemLabel: item.systemLabel || "N/A", systemMessage: item.systemMessage || "N/A", diff --git a/front/assets/js/partials/operation/manage/clustercreate.js b/front/assets/js/partials/operation/manage/clustercreate.js index 5f14c098..338ce71b 100644 --- a/front/assets/js/partials/operation/manage/clustercreate.js +++ b/front/assets/js/partials/operation/manage/clustercreate.js @@ -391,23 +391,100 @@ export async function createNode() { webconsolejs["common/api/services/pmk_api"].createNode(k8sClusterId, selectedNsId, Create_Node_Config_Arr) } -export async function addNewNodeGroup() { - // isNode = false +// Extract region from connectionName +// e.g., "aws-ap-northeast-2" -> "[aws] aws-ap-northeast-2" +function extractRegionFromConnection(connectionName, provider) { + if (!connectionName || !provider) return ''; + + // Return in the format: [provider] connectionName + return '[' + provider + '] ' + connectionName; +} + +// Load region list for Add NodeGroup form +async function loadRegionListForNodeGroup(currentProvider, currentRegion) { + try { + // Call getRegion API + var regionList = await webconsolejs["common/api/services/pmk_api"].getRegionList(); + + if (!regionList || regionList.length === 0) { + // If API fails, show only current region + $("#node_cluster_region").html( + '' + ); + return; + } + + // Filter regions by current provider + var filteredRegions = regionList.filter(region => { + return region.ProviderName.toLowerCase() === currentProvider.toLowerCase(); + }); + + // Build region dropdown + var html = ''; + filteredRegions.forEach(region => { + var regionValue = '[' + region.ProviderName + '] ' + region.RegionName; + var isSelected = regionValue === currentRegion ? ' selected' : ''; + html += ''; + }); + + $("#node_cluster_region").empty(); + $("#node_cluster_region").append(html); + + // Add change event listener for region + $("#node_cluster_region").off('change').on('change', function() { + var selectedRegion = $(this).val(); + if (selectedRegion) { + // Filter cloud connections based on selected region + updateNodeClusterConnectionsByRegion(selectedRegion); + } + }); + + } catch (error) { + console.error('Failed to load region list:', error); + // Fallback: show only current region + $("#node_cluster_region").html( + '' + ); + } +} - // var providerList = await webconsolejs["common/api/services/pmk_api"].getProviderList() - // // provider set - // await setProviderList(providerList) +// Update cloud connections when region changes +async function updateNodeClusterConnectionsByRegion(selectedRegion) { + var selectedWorkspaceProject = await webconsolejs["partials/layout/navbar"].workspaceProjectInit(); - // // call getRegion API - // var regionList = await webconsolejs["common/api/services/pmk_api"].getRegionList() - // // region set - // await setRegionList(regionList) + try { + // Get all cloud connections + var connectionList = await webconsolejs["common/api/services/pmk_api"].getCloudConnection(); + + // Extract region name from selectedRegion (e.g., "[aws] aws-ap-northeast-2") + var regionMatch = selectedRegion.match(/\[.*?\]\s+(.*)/); + var regionName = regionMatch ? regionMatch[1] : selectedRegion; - // // call cloudconnection - // var connectionList = await webconsolejs["common/api/services/pmk_api"].getCloudConnection() - // // cloudconnection set - // await setCloudConnection(connectionList) + // Filter connections that match the selected region + var filteredConnections = connectionList.filter(conn => + conn.toLowerCase().includes(regionName.toLowerCase()) + ); + // Update connection dropdown + var html = ''; + filteredConnections.forEach(conn => { + html += ''; + }); + + $("#node_cluster_cloudconnection").empty(); + $("#node_cluster_cloudconnection").append(html); + + } catch (error) { + console.error('Failed to update connections:', error); + } +} + +export async function addNewNodeGroup() { Create_Cluster_Config_Arr = new Array(); Create_Node_Config_Arr = new Array(); @@ -426,28 +503,64 @@ export async function addNewNodeGroup() { // The button is only enabled when cluster status is Active // See updateAddNodeGroupButtonState() in pmk.js - var cluster_name = selectedCluster[0].name - var cluster_desc = selectedCluster[0].description - var cluster_connection = selectedCluster[0].provider// 임시 - var cluster_vpc = selectedCluster[0].vpc - var cluster_subnet = selectedCluster[0].subnet - var cluster_securitygroup = selectedCluster[0].securitygroup - var cluster_version = selectedCluster[0].version - - $("#node_cluster_name").val(cluster_name) - $("#node_cluster_desc").val(cluster_desc) - // $("#node_cluster_connection").val(cluster_connection) - $("#node_cluster_cloudconnection").html(''); - // provider, region, connection, vpc, subnet, sg, cluster version 채워넣어 펼치기 - $("#node_cluster_vpc").html(''); - $("#node_cluster_subnet").html(''); - $("#node_cluster_sg").html(''); - $("#node_cluster_version").html(''); + var cluster_name = selectedCluster[0].name; + var cluster_desc = selectedCluster[0].description; + var cluster_provider = selectedCluster[0].provider; + var cluster_connection = selectedCluster[0].connectionName; + var cluster_vpc = selectedCluster[0].vpc; + var cluster_subnet = selectedCluster[0].subnet; + var cluster_securitygroup = selectedCluster[0].securitygroup; + var cluster_version = selectedCluster[0].version; + + // Extract region from connectionName + var cluster_region = extractRegionFromConnection(cluster_connection, cluster_provider); + + // Set basic cluster information + $("#node_cluster_name").val(cluster_name); + $("#node_cluster_desc").val(cluster_desc); + + // Set Provider (fixed, single option, disabled) + $("#node_cluster_provider").html( + '' + ); + $("#node_cluster_provider").prop('disabled', true); + + // Load region list from API + await loadRegionListForNodeGroup(cluster_provider, cluster_region); + + // Set other fields + $("#node_cluster_cloudconnection").html( + '' + ); + $("#node_cluster_vpc").html( + '' + ); + $("#node_cluster_subnet").html( + '' + ); + $("#node_cluster_sg").html( + '' + ); + $("#node_cluster_version").html( + '' + ); // Navigate to Add NodeGroup section (following existing pattern) window.location.hash = "#addnode"; - isNodeGroup = true + isNodeGroup = true; } export async function addNewPmk() { From 38779025994f2870de84095ae3edbc90391e9efa Mon Sep 17 00:00:00 2001 From: dogfootman Date: Tue, 11 Nov 2025 10:29:18 +0900 Subject: [PATCH 7/9] [refactor]: Change Provider and Region to display-only fields - Change Region field from selectable to fixed (disabled) - Remove loadRegionListForNodeGroup() helper function - Remove updateNodeClusterConnectionsByRegion() helper function - Keep extractRegionFromConnection() for region string formatting - Both Provider and Region now show cluster info only (not editable) - Improve performance by removing unnecessary API calls - Align with API behavior (createNode does not use Provider/Region) --- .../operation/manage/clustercreate.js | 95 ++----------------- 1 file changed, 8 insertions(+), 87 deletions(-) diff --git a/front/assets/js/partials/operation/manage/clustercreate.js b/front/assets/js/partials/operation/manage/clustercreate.js index 338ce71b..6cce8683 100644 --- a/front/assets/js/partials/operation/manage/clustercreate.js +++ b/front/assets/js/partials/operation/manage/clustercreate.js @@ -400,90 +400,6 @@ function extractRegionFromConnection(connectionName, provider) { return '[' + provider + '] ' + connectionName; } -// Load region list for Add NodeGroup form -async function loadRegionListForNodeGroup(currentProvider, currentRegion) { - try { - // Call getRegion API - var regionList = await webconsolejs["common/api/services/pmk_api"].getRegionList(); - - if (!regionList || regionList.length === 0) { - // If API fails, show only current region - $("#node_cluster_region").html( - '' - ); - return; - } - - // Filter regions by current provider - var filteredRegions = regionList.filter(region => { - return region.ProviderName.toLowerCase() === currentProvider.toLowerCase(); - }); - - // Build region dropdown - var html = ''; - filteredRegions.forEach(region => { - var regionValue = '[' + region.ProviderName + '] ' + region.RegionName; - var isSelected = regionValue === currentRegion ? ' selected' : ''; - html += ''; - }); - - $("#node_cluster_region").empty(); - $("#node_cluster_region").append(html); - - // Add change event listener for region - $("#node_cluster_region").off('change').on('change', function() { - var selectedRegion = $(this).val(); - if (selectedRegion) { - // Filter cloud connections based on selected region - updateNodeClusterConnectionsByRegion(selectedRegion); - } - }); - - } catch (error) { - console.error('Failed to load region list:', error); - // Fallback: show only current region - $("#node_cluster_region").html( - '' - ); - } -} - -// Update cloud connections when region changes -async function updateNodeClusterConnectionsByRegion(selectedRegion) { - var selectedWorkspaceProject = await webconsolejs["partials/layout/navbar"].workspaceProjectInit(); - - try { - // Get all cloud connections - var connectionList = await webconsolejs["common/api/services/pmk_api"].getCloudConnection(); - - // Extract region name from selectedRegion (e.g., "[aws] aws-ap-northeast-2") - var regionMatch = selectedRegion.match(/\[.*?\]\s+(.*)/); - var regionName = regionMatch ? regionMatch[1] : selectedRegion; - - // Filter connections that match the selected region - var filteredConnections = connectionList.filter(conn => - conn.toLowerCase().includes(regionName.toLowerCase()) - ); - - // Update connection dropdown - var html = ''; - filteredConnections.forEach(conn => { - html += ''; - }); - - $("#node_cluster_cloudconnection").empty(); - $("#node_cluster_cloudconnection").append(html); - - } catch (error) { - console.error('Failed to update connections:', error); - } -} - export async function addNewNodeGroup() { Create_Cluster_Config_Arr = new Array(); Create_Node_Config_Arr = new Array(); @@ -519,7 +435,7 @@ export async function addNewNodeGroup() { $("#node_cluster_name").val(cluster_name); $("#node_cluster_desc").val(cluster_desc); - // Set Provider (fixed, single option, disabled) + // Set Provider (fixed, single option, disabled - display only) $("#node_cluster_provider").html( '