diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index 41d000d..4e8d6e6 100644 Binary files a/routes/__pycache__/main.cpython-313.pyc and b/routes/__pycache__/main.cpython-313.pyc differ diff --git a/routes/admin_api.py b/routes/admin_api.py index fad2521..cce01af 100644 --- a/routes/admin_api.py +++ b/routes/admin_api.py @@ -756,6 +756,46 @@ def list_gitea_repos(): except Exception as e: return jsonify({'message': f'Failed to list repositories: {str(e)}'}), 400 +@admin_api.route('/list-gitea-branches', methods=['POST']) +@csrf.exempt +def list_gitea_branches(): + """List branches from a Gitea repository""" + data = request.get_json() + if not data or 'url' not in data or 'token' not in data or 'repo' not in data: + return jsonify({'message': 'Missing required fields'}), 400 + + try: + # Try different authentication methods + headers = { + 'Accept': 'application/json' + } + + # First try token in Authorization header + headers['Authorization'] = f'token {data["token"]}' + + # Get repository branches + response = requests.get( + f'{data["url"]}/api/v1/repos/{data["repo"]}/branches', + headers=headers + ) + + # If that fails, try token as query parameter + if response.status_code != 200: + response = requests.get( + f'{data["url"]}/api/v1/repos/{data["repo"]}/branches?token={data["token"]}', + headers={'Accept': 'application/json'} + ) + + if response.status_code == 200: + return jsonify({ + 'branches': response.json() + }), 200 + else: + return jsonify({'message': f'Failed to list branches: {response.json().get("message", "Unknown error")}'}), 400 + + except Exception as e: + return jsonify({'message': f'Failed to list branches: {str(e)}'}), 400 + @admin_api.route('/list-gitlab-repos', methods=['POST']) @csrf.exempt def list_gitlab_repos(): diff --git a/routes/main.py b/routes/main.py index cae30ba..e568619 100644 --- a/routes/main.py +++ b/routes/main.py @@ -379,11 +379,13 @@ def init_routes(main_bp): # Get connection settings portainer_settings = KeyValueSettings.get_value('portainer_settings') nginx_settings = KeyValueSettings.get_value('nginx_settings') + git_settings = KeyValueSettings.get_value('git_settings') return render_template('main/instances.html', instances=instances, portainer_settings=portainer_settings, - nginx_settings=nginx_settings) + nginx_settings=nginx_settings, + git_settings=git_settings) @main_bp.route('/instances/add', methods=['POST']) @login_required diff --git a/templates/main/instances.html b/templates/main/instances.html index 1bb01e4..e5c8afc 100644 --- a/templates/main/instances.html +++ b/templates/main/instances.html @@ -35,7 +35,7 @@ - + @@ -156,18 +156,22 @@
2
-
Configuration
+
Repository
3
-
Resources
+
Company
4
-
Review
+
Variables
5
+
White Label
+
+
+
6
Launch
@@ -176,9 +180,6 @@
-

Connection Verification

-

Let's verify that your connections are properly configured before proceeding.

-
@@ -223,24 +224,149 @@
-

Configuration

-

Configure your instance settings.

+
+
+
+
+ + +
Select the repository containing your application code
+
+
+ + +
Select the branch to deploy
+
+
+
+
-

Resources

-

Set up your instance resources.

+
+
+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
-

Review

-

Review your instance configuration.

+
Port Configuration
+
+ + +
Ports below 1024 are reserved for system use. Suggested port: 3000
+
- +
+
+
+
+
White Label Configuration
+
+
+
+ +
+ + +
+
Default: RGB(22,118,123)
+
+
+
+
+ +
+ + +
+
Default: RGB(116,27,95)
+
+
+
+
+
Preview
+
+
+ Primary Color Sample +
+
+ Secondary Color Sample +
+
+
+
+
+
+
+ + +

Launch

Ready to launch your instance!

