diff --git a/front/assets/js/common/api/http.js b/front/assets/js/common/api/http.js index 621ef113..9720141f 100644 --- a/front/assets/js/common/api/http.js +++ b/front/assets/js/common/api/http.js @@ -1,117 +1,248 @@ import axios from 'axios'; -export async function commonAPIPost(url, data, attempt) { - activePageLoader() - if (attempt === undefined) { - attempt = false; +// 활성 프로그레스 toast 추적 / Track active progress toasts +const activeProgressToasts = new Map(); + +/** + * API 호출 시작 시 개별 progress toast 표시 + * Show individual progress toast when API call starts + * + * @param {string} url - API URL + * @param {string} label - 사용자에게 표시할 레이블 / Label to display to user + * @returns {string} toastId - 생성된 toast의 ID / Created toast ID + */ +function showAPIProgressToast(url, label) { + const toastId = `api-progress-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + + if (webconsolejs && webconsolejs['common/utils/toast'] && + webconsolejs['common/utils/toast'].showProgressToast) { + webconsolejs['common/utils/toast'].showProgressToast( + toastId, + label || 'Loading...' + ); + + activeProgressToasts.set(toastId, { + url, + label, + startTime: Date.now() + }); + } + + return toastId; +} + +/** + * API 완료 시 progress toast 제거 + * Hide progress toast when API completes + * + * @param {string} toastId - 제거할 toast ID / Toast ID to remove + * @param {boolean} success - 성공 여부 / Success status + * @param {string} message - 완료 메시지 (선택) / Completion message (optional) + */ +function hideAPIProgressToast(toastId, success = true, message) { + if (toastId && activeProgressToasts.has(toastId)) { + if (webconsolejs && webconsolejs['common/utils/toast']) { + // Progress toast 제거 / Remove progress toast + if (webconsolejs['common/utils/toast'].hideToast) { + webconsolejs['common/utils/toast'].hideToast(toastId); + } + + // 성공/실패 toast 표시 (선택사항) / Show success/failure toast (optional) + if (message && webconsolejs['common/util'] && webconsolejs['common/util'].showToast) { + webconsolejs['common/util'].showToast( + message, + success ? 'success' : 'error', + success ? 2000 : 5000 + ); + } } - console.log("#### commonAPIPost", ); - console.log("Request URL :", url); - console.log("Request Data :", JSON.stringify(data)); - console.log("-----------------------"); - try { - if( data === undefined || data === null) { - var response = await axios.post(url); - } else if (data.formData instanceof FormData) { - // FormData 처리 분기 - axios 사용 - console.log("FormData detected, sending with axios"); - - // pathParams가 있으면 FormData에 추가 - if (data.pathParams) { - for (const [key, value] of Object.entries(data.pathParams)) { - data.formData.append(key, value); - } - console.log("Added pathParams to FormData:", data.pathParams); - } - - // queryParams가 있으면 FormData에 추가 - if (data.queryParams) { - for (const [key, value] of Object.entries(data.queryParams)) { - data.formData.append(key, value); - } - console.log("Added queryParams to FormData:", data.queryParams); - } - - // FormData 사용 시 Content-Type 헤더를 설정하지 않음 - // 브라우저가 자동으로 boundary 정보와 함께 올바른 헤더를 설정 - var response = await axios.post(url, data.formData); - } else { - var response = await axios.post(url, data); + + activeProgressToasts.delete(toastId); + } +} + +export async function commonAPIPost(url, data, attempt, options = {}) { + // Loader Type 결정 / Determine loader type + // options.loaderType: 'page' | 'toast' | 'none' + const loaderType = options.loaderType || 'page'; // 기본값: page + let toastId = null; + + // Loader 시작 / Start loader + if (loaderType === 'toast') { + toastId = showAPIProgressToast(url, options.progressLabel); + } else if (loaderType === 'page') { + activePageLoader(); + } + // loaderType === 'none'이면 로더 표시 안 함 / No loader if 'none' + + if (attempt === undefined) { + attempt = false; + } + + console.log("#### commonAPIPost"); + console.log("Request URL :", url); + console.log("Request Data :", JSON.stringify(data)); + console.log("Loader Type :", loaderType); + console.log("-----------------------"); + + try { + if (data === undefined || data === null) { + var response = await axios.post(url); + } else if (data.formData instanceof FormData) { + // FormData 처리 분기 - axios 사용 + console.log("FormData detected, sending with axios"); + + // pathParams가 있으면 FormData에 추가 + if (data.pathParams) { + for (const [key, value] of Object.entries(data.pathParams)) { + data.formData.append(key, value); } - console.log("#### commonAPIPost Response"); - console.log("Response status : ", response.status); - console.log("Response from : ",url, response.data); - console.log("----------------------------"); - deactivePageLoader() - return response; - } catch (error) { - console.log("#### commonAPIPost Error"); - console.log("Error from : ",url, error.response ? error.response.status : error.message); - console.log("----------------------------"); - if (!attempt || attempt === undefined) { - if (error.response && error.response.status === 429) { - webconsolejs["common/util"].showToast("Too many requests. Please try again later.", 'warning'); - return error; - } - // 404 에러는 데이터가 없는 정상적인 상황이므로 토큰 갱신하지 않음 - if (error.response && error.response.status === 404) { - console.log("Resource not found (404) - this may be normal for empty data"); - return error; - } - // 401 Unauthorized는 토큰 만료 또는 인증 실패 - if (error.response && error.response.status === 401) { - const authrefreshStatus = await webconsolejs["common/cookie/authcookie"].refreshCookieAccessToken(); - if (authrefreshStatus) { - console.log("refreshCookieAccessToken success. Retrying request with refreshed token..."); - return commonAPIPost(url, data, true); - } else { - webconsolejs["common/util"].showToast("Session has expired. Please login again.", 'error'); - window.location = "/auth/login"; - return; - } - } - // 403 Forbidden은 권한 부족 - if (error.response && error.response.status === 403) { - webconsolejs["common/util"].showToast("Insufficient permissions. Please contact your administrator.", 'error'); - return error; - } - // 500 Internal Server Error는 서버 오류 - if (error.response && error.response.status === 500) { - webconsolejs["common/util"].showToast("Server error occurred. Please try again later.", 'error'); - return error; - } - // 기타 HTTP 에러 - if (error.response && (error.response.status !== 200)) { - const authrefreshStatus = await webconsolejs["common/cookie/authcookie"].refreshCookieAccessToken(); - if (authrefreshStatus) { - console.log("refreshCookieAccessToken success. Retrying request with refreshed token..."); - return commonAPIPost(url, data, true); - } else { - // 토큰 갱신 실패 시 에러 메시지만 표시하고 로그인 페이지로 리다이렉트하지 않음 - webconsolejs["common/util"].showToast("An error occurred. Please try again later.", 'error'); - return error; - } - } + console.log("Added pathParams to FormData:", data.pathParams); + } + + // queryParams가 있으면 FormData에 추가 + if (data.queryParams) { + for (const [key, value] of Object.entries(data.queryParams)) { + data.formData.append(key, value); } - deactivePageLoader(); - - // 네트워크 오류나 기타 예외 상황 처리 - if (!error.response) { - // 네트워크 오류 (서버에 연결할 수 없음) - if (error.code === 'ECONNABORTED') { - webconsolejs["common/util"].showToast("Request timeout. Please check your network connection and try again.", 'error'); - } else if (error.code === 'ERR_NETWORK') { - webconsolejs["common/util"].showToast("Network connection failed. Please check your internet connection and try again.", 'error'); - } else { - webconsolejs["common/util"].showToast("An error occurred while processing the request: " + error.message, 'error'); - } + console.log("Added queryParams to FormData:", data.queryParams); + } + + // FormData 사용 시 Content-Type 헤더를 설정하지 않음 + // 브라우저가 자동으로 boundary 정보와 함께 올바른 헤더를 설정 + var response = await axios.post(url, data.formData); + } else { + var response = await axios.post(url, data); + } + + console.log("#### commonAPIPost Response"); + console.log("Response status : ", response.status); + console.log("Response from : ", url, response.data); + console.log("----------------------------"); + + // Loader 종료 / End loader + if (loaderType === 'toast' && toastId) { + hideAPIProgressToast(toastId, true, options.successMessage); + } else if (loaderType === 'page') { + deactivePageLoader(); + } + + return response; + } catch (error) { + console.log("#### commonAPIPost Error"); + console.log("Error from : ", url, error.response ? error.response.status : error.message); + console.log("----------------------------"); + + if (!attempt || attempt === undefined) { + if (error.response && error.response.status === 429) { + webconsolejs["common/util"].showToast("Too many requests. Please try again later.", 'warning'); + // Loader 종료 / End loader + if (loaderType === 'toast' && toastId) { + hideAPIProgressToast(toastId, false); + } else if (loaderType === 'page') { + deactivePageLoader(); + } + return error; + } + // 404 에러는 데이터가 없는 정상적인 상황이므로 토큰 갱신하지 않음 + if (error.response && error.response.status === 404) { + console.log("Resource not found (404) - this may be normal for empty data"); + // Loader 종료 / End loader + if (loaderType === 'toast' && toastId) { + hideAPIProgressToast(toastId, false); + } else if (loaderType === 'page') { + deactivePageLoader(); + } + return error; + } + // 401 Unauthorized는 토큰 만료 또는 인증 실패 + if (error.response && error.response.status === 401) { + const authrefreshStatus = await webconsolejs["common/cookie/authcookie"].refreshCookieAccessToken(); + if (authrefreshStatus) { + console.log("refreshCookieAccessToken success. Retrying request with refreshed token..."); + return commonAPIPost(url, data, true, options); } else { - // HTTP 에러가 있지만 위에서 처리되지 않은 경우 - webconsolejs["common/util"].showToast("An error occurred while processing the request. (Status code: " + error.response.status + ")", 'error'); + // Loader 종료 / End loader + if (loaderType === 'toast' && toastId) { + hideAPIProgressToast(toastId, false); + } else if (loaderType === 'page') { + deactivePageLoader(); + } + webconsolejs["common/util"].showToast("Session has expired. Please login again.", 'error'); + window.location = "/auth/login"; + return; } - + } + // 403 Forbidden은 권한 부족 + if (error.response && error.response.status === 403) { + // Loader 종료 / End loader + if (loaderType === 'toast' && toastId) { + hideAPIProgressToast(toastId, false); + } else if (loaderType === 'page') { + deactivePageLoader(); + } + webconsolejs["common/util"].showToast("Insufficient permissions. Please contact your administrator.", 'error'); return error; + } + // 500 Internal Server Error는 서버 오류 + if (error.response && error.response.status === 500) { + // Loader 종료 / End loader + if (loaderType === 'toast' && toastId) { + hideAPIProgressToast(toastId, false); + } else if (loaderType === 'page') { + deactivePageLoader(); + } + webconsolejs["common/util"].showToast("Server error occurred. Please try again later.", 'error'); + return error; + } + // 기타 HTTP 에러 + if (error.response && (error.response.status !== 200)) { + const authrefreshStatus = await webconsolejs["common/cookie/authcookie"].refreshCookieAccessToken(); + if (authrefreshStatus) { + console.log("refreshCookieAccessToken success. Retrying request with refreshed token..."); + return commonAPIPost(url, data, true, options); + } else { + // Loader 종료 / End loader + if (loaderType === 'toast' && toastId) { + hideAPIProgressToast(toastId, false); + } else if (loaderType === 'page') { + deactivePageLoader(); + } + // 토큰 갱신 실패 시 에러 메시지만 표시하고 로그인 페이지로 리다이렉트하지 않음 + webconsolejs["common/util"].showToast("An error occurred. Please try again later.", 'error'); + return error; + } + } + } + + // Loader 종료 / End loader + if (loaderType === 'toast' && toastId) { + hideAPIProgressToast(toastId, false); + } else if (loaderType === 'page') { + deactivePageLoader(); + } + + // 네트워크 오류나 기타 예외 상황 처리 + if (!error.response) { + // 네트워크 오류 (서버에 연결할 수 없음) + if (error.code === 'ECONNABORTED') { + webconsolejs["common/util"].showToast("Request timeout. Please check your network connection and try again.", 'error'); + } else if (error.code === 'ERR_NETWORK') { + webconsolejs["common/util"].showToast("Network connection failed. Please check your internet connection and try again.", 'error'); + } else { + webconsolejs["common/util"].showToast("An error occurred while processing the request: " + error.message, 'error'); + } + } else { + // HTTP 에러가 있지만 위에서 처리되지 않은 경우 + if (error.response.status) { + webconsolejs["common/util"].showToast("An error occurred while processing the request. (Status code: " + error.response.status + ")", 'error'); + } else { + webconsolejs["common/util"].showToast("An error occurred while processing the request: " + error.message, 'error'); + } } + + return error; + } } export async function commonAPIPostWithoutRetry(url, data) { diff --git a/front/assets/js/common/api/services/pmk_api.js b/front/assets/js/common/api/services/pmk_api.js index 30da3227..74cb52dc 100644 --- a/front/assets/js/common/api/services/pmk_api.js +++ b/front/assets/js/common/api/services/pmk_api.js @@ -1,7 +1,7 @@ // PMK API 관련 // 받아온 project(namespace)로 PmkList GET -export async function getClusterList(nsId) { +export async function getClusterList(nsId, options = {}) { if (nsId == "") { alert("Project has not set") @@ -17,14 +17,16 @@ export async function getClusterList(nsId) { var controller = "/api/" + "mc-infra-manager/" + "GetAllK8sCluster"; const response = await webconsolejs["common/api/http"].commonAPIPost( controller, - data + data, + false, + options ) var pmkList = response.data.responseData; return pmkList } -export async function getCluster(nsId, clusterId) { +export async function getCluster(nsId, clusterId, options = {}) { // Validation: Check nsId if (!nsId || nsId === "") { webconsolejs['partials/layout/modal'].commonShowDefaultModal( @@ -55,7 +57,9 @@ export async function getCluster(nsId, clusterId) { var controller = "/api/" + "mc-infra-manager/" + "Getk8scluster"; const response = await webconsolejs["common/api/http"].commonAPIPost( controller, - data + data, + false, + options ); // error check를 위해 response를 return @@ -305,7 +309,7 @@ export async function getProviderList() { return response.data.responseData.output } -export async function getRegionList() { +export async function getRegionList(options = {}) { // let data = { // pathParams: { @@ -317,13 +321,15 @@ export async function getRegionList() { let controller = "/api/" + "mc-infra-manager/" + "RetrieveRegionListFromCsp"; let response = await webconsolejs["common/api/http"].commonAPIPost( controller, - + undefined, + false, + options ); return response.data.responseData.region } -export async function getCloudConnection() { +export async function getCloudConnection(options = {}) { // test @@ -335,7 +341,9 @@ export async function getCloudConnection() { let controller = "/api/" + "mc-infra-manager/" + "GetConnConfigList"; let response = await webconsolejs["common/api/http"].commonAPIPost( controller, - data + data, + false, + options ); return response.data.responseData.connectionconfig @@ -414,12 +422,13 @@ export async function createNode(k8sClusterId, nsId, Create_Node_Config_Arr) { data ); - // 성공 처리 - if (response && response.status === 200) { + // 성공 처리 (200 OK 또는 201 Created) + if (response && (response.status === 200 || response.status === 201)) { webconsolejs["common/util"].showToast('Node group creation request completed successfully', 'success'); return response; } else { console.error('Node creation failed:', response); + console.error('Response status:', response?.status, 'Type:', typeof response?.status); webconsolejs["common/util"].showToast('Failed to create node group', 'error'); return response; } @@ -647,7 +656,7 @@ export function calculateVmStatusCount(aPmk) { return vmStatusCountMap; } -export function pmkDelete(nsId, k8sClusterId) { +export function pmkDelete(nsId, k8sClusterId, options = {}) { // API 레벨 Validation (추가 안전장치) if (!nsId || nsId === '' || !k8sClusterId || k8sClusterId === '') { console.error('Invalid parameters for PMK deletion:', { @@ -670,12 +679,14 @@ export function pmkDelete(nsId, k8sClusterId) { let controller = '/api/' + 'mc-infra-manager/' + 'Deletek8scluster'; let response = webconsolejs['common/api/http'].commonAPIPost( controller, - data + data, + false, + options ); return response; } -export function nodeGroupDelete(nsId, k8sClusterId, k8sNodeGroupName) { +export function nodeGroupDelete(nsId, k8sClusterId, k8sNodeGroupName, options = {}) { // API 레벨 Validation (추가 안전장치) if (!nsId || nsId === '' || !k8sClusterId || k8sClusterId === '' || @@ -702,7 +713,9 @@ export function nodeGroupDelete(nsId, k8sClusterId, k8sNodeGroupName) { let controller = '/api/' + 'mc-infra-manager/' + 'Deletek8snodegroup'; let response = webconsolejs['common/api/http'].commonAPIPost( controller, - data + data, + false, + options ); return response; } diff --git a/front/assets/js/common/utils/listRefreshPattern.js b/front/assets/js/common/utils/listRefreshPattern.js new file mode 100644 index 00000000..eb92b225 --- /dev/null +++ b/front/assets/js/common/utils/listRefreshPattern.js @@ -0,0 +1,194 @@ +/** + * List Refresh Pattern 유틸리티 + * List Refresh Pattern Utility + * + * 목록 화면의 일관된 refresh 동작을 제공하는 공통 패턴 + * Provides consistent refresh behavior for list screens + * + * @module listRefreshPattern + */ + +/** + * 범용 List Refresh 패턴 실행 + * Execute universal list refresh pattern + * + * @param {Object} config - Refresh 설정 객체 / Refresh configuration object + * @param {Function} config.getSelectionId - 현재 선택된 항목 ID를 반환하는 함수 / Function to get current selection ID + * @param {Array} config.detailElementIds - 숨겨야 할 상세 영역 element ID 배열 / Array of detail element IDs to hide + * @param {Array} config.detailElementsToEmpty - 내용을 비워야 할 element ID 배열 / Array of element IDs to empty + * @param {Array} config.formsToClose - 닫아야 할 폼 element ID 배열 / Array of form element IDs to close + * @param {Function} config.fetchListData - 목록 데이터를 가져오는 async 함수 / Async function to fetch list data + * @param {Function} config.updateListCallback - 가져온 데이터로 목록을 업데이트하는 함수 / Function to update list with fetched data + * @param {Function} config.getRowById - ID로 row 객체를 가져오는 함수 / Function to get row object by ID + * @param {Function} config.selectRow - row를 선택하는 함수 / Function to select a row + * @param {Function} config.showDetailData - 선택된 항목의 상세 정보를 표시하는 async 함수 / Async function to show detail data + * @param {Function} config.clearSelectionState - 선택 상태를 초기화하는 함수 / Function to clear selection state + * @param {string} config.errorMessage - 에러 메시지 (선택사항) / Error message (optional) + * @returns {Promise} - { success: boolean, state: Object, error: Error } + */ +export async function execute(config) { + try { + // 설정 검증 / Validate configuration + if (!validateConfig(config)) { + console.error('Invalid refresh config:', config); + return { success: false, error: 'Invalid configuration' }; + } + + // 1. 현재 선택 상태 저장 / Save current selection state + const state = saveState(config); + + // 2. UI 초기화 / Reset UI + resetUI(config); + + // 3. 데이터 조회 및 업데이트 / Fetch and update data + await refreshData(config); + + // 4. 상태 복원 / Restore state + await restoreState(config, state); + + return { success: true, state }; + } catch (error) { + console.error('List refresh pattern error:', error); + handleError(config, error); + return { success: false, error }; + } +} + +/** + * 설정 객체 검증 + * Validate configuration object + * + * @param {Object} config - 검증할 설정 객체 / Configuration object to validate + * @returns {boolean} - 검증 결과 / Validation result + */ +function validateConfig(config) { + const required = ['fetchListData', 'updateListCallback']; + return required.every(key => typeof config[key] === 'function'); +} + +/** + * 현재 상태 저장 + * Save current state + * + * @param {Object} config - 설정 객체 / Configuration object + * @returns {Object} - 저장된 상태 / Saved state + */ +function saveState(config) { + return { + selectedId: config.getSelectionId ? config.getSelectionId() : null, + timestamp: Date.now() + }; +} + +/** + * UI 초기화 + * Reset UI elements + * + * @param {Object} config - 설정 객체 / Configuration object + */ +function resetUI(config) { + // 상세 영역 숨기기 / Hide detail areas + if (config.detailElementIds && Array.isArray(config.detailElementIds)) { + config.detailElementIds.forEach(id => { + $(`#${id}`).hide(); + }); + } + + // 내용 비우기 / Empty content areas + if (config.detailElementsToEmpty && Array.isArray(config.detailElementsToEmpty)) { + config.detailElementsToEmpty.forEach(id => { + $(`#${id}`).empty(); + }); + } + + // 폼 닫기 / Close forms + if (config.formsToClose && Array.isArray(config.formsToClose)) { + config.formsToClose.forEach(formId => { + const form = document.getElementById(formId); + if (form && form.classList.contains('active')) { + webconsolejs['partials/layout/navigatePages'].toggleSubElement(form); + } + }); + } +} + +/** + * 데이터 조회 및 목록 업데이트 + * Fetch data and update list + * + * @param {Object} config - 설정 객체 / Configuration object + */ +async function refreshData(config) { + const data = await config.fetchListData(); + config.updateListCallback(data); +} + +/** + * 선택 상태 복원 + * Restore selection state + * + * @param {Object} config - 설정 객체 / Configuration object + * @param {Object} state - 저장된 상태 / Saved state + */ +async function restoreState(config, state) { + if (!state.selectedId || !config.getRowById) { + return; + } + + const row = config.getRowById(state.selectedId); + + if (row) { + // 항목이 여전히 존재하면 선택 복원 / Item still exists, restore selection + if (config.selectRow) { + config.selectRow(state.selectedId); + } + if (config.showDetailData) { + await config.showDetailData(); + } + } else { + // 항목이 삭제되었으면 상태 초기화 / Item deleted, clear state + if (config.clearSelectionState) { + config.clearSelectionState(); + } + } +} + +/** + * 에러 처리 + * Handle error + * + * @param {Object} config - 설정 객체 / Configuration object + * @param {Error} error - 에러 객체 / Error object + */ +function handleError(config, error) { + const message = config.errorMessage || 'Failed to refresh list. Please try again.'; + if (webconsolejs && webconsolejs['common/util'] && webconsolejs['common/util'].showToast) { + webconsolejs['common/util'].showToast(message, 'error'); + } else { + console.error(message, error); + } +} + +// Export functions +export const ListRefreshPattern = { + execute, + validateConfig, + saveState, + resetUI, + refreshData, + restoreState, + handleError +}; + +// Webpack에 등록 / Register to webpack +if (typeof webconsolejs === 'undefined') { + window.webconsolejs = {}; +} +if (typeof webconsolejs['common/utils/listRefreshPattern'] === 'undefined') { + webconsolejs['common/utils/listRefreshPattern'] = ListRefreshPattern; +} + +// Default export +export default ListRefreshPattern; + + diff --git a/front/assets/js/pages/configuration/workspace/manage.js b/front/assets/js/pages/configuration/workspace/manage.js index e4326785..ad727a03 100644 --- a/front/assets/js/pages/configuration/workspace/manage.js +++ b/front/assets/js/pages/configuration/workspace/manage.js @@ -254,6 +254,13 @@ export async function deleteWorkspace() { "workspaceId": workspace.workspace_id, } }; + + // const data = { + // request: { + // "name": name, + // "description": desc, + // } + // } let controller = "/api/" + "deleteworkspace"; let response = await webconsolejs["common/api/http"].commonAPIPost( controller, diff --git a/front/assets/js/pages/operation/manage/pmk.js b/front/assets/js/pages/operation/manage/pmk.js index 1becbac5..2f7b9474 100644 --- a/front/assets/js/pages/operation/manage/pmk.js +++ b/front/assets/js/pages/operation/manage/pmk.js @@ -1,9 +1,90 @@ import { TabulatorFull as Tabulator } from "tabulator-tables"; +/** + * =================================================================== + * PMK WORKLOADS PAGE - LOADER STRATEGY + * =================================================================== + * 📄 Page Loader: Create, Delete, Update, Synchronous Fetch operations + * 🔔 Toast Loader: Asynchronous background data loading + * ⚪ No Loader: Background status updates + * =================================================================== + */ + +// PMK Loader Configuration / PMK 로더 설정 +const PMK_LOADER_CONFIG = { + // 생성 작업 / Create operations + create: { + cluster: { loaderType: 'page' }, + nodeGroup: { loaderType: 'page' } + }, + + // 삭제 작업 / Delete operations + delete: { + cluster: { loaderType: 'page' }, + nodeGroup: { loaderType: 'page' } + }, + + // 조회 작업 / Fetch operations + fetch: { + // 동기 조회 - Page Loader (사용자가 결과를 기다려야 함) + clusterList: { + loaderType: 'page' // 변경: GetAllK8sCluster는 동기적으로 기다려야 함 + }, + clusterDetail: { + loaderType: 'page' // 변경: Getk8scluster는 동기적으로 기다려야 함 + }, + + // 비동기 조회 - Toast Loader (백그라운드 데이터) + monitoring: { + loaderType: 'toast', + progressLabel: 'Loading Monitoring Data...', + successMessage: null + } + } +}; + +// PMK API Helper / PMK API 헬퍼 +const PmkApiHelper = { + // 조회 작업 / Fetch operations + async getClusterList(nsId) { + return await webconsolejs["common/api/services/pmk_api"].getClusterList( + nsId, + PMK_LOADER_CONFIG.fetch.clusterList + ); + }, + + async getClusterDetail(nsId, clusterId) { + return await webconsolejs["common/api/services/pmk_api"].getCluster( + nsId, + clusterId, + PMK_LOADER_CONFIG.fetch.clusterDetail + ); + }, + + // 삭제 작업 / Delete operations + async deleteCluster(nsId, clusterId) { + return await webconsolejs["common/api/services/pmk_api"].pmkDelete( + nsId, + clusterId, + PMK_LOADER_CONFIG.delete.cluster + ); + }, + + async deleteNodeGroup(nsId, clusterId, nodeGroupName) { + return await webconsolejs["common/api/services/pmk_api"].nodeGroupDelete( + nsId, + clusterId, + nodeGroupName, + PMK_LOADER_CONFIG.delete.nodeGroup + ); + } +}; + // navBar에 있는 object인데 직접 handling( onchange) $("#select-current-project").on('change', async function () { let project = { "Id": this.value, "Name": this.options[this.selectedIndex].text, "NsId": this.options[this.selectedIndex].text } webconsolejs["common/api/services/workspace_api"].setCurrentProject(project)// 세션에 저장 + // Using direct API call with default page loader for project change var respPmkList = await webconsolejs["common/api/services/pmk_api"].getClusterList(project.NsId); getPmkListCallbackSuccess(project.NsId, respPmkList); }) @@ -100,21 +181,82 @@ async function initPmk() { } // pmk목록 조회. init, refresh 에서 사용 +/** + * PMK 목록 새로고침 + * Refresh PMK list + * + * List Refresh Pattern을 사용하여 일관된 refresh 동작 제공 + * Uses List Refresh Pattern for consistent refresh behavior + * + * 적용 시나리오 / Applied scenarios: + * - 화면 최초 로드 시 / Initial screen load + * - Refresh 아이콘 클릭 시 / Refresh icon click + * - NodeGroup 추가/삭제 후 / After NodeGroup add/delete + * - Cluster 삭제 후 / After Cluster delete + */ export async function refreshPmkList() { - if (selectedWorkspaceProject.projectId != "") { - var selectedProjectId = selectedWorkspaceProject.projectId; - var selectedNsId = selectedWorkspaceProject.nsId; + if (selectedWorkspaceProject.projectId != "") { + var selectedProjectId = selectedWorkspaceProject.projectId; + var selectedNsId = selectedWorkspaceProject.nsId; + + // List Refresh Pattern 설정 / List Refresh Pattern configuration + const config = { + // 현재 선택 ID 가져오기 / Get current selection ID + getSelectionId: () => currentPmkId, + + // 숨길 상세 영역 / Detail areas to hide + detailElementIds: ['cluster_info'], + + // 내용을 비울 영역 / Areas to empty + detailElementsToEmpty: ['pmk_nodegroup_info_box', 'pmk_node_info_box'], - //getPmkList();// project가 선택되어 있으면 pmk목록을 조회한다. - var respPmkList = await webconsolejs["common/api/services/pmk_api"].getClusterList(selectedNsId); + // 닫을 폼 / Forms to close + formsToClose: ['nodegroup_configuration'], + + // 목록 데이터 조회 / Fetch list data + fetchListData: async () => { + return await PmkApiHelper.getClusterList(selectedNsId); + }, + + // 목록 업데이트 / Update list + updateListCallback: (respPmkList) => { getPmkListCallbackSuccess(selectedProjectId, respPmkList); + }, - if (currentPmkId != undefined) { - toggleRowSelection(currentPmkId) - getSelectedPmkData() + // Row 가져오기 / Get row by ID + getRowById: (id) => { + try { + return pmkListTable.getRow(id); + } catch (e) { + return null; } - //////////////////// pmkId를 set하고 조회 완료. //////////////// - } + }, + + // Row 선택 / Select row + selectRow: (id) => { + toggleRowSelection(id); + }, + + // 상세 정보 표시 / Show detail data + showDetailData: async () => { + await getSelectedPmkData(); + }, + + // 선택 상태 초기화 / Clear selection state + clearSelectionState: () => { + currentPmkId = ''; + currentNodeGroupName = ''; + currentProvider = ''; + selectedClusterData = {}; + }, + + // 에러 메시지 / Error message + errorMessage: 'Failed to refresh PMK list. Please try again.' + }; + + // Pattern 실행 / Execute pattern + await webconsolejs['common/utils/listRefreshPattern'].execute(config); + } } // getPmkList 호출 성공 시 @@ -146,6 +288,10 @@ function mappingTablePmkData(totalPmkListObj) { const securityGroup = (network.SecurityGroupIIDs && network.SecurityGroupIIDs[0] && network.SecurityGroupIIDs[0].SystemId) || "N/A"; const version = item.spiderViewK8sClusterDetail?.Version || "N/A"; const nodeGroupCount = item.spiderViewK8sClusterDetail?.NodeGroupList?.length || 0; + + // Status 직접 사용 (Cluster Info와 동일하게) + const clusterStatus = item.spiderViewK8sClusterDetail?.Status || "N/A"; + return { name: item.name, id: item.id, @@ -157,6 +303,7 @@ function mappingTablePmkData(totalPmkListObj) { // TODO : ima, provider api res 변경되면 수정 providerImg: item.connectionConfig.providerName || "", // providerImg 값을 추가해야 함 (필요시) provider: item.connectionConfig.providerName || "N/A", + status: clusterStatus, vpc: vpc, subnet: subnet, securitygroup: securityGroup, @@ -174,7 +321,7 @@ export async function getSelectedPmkData() { var selectedNsId = selectedWorkspaceProject.nsId; try { - var pmkResp = await webconsolejs["common/api/services/pmk_api"].getCluster(selectedNsId, currentPmkId); + var pmkResp = await PmkApiHelper.getClusterDetail(selectedNsId, currentPmkId); // Check if pmkResp exists if (!pmkResp) { @@ -218,7 +365,7 @@ export async function getSelectedPmkData() { } // pmk 삭제 -export function deletePmk() { +export async function deletePmk() { // Validation 1: PMK가 선택되었는지 확인 if (!currentPmkId || currentPmkId === '') { webconsolejs['partials/layout/modal'].commonShowDefaultModal( @@ -238,12 +385,71 @@ export function deletePmk() { return; } - // Validation 통과 후 API 호출 - webconsolejs['common/api/services/pmk_api'].pmkDelete(selectedNsId, currentPmkId); + // Validation 3: Tencent 클러스터의 경우 NodeGroup이 없어야 삭제 가능 + if (currentProvider && currentProvider.toLowerCase() === 'tencent') { + // selectedClusterData에서 NodeGroup 목록 확인 + var nodeGroupList = selectedClusterData?.responseData?.spiderViewK8sClusterDetail?.NodeGroupList || + selectedClusterData?.spiderViewK8sClusterDetail?.NodeGroupList || + []; + + if (Array.isArray(nodeGroupList) && nodeGroupList.length > 0) { + webconsolejs['partials/layout/modal'].commonShowDefaultModal( + 'Tencent Cluster Delete Restriction', + 'Tencent clusters can only be deleted when there are no NodeGroups.
' + + 'Please delete all NodeGroups first.

' + + 'Current NodeGroups: ' + nodeGroupList.length + '' + ); + return; + } + } + + // 삭제 요청만 보내고 결과를 기다리지 않음 (fire and forget) + PmkApiHelper.deleteCluster( + selectedNsId, + currentPmkId + ); + + // 즉시 Toast 메시지 표시 + webconsolejs['common/util'].showToast('Cluster deletion request has been sent', 'info'); + + // 전역 변수 초기화 + currentPmkId = ''; + currentNodeGroupName = ''; + currentProvider = ''; + selectedClusterData = {}; + + // PMK 상세 정보 초기화 + $('#cluster_info_name').text('N/A'); + $('#cluster_info_version').text('N/A'); + $('#cluster_info_status').text('N/A'); + $('#cluster_info_vpc').text('N/A'); + $('#cluster_info_subnet').text('N/A'); + $('#cluster_info_securitygroup').text('N/A'); + $('#cluster_info_cloudconnection').text('N/A'); + $('#cluster_info_endpoint').text('N/A'); + + // NodeGroup List 초기화 + $('#pmk_nodegroup_info_box').empty(); + + // Node 상세 정보 초기화 + $('#pmk_node_info_box').empty(); + + // NodeGroup Info 영역 초기화 및 숨기기 + clearServerInfo(); + const nodeGroupInfoDiv = document.getElementById("nodeGroup_info"); + if (nodeGroupInfoDiv && nodeGroupInfoDiv.classList.contains("active")) { + webconsolejs["partials/layout/navigatePages"].toggleElement(nodeGroupInfoDiv); + } + + // Cluster Info 영역 숨기기 (초기 화면처럼) + $('#cluster_info').hide(); + + // PMK 목록 새로고침 + await refreshPmkList(); } // nodegroup 삭제 -export function deleteNodeGroup() { +export async function deleteNodeGroup() { // Validation 1: NodeGroup이 선택되었는지 확인 if (!currentNodeGroupName || currentNodeGroupName === '') { webconsolejs['partials/layout/modal'].commonShowDefaultModal( @@ -272,16 +478,38 @@ export function deleteNodeGroup() { return; } - // Validation 통과 후 API 호출 - webconsolejs['common/api/services/pmk_api'].nodeGroupDelete( + // 삭제 요청만 보내고 결과를 기다리지 않음 (fire and forget) + PmkApiHelper.deleteNodeGroup( selectedNsId, currentPmkId, currentNodeGroupName ); + + // 즉시 메시지 표시 + webconsolejs['common/util'].showToast('NodeGroup deletion request has been sent', 'info'); + + // 선택된 NodeGroup 정보 초기화 + currentNodeGroupName = ''; + + // Node 상세 정보 초기화 + $('#pmk_node_info_box').empty(); + + // NodeGroup Info 영역 초기화 및 숨기기 + clearServerInfo(); + const nodeGroupInfoDiv = document.getElementById("nodeGroup_info"); + if (nodeGroupInfoDiv && nodeGroupInfoDiv.classList.contains("active")) { + webconsolejs["partials/layout/navigatePages"].toggleElement(nodeGroupInfoDiv); + } + + // PMK 목록 새로고침 (ListRefreshPattern이 자동으로 상세 정보 표시) + await refreshPmkList(); } // 클릭한 pmk의 info값 세팅 function setPmkInfoData(pmkData) { + // Cluster Info 영역 표시 + $('#cluster_info').show(); + var clusterData = pmkData.responseData; var clusterDetailData = clusterData.spiderViewK8sClusterDetail; var pmkNetwork = clusterDetailData?.Network || {}; @@ -293,8 +521,10 @@ function setPmkInfoData(pmkData) { try { - var pmkName = clusterData.name; - var pmkID = clusterData.id + // Name, CspName, CspId 구분 + var pmkName = clusterData.name || "N/A"; + var pmkCspName = clusterDetailData?.IId?.NameId || "N/A"; + var pmkCspId = clusterDetailData?.IId?.SystemId || "N/A"; var pmkVersion = clusterDetailData?.Version || "N/A"; pmkStatus = clusterDetailData?.Status || "N/A"; @@ -315,7 +545,8 @@ function setPmkInfoData(pmkData) { // var totalNodeGroupCount = (clusterDetailData.NodeGroupList == null) ? 0 : clusterDetailData.NodeGroupList.length; $("#cluster_info_name").text(pmkName); - // $("#cluster_info_name").text(pmkName + " / " + pmkID); + $("#cluster_info_cspname").text(pmkCspName); + $("#cluster_info_cspid").text(pmkCspId); $("#cluster_info_version").text(pmkVersion); $("#cluster_info_status").text(pmkStatus); @@ -370,7 +601,7 @@ function displayNodeGroupStatusList(pmkID, clusterProvider, clusterData) { nodeGroupList.forEach((aNodeGroup) => { var nodeID = aNodeGroup.IId.SystemId; - var nodeName = aNodeGroup.IId.name; + var nodeName = aNodeGroup.IId.NameId; var nodeStatus = aNodeGroup.Status; if (clusterProvider === "azure") { @@ -379,20 +610,34 @@ function displayNodeGroupStatusList(pmkID, clusterProvider, clusterData) { } var nodeStatusClass = webconsolejs["common/api/services/pmk_api"].getVmStatusStyleClass(nodeStatus); + // 텍스트 길이 제한 (10자 초과 시 ... 표시) + var displayName = nodeName.length > 10 ? nodeName.substring(0, 10) + '...' : nodeName; + nodeLi += `
  • + style="display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + padding: 10px 15px; + min-width: 150px; + min-height: 60px; + cursor: pointer;" + onclick="webconsolejs['pages/operation/manage/pmk'].toggleNodeCheck('${pmkID}', '${nodeID}')" + title="${nodeName}"> - ${nodeID} + ${displayName}
  • `; @@ -481,6 +726,8 @@ export async function nodeGroupDetailInfo(pmkID, aNodeObject, nodeID) { var ngName = nodeGroupDetail.IId.NameId || nodeGroupDetail.IId.SystemId || aNode.cspResourceId currentNodeGroupName = ngName + var ngId = aNode.cspResourceId || nodeGroupDetail.IId.SystemId || 'N/A' + var ngStatus = aNode.status || 'N/A' var ngImage = nodeGroupDetail.ImageIID.NameId || "AL2023_x86_64_STANDARD" var ngSpec = nodeGroupDetail.VMSpecName || "t3.medium" @@ -495,6 +742,8 @@ export async function nodeGroupDetailInfo(pmkID, aNodeObject, nodeID) { // Info SET $("#ng_info_name").text(ngName) + $("#ng_info_id").text(ngId) + $("#ng_info_status").text(ngStatus) $("#ng_info_image").text(ngImage) $("#ng_info_spec").text(ngSpec) @@ -529,6 +778,46 @@ function displayNodeStatusList(nodeData) { } } +// Cluster Info 초기화 +function clearClusterInfo() { + // Cluster Info 필드 초기화 + $("#cluster_info_name").text("N/A"); + $("#cluster_info_cspname").text("N/A"); + $("#cluster_info_cspid").text("N/A"); + $("#cluster_info_version").text("N/A"); + $("#cluster_info_status").text("N/A"); + $("#cluster_info_vpc").text("N/A"); + $("#cluster_info_subnet").text("N/A"); + $("#cluster_info_securitygroup").text("N/A"); + $("#cluster_info_cloudconnection").text("N/A"); + $("#cluster_info_endpoint").text("N/A"); +} + +// NodeGroup List & Info 초기화 +function clearNodeGroupInfo() { + // NodeGroup 선택 상태 초기화 + currentNodeGroupName = ''; + + // NodeGroup List 영역 비우기 + $('#pmk_nodegroup_info_box').empty(); + + // Node 목록 비우기 + $('#pmk_node_info_box').empty(); + + // NodeGroup Info 초기화 (clearServerInfo의 NodeGroup 부분) + $("#ng_info_name").text(""); + $("#ng_info_id").text(""); + $("#ng_info_status").text(""); + $("#ng_info_image").text(""); + $("#ng_info_spec").text(""); + $("#ng_info_keypair").text(""); + $("#ng_info_desirednodesize").text(""); + $("#ng_info_nodesize").text(""); + $("#ng_info_autoscaling").text(""); + $("#ng_info_rootdisktype").text(""); + $("#ng_info_rootdisksize").text(""); +} + // vm 세부 정보 초기화 function clearServerInfo() { @@ -538,6 +827,19 @@ function clearServerInfo() { $("#server_info_name").val("") $("#server_info_desc").val("") + // NodeGroup Info 초기화 + $("#ng_info_name").text("") + $("#ng_info_id").text("") + $("#ng_info_status").text("") + $("#ng_info_image").text("") + $("#ng_info_spec").text("") + $("#ng_info_keypair").text("") + $("#ng_info_desirednodesize").text("") + $("#ng_info_nodesize").text("") + $("#ng_info_autoscaling").text("") + $("#ng_info_rootdisktype").text("") + $("#ng_info_rootdisksize").text("") + // ip information $("#server_info_public_ip").val("") $("#server_detail_info_public_ip_text").text("") @@ -749,11 +1051,37 @@ function initPmkTable() { headerSort: false, width: 60, }, + { + title: "ProviderImg", + field: "providerImg", + formatter: providerFormatter, + vertAlign: "middle", + hozAlign: "center", + headerSort: false, + }, + { + title: "Status", + field: "status", + vertAlign: "middle", + hozAlign: "center", + }, { title: "Name", field: "name", vertAlign: "middle" }, + { + title: "Node Group", + field: "nodegroup", + vertAlign: "middle", + hozAlign: "center", + maxWidth: 150, + }, + { + title: "VPC", + field: "vpc", + vertAlign: "middle" + }, { title: "Id", field: "id", @@ -774,25 +1102,12 @@ function initPmkTable() { field: "systemMessage", visible: false }, - { - title: "ProviderImg", - field: "providerImg", - formatter: providerFormatter, - vertAlign: "middle", - hozAlign: "center", - headerSort: false, - }, { title: "Provider", field: "provider", formatter: providerFormatterString, visible: false }, - { - title: "VPC", - field: "vpc", - vertAlign: "middle" - }, { title: "Subnet", field: "subnet", @@ -808,13 +1123,6 @@ function initPmkTable() { field: "version", vertAlign: "middle", visible: false, - }, - { - title: "Node Group", - field: "nodegroup", - vertAlign: "middle", - hozAlign: "center", - maxWidth: 150, } ]; @@ -826,11 +1134,17 @@ function initPmkTable() { // vmid 초기화 for vmlifecycle // selectedClusterId = "" + // 1. 기존 UI 먼저 초기화 + clearClusterInfo(); + clearNodeGroupInfo(); + + // 2. 새로운 PMK ID 설정 currentPmkId = row.getCell("id").getValue(); - // 표에서 선택된 PmkInfo + + // 3. 표에서 선택된 PmkInfo 조회 getSelectedPmkData() - // Cluster Terminal 버튼 상태 업데이트 + // 4. Cluster Terminal 버튼 상태 업데이트 updateClusterRemoteCmdButtonState(); }); @@ -1017,21 +1331,9 @@ export async function initFormDynamic() { // Dynamic 폼용 데이터 직접 로드 async function loadFormDynamicData() { try { - // Provider 목록 로드 - const providerList = await webconsolejs["common/api/services/pmk_api"].getProviderList(); - if (providerList && Array.isArray(providerList)) { - const sortedProviders = providerList.map(str => str.toUpperCase()).sort(); - - let html = ''; - sortedProviders.forEach(item => { - html += ``; - }); - - $("#cluster_provider_dynamic").empty().append(html); - } - - // Region 목록 로드 - const regionList = await webconsolejs["common/api/services/pmk_api"].getRegionList(); + // Provider 목록은 HTML partial component로 이미 렌더링됨 + // Region 목록 로드 (백그라운드, 로더 없음) + const regionList = await webconsolejs["common/api/services/pmk_api"].getRegionList({ loaderType: 'none' }); if (regionList && Array.isArray(regionList)) { let html = ''; regionList.forEach(region => { @@ -1044,8 +1346,8 @@ async function loadFormDynamicData() { $("#cluster_region_dynamic").empty().append(html); } - // Cloud Connection 목록 로드 - const cloudConnection = await webconsolejs["common/api/services/pmk_api"].getCloudConnection(); + // Cloud Connection 목록 로드 (백그라운드, 로더 없음) + const cloudConnection = await webconsolejs["common/api/services/pmk_api"].getCloudConnection({ loaderType: 'none' }); if (cloudConnection && Array.isArray(cloudConnection)) { const connectionNames = cloudConnection.map(item => item.configName).sort(); @@ -1093,8 +1395,8 @@ async function updateFormDynamicConfigurationFiltering() { // provider 선택시 region, connection filtering if (selectedProvider !== "" && selectedRegion === "") { try { - // Region 필터링 - 선택된 Provider의 Region만 표시 - const regionList = await webconsolejs["common/api/services/pmk_api"].getRegionList(); + // Region 필터링 - 선택된 Provider의 Region만 표시 (백그라운드, 로더 없음) + const regionList = await webconsolejs["common/api/services/pmk_api"].getRegionList({ loaderType: 'none' }); if (regionList && Array.isArray(regionList)) { const filteredRegions = regionList.filter(region => region.ProviderName && region.ProviderName.toUpperCase() === selectedProvider @@ -1111,8 +1413,8 @@ async function updateFormDynamicConfigurationFiltering() { $("#cluster_region_dynamic").empty().append(html); } - // Connection 필터링 - 선택된 Provider의 Connection만 표시 - const cloudConnection = await webconsolejs["common/api/services/pmk_api"].getCloudConnection(); + // Connection 필터링 - 선택된 Provider의 Connection만 표시 (백그라운드, 로더 없음) + const cloudConnection = await webconsolejs["common/api/services/pmk_api"].getCloudConnection({ loaderType: 'none' }); if (cloudConnection && Array.isArray(cloudConnection)) { const lowerSelectedProvider = selectedProvider.toLowerCase(); const filteredConnections = cloudConnection.filter(connection => @@ -1145,7 +1447,7 @@ async function updateFormDynamicConfigurationFiltering() { const regionName = selectedRegion.replace(cspRegex, '').trim(); if (provider && regionName) { - const cloudConnection = await webconsolejs["common/api/services/pmk_api"].getCloudConnection(); + const cloudConnection = await webconsolejs["common/api/services/pmk_api"].getCloudConnection({ loaderType: 'none' }); if (cloudConnection && Array.isArray(cloudConnection)) { // Provider + Region으로 정확한 Connection 필터링 const filteredConnections = cloudConnection.filter(connection => { @@ -1224,7 +1526,7 @@ export async function deployPmkDynamic() { // 필수 필드 검증 if (!clusterData.name || !clusterData.provider || !clusterData.region || !clusterData.connection) { - alert("please fill in all required fields"); + webconsolejs['common/util'].showToast('Please fill in all required fields', 'warning'); return; } @@ -1242,7 +1544,7 @@ export async function deployPmkDynamic() { commonSpec = $("#nodegroup_commonSpecId_dynamic").val(); commonImage = $("#nodegroup_image_dynamic").val(); if (!commonSpec) { - alert("please select NodeGroup spec"); + webconsolejs['common/util'].showToast('Please select NodeGroup spec', 'warning'); return; } } else { @@ -1255,8 +1557,12 @@ export async function deployPmkDynamic() { commonImage = "default"; break; case 'alibaba': - commonSpec = "alibaba+ap-northeast-2+ecs.g6e.xlarge"; - commonImage = "alibaba+ubuntu_22_04_arm64_20g_alibase_20250625.vhd"; + //commonSpec = "alibaba+ap-northeast-2+ecs.g6e.xlarge";// tb에 미등록된 spec임. + commonSpec = "alibaba+ap-northeast-2+ecs.t6-c1m4.xlarge"; + //commonImage = "alibaba+ubuntu_22_04_arm64_20g_alibase_20250625.vhd"; + //commonImage = "alibaba+ubuntu_20_04_arm64_20g_alibase_20250625.vhd"; + commonImage = "ubuntu_20_04_arm64_20g_alibase_20250625.vhd"; + //commonImage = "alibaba+ubuntu_22_04_x64_20G_alibase_20250722.vhd"; break; case 'azure': commonSpec = "azure+koreacentral+standard_b4ms"; @@ -1266,6 +1572,10 @@ export async function deployPmkDynamic() { commonSpec = "nhncloud+kr1+m2.c4m8"; commonImage = "nhncloud+kr1+ubuntu20.04container"; break; + case 'tencent': + commonSpec = "tencent+ap-seoul+s5.medium2"; + commonImage = "img-487zeit5"; + break; default: // 기타 CSP는 빈값으로 설정 commonSpec = ""; @@ -1274,14 +1584,14 @@ export async function deployPmkDynamic() { } } - // 사전 검증 API 호출 + // 사전 검증 API 호출 (동기 - 결과 확인 필요) const checkResult = await webconsolejs["common/api/services/pmk_api"].checkK8sClusterDynamic( selectedWorkspaceProject.nsId, commonSpec ); if (!checkResult || checkResult.status !== 200) { - alert("failed to pre-validate. please check the settings"); + webconsolejs['common/util'].showToast('Failed to pre-validate. Please check the settings', 'error'); return; } @@ -1316,64 +1626,89 @@ export async function deployPmkDynamic() { if (isNodeGroupVisible) { // NodeGroup 필수 필드 검증 if (!createData.nodeGroupName) { - alert("please input NodeGroup name"); + webconsolejs['common/util'].showToast('Please input NodeGroup name', 'warning'); return; } } - // 동적 클러스터 생성 API 호출 - const result = await webconsolejs["common/api/services/pmk_api"].createK8sClusterDynamic( + // available k8sversion 조회 + if(clusterData.provider.toLowerCase() === 'alibaba'){ + //createData.k8sVersion = "1.33.3-aliyun.1"; + //createData.k8sVersion = "1.33";//(사용못함 format 안맞음) + //createData.k8sVersion = "1.31.9-aliyun.1";// + //createData.k8sVersion = "1.22.15-aliyun.1"; + // createData.k8sVersion = "1.32.7-aliyun.1"; + createData.k8sVersion = "1.32.7-aliyun.1"; + } + // const k8sVersionList = await webconsolejs["common/api/services/pmk_api"].getAvailableK8sVersionList( + // selectedWorkspaceProject.nsId + // ); + // if (k8sVersionList && k8sVersionList.status === 200) { + // console.log(k8sVersionList); + // } + // // 가져온 k8sversion 중 가장 최신 버전 선택 + // if (k8sVersionList && k8sVersionList.data && k8sVersionList.data.responseData && k8sVersionList.data.responseData.length > 0) { + // const latestK8sVersion = k8sVersionList.data.responseData[0]; + // if (!latestK8sVersion) { + // if(clusterData.provider.toLowerCase() === 'alibaba'){ + // latestK8sVersion = "1.33.3-aliyun.1"; + // } + // }else{ + // createData.k8sVersion = latestK8sVersion; + // } + // } + + // 동적 클러스터 생성 API 호출 (비동기 - 결과를 기다리지 않음) + webconsolejs["common/api/services/pmk_api"].createK8sClusterDynamic( selectedWorkspaceProject.nsId, createData ); - if (result && result.status === 200) { - alert("Cluster created successfully"); - - // 폼 초기화 - $("#cluster_name_dynamic").val(""); - $("#cluster_desc_dynamic").val(""); - $("#cluster_provider_dynamic").val(""); - $("#cluster_region_dynamic").val(""); - $("#cluster_cloudconnection_dynamic").val(""); - - // NodeGroup 폼이 표시되어 있었다면 초기화 - if (isNodeGroupVisible) { - $("#nodegroup_name_dynamic").val(""); - $("#nodegroup_spec_dynamic").val(""); - $("#nodegroup_provider_dynamic").val(""); - $("#nodegroup_connectionName_dynamic").val(""); - $("#nodegroup_commonSpecId_dynamic").val(""); - $("#nodegroup_image_dynamic").val(""); - $("#nodegroup_minnodesize_dynamic").val(""); - $("#nodegroup_maxnodesize_dynamic").val(""); - $("#nodegroup_autoscaling_dynamic").val(""); - $("#nodegroup_rootdisk_dynamic").val(""); - $("#nodegroup_rootdisksize_dynamic").val(""); - $("#nodegroup_desirednodesize_dynamic").val("1"); - - // NodeGroup 폼 숨기기 - hideNodeGroupFormDynamic(); - } + // 즉시 Toast 메시지 표시 + webconsolejs['common/util'].showToast('Cluster creation request has been sent', 'info'); - // Create Cluster 카드의 Deploy 버튼 표시 - $("#createcluster .card-footer").show(); + // 폼 초기화 + $("#cluster_name_dynamic").val(""); + $("#cluster_desc_dynamic").val(""); + $("#cluster_provider_dynamic").val(""); + $("#cluster_region_dynamic").val(""); + $("#cluster_cloudconnection_dynamic").val(""); - // PMK 목록 새로고침 - await refreshPmkList(); + // NodeGroup 폼이 표시되어 있었다면 초기화 + if (isNodeGroupVisible) { + $("#nodegroup_name_dynamic").val(""); + $("#nodegroup_spec_dynamic").val(""); + $("#nodegroup_provider_dynamic").val(""); + $("#nodegroup_connectionName_dynamic").val(""); + $("#nodegroup_commonSpecId_dynamic").val(""); + $("#nodegroup_image_dynamic").val(""); + $("#nodegroup_minnodesize_dynamic").val(""); + $("#nodegroup_maxnodesize_dynamic").val(""); + $("#nodegroup_autoscaling_dynamic").val(""); + $("#nodegroup_rootdisk_dynamic").val(""); + $("#nodegroup_rootdisksize_dynamic").val(""); + $("#nodegroup_desirednodesize_dynamic").val("1"); + + // NodeGroup 폼 숨기기 + hideNodeGroupFormDynamic(); + } - // 클러스터 생성 폼 섹션을 닫기 (NodeGroup이 표시되어 있든 없든 항상 실행) - const createClusterSection = document.querySelector('#createcluster'); - if (createClusterSection && createClusterSection.classList.contains('active')) { - webconsolejs["partials/layout/navigatePages"].toggleElement(createClusterSection); - } + // Create Cluster 카드의 Deploy 버튼 표시 + $("#createcluster .card-footer").show(); - } else { - alert("failed to create cluster"); + // 2초 대기 후 PMK 목록 새로고침 (CSP에 생성 명령이 전달되는 시간 고려) + await new Promise(resolve => setTimeout(resolve, 2000)); + await refreshPmkList(); + + // 클러스터 생성 폼 섹션을 닫기 (NodeGroup이 표시되어 있든 없든 항상 실행) + const createClusterSection = document.querySelector('#createcluster'); + if (createClusterSection && createClusterSection.classList.contains('active')) { + webconsolejs["partials/layout/navigatePages"].toggleElement(createClusterSection); } + } catch (error) { console.error("failed to create cluster:", error); - alert("failed to create cluster"); + webconsolejs['common/util'].showToast('Failed to create cluster', 'error'); } } diff --git a/front/assets/js/partials/operation/manage/clustercreate.js b/front/assets/js/partials/operation/manage/clustercreate.js index 4256414d..3a8d13b5 100644 --- a/front/assets/js/partials/operation/manage/clustercreate.js +++ b/front/assets/js/partials/operation/manage/clustercreate.js @@ -8,6 +8,72 @@ export function iniClusterkCreate() { // partial init functions webconsolejs["partials/operation/manage/clusterrecommendation"].initClusterRecommendation(webconsolejs["partials/operation/manage/clustercreate"].callbackClusterRecommendation);// recommend popup에서 사용하는 table 정의. + + // Desired Node Size +/- 버튼 이벤트 리스너 설정 + setupDesiredNodeSizeButtons(); +} + +// Desired Node Size +/- 버튼 이벤트 리스너 설정 +function setupDesiredNodeSizeButtons() { + // 기존 이벤트 핸들러 제거 (중복 방지) + $(document).off('click', '#nodegroup_configuration .input-number-decrement'); + $(document).off('click', '#nodegroup_configuration .input-number-increment'); + $(document).off('change', '#node_minnodesize'); + $(document).off('change', '#node_maxnodesize'); + + // Decrement 버튼 (-) 이벤트 핸들러 + $(document).on('click', '#nodegroup_configuration .input-number-decrement', function (e) { + e.preventDefault(); + e.stopPropagation(); + + const input = $(this).siblings('.input-number'); + const currentValue = parseInt(input.val()) || 1; + const minNodeSize = parseInt($('#node_minnodesize').val()) || 1; + + // minNodeSize 이상으로 유지 + if (currentValue > minNodeSize) { + input.val(currentValue - 1); + } + }); + + // Increment 버튼 (+) 이벤트 핸들러 + $(document).on('click', '#nodegroup_configuration .input-number-increment', function (e) { + e.preventDefault(); + e.stopPropagation(); + + const input = $(this).siblings('.input-number'); + const currentValue = parseInt(input.val()) || 1; + const maxNodeSize = parseInt($('#node_maxnodesize').val()) || 5; + + // maxNodeSize 이하로 유지 + if (currentValue < maxNodeSize) { + input.val(currentValue + 1); + } + }); + + // minNodeSize 변경 시 Desired Node Size 자동 조정 + $(document).on('change', '#node_minnodesize', function () { + const minNodeSize = parseInt($(this).val()) || 1; + const desiredInput = $('#node_desirednodesize'); + const currentDesired = parseInt(desiredInput.val()) || 1; + + // Desired Node Size가 minNodeSize보다 작으면 minNodeSize로 설정 + if (currentDesired < minNodeSize) { + desiredInput.val(minNodeSize); + } + }); + + // maxNodeSize 변경 시 Desired Node Size 자동 조정 + $(document).on('change', '#node_maxnodesize', function () { + const maxNodeSize = parseInt($(this).val()) || 5; + const desiredInput = $('#node_desirednodesize'); + const currentDesired = parseInt(desiredInput.val()) || 1; + + // Desired Node Size가 maxNodeSize보다 크면 maxNodeSize로 설정 + if (currentDesired > maxNodeSize) { + desiredInput.val(maxNodeSize); + } + }); } // callback PopupData @@ -280,6 +346,7 @@ var isNodeGroup = false // mci 생성(false) / vm 추가(true) var Create_Cluster_Config_Arr = new Array(); var Create_Node_Config_Arr = new Array(); var nodeGroup_data_cnt = 0 +var currentEditingNodeGroupIndex = null; // Edit 모드 추적용 변수 // 서버 더하기버튼 클릭시 서버정보 입력area 보이기/숨기기 @@ -371,11 +438,23 @@ export async function displayNewNodeForm() { function getPlusVm(vmElementId) { var append = ""; - append = append + '
  • '; + append = append + '
  • '; append = append + "+ NodeGroup" append = append + '
  • '; return append; } + +// + NodeGroup 클릭 시 Create 모드 시작 +export function startCreateMode() { + currentEditingNodeGroupIndex = null; // Create 모드로 초기화 + console.log("Create mode: Starting new NodeGroup creation"); + + // NodeGroup Configuration 폼 표시 + var div = document.getElementById("nodegroup_configuration"); + if (div && !div.classList.contains('show')) { + webconsolejs["partials/layout/navigatePages"].toggleSubElement(div); + } +} // 서버정보 입력 area에서 'DONE'버튼 클릭시 array에 담고 form을 초기화 var totalDeployServerCount = 0; @@ -395,7 +474,60 @@ export async function createNode() { var selectedWorkspaceProject = await webconsolejs["partials/layout/navbar"].workspaceProjectInit(); var selectedNsId = selectedWorkspaceProject.nsId; var k8sClusterId = webconsolejs["pages/operation/manage/pmk"].selectedPmkObj[0].id - webconsolejs["common/api/services/pmk_api"].createNode(k8sClusterId, selectedNsId, Create_Node_Config_Arr) + + // NodeGroup 생성 요청만 보내고 결과를 기다리지 않음 (fire and forget) + webconsolejs["common/api/services/pmk_api"].createNode( + k8sClusterId, + selectedNsId, + Create_Node_Config_Arr + ); + + // 즉시 메시지 표시 + webconsolejs['common/util'].showToast('NodeGroup creation request has been sent', 'info'); + + // NodeGroup Configuration 폼 닫기 + var nodeGroupConfigDiv = document.getElementById("nodegroup_configuration"); + if (nodeGroupConfigDiv) { + webconsolejs["partials/layout/navigatePages"].toggleSubElement(nodeGroupConfigDiv); + } + + // Add Node 영역 숨기기 + var addNodeDiv = document.getElementById("addnode"); + if (addNodeDiv && addNodeDiv.classList.contains("active")) { + webconsolejs["partials/layout/navigatePages"].toggleElement(addNodeDiv); + } + + // Add NodeGroup 폼 초기화 + Create_Node_Config_Arr = new Array(); + nodeGroup_data_cnt = 0; + + // addnodegroup_list 초기화 (+ NodeGroup 버튼만 남기기) + var addNodeGroupList = document.getElementById("addnodegroup_list"); + if (addNodeGroupList) { + var plusIcon = document.getElementById("addnodegroup_plusIcon"); + addNodeGroupList.innerHTML = ''; + if (plusIcon) { + addNodeGroupList.appendChild(plusIcon); + } else { + // + NodeGroup 버튼이 없으면 다시 생성 + var li = document.createElement('li'); + li.className = 'removebullet btn btn-secondary-lt'; + li.id = 'addnodegroup_plusIcon'; + li.onclick = function() { + webconsolejs['partials/operation/manage/clustercreate'].displayNewNodeForm(); + }; + li.textContent = '+ NodeGroup'; + addNodeGroupList.appendChild(li); + } + } + + // PMK 목록 새로고침 + if (webconsolejs["pages/operation/manage/pmk"] && + typeof webconsolejs["pages/operation/manage/pmk"].refreshPmkList === 'function') { + await webconsolejs["pages/operation/manage/pmk"].refreshPmkList(); + } + + console.log("NodeGroup creation request sent and PMK list refreshed"); } // Extract region from connectionName @@ -410,6 +542,7 @@ function extractRegionFromConnection(connectionName, provider) { export async function addNewNodeGroup() { Create_Cluster_Config_Arr = new Array(); Create_Node_Config_Arr = new Array(); + currentEditingNodeGroupIndex = null; // Create 모드로 초기화 var selectedCluster = webconsolejs["pages/operation/manage/pmk"].selectedPmkObj; @@ -498,13 +631,13 @@ export async function addNewPmk() { // provider set await setProviderList(providerList) - // call getRegion API - var regionList = await webconsolejs["common/api/services/pmk_api"].getRegionList() + // call getRegion API (백그라운드, 로더 없음) + var regionList = await webconsolejs["common/api/services/pmk_api"].getRegionList({ loaderType: 'none' }) // region set await setRegionList(regionList) - // call cloudconnection - var connectionList = await webconsolejs["common/api/services/pmk_api"].getCloudConnection() + // call cloudconnection (백그라운드, 로더 없음) + var connectionList = await webconsolejs["common/api/services/pmk_api"].getCloudConnection({ loaderType: 'none' }) // cloudconnection set await setCloudConnection(connectionList) @@ -681,6 +814,9 @@ export function clusterFormDone_btn() { { id: '#node_name', message: 'NodeGroup name is required' }, { id: '#node_specid', message: 'Spec is required' }, { id: '#node_imageid', message: 'Image is required' }, + { id: '#node_minnodesize', message: 'Min Node Size is required' }, + { id: '#node_maxnodesize', message: 'Max Node Size is required' }, + { id: '#node_sshkey', message: 'SSH Key is required' }, { id: '#node_autoscaling', message: 'AutoScaling option is required' } ]; @@ -736,54 +872,80 @@ export function clusterFormDone_btn() { $("#n_autoscaling").val(onAutoScaling); $("#n_desirednodesize").val(desiredNodeSize || "1"); + // 4. NodeGroup 데이터 객체 생성 + var nodeGroupData = { + "desiredNodeSize": desiredNodeSize || "", + "imageId": imageId || "", + "maxNodeSize": maxNodeSize || "", + "minNodeSize": minNodeSize || "", + "name": nodeGroupName, + "onAutoScaling": onAutoScaling || "false", + "rootDiskSize": rootDiskSize || "", + "rootDiskType": rootDiskType || "", + "specId": specId || "", + "sshKeyId": sshKeyId || "" + }; + if (nodeGroupName) { - cluster_form["k8sNodeGroupList"] = [ - { - "desiredNodeSize": desiredNodeSize || "", - "imageId": imageId || "", - "maxNodeSize": maxNodeSize || "", - "minNodeSize": minNodeSize || "", - "name": nodeGroupName, - "onAutoScaling": onAutoScaling || "false", - "rootDiskSize": rootDiskSize || "", - "rootDiskType": rootDiskType || "", - "specId": specId || "", - "sshKeyId": sshKeyId || "" - } - ]; + cluster_form["k8sNodeGroupList"] = [nodeGroupData]; } - // 4. 배열에 저장 - var nodeGroup_name = nodeGroupName; // cluster_form.name이 아닌 nodeGroupName 사용 + + var nodeGroup_name = nodeGroupName; var nodeGroup_cnt = parseInt(desiredNodeSize) || 1; - var add_nodegroup_html = ""; + var displayNodegroupCnt = '(' + nodeGroup_cnt + ')'; - Create_Cluster_Config_Arr.push(cluster_form); - if (isNodeGroup) { - Create_Node_Config_Arr.push(cluster_form["k8sNodeGroupList"][0]); - } + // Edit 모드 vs Create 모드 구분 + if (currentEditingNodeGroupIndex !== null) { + // **Edit 모드**: 기존 NodeGroup 업데이트 + console.log("Edit mode: Updating NodeGroup at index", currentEditingNodeGroupIndex); + + // 배열의 기존 데이터 업데이트 + Create_Node_Config_Arr[currentEditingNodeGroupIndex] = nodeGroupData; + Create_Cluster_Config_Arr[currentEditingNodeGroupIndex] = cluster_form; + + // HTML 리스트 항목 업데이트 (기존 항목 찾아서 텍스트만 변경) + var targetLi = $("#nodegroup_list li").eq(currentEditingNodeGroupIndex + 1); // +1은 plusIcon 때문 + if (targetLi.length > 0) { + targetLi.text(nodeGroup_name + displayNodegroupCnt); + // onclick 이벤트 다시 설정 + targetLi.attr('onclick', "webconsolejs['partials/operation/manage/clustercreate'].view_ngForm('" + currentEditingNodeGroupIndex + "')"); + } + + // Edit 모드 종료 + currentEditingNodeGroupIndex = null; + + } else { + // **Create 모드**: 새 NodeGroup 추가 + console.log("Create mode: Adding new NodeGroup"); + + // 배열에 저장 + Create_Cluster_Config_Arr.push(cluster_form); + if (isNodeGroup) { + Create_Node_Config_Arr.push(nodeGroupData); + } - // 5. HTML 생성 (NodeGroup 리스트 항목) - var displayNodegroupCnt = '(' + nodeGroup_cnt + ')'; - add_nodegroup_html += '
  • ' - + nodeGroup_name + displayNodegroupCnt - + '
  • '; + // HTML 생성 (NodeGroup 리스트 항목) + var add_nodegroup_html = '
  • ' + + nodeGroup_name + displayNodegroupCnt + + '
  • '; - // 6. 폼 토글 (먼저 실행) - var div = document.getElementById("nodegroup_configuration"); - webconsolejs["partials/layout/navigatePages"].toggleSubElement(div); + // plusIcon 제거 및 리스트 업데이트 + var ngEleId = "nodegroup"; + if (isNodeGroup) { + ngEleId = "addnodegroup"; + } + + $("#" + ngEleId + "_plusIcon").remove(); + $("#" + ngEleId + "_list").append(add_nodegroup_html); + $("#" + ngEleId + "_list").prepend(getPlusVm(ngEleId)); - // 7. plusIcon 제거 및 리스트 업데이트 - var ngEleId = "nodegroup"; - if (isNodeGroup) { - ngEleId = "addnodegroup"; + // 카운터 증가 + nodeGroup_data_cnt++; } - - $("#" + ngEleId + "_plusIcon").remove(); - $("#" + ngEleId + "_list").append(add_nodegroup_html); - $("#" + ngEleId + "_list").prepend(getPlusVm(ngEleId)); - // 8. 카운터 증가 - nodeGroup_data_cnt++; + // 폼 토글 + var div = document.getElementById("nodegroup_configuration"); + webconsolejs["partials/layout/navigatePages"].toggleSubElement(div); // 9. 폼 초기화 $("#cluster_form").each(function () { @@ -884,8 +1046,48 @@ export function addNodeFormDone_btn() { } export function view_ngForm(cnt){ + // NodeGroup Configuration 폼 표시 var div = document.getElementById("nodegroup_configuration"); - webconsolejs["partials/layout/navigatePages"].toggleElement(div) + webconsolejs["partials/layout/navigatePages"].toggleElement(div); + + // 배열에서 해당 NodeGroup 데이터 가져오기 + if (cnt !== undefined && Create_Node_Config_Arr[cnt]) { + // Edit 모드 활성화 + currentEditingNodeGroupIndex = cnt; + + var nodeGroupData = Create_Node_Config_Arr[cnt]; + + // Form 필드에 기존 데이터 채우기 + $("#node_name").val(nodeGroupData.name || ""); + $("#node_specid").val(nodeGroupData.specId || ""); + $("#node_commonSpecId").val(nodeGroupData.specId || ""); + $("#node_imageid").val(nodeGroupData.imageId || ""); + $("#node_minnodesize").val(nodeGroupData.minNodeSize || ""); + $("#node_maxnodesize").val(nodeGroupData.maxNodeSize || ""); + $("#node_sshkey").val(nodeGroupData.sshKeyId || ""); + $("#node_rootdisk").val(nodeGroupData.rootDiskType || ""); + $("#node_rootdisksize").val(nodeGroupData.rootDiskSize || ""); + $("#node_autoscaling").val(nodeGroupData.onAutoScaling || "false"); + $("#node_desirednodesize").val(nodeGroupData.desiredNodeSize || "1"); + + // Hidden 필드에도 설정 + $("#n_name").val(nodeGroupData.name || ""); + $("#n_specid").val(nodeGroupData.specId || ""); + $("#n_imageid").val(nodeGroupData.imageId || ""); + $("#n_minnodesize").val(nodeGroupData.minNodeSize || ""); + $("#n_maxnodesize").val(nodeGroupData.maxNodeSize || ""); + $("#n_sshkey").val(nodeGroupData.sshKeyId || ""); + $("#n_rootdisk").val(nodeGroupData.rootDiskType || ""); + $("#n_rootdisksize").val(nodeGroupData.rootDiskSize || ""); + $("#n_autoscaling").val(nodeGroupData.onAutoScaling || "false"); + $("#n_desirednodesize").val(nodeGroupData.desiredNodeSize || "1"); + + console.log("Edit mode: Loaded NodeGroup data at index", cnt, ":", nodeGroupData); + } else { + // Create 모드 (+ NodeGroup 클릭 시) + currentEditingNodeGroupIndex = null; + console.log("Create mode: New NodeGroup"); + } } // PMK용 Server Recommendation 콜백 함수 (Runtime nodegroup_configuration 폼용) diff --git a/front/assets/js/partials/operation/manage/pmk_imagerecommendation.js b/front/assets/js/partials/operation/manage/pmk_imagerecommendation.js index 752c57e9..eeaf1b15 100644 --- a/front/assets/js/partials/operation/manage/pmk_imagerecommendation.js +++ b/front/assets/js/partials/operation/manage/pmk_imagerecommendation.js @@ -209,7 +209,7 @@ export async function getRecommendImageInfoPmk() { providerName: provider, // 개별 전달 regionName: region, // 개별 전달 matchedSpecId: specId, // 유지 - isKubernetesImage: true, // PMK 필수 + // isKubernetesImage: true, // PMK 필수 maxResults: 100 }; diff --git a/front/assets/js/partials/operation/manage/pmk_serverrecommendation.js b/front/assets/js/partials/operation/manage/pmk_serverrecommendation.js index cc472767..6c802a48 100644 --- a/front/assets/js/partials/operation/manage/pmk_serverrecommendation.js +++ b/front/assets/js/partials/operation/manage/pmk_serverrecommendation.js @@ -20,24 +20,8 @@ export function initServerRecommendationPmk(callbackfunction) { // PMK용 서버 추천 모달 이벤트 설정 function setupServerModalEventsPmk() { - // MCI용과 동일하게 단순한 이벤트 리스너만 등록 - - // Bootstrap 5 방식 - if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { - var pmkModal = document.getElementById('spec-search-pmk'); - if (pmkModal) { - pmkModal.addEventListener('shown.bs.modal', function() { - // 모달이 열렸을 때의 처리 - }); - } - } - - // jQuery 방식 - if (typeof $ !== 'undefined' && $.fn.modal) { - $("#spec-search-pmk").on('shown.bs.modal', function() { - // 모달이 열렸을 때의 처리 - }); - } + // Provider 옵션은 HTML partial component로 이미 렌더링됨 + // 추가 초기화가 필요한 경우 여기에 작성 } function initRecommendSpecTablePmk() { diff --git a/front/templates/pages/operations/manage/workloads/pmkworkloads.html b/front/templates/pages/operations/manage/workloads/pmkworkloads.html index d8801635..4e867f95 100644 --- a/front/templates/pages/operations/manage/workloads/pmkworkloads.html +++ b/front/templates/pages/operations/manage/workloads/pmkworkloads.html @@ -380,7 +380,7 @@

    Create Cluster

    class="form-select" id="cluster_provider_dynamic" > - + <%= partial("partials/common/provider_select.html") %>
    @@ -1279,8 +1279,9 @@

    Drop files here or click to upload.

    -<%= javascriptTag("pages/operation/manage/pmk.js") %> <%= -javascriptTag("common/api/services/pmk_api.js") %> <%= +<%= javascriptTag("common/utils/listRefreshPattern.js") %> +<%= javascriptTag("pages/operation/manage/pmk.js") %> +<%= javascriptTag("common/api/services/pmk_api.js") %> <%= javascriptTag("common/api/services/mci_api.js") %> <%= javascriptTag("common/api/services/vmimage_api.js") %> <%= javascriptTag("partials/operation/manage/serverrecommendation.js") %> <%= diff --git a/front/templates/partials/common/_provider_select.html b/front/templates/partials/common/_provider_select.html new file mode 100644 index 00000000..f3a2944b --- /dev/null +++ b/front/templates/partials/common/_provider_select.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/front/templates/partials/operation/manage/_clusterinfo.html b/front/templates/partials/operation/manage/_clusterinfo.html index 86207141..d1a19dda 100644 --- a/front/templates/partials/operation/manage/_clusterinfo.html +++ b/front/templates/partials/operation/manage/_clusterinfo.html @@ -27,6 +27,14 @@

    Name
    +
    +
    CspName
    +
    +
    +
    +
    CspId
    +
    +
    Version
    diff --git a/front/templates/partials/operation/manage/_nodegroupinfo.html b/front/templates/partials/operation/manage/_nodegroupinfo.html index b662b2e8..4f662553 100644 --- a/front/templates/partials/operation/manage/_nodegroupinfo.html +++ b/front/templates/partials/operation/manage/_nodegroupinfo.html @@ -14,6 +14,20 @@

    Name

    +
    +
    Id
    +
    +
    +
    +
    Status
    +
    +
    + + + +
    +
    +
    Image
    @@ -24,16 +38,16 @@

    +
    +
    Key Pair
    +
    +
    -
    -
    Key Pair
    -
    -
    Desired Node Size
    @@ -47,12 +61,6 @@

    id="ng_info_nodesize" >

    -
    -
    -
    -
    -
    -
    Auto Scaling
    id="ng_info_autoscaling" >
    +
    +
    +
    + +
    +
    +
    +
    Root Disk Type
    diff --git a/front/templates/partials/operation/manage/_pmk_serverrecommendation.html b/front/templates/partials/operation/manage/_pmk_serverrecommendation.html index 2c7378a7..f70092c1 100644 --- a/front/templates/partials/operation/manage/_pmk_serverrecommendation.html +++ b/front/templates/partials/operation/manage/_pmk_serverrecommendation.html @@ -180,15 +180,7 @@

    name="spec-provider-filter-pmk" onchange="webconsolejs['partials/operation/manage/pmk_serverrecommendation'].filterByProviderPmk(this.value)" > - - - - - - - - - + <%= partial("partials/common/provider_select.html") %>