// [DEF:GitServiceClient:Module] /** * @TIER: STANDARD * @SEMANTICS: git, service, api, client * @PURPOSE: API client for Git operations, managing the communication between frontend and backend. * @LAYER: Service * @RELATION: DEPENDS_ON -> specs/011-git-integration-dashboard/contracts/api.md */ import { requestApi } from '../lib/api'; const API_BASE = '/git'; function buildDashboardRepoEndpoint(dashboardRef, suffix, envId = null) { const encodedRef = encodeURIComponent(String(dashboardRef)); const endpoint = `${API_BASE}/repositories/${encodedRef}${suffix}`; if (!envId) return endpoint; const sep = endpoint.includes('?') ? '&' : '?'; return `${endpoint}${sep}env_id=${encodeURIComponent(String(envId))}`; } // [DEF:gitService:Action] export const gitService = { /** * [DEF:getConfigs:Function] * @purpose Fetches all Git server configurations. * @pre User must be authenticated. * @post Returns a list of Git server configurations. * @returns {Promise} List of configs. */ async getConfigs() { console.log('[getConfigs][Action] Fetching Git configs'); return requestApi(`${API_BASE}/config`); }, /** * [DEF:createConfig:Function] * @purpose Creates a new Git server configuration. * @pre Config object must be valid. * @post New config is created and returned. * @param {Object} config - Configuration details. * @returns {Promise} Created config. */ async createConfig(config) { console.log('[createConfig][Action] Creating Git config'); return requestApi(`${API_BASE}/config`, 'POST', config); }, /** * [DEF:deleteConfig:Function] * @purpose Deletes an existing Git server configuration. * @pre configId must exist. * @post Config is deleted from the backend. * @param {string} configId - ID of the config to delete. * @returns {Promise} Result of deletion. */ async deleteConfig(configId) { console.log(`[deleteConfig][Action] Deleting Git config ${configId}`); return requestApi(`${API_BASE}/config/${configId}`, 'DELETE'); }, /** * [DEF:updateConfig:Function] * @purpose Updates an existing Git server configuration. * @pre configId must exist and configData must be valid. * @post Config is updated and returned. * @param {string} configId - ID of the config to update. * @param {Object} config - Updated configuration details. * @returns {Promise} Updated config. */ async updateConfig(configId, config) { console.log(`[updateConfig][Action] Updating Git config ${configId}`); return requestApi(`${API_BASE}/config/${configId}`, 'PUT', config); }, /** * [DEF:testConnection:Function] * @purpose Tests the connection to a Git server with provided credentials. * @pre Config must contain valid URL and PAT. * @post Returns connection status (success/failure). * @param {Object} config - Configuration to test. * @returns {Promise} Connection test result. */ async testConnection(config) { console.log('[testConnection][Action] Testing Git connection'); return requestApi(`${API_BASE}/config/test`, 'POST', config); }, /** * [DEF:listGiteaRepositories:Function] * @purpose Lists repositories on Gitea for a saved Git configuration. * @pre configId must reference a GITEA config. * @post Returns repository metadata. * @param {string} configId - Git configuration ID. * @returns {Promise} List of Gitea repositories. */ async listGiteaRepositories(configId) { console.log(`[listGiteaRepositories][Action] Listing Gitea repositories for config ${configId}`); return requestApi(`${API_BASE}/config/${configId}/gitea/repos`); }, /** * [DEF:createGiteaRepository:Function] * @purpose Creates a new repository on Gitea for a saved Git configuration. * @pre configId must reference a GITEA config. * @post Repository is created on Gitea. * @param {string} configId - Git configuration ID. * @param {Object} payload - {name, private, description, auto_init, default_branch} * @returns {Promise} Created repository payload. */ async createGiteaRepository(configId, payload) { console.log(`[createGiteaRepository][Action] Creating Gitea repository ${payload?.name} for config ${configId}`); return requestApi(`${API_BASE}/config/${configId}/gitea/repos`, 'POST', payload); }, /** * [DEF:createRemoteRepository:Function] * @purpose Creates repository on remote provider selected by Git config. * @pre configId exists and points to supported provider config. * @post Remote repository created and normalized payload returned. * @param {string} configId - Git configuration ID. * @param {Object} payload - {name, private, description, auto_init, default_branch} * @returns {Promise} Created remote repository payload. */ async createRemoteRepository(configId, payload) { console.log(`[createRemoteRepository][Action] Creating remote repository ${payload?.name} for config ${configId}`); return requestApi(`${API_BASE}/config/${configId}/repositories`, 'POST', payload); }, /** * [DEF:deleteGiteaRepository:Function] * @purpose Deletes a repository on Gitea for a saved Git configuration. * @pre configId must reference a GITEA config. * @post Repository is deleted on Gitea. * @param {string} configId - Git configuration ID. * @param {string} owner - Repository owner. * @param {string} repoName - Repository name. * @returns {Promise} Deletion result. */ async deleteGiteaRepository(configId, owner, repoName) { console.log(`[deleteGiteaRepository][Action] Deleting Gitea repository ${owner}/${repoName} for config ${configId}`); return requestApi(`${API_BASE}/config/${configId}/gitea/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}`, 'DELETE'); }, /** * [DEF:initRepository:Function] * @purpose Initializes or clones a Git repository for a dashboard. * @pre Dashboard must exist and config_id must be valid. * @post Repository is initialized on the backend. * @param {string|number} dashboardRef - Dashboard slug or id. * @param {string} configId - ID of the Git config. * @param {string} remoteUrl - URL of the remote repository. * @returns {Promise} Initialization result. */ async initRepository(dashboardRef, configId, remoteUrl, envId = null) { console.log(`[initRepository][Action] Initializing repo for dashboard ${dashboardRef}`); return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/init', envId), 'POST', { config_id: configId, remote_url: remoteUrl }); }, /** * [DEF:getRepositoryBinding:Function] * @purpose Fetches repository binding metadata (config/provider) for dashboard. * @pre Repository should be initialized for dashboard. * @post Returns provider and config details for current repository. * @param {string|number} dashboardRef - Dashboard slug or id. * @returns {Promise} Repository binding payload. */ async getRepositoryBinding(dashboardRef, envId = null) { console.log(`[getRepositoryBinding][Action] Fetching repository binding for dashboard ${dashboardRef}`); return requestApi(buildDashboardRepoEndpoint(dashboardRef, '', envId)); }, /** * [DEF:getBranches:Function] * @purpose Retrieves the list of branches for a dashboard's repository. * @pre Repository must be initialized. * @post Returns a list of branches. * @param {string|number} dashboardRef - Dashboard slug or id. * @returns {Promise} List of branches. */ async getBranches(dashboardRef, envId = null) { console.log(`[getBranches][Action] Fetching branches for dashboard ${dashboardRef}`); return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/branches', envId)); }, /** * [DEF:createBranch:Function] * @purpose Creates a new branch in the dashboard's repository. * @pre Source branch must exist. * @post New branch is created. * @param {number} dashboardId - ID of the dashboard. * @param {string} name - New branch name. * @param {string} fromBranch - Source branch name. * @returns {Promise} Creation result. */ async createBranch(dashboardRef, name, fromBranch, envId = null) { console.log(`[createBranch][Action] Creating branch ${name} for dashboard ${dashboardRef}`); return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/branches', envId), 'POST', { name, from_branch: fromBranch }); }, /** * [DEF:checkoutBranch:Function] * @purpose Switches the repository to a different branch. * @pre Target branch must exist. * @post Repository head is moved to the target branch. * @param {number} dashboardId - ID of the dashboard. * @param {string} name - Branch name to checkout. * @returns {Promise} Checkout result. */ async checkoutBranch(dashboardRef, name, envId = null) { console.log(`[checkoutBranch][Action] Checking out branch ${name} for dashboard ${dashboardRef}`); return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/checkout', envId), 'POST', { name }); }, /** * [DEF:commit:Function] * @purpose Stages and commits changes to the repository. * @pre Message must not be empty. * @post Changes are committed to the current branch. * @param {number} dashboardId - ID of the dashboard. * @param {string} message - Commit message. * @param {Array} files - Optional list of files to commit. * @returns {Promise} Commit result. */ async commit(dashboardRef, message, files, envId = null) { console.log(`[commit][Action] Committing changes for dashboard ${dashboardRef}`); return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/commit', envId), 'POST', { message, files }); }, /** * [DEF:push:Function] * @purpose Pushes local commits to the remote repository. * @pre Remote must be configured and accessible. * @post Remote is updated with local commits. * @param {number} dashboardId - ID of the dashboard. * @returns {Promise} Push result. */ async push(dashboardRef, envId = null) { console.log(`[push][Action] Pushing changes for dashboard ${dashboardRef}`); return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/push', envId), 'POST'); }, /** * [DEF:deleteRepository:Function] * @purpose Deletes local repository binding and workspace for dashboard. * @pre Dashboard reference must resolve on backend. * @post Repository record and local folder are removed. * @param {string|number} dashboardRef - Dashboard slug or id. * @returns {Promise} Deletion result. */ async deleteRepository(dashboardRef, envId = null) { console.log(`[deleteRepository][Action] Deleting repository for dashboard ${dashboardRef}`); return requestApi(buildDashboardRepoEndpoint(dashboardRef, '', envId), 'DELETE'); }, /** * [DEF:pull:Function] * @purpose Pulls changes from the remote repository. * @pre Remote must be configured and accessible. * @post Local repository is updated with remote changes. * @param {number} dashboardId - ID of the dashboard. * @returns {Promise} Pull result. */ async pull(dashboardRef, envId = null) { console.log(`[pull][Action] Pulling changes for dashboard ${dashboardRef}`); return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/pull', envId), 'POST'); }, /** * [DEF:getMergeStatus:Function] * @purpose Retrieves unfinished-merge status for repository. * @pre Repository must exist. * @post Returns merge status payload. * @param {string|number} dashboardRef - Dashboard slug or id. * @returns {Promise} Merge status details. */ async getMergeStatus(dashboardRef, envId = null) { console.log(`[getMergeStatus][Action] Fetching merge status for dashboard ${dashboardRef}`); return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/merge/status', envId)); }, /** * [DEF:getMergeConflicts:Function] * @purpose Retrieves merge conflicts list for repository. * @pre Unfinished merge should be in progress. * @post Returns conflict files with mine/theirs previews. * @param {string|number} dashboardRef - Dashboard slug or id. * @returns {Promise} List of conflict files. */ async getMergeConflicts(dashboardRef, envId = null) { console.log(`[getMergeConflicts][Action] Fetching merge conflicts for dashboard ${dashboardRef}`); return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/merge/conflicts', envId)); }, /** * [DEF:resolveMergeConflicts:Function] * @purpose Applies conflict resolution strategies and stages resolved files. * @pre resolutions contains file_path/resolution entries. * @post Conflicts are resolved and staged. * @param {string|number} dashboardRef - Dashboard slug or id. * @param {Array} resolutions - Resolution entries. * @returns {Promise} Resolve result. */ async resolveMergeConflicts(dashboardRef, resolutions, envId = null) { console.log(`[resolveMergeConflicts][Action] Resolving ${Array.isArray(resolutions) ? resolutions.length : 0} conflicts for dashboard ${dashboardRef}`); return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/merge/resolve', envId), 'POST', { resolutions: Array.isArray(resolutions) ? resolutions : [] }); }, /** * [DEF:abortMerge:Function] * @purpose Aborts current unfinished merge. * @pre Repository exists. * @post Merge state is aborted or reported as absent. * @param {string|number} dashboardRef - Dashboard slug or id. * @returns {Promise} Abort operation result. */ async abortMerge(dashboardRef, envId = null) { console.log(`[abortMerge][Action] Aborting merge for dashboard ${dashboardRef}`); return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/merge/abort', envId), 'POST'); }, /** * [DEF:continueMerge:Function] * @purpose Finalizes unfinished merge by creating merge commit. * @pre All conflicts are resolved. * @post Merge commit is created. * @param {string|number} dashboardRef - Dashboard slug or id. * @param {string} message - Optional commit message. * @returns {Promise} Continue result. */ async continueMerge(dashboardRef, message = '', envId = null) { console.log(`[continueMerge][Action] Continuing merge for dashboard ${dashboardRef}`); return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/merge/continue', envId), 'POST', { message: String(message || '').trim() || null }); }, /** * [DEF:getEnvironments:Function] * @purpose Retrieves available deployment environments. * @post Returns a list of environments. * @returns {Promise} List of environments. */ async getEnvironments() { console.log('[getEnvironments][Action] Fetching environments'); return requestApi(`${API_BASE}/environments`); }, /** * [DEF:deploy:Function] * @purpose Deploys a dashboard to a target environment. * @pre Environment must be active and accessible. * @post Dashboard is imported into the target Superset instance. * @param {number} dashboardId - ID of the dashboard. * @param {string} environmentId - ID of the target environment. * @returns {Promise} Deployment result. */ async deploy(dashboardRef, environmentId, envId = null) { console.log(`[deploy][Action] Deploying dashboard ${dashboardRef} to environment ${environmentId}`); return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/deploy', envId), 'POST', { environment_id: environmentId }); }, /** * [DEF:getHistory:Function] * @purpose Retrieves the commit history for a dashboard. * @param {number} dashboardId - ID of the dashboard. * @param {number} limit - Maximum number of commits to return. * @returns {Promise} List of commits. */ async getHistory(dashboardRef, limit = 50, envId = null) { console.log(`[getHistory][Action] Fetching history for dashboard ${dashboardRef}`); return requestApi(buildDashboardRepoEndpoint(dashboardRef, `/history?limit=${limit}`, envId)); }, /** * [DEF:sync:Function] * @purpose Synchronizes the local dashboard state with the Git repository. * @param {number} dashboardId - ID of the dashboard. * @param {string|null} sourceEnvId - Optional source environment ID. * @returns {Promise} Sync result. */ async sync(dashboardRef, sourceEnvId = null, envId = null) { console.log(`[sync][Action] Syncing dashboard ${dashboardRef}`); const params = new URLSearchParams(); if (sourceEnvId) params.append('source_env_id', String(sourceEnvId)); if (envId) params.append('env_id', String(envId)); const query = params.toString(); const endpoint = `${API_BASE}/repositories/${encodeURIComponent(String(dashboardRef))}/sync${query ? `?${query}` : ''}`; return requestApi(endpoint, 'POST'); }, /** * [DEF:getStatus:Function] * @purpose Fetches the current Git status for a dashboard repository. * @pre dashboardId must be a valid integer. * @post Returns a status object with dirty files and branch info. * @param {number} dashboardId - The ID of the dashboard. * @returns {Promise} Status details. */ async getStatus(dashboardRef, envId = null) { console.log(`[getStatus][Action] Fetching status for dashboard ${dashboardRef}`); return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/status', envId)); }, /** * [DEF:getStatusesBatch:Function] * @purpose Fetches Git statuses for multiple dashboards in a single request. * @pre dashboardIds must be an array of dashboard IDs. * @post Returns a map of dashboard_id -> status payload. * @param {Array} dashboardIds - Dashboard IDs. * @returns {Promise} Batch status response. */ async getStatusesBatch(dashboardIds) { console.log(`[getStatusesBatch][Action] Fetching statuses for ${dashboardIds.length} dashboards`); return requestApi(`${API_BASE}/repositories/status/batch`, 'POST', { dashboard_ids: dashboardIds }); }, /** * [DEF:getDiff:Function] * @purpose Retrieves the diff for specific files or the whole repository. * @pre dashboardId must be a valid integer. * @post Returns the Git diff string. * @param {number} dashboardId - The ID of the dashboard. * @param {string|null} filePath - Optional specific file path. * @param {boolean} staged - Whether to show staged changes. * @returns {Promise} The diff content. */ async getDiff(dashboardRef, filePath = null, staged = false, envId = null) { console.log(`[getDiff][Action] Fetching diff for dashboard ${dashboardRef} (file: ${filePath}, staged: ${staged})`); let endpoint = `${API_BASE}/repositories/${encodeURIComponent(String(dashboardRef))}/diff`; const params = new URLSearchParams(); if (filePath) params.append('file_path', filePath); if (staged) params.append('staged', 'true'); if (envId) params.append('env_id', String(envId)); if (params.toString()) endpoint += `?${params.toString()}`; return requestApi(endpoint); }, /** * [DEF:promote:Function] * @purpose Promotes changes between branches via MR or direct merge. * @pre Dashboard repository must be initialized. * @post Returns promotion metadata (MR URL or direct merge status). * @param {string|number} dashboardRef - Dashboard slug or id. * @param {Object} payload - {from_branch,to_branch,mode,title,description,reason,draft,remove_source_branch} * @param {string|null} envId - Environment id for slug resolution. * @returns {Promise} Promotion result. */ async promote(dashboardRef, payload, envId = null) { console.log(`[promote][Action] Promoting ${payload?.from_branch} -> ${payload?.to_branch} for dashboard ${dashboardRef} mode=${payload?.mode}`); return requestApi(buildDashboardRepoEndpoint(dashboardRef, '/promote', envId), 'POST', payload); } }; // [/DEF:gitService:Action] // [/DEF:GitServiceClient:Module]