@@ -448,6 +574,9 @@ let authModal; let launchStepsModal; let currentStep = 1; +// Update the total number of steps +const totalSteps = 6; + document.addEventListener('DOMContentLoaded', function() { addInstanceModal = new bootstrap.Modal(document.getElementById('addInstanceModal')); editInstanceModal = new bootstrap.Modal(document.getElementById('editInstanceModal')); @@ -484,6 +613,47 @@ document.addEventListener('DOMContentLoaded', function() { // Set up periodic status checks (every 30 seconds) setInterval(checkAllInstanceStatuses, 30000); + + // Update color picker functionality + const primaryColor = document.getElementById('primaryColor'); + const secondaryColor = document.getElementById('secondaryColor'); + const primaryColorRGB = document.getElementById('primaryColorRGB'); + const secondaryColorRGB = document.getElementById('secondaryColorRGB'); + const primaryPreview = document.getElementById('primaryPreview'); + const secondaryPreview = document.getElementById('secondaryPreview'); + + function updateColorPreview() { + // Update preview boxes directly instead of using CSS variables + primaryPreview.style.backgroundColor = primaryColor.value; + secondaryPreview.style.backgroundColor = secondaryColor.value; + } + + function hexToRgb(hex) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? + `${parseInt(result[1], 16)},${parseInt(result[2], 16)},${parseInt(result[3], 16)}` : + null; + } + + function rgbToHex(r, g, b) { + return '#' + [r, g, b].map(x => { + const hex = x.toString(16); + return hex.length === 1 ? '0' + hex : hex; + }).join(''); + } + + primaryColor.addEventListener('input', function() { + primaryColorRGB.value = hexToRgb(this.value); + updateColorPreview(); + }); + + secondaryColor.addEventListener('input', function() { + secondaryColorRGB.value = hexToRgb(this.value); + updateColorPreview(); + }); + + // Initialize color preview + updateColorPreview(); }); // Function to check status of all instances @@ -1105,51 +1275,48 @@ function nextStepLaunchInstance() { } function updateStepDisplay() { - // Update step indicators - document.querySelectorAll('.step-item').forEach((item, index) => { - const stepNum = index + 1; - item.classList.remove('active', 'completed'); - if (stepNum === currentStep) { - item.classList.add('active'); - } else if (stepNum < currentStep) { - item.classList.add('completed'); - } + // Hide all steps + document.querySelectorAll('.step-pane').forEach(pane => { + pane.classList.remove('active'); }); - // Update step content - document.querySelectorAll('.step-pane').forEach((pane, index) => { - pane.classList.remove('active'); - if (index + 1 === currentStep) { - pane.classList.add('active'); - } + // Show current step + document.getElementById(`step${currentStep}`).classList.add('active'); + + // Update step indicators + document.querySelectorAll('.step-item').forEach(item => { + const step = parseInt(item.getAttribute('data-step')); + item.classList.toggle('active', step === currentStep); + item.classList.toggle('completed', step < currentStep); }); // Update buttons - const prevButton = document.querySelector('#launchStepsModal .btn-secondary'); - const nextButton = document.querySelector('#launchStepsModal .btn-primary'); + const prevButton = document.querySelector('.modal-footer .btn-secondary'); + const nextButton = document.querySelector('.modal-footer .btn-primary'); prevButton.style.display = currentStep === 1 ? 'none' : 'inline-block'; - nextButton.innerHTML = currentStep === 5 ? 'Launch ' : 'Next '; + nextButton.innerHTML = currentStep === totalSteps ? 'Launch Instance' : 'Next '; + + // If we're on step 4, get the next available port + if (currentStep === 4) { + getNextAvailablePort(); + } } function nextStep() { - if (currentStep < 5) { - // If we're on step 1, verify connections before proceeding - if (currentStep === 1) { - const portainerStatus = document.getElementById('portainerStatus'); - const nginxStatus = document.getElementById('nginxStatus'); - - if (!portainerStatus.innerHTML.includes('Connected') || !nginxStatus.innerHTML.includes('Connected')) { - alert('Please ensure all connections are working before proceeding.'); - return; - } - } - + if (currentStep === 2 && !validateStep2()) { + return; + } + if (currentStep === 3 && !validateStep3()) { + return; + } + + if (currentStep < totalSteps) { currentStep++; updateStepDisplay(); } else { - // Handle launch - console.log('Launching instance...'); + // Handle final step (launch instance) + launchInstance(); } } @@ -1232,5 +1399,226 @@ async function verifyConnections() { nginxDetails.innerHTML = `${error.message || 'Error checking NGINX connection'}`; } } + +// Repository Selection Functions +async function loadRepositories() { + const repoSelect = document.getElementById('giteaRepoSelect'); + const branchSelect = document.getElementById('giteaBranchSelect'); + + try { + const gitSettings = JSON.parse('{{ git_settings|tojson|safe }}'); + if (!gitSettings || !gitSettings.url || !gitSettings.token) { + throw new Error('No Git settings found. Please configure Git in the settings page.'); + } + + // Load repositories using the correct endpoint + const repoResponse = await fetch('/api/admin/list-gitea-repos', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content + }, + body: JSON.stringify({ + url: gitSettings.url, + token: gitSettings.token + }) + }); + + if (!repoResponse.ok) { + throw new Error('Failed to load repositories'); + } + + const data = await repoResponse.json(); + + if (data.repositories) { + repoSelect.innerHTML = '' + + data.repositories.map(repo => + `` + ).join(''); + repoSelect.disabled = false; + + // If we have a saved repository, load its branches + if (gitSettings.repo) { + loadBranches(gitSettings.repo); + } + } else { + repoSelect.innerHTML = ''; + repoSelect.disabled = true; + } + } catch (error) { + console.error('Error loading repositories:', error); + repoSelect.innerHTML = ``; + repoSelect.disabled = true; + } +} + +async function loadBranches(repoId) { + const branchSelect = document.getElementById('giteaBranchSelect'); + + try { + const gitSettings = JSON.parse('{{ git_settings|tojson|safe }}'); + if (!gitSettings || !gitSettings.url || !gitSettings.token) { + throw new Error('No Git settings found. Please configure Git in the settings page.'); + } + + const response = await fetch('/api/admin/list-gitea-branches', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content + }, + body: JSON.stringify({ + url: gitSettings.url, + token: gitSettings.token, + repo: repoId + }) + }); + + if (!response.ok) { + throw new Error('Failed to load branches'); + } + + const data = await response.json(); + + if (data.branches) { + branchSelect.innerHTML = '' + + data.branches.map(branch => + `` + ).join(''); + branchSelect.disabled = false; + } else { + branchSelect.innerHTML = ''; + branchSelect.disabled = true; + } + } catch (error) { + console.error('Error loading branches:', error); + branchSelect.innerHTML = ``; + branchSelect.disabled = true; + } +} + +// Event Listeners for Repository Selection +document.getElementById('giteaRepoSelect').addEventListener('change', function() { + const repoId = this.value; + if (repoId) { + loadBranches(repoId); + } else { + document.getElementById('giteaBranchSelect').innerHTML = ''; + document.getElementById('giteaBranchSelect').disabled = true; + } +}); + +// Load repositories when step 2 is shown +document.addEventListener('DOMContentLoaded', function() { + const step2Content = document.getElementById('step2'); + const observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + if (mutation.target.classList.contains('active')) { + loadRepositories(); + } + }); + }); + + observer.observe(step2Content, { attributes: true, attributeFilter: ['class'] }); +}); + +// Function to get the next available port +async function getNextAvailablePort() { + try { + // Get all existing instances + const response = await fetch('/instances'); + const text = await response.text(); + const parser = new DOMParser(); + const doc = parser.parseFromString(text, 'text/html'); + + // Get all port numbers from the table + const portCells = doc.querySelectorAll('table tbody tr td:first-child'); + const ports = Array.from(portCells) + .map(cell => { + const portText = cell.textContent.trim(); + // Only parse if it's a number + return /^\d+$/.test(portText) ? parseInt(portText) : null; + }) + .filter(port => port !== null) + .sort((a, b) => a - b); // Sort numerically + + console.log('Found existing ports:', ports); + + // Find the next available port + let nextPort = 10339; // Start from the next port after your highest + while (ports.includes(nextPort)) { + nextPort++; + } + + console.log('Next available port:', nextPort); + + // Set the suggested port + document.getElementById('port').value = nextPort; + } catch (error) { + console.error('Error getting next available port:', error); + // Default to 10339 if there's an error + document.getElementById('port').value = 10339; + } +} + +// Function to validate step 2 +function validateStep2() { + const repositorySelect = document.getElementById('giteaRepoSelect'); + const branchSelect = document.getElementById('giteaBranchSelect'); + + if (!repositorySelect.value) { + alert('Please select a repository'); + return false; + } + + if (!branchSelect.value) { + alert('Please select a branch'); + return false; + } + + return true; +} + +// Function to validate step 3 +function validateStep3() { + const requiredFields = [ + { id: 'companyName', name: 'Company Name' }, + { id: 'streetAddress', name: 'Street Address' }, + { id: 'city', name: 'City' }, + { id: 'zipCode', name: 'ZIP Code' }, + { id: 'country', name: 'Country' }, + { id: 'email', name: 'Email' }, + { id: 'phone', name: 'Phone' } + ]; + + const missingFields = requiredFields.filter(field => { + const element = document.getElementById(field.id); + return !element.value.trim(); + }); + + if (missingFields.length > 0) { + const fieldNames = missingFields.map(field => field.name).join(', '); + alert(`Please fill in all required fields: ${fieldNames}`); + return false; + } + + // Validate email format + const email = document.getElementById('email').value; + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + alert('Please enter a valid email address'); + return false; + } + + // Validate phone format (basic validation) + const phone = document.getElementById('phone').value; + const phoneRegex = /^[\d\s\-\+\(\)]{10,}$/; + if (!phoneRegex.test(phone)) { + alert('Please enter a valid phone number'); + return false; + } + + return true; +} {% endblock %} \ No newline at end of file diff --git a/templates/settings/tabs/connections.html b/templates/settings/tabs/connections.html index 179afe3..2a27be6 100644 --- a/templates/settings/tabs/connections.html +++ b/templates/settings/tabs/connections.html @@ -17,18 +17,18 @@
- + -
The URL of your Portainer instance
+
The URL of your Portainer server
-
You can generate this in Portainer under Settings > API Keys
+
You can generate this in your Portainer user settings
- -
-
-
-
- GitLab Connection -
- -
-
- -
- - -
The URL of your GitLab server (use https://gitlab.com for GitLab.com)
-
-
- - -
-
- - -
You can generate this in your GitLab user settings > Access Tokens
-
-
- -
- - -
-
Select the repository to connect to
-
-
- -
- -
-
-
-
@@ -349,59 +291,11 @@ async function loadGiteaRepos() { } } -// Load GitLab Repositories -async function loadGitlabRepos() { - const url = document.getElementById('gitlabUrl').value; - const token = document.getElementById('gitlabToken').value; - const repoSelect = document.getElementById('gitlabRepo'); - const currentRepo = repoSelect.value; // Store current selection - - if (!url || !token) { - showError('Please fill in the server URL and access token'); - return; - } - - try { - const response = await fetch('/api/admin/list-gitlab-repos', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRF-Token': getCsrfToken() - }, - body: JSON.stringify({ url, token }) - }); - - const data = await response.json(); - - if (response.ok) { - repoSelect.innerHTML = ''; - - data.repositories.forEach(repo => { - const option = document.createElement('option'); - option.value = repo.path_with_namespace; - option.textContent = repo.path_with_namespace; - if (repo.path_with_namespace === currentRepo) { // Restore selection - option.selected = true; - } - repoSelect.appendChild(option); - }); - } else { - throw new Error(data.message || 'Failed to load repositories'); - } - } catch (error) { - showError(error.message); - } -} - // Load repositories on page load if settings exist document.addEventListener('DOMContentLoaded', function() { const gitSettings = JSON.parse('{{ git_settings|tojson|safe }}'); - if (gitSettings) { - if (gitSettings.provider === 'gitea' && gitSettings.url && gitSettings.token) { - loadGiteaRepos(); - } else if (gitSettings.provider === 'gitlab' && gitSettings.url && gitSettings.token) { - loadGitlabRepos(); - } + if (gitSettings && gitSettings.provider === 'gitea' && gitSettings.url && gitSettings.token) { + loadGiteaRepos(); } }); @@ -414,39 +308,21 @@ async function testGitConnection(provider) { saveModal.show(); try { - let data = {}; - if (provider === 'gitea') { - const url = document.getElementById('giteaUrl').value; - const username = document.getElementById('giteaUsername').value; - const token = document.getElementById('giteaToken').value; + const url = document.getElementById('giteaUrl').value; + const username = document.getElementById('giteaUsername').value; + const token = document.getElementById('giteaToken').value; - if (!url || !username || !token) { - throw new Error('Please fill in all required fields'); - } - - data = { - provider: 'gitea', - url: url, - username: username, - token: token - }; - } else if (provider === 'gitlab') { - const url = document.getElementById('gitlabUrl').value; - const username = document.getElementById('gitlabUsername').value; - const token = document.getElementById('gitlabToken').value; - - if (!url || !username || !token) { - throw new Error('Please fill in all required fields'); - } - - data = { - provider: 'gitlab', - url: url, - username: username, - token: token - }; + if (!url || !username || !token) { + throw new Error('Please fill in all required fields'); } + const data = { + provider: 'gitea', + url: url, + username: username, + token: token + }; + const response = await fetch('/settings/test-git-connection', { method: 'POST', headers: { @@ -664,47 +540,27 @@ async function saveGitConnection(event, provider) { messageElement.className = ''; try { - let data = {}; - if (provider === 'gitea') { - const url = document.getElementById('giteaUrl').value; - const username = document.getElementById('giteaUsername').value; - const token = document.getElementById('giteaToken').value; - const repo = document.getElementById('giteaRepo').value; - const password = document.getElementById('giteaPassword').value; - const otp = document.getElementById('giteaOtp').value; + const url = document.getElementById('giteaUrl').value; + const username = document.getElementById('giteaUsername').value; + const token = document.getElementById('giteaToken').value; + const repo = document.getElementById('giteaRepo').value; + const password = document.getElementById('giteaPassword').value; + const otp = document.getElementById('giteaOtp').value; - if (!url || !username || !token || !repo) { - throw new Error('Please fill in all required fields'); - } - - data = { - provider: 'gitea', - url: url, - username: username, - token: token, - repo: repo, - password: password, - otp: otp - }; - } else if (provider === 'gitlab') { - const url = document.getElementById('gitlabUrl').value; - const username = document.getElementById('gitlabUsername').value; - const token = document.getElementById('gitlabToken').value; - const repo = document.getElementById('gitlabRepo').value; - - if (!url || !username || !token || !repo) { - throw new Error('Please fill in all required fields'); - } - - data = { - provider: 'gitlab', - url: url, - username: username, - token: token, - repo: repo - }; + if (!url || !username || !token || !repo) { + throw new Error('Please fill in all required fields'); } + const data = { + provider: 'gitea', + url: url, + username: username, + token: token, + repo: repo, + password: password, + otp: otp + }; + const response = await fetch('/settings/save-git-connection', { method: 'POST', headers: {
NamePORT Company Rooms Conversations