parent
e01beb992d
commit
a23d9e8de8
Binary file not shown.
@ -1,557 +0,0 @@
|
|||||||
// ============================================
|
|
||||||
// CHARGER LES DASHBOARDS DYNAMIQUEMENT
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
// Charger les applications
|
|
||||||
function loadAvailableApps() {
|
|
||||||
console.log("loadAvailableApps called");
|
|
||||||
|
|
||||||
const apiUrl = '/en-US/splunkd/__raw/services/apps/local?output_mode=json&count=0';
|
|
||||||
|
|
||||||
fetch(apiUrl)
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('HTTP ' + response.status);
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
console.log("Apps Response: Found " + (data.entry ? data.entry.length : 0) + " apps");
|
|
||||||
|
|
||||||
if (data.entry && data.entry.length > 0) {
|
|
||||||
const apps = data.entry
|
|
||||||
.filter(item => {
|
|
||||||
// Filtrer les apps système
|
|
||||||
const isHidden = item.content && item.content.is_visible === 0;
|
|
||||||
const appName = item.name;
|
|
||||||
// Exclure les apps système
|
|
||||||
const systemApps = ['launcher', 'splunk_monitoring_console', 'introspection'];
|
|
||||||
return !isHidden && !systemApps.includes(appName);
|
|
||||||
})
|
|
||||||
.map(item => ({
|
|
||||||
id: item.name,
|
|
||||||
name: item.content.label || item.name,
|
|
||||||
description: item.content.description || ''
|
|
||||||
}))
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
|
|
||||||
console.log("Filtered apps: " + apps.length);
|
|
||||||
populateAppsList(apps);
|
|
||||||
} else {
|
|
||||||
console.warn("No apps found");
|
|
||||||
showAppsEmpty();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error("API Error:", error);
|
|
||||||
showAppsEmpty();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function populateAppsList(apps) {
|
|
||||||
console.log("populateAppsList called with", apps.length, "apps");
|
|
||||||
|
|
||||||
const container = document.getElementById('dashboard-list');
|
|
||||||
|
|
||||||
if (!container) {
|
|
||||||
console.error("dashboard-list container not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!apps || apps.length === 0) {
|
|
||||||
showAppsEmpty();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = '<div class="app-header">';
|
|
||||||
html += '<span>Select Applications</span>';
|
|
||||||
html += '<input type="checkbox" id="select-all">';
|
|
||||||
html += '</div>';
|
|
||||||
|
|
||||||
apps.forEach((app, index) => {
|
|
||||||
const checkboxId = 'app-' + index;
|
|
||||||
html += '<div class="app-item">';
|
|
||||||
html += '<input type="checkbox" id="' + checkboxId + '" value="' + app.id + '" data-app="true" data-name="' + app.name + '">';
|
|
||||||
html += '<label for="' + checkboxId + '"><strong>' + app.name + '</strong> <span class="app-badge">' + app.id + '</span>';
|
|
||||||
if (app.description) {
|
|
||||||
html += '<div style="font-size: 11px; color: #999; margin-top: 3px;">' + app.description + '</div>';
|
|
||||||
}
|
|
||||||
html += '</label>';
|
|
||||||
html += '</div>';
|
|
||||||
});
|
|
||||||
|
|
||||||
container.innerHTML = html;
|
|
||||||
console.log('Successfully populated ' + apps.length + ' apps');
|
|
||||||
|
|
||||||
// Ajouter les event listeners
|
|
||||||
addCheckboxListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
function addCheckboxListeners() {
|
|
||||||
console.log("addCheckboxListeners called");
|
|
||||||
|
|
||||||
const selectAllCheckbox = document.getElementById('select-all');
|
|
||||||
if (selectAllCheckbox) {
|
|
||||||
selectAllCheckbox.addEventListener('change', function() {
|
|
||||||
toggleSelectAll(this);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const appCheckboxes = document.querySelectorAll('#dashboard-list input[type="checkbox"][data-app]');
|
|
||||||
appCheckboxes.forEach(checkbox => {
|
|
||||||
checkbox.addEventListener('change', function() {
|
|
||||||
console.log("App checkbox changed");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showAppsEmpty() {
|
|
||||||
const container = document.getElementById('dashboard-list');
|
|
||||||
if (container) {
|
|
||||||
container.innerHTML = '<div class="dashboard-empty">No apps found</div>';
|
|
||||||
}
|
|
||||||
console.log("Displayed empty state");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attendre que le DOM soit chargé
|
|
||||||
function initScript() {
|
|
||||||
console.log("initScript called");
|
|
||||||
|
|
||||||
// Charger les applications
|
|
||||||
loadAvailableApps();
|
|
||||||
|
|
||||||
// Charger les credentials sauvegardés
|
|
||||||
loadSavedCredentials();
|
|
||||||
|
|
||||||
// Attacher les event listeners au bouton
|
|
||||||
const pushBtn = document.getElementById('push-btn');
|
|
||||||
if (pushBtn) {
|
|
||||||
console.log("Push button found, attaching listener");
|
|
||||||
pushBtn.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
console.log("Push button clicked!");
|
|
||||||
pushDashboards();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.warn("Push button not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attacher l'event listener pour sauvegarder les credentials
|
|
||||||
const saveCheckbox = document.getElementById('save-credentials');
|
|
||||||
if (saveCheckbox) {
|
|
||||||
saveCheckbox.addEventListener('change', function() {
|
|
||||||
if (this.checked) {
|
|
||||||
saveCredentials();
|
|
||||||
} else {
|
|
||||||
clearSavedCredentials();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadSavedCredentials() {
|
|
||||||
console.log("Loading saved credentials...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const savedUrl = getCookie('git_pusher_url');
|
|
||||||
const savedToken = getCookie('git_pusher_token');
|
|
||||||
const savedBranch = getCookie('git_pusher_branch');
|
|
||||||
|
|
||||||
if (savedUrl) {
|
|
||||||
document.getElementById('git-url').value = decodeURIComponent(savedUrl);
|
|
||||||
console.log("Loaded saved URL from cookie");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (savedToken) {
|
|
||||||
document.getElementById('git-token').value = decodeURIComponent(savedToken);
|
|
||||||
console.log("Loaded saved token from cookie");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (savedBranch) {
|
|
||||||
document.getElementById('git-branch').value = decodeURIComponent(savedBranch);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (savedUrl && savedToken) {
|
|
||||||
document.getElementById('save-credentials').checked = true;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Could not load saved credentials:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveCredentials() {
|
|
||||||
console.log("Saving credentials...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const gitUrl = document.getElementById('git-url').value;
|
|
||||||
const gitToken = document.getElementById('git-token').value;
|
|
||||||
const gitBranch = document.getElementById('git-branch').value;
|
|
||||||
|
|
||||||
if (gitUrl && gitToken) {
|
|
||||||
setCookie('git_pusher_url', gitUrl, 30);
|
|
||||||
setCookie('git_pusher_token', gitToken, 30);
|
|
||||||
setCookie('git_pusher_branch', gitBranch, 30);
|
|
||||||
console.log("Credentials saved to cookies");
|
|
||||||
showSuccess("Credentials saved locally");
|
|
||||||
} else {
|
|
||||||
showError("Please fill in URL and Token before saving");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error saving credentials:", e);
|
|
||||||
showError("Could not save credentials");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearSavedCredentials() {
|
|
||||||
console.log("Clearing saved credentials...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
deleteCookie('git_pusher_url');
|
|
||||||
deleteCookie('git_pusher_token');
|
|
||||||
deleteCookie('git_pusher_branch');
|
|
||||||
console.log("Credentials cleared from cookies");
|
|
||||||
showSuccess("Credentials cleared");
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error clearing credentials:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonctions utilitaires pour les cookies
|
|
||||||
function setCookie(name, value, days) {
|
|
||||||
const d = new Date();
|
|
||||||
d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
|
|
||||||
const expires = "expires=" + d.toUTCString();
|
|
||||||
document.cookie = name + "=" + encodeURIComponent(value) + ";" + expires + ";path=/";
|
|
||||||
console.log("Cookie set: " + name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCookie(name) {
|
|
||||||
const nameEQ = name + "=";
|
|
||||||
const ca = document.cookie.split(';');
|
|
||||||
for (let i = 0; i < ca.length; i++) {
|
|
||||||
let c = ca[i].trim();
|
|
||||||
if (c.indexOf(nameEQ) === 0) {
|
|
||||||
return c.substring(nameEQ.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteCookie(name) {
|
|
||||||
setCookie(name, "", -1);
|
|
||||||
console.log("Cookie deleted: " + name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
console.log("DOM Ready - Initializing script...");
|
|
||||||
setTimeout(function() {
|
|
||||||
initScript();
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log("DOM already ready - Initializing script...");
|
|
||||||
setTimeout(function() {
|
|
||||||
initScript();
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFormKeyValue() {
|
|
||||||
try {
|
|
||||||
// Chercher dans les meta tags du DOM
|
|
||||||
const metaTag = document.querySelector('meta[name="splunk_form_key"]');
|
|
||||||
if (metaTag) {
|
|
||||||
return metaTag.getAttribute('content');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chercher dans les cookies
|
|
||||||
const cookies = document.cookie.split(';');
|
|
||||||
for (let cookie of cookies) {
|
|
||||||
const [name, value] = cookie.trim().split('=');
|
|
||||||
if (name === 'splunk_form_key') {
|
|
||||||
return decodeURIComponent(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn("Could not find form key, proceeding without it");
|
|
||||||
return '';
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error getting form key:", e);
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadAvailableApps() {
|
|
||||||
console.log("loadAvailableApps called");
|
|
||||||
|
|
||||||
const apiUrl = '/en-US/splunkd/__raw/services/apps/local?output_mode=json&count=0';
|
|
||||||
|
|
||||||
fetch(apiUrl)
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('HTTP ' + response.status);
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
console.log("Apps Response: Found " + (data.entry ? data.entry.length : 0) + " apps");
|
|
||||||
|
|
||||||
if (data.entry && data.entry.length > 0) {
|
|
||||||
const apps = data.entry
|
|
||||||
.filter(item => {
|
|
||||||
// Filtrer les apps système
|
|
||||||
const isHidden = item.content && item.content.is_visible === 0;
|
|
||||||
const appName = item.name;
|
|
||||||
// Exclure les apps système
|
|
||||||
const systemApps = ['launcher', 'splunk_monitoring_console', 'introspection'];
|
|
||||||
return !isHidden && !systemApps.includes(appName);
|
|
||||||
})
|
|
||||||
.map(item => ({
|
|
||||||
id: item.name,
|
|
||||||
name: item.content.label || item.name,
|
|
||||||
description: item.content.description || ''
|
|
||||||
}))
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name));
|
|
||||||
|
|
||||||
console.log("Filtered apps: " + apps.length);
|
|
||||||
populateAppsList(apps);
|
|
||||||
} else {
|
|
||||||
console.warn("No apps found");
|
|
||||||
showAppsEmpty();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error("API Error:", error);
|
|
||||||
showAppsEmpty();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function populateAppsList(apps) {
|
|
||||||
console.log("populateAppsList called with", apps.length, "apps");
|
|
||||||
|
|
||||||
const container = document.getElementById('dashboard-list');
|
|
||||||
|
|
||||||
if (!container) {
|
|
||||||
console.error("dashboard-list container not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!apps || apps.length === 0) {
|
|
||||||
showAppsEmpty();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let html = '<div class="select-all-group">';
|
|
||||||
html += '<input type="checkbox" id="select-all" onchange="toggleSelectAll(this)">';
|
|
||||||
html += '<label for="select-all">Select All (' + apps.length + ' apps)</label>';
|
|
||||||
html += '</div>';
|
|
||||||
|
|
||||||
apps.forEach((app, index) => {
|
|
||||||
const checkboxId = 'app-' + index;
|
|
||||||
html += '<div class="dashboard-item">';
|
|
||||||
html += '<input type="checkbox" id="' + checkboxId + '" value="' + app.id + '" data-app="' + app.id + '" data-name="' + app.name + '">';
|
|
||||||
html += '<label for="' + checkboxId + '"><strong>' + app.name + '</strong> <span class="app-badge">' + app.id + '</span>';
|
|
||||||
if (app.description) {
|
|
||||||
html += '<div style="font-size: 11px; color: #666; margin-top: 3px;">' + app.description + '</div>';
|
|
||||||
}
|
|
||||||
html += '</label>';
|
|
||||||
html += '</div>';
|
|
||||||
});
|
|
||||||
|
|
||||||
container.innerHTML = html;
|
|
||||||
console.log('Successfully populated ' + apps.length + ' apps');
|
|
||||||
}
|
|
||||||
|
|
||||||
function showAppsEmpty() {
|
|
||||||
const container = document.getElementById('dashboard-list');
|
|
||||||
if (container) {
|
|
||||||
container.innerHTML = '<div class="dashboard-empty">No apps found</div>';
|
|
||||||
}
|
|
||||||
console.log("Displayed empty state");
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleSelectAll(checkbox) {
|
|
||||||
const checkboxes = document.querySelectorAll('#dashboard-list input[type="checkbox"][data-app]');
|
|
||||||
checkboxes.forEach(cb => cb.checked = checkbox.checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// POUSSER LES DASHBOARDS VERS GIT
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
function pushDashboards() {
|
|
||||||
console.log("pushDashboards called");
|
|
||||||
|
|
||||||
const gitUrl = document.getElementById('git-url').value;
|
|
||||||
const gitBranch = document.getElementById('git-branch').value;
|
|
||||||
const gitToken = document.getElementById('git-token').value;
|
|
||||||
const commitMessage = document.getElementById('commit-message').value;
|
|
||||||
|
|
||||||
console.log("Git URL:", gitUrl);
|
|
||||||
console.log("Git Branch:", gitBranch);
|
|
||||||
console.log("Commit Message:", commitMessage);
|
|
||||||
|
|
||||||
const checkboxes = document.querySelectorAll('#dashboard-list input[type="checkbox"]:not(#select-all):checked');
|
|
||||||
const selectedApps = Array.from(checkboxes).map(cb => ({
|
|
||||||
id: cb.value,
|
|
||||||
name: cb.getAttribute('data-name')
|
|
||||||
}));
|
|
||||||
|
|
||||||
console.log("Selected apps:", selectedApps);
|
|
||||||
|
|
||||||
// Validation
|
|
||||||
if (!gitUrl.trim()) {
|
|
||||||
console.warn("Validation failed: No Git URL");
|
|
||||||
showError('Please enter a Git repository URL');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!gitToken.trim()) {
|
|
||||||
console.warn("Validation failed: No Git token");
|
|
||||||
showError('Please enter your Git token or password');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!commitMessage.trim()) {
|
|
||||||
console.warn("Validation failed: No commit message");
|
|
||||||
showError('Please enter a commit message');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedApps.length === 0) {
|
|
||||||
console.warn("Validation failed: No apps selected");
|
|
||||||
showError('Please select at least one application');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Validation passed, showing loading state...");
|
|
||||||
|
|
||||||
// Afficher le loading
|
|
||||||
document.getElementById('loading').style.display = 'block';
|
|
||||||
document.getElementById('success-msg').style.display = 'none';
|
|
||||||
document.getElementById('error-msg').style.display = 'none';
|
|
||||||
document.getElementById('push-btn').disabled = true;
|
|
||||||
|
|
||||||
// Sauvegarder les credentials si la case est cochée
|
|
||||||
if (document.getElementById('save-credentials').checked) {
|
|
||||||
try {
|
|
||||||
setCookie('git_pusher_url', gitUrl, 30);
|
|
||||||
setCookie('git_pusher_token', gitToken, 30);
|
|
||||||
setCookie('git_pusher_branch', gitBranch, 30);
|
|
||||||
console.log("Credentials auto-saved to cookies");
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Could not auto-save credentials:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Préparer les données - passer les apps au lieu des dashboards
|
|
||||||
const payload = {
|
|
||||||
git_url: gitUrl,
|
|
||||||
git_branch: gitBranch,
|
|
||||||
git_token: gitToken,
|
|
||||||
apps: selectedApps,
|
|
||||||
commit_message: commitMessage,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
user: getCurrentUser()
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("Payload prepared:", payload);
|
|
||||||
|
|
||||||
// Appeler le script Python via serveur
|
|
||||||
callPushScript(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
function callPushScript(payload) {
|
|
||||||
console.log("callPushScript called");
|
|
||||||
console.log("Payload:", payload);
|
|
||||||
|
|
||||||
// Construire l'URL vers le serveur Python sur le port 9999 en HTTP
|
|
||||||
const hostname = window.location.hostname;
|
|
||||||
const baseUrl = `http://${hostname}:9999`;
|
|
||||||
|
|
||||||
const url = new URL('/push', baseUrl);
|
|
||||||
|
|
||||||
// Ajouter les paramètres en query string
|
|
||||||
url.searchParams.append('git_url', payload.git_url);
|
|
||||||
url.searchParams.append('git_branch', payload.git_branch);
|
|
||||||
url.searchParams.append('git_token', payload.git_token);
|
|
||||||
url.searchParams.append('commit_message', payload.commit_message);
|
|
||||||
|
|
||||||
// Encoder correctement les apps en JSON
|
|
||||||
const appsJson = JSON.stringify(payload.apps);
|
|
||||||
console.log("Apps JSON:", appsJson);
|
|
||||||
url.searchParams.append('apps', appsJson);
|
|
||||||
|
|
||||||
url.searchParams.append('user', payload.user);
|
|
||||||
|
|
||||||
console.log("Calling:", url.toString());
|
|
||||||
|
|
||||||
fetch(url.toString(), {
|
|
||||||
method: 'POST',
|
|
||||||
mode: 'no-cors'
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
// Avec no-cors, on ne peut pas lire response.json()
|
|
||||||
// Donc on suppose que si la requête arrive au serveur, c'est bon
|
|
||||||
console.log("Request sent successfully");
|
|
||||||
document.getElementById('loading').style.display = 'none';
|
|
||||||
document.getElementById('push-btn').disabled = false;
|
|
||||||
showSuccess('Push request sent! Check server logs for details.');
|
|
||||||
resetForm();
|
|
||||||
return;
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Fetch error:', error);
|
|
||||||
document.getElementById('loading').style.display = 'none';
|
|
||||||
document.getElementById('push-btn').disabled = false;
|
|
||||||
showError('Network error: ' + error.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCurrentUser() {
|
|
||||||
try {
|
|
||||||
// Essayer plusieurs méthodes
|
|
||||||
if (Splunk && Splunk.util && typeof Splunk.util.getCurrentUser === 'function') {
|
|
||||||
return Splunk.util.getCurrentUser();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: chercher dans le DOM ou retourner 'unknown'
|
|
||||||
return 'unknown_user';
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Could not get current user:", e);
|
|
||||||
return 'unknown_user';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSuccess(message) {
|
|
||||||
const successMsg = document.getElementById('success-msg');
|
|
||||||
document.getElementById('success-text').textContent = message;
|
|
||||||
successMsg.style.display = 'block';
|
|
||||||
setTimeout(() => {
|
|
||||||
successMsg.style.display = 'none';
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showError(message) {
|
|
||||||
const errorMsg = document.getElementById('error-msg');
|
|
||||||
document.getElementById('error-text').textContent = 'X ' + message;
|
|
||||||
errorMsg.style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetForm(showConfirm = false) {
|
|
||||||
document.getElementById('git-url').value = '';
|
|
||||||
document.getElementById('git-branch').value = 'main';
|
|
||||||
document.getElementById('git-token').value = '';
|
|
||||||
document.getElementById('commit-message').value = '';
|
|
||||||
document.querySelectorAll('#dashboard-list input[type="checkbox"]').forEach(cb => cb.checked = false);
|
|
||||||
|
|
||||||
// Demander si l'utilisateur veut aussi effacer les credentials sauvegardés
|
|
||||||
// SEULEMENT si showConfirm = true (c'est-à-dire si l'utilisateur a cliqué sur Reset)
|
|
||||||
if (showConfirm && document.getElementById('save-credentials').checked) {
|
|
||||||
const confirmClear = confirm('Do you want to clear saved credentials?');
|
|
||||||
if (confirmClear) {
|
|
||||||
clearSavedCredentials();
|
|
||||||
document.getElementById('save-credentials').checked = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
This is where you put any scripts you want to add to this app.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
919562
|
|
||||||
@ -1,340 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import tempfile
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
from datetime import datetime
|
|
||||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
||||||
from urllib.parse import parse_qs, urlparse
|
|
||||||
|
|
||||||
# Configuration du logging
|
|
||||||
log_dir = '/opt/splunk/var/log/splunk'
|
|
||||||
os.makedirs(log_dir, exist_ok=True)
|
|
||||||
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
||||||
handlers=[
|
|
||||||
logging.FileHandler(os.path.join(log_dir, 'git_pusher.log')),
|
|
||||||
logging.StreamHandler()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
logger = logging.getLogger('git_pusher')
|
|
||||||
|
|
||||||
|
|
||||||
class GitPusherRequestHandler(BaseHTTPRequestHandler):
|
|
||||||
"""Handler pour les requêtes HTTP"""
|
|
||||||
|
|
||||||
def do_OPTIONS(self):
|
|
||||||
"""Traiter les requêtes OPTIONS (CORS preflight)"""
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header('Access-Control-Allow-Origin', '*')
|
|
||||||
self.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
|
|
||||||
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
|
||||||
self.end_headers()
|
|
||||||
|
|
||||||
def do_POST(self):
|
|
||||||
"""Traiter les requêtes POST"""
|
|
||||||
# Envoyer les headers CORS EN PREMIER
|
|
||||||
self.send_response(200)
|
|
||||||
self.send_header('Content-type', 'application/json')
|
|
||||||
self.send_header('Access-Control-Allow-Origin', '*')
|
|
||||||
self.send_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
|
|
||||||
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
|
||||||
self.end_headers()
|
|
||||||
|
|
||||||
try:
|
|
||||||
logger.info(f"POST request to {self.path}")
|
|
||||||
|
|
||||||
# Parser l'URL et les paramètres
|
|
||||||
parsed_url = urlparse(self.path)
|
|
||||||
query_params = parse_qs(parsed_url.query)
|
|
||||||
|
|
||||||
logger.info(f"Query params keys: {list(query_params.keys())}")
|
|
||||||
|
|
||||||
# Extraire les paramètres
|
|
||||||
git_url = query_params.get('git_url', [''])[0]
|
|
||||||
git_branch = query_params.get('git_branch', ['main'])[0]
|
|
||||||
git_token = query_params.get('git_token', [''])[0]
|
|
||||||
commit_message = query_params.get('commit_message', [''])[0]
|
|
||||||
|
|
||||||
# Accepter soit 'apps' soit 'dashboards'
|
|
||||||
apps_json = query_params.get('apps', query_params.get('dashboards', ['[]']))[0]
|
|
||||||
user = query_params.get('user', ['unknown'])[0]
|
|
||||||
|
|
||||||
logger.info(f"Parameters received: git_url={git_url}, branch={git_branch}, user={user}")
|
|
||||||
logger.info(f"Raw apps_json: '{apps_json}'")
|
|
||||||
|
|
||||||
# Parser les apps
|
|
||||||
try:
|
|
||||||
# parse_qs décode déjà, mais au cas où
|
|
||||||
if isinstance(apps_json, str):
|
|
||||||
apps = json.loads(apps_json)
|
|
||||||
else:
|
|
||||||
apps = apps_json
|
|
||||||
except (json.JSONDecodeError, TypeError) as e:
|
|
||||||
logger.error(f"JSON parse error: {e} - trying to parse: {apps_json}")
|
|
||||||
apps = []
|
|
||||||
|
|
||||||
logger.info(f"Parsed apps: {len(apps)} items - {apps}")
|
|
||||||
|
|
||||||
# Valider
|
|
||||||
if not git_url or not git_token or not commit_message or not apps:
|
|
||||||
logger.warning(f"Validation failed: git_url={bool(git_url)}, git_token={bool(git_token)}, commit_message={bool(commit_message)}, apps={len(apps)}")
|
|
||||||
response = {
|
|
||||||
'status': 'error',
|
|
||||||
'message': 'Missing required parameters'
|
|
||||||
}
|
|
||||||
self.wfile.write(json.dumps(response).encode())
|
|
||||||
return
|
|
||||||
|
|
||||||
# Créer un répertoire temporaire
|
|
||||||
temp_dir = tempfile.mkdtemp(prefix='splunk_git_')
|
|
||||||
logger.info(f"Created temp directory: {temp_dir}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Préparer l'URL Git avec le token
|
|
||||||
git_url_with_token = self.prepare_git_url(git_url, git_token)
|
|
||||||
|
|
||||||
logger.info(f"Git URL prepared (token inserted)")
|
|
||||||
logger.debug(f"Git URL with token: {git_url_with_token}")
|
|
||||||
logger.info("Cloning repository...")
|
|
||||||
self.clone_repository(temp_dir, git_url_with_token, git_branch)
|
|
||||||
|
|
||||||
# Récupérer TOUTES les applications (dossiers complets)
|
|
||||||
logger.info("Fetching applications from Splunk...")
|
|
||||||
dashboard_contents = self.fetch_apps_directories(apps)
|
|
||||||
|
|
||||||
# Créer le dossier apps
|
|
||||||
apps_dir = os.path.join(temp_dir, 'apps')
|
|
||||||
os.makedirs(apps_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# Copier les applications
|
|
||||||
logger.info("Copying applications to repository...")
|
|
||||||
for app_data in dashboard_contents:
|
|
||||||
app_name = app_data['name']
|
|
||||||
app_path = app_data['path']
|
|
||||||
dest_path = os.path.join(apps_dir, app_name)
|
|
||||||
|
|
||||||
if os.path.exists(app_path):
|
|
||||||
logger.info(f"Copying app {app_name} from {app_path}")
|
|
||||||
# Supprimer le dossier s'il existe déjà
|
|
||||||
if os.path.exists(dest_path):
|
|
||||||
logger.info(f"Removing existing app directory: {dest_path}")
|
|
||||||
shutil.rmtree(dest_path)
|
|
||||||
# Copier le dossier
|
|
||||||
shutil.copytree(app_path, dest_path)
|
|
||||||
logger.info(f"Copied app: {app_name}")
|
|
||||||
else:
|
|
||||||
logger.warning(f"App path not found: {app_path}")
|
|
||||||
|
|
||||||
# Configurer git
|
|
||||||
logger.info("Configuring git...")
|
|
||||||
subprocess.run(['git', 'config', 'user.email', 'splunk@splunk.local'],
|
|
||||||
cwd=temp_dir, capture_output=True)
|
|
||||||
subprocess.run(['git', 'config', 'user.name', 'Splunk Git Pusher'],
|
|
||||||
cwd=temp_dir, capture_output=True)
|
|
||||||
|
|
||||||
# Commit et push
|
|
||||||
logger.info("Adding files...")
|
|
||||||
subprocess.run(['git', 'add', '-A'], cwd=temp_dir, capture_output=True)
|
|
||||||
|
|
||||||
full_message = f"{commit_message}\n\nPushed by: {user}\nTimestamp: {datetime.now().isoformat()}"
|
|
||||||
logger.info("Committing...")
|
|
||||||
result = subprocess.run(['git', 'commit', '-m', full_message],
|
|
||||||
cwd=temp_dir, capture_output=True, text=True)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
logger.warning(f"Commit may have failed or had no changes: {result.stderr}")
|
|
||||||
|
|
||||||
logger.info("Pushing...")
|
|
||||||
result = subprocess.run(['git', 'push', 'origin', git_branch],
|
|
||||||
cwd=temp_dir, capture_output=True, text=True, timeout=60)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
raise Exception(f"Push failed: {result.stderr}")
|
|
||||||
|
|
||||||
logger.info("Push successful!")
|
|
||||||
response = {
|
|
||||||
'status': 'success',
|
|
||||||
'message': f'Successfully pushed {len(dashboard_contents)} dashboards from {len(apps)} application(s) to Git',
|
|
||||||
'dashboards_pushed': len(dashboard_contents)
|
|
||||||
}
|
|
||||||
self.wfile.write(json.dumps(response).encode())
|
|
||||||
|
|
||||||
finally:
|
|
||||||
logger.info(f"Cleaning up {temp_dir}")
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error: {str(e)}", exc_info=True)
|
|
||||||
response = {
|
|
||||||
'status': 'error',
|
|
||||||
'message': f'Error: {str(e)}'
|
|
||||||
}
|
|
||||||
self.wfile.write(json.dumps(response).encode())
|
|
||||||
|
|
||||||
def log_message(self, format, *args):
|
|
||||||
"""Éviter les logs HTTP par défaut"""
|
|
||||||
logger.debug(format % args)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def prepare_git_url(git_url, token):
|
|
||||||
"""Préparer l'URL Git avec le token inséré"""
|
|
||||||
logger.info(f"Preparing git URL with token")
|
|
||||||
|
|
||||||
# Si l'URL contient déjà un token (format: https://user:token@host/repo)
|
|
||||||
# on le remplace
|
|
||||||
if '@' in git_url:
|
|
||||||
# Extraire la partie sans le token
|
|
||||||
protocol = git_url.split('://')[0]
|
|
||||||
rest = git_url.split('://', 1)[1]
|
|
||||||
host_and_path = rest.split('@', 1)[1] if '@' in rest else rest
|
|
||||||
return f"{protocol}://{token}@{host_and_path}"
|
|
||||||
|
|
||||||
# Si l'URL est juste https://host/repo (sans credentials)
|
|
||||||
if git_url.startswith('https://') or git_url.startswith('http://'):
|
|
||||||
protocol = git_url.split('://')[0]
|
|
||||||
host_and_path = git_url.split('://', 1)[1]
|
|
||||||
# Insérer le token au format user:token@host ou juste token@host
|
|
||||||
return f"{protocol}://{token}@{host_and_path}"
|
|
||||||
|
|
||||||
return git_url
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def clone_repository(dest_dir, git_url, branch):
|
|
||||||
"""Cloner le repository"""
|
|
||||||
try:
|
|
||||||
cmd = ['git', 'clone', '--depth', '1', '--branch', branch, git_url, dest_dir]
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
raise Exception(f"Clone failed: {result.stderr}")
|
|
||||||
|
|
||||||
logger.info("Repository cloned successfully")
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
raise Exception("Git clone operation timed out")
|
|
||||||
except FileNotFoundError:
|
|
||||||
raise Exception("Git is not installed on this system")
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def fetch_apps_directories(apps):
|
|
||||||
"""Récupérer les dossiers complets des applications"""
|
|
||||||
logger.info(f"Fetching directories for {len(apps)} applications")
|
|
||||||
|
|
||||||
splunk_home = '/opt/splunk'
|
|
||||||
apps_base_path = os.path.join(splunk_home, 'etc', 'apps')
|
|
||||||
|
|
||||||
app_directories = []
|
|
||||||
|
|
||||||
for app in apps:
|
|
||||||
app_id = app.get('id') or app.get('app_id')
|
|
||||||
app_path = os.path.join(apps_base_path, app_id)
|
|
||||||
|
|
||||||
logger.info(f"Checking app directory: {app_path}")
|
|
||||||
|
|
||||||
if os.path.isdir(app_path):
|
|
||||||
app_directories.append({
|
|
||||||
'name': app_id,
|
|
||||||
'path': app_path,
|
|
||||||
'size': sum(os.path.getsize(os.path.join(dirpath, filename))
|
|
||||||
for dirpath, dirnames, filenames in os.walk(app_path)
|
|
||||||
for filename in filenames)
|
|
||||||
})
|
|
||||||
logger.info(f"Found app: {app_id} at {app_path}")
|
|
||||||
else:
|
|
||||||
logger.warning(f"App directory not found: {app_path}")
|
|
||||||
|
|
||||||
logger.info(f"Successfully found {len(app_directories)} application directories")
|
|
||||||
return app_directories
|
|
||||||
"""Récupérer TOUS les dashboards de chaque application"""
|
|
||||||
logger.info(f"Fetching dashboards from {len(apps)} applications")
|
|
||||||
|
|
||||||
import urllib.request
|
|
||||||
import urllib.error
|
|
||||||
import ssl
|
|
||||||
import base64
|
|
||||||
|
|
||||||
# Ignorer les certificats SSL auto-signés
|
|
||||||
ssl_context = ssl.create_default_context()
|
|
||||||
ssl_context.check_hostname = False
|
|
||||||
ssl_context.verify_mode = ssl.CERT_NONE
|
|
||||||
|
|
||||||
dashboard_contents = []
|
|
||||||
|
|
||||||
# Lire le fichier de configuration Splunk pour obtenir les credentials
|
|
||||||
# Ou utiliser des credentials par défaut
|
|
||||||
splunk_username = os.environ.get('SPLUNK_USERNAME', 'admin')
|
|
||||||
splunk_password = os.environ.get('SPLUNK_PASSWORD', 'changeme')
|
|
||||||
|
|
||||||
# Créer l'authentification Basic
|
|
||||||
credentials = base64.b64encode(f"{splunk_username}:{splunk_password}".encode()).decode()
|
|
||||||
|
|
||||||
for app in apps:
|
|
||||||
app_id = app.get('id') or app.get('app_id')
|
|
||||||
logger.info(f"Fetching all dashboards from app: {app_id}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Récupérer la liste de TOUS les dashboards de cette app
|
|
||||||
api_url = f"https://127.0.0.1:8089/servicesNS/-/{app_id}/data/ui/views?output_mode=json&count=0"
|
|
||||||
|
|
||||||
logger.debug(f"API URL: {api_url}")
|
|
||||||
|
|
||||||
req = urllib.request.Request(api_url)
|
|
||||||
req.add_header('Authorization', f'Basic {credentials}')
|
|
||||||
|
|
||||||
with urllib.request.urlopen(req, timeout=15, context=ssl_context) as response:
|
|
||||||
api_data = json.loads(response.read().decode('utf-8'))
|
|
||||||
|
|
||||||
if 'entry' in api_data and len(api_data['entry']) > 0:
|
|
||||||
for entry in api_data['entry']:
|
|
||||||
try:
|
|
||||||
dashboard_id = entry.get('name')
|
|
||||||
content = entry.get('content', {})
|
|
||||||
|
|
||||||
# eai:data contient le XML complet du dashboard
|
|
||||||
dashboard_xml = content.get('eai:data', '')
|
|
||||||
|
|
||||||
if dashboard_xml:
|
|
||||||
dashboard_contents.append({
|
|
||||||
'id': f"{app_id}_{dashboard_id}",
|
|
||||||
'app': app_id,
|
|
||||||
'content': dashboard_xml,
|
|
||||||
'name': dashboard_id
|
|
||||||
})
|
|
||||||
logger.debug(f"Fetched: {dashboard_id} from {app_id}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error processing dashboard entry: {str(e)}")
|
|
||||||
|
|
||||||
logger.info(f"Found {len([d for d in dashboard_contents if d['app'] == app_id])} dashboards in {app_id}")
|
|
||||||
else:
|
|
||||||
logger.warning(f"No dashboards found in app {app_id}")
|
|
||||||
|
|
||||||
except urllib.error.HTTPError as e:
|
|
||||||
logger.error(f"HTTP {e.code} when fetching app {app_id}: {e.reason}")
|
|
||||||
except urllib.error.URLError as e:
|
|
||||||
logger.error(f"Cannot reach Splunk API for app {app_id}: {e.reason}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error fetching dashboards from {app_id}: {str(e)}")
|
|
||||||
|
|
||||||
logger.info(f"Successfully fetched {len(dashboard_contents)} dashboards total")
|
|
||||||
return dashboard_contents
|
|
||||||
|
|
||||||
|
|
||||||
def start_server(port=9999):
|
|
||||||
"""Démarrer le serveur HTTP"""
|
|
||||||
server = HTTPServer(('0.0.0.0', port), GitPusherRequestHandler)
|
|
||||||
logger.info(f"Git Pusher server listening on 0.0.0.0:{port} (HTTP)")
|
|
||||||
server.serve_forever()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# Démarrer le serveur en background
|
|
||||||
port = 9999
|
|
||||||
logger.info(f"Starting Git Pusher on port {port}")
|
|
||||||
start_server(port)
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
export SPLUNK_USERNAME=admin
|
|
||||||
export SPLUNK_PASSWORD='2312Jocpam!?'
|
|
||||||
python3 /opt/splunk/etc/apps/pusher_app/bin/git_pusher.py > /opt/splunk/var/log/splunk/git_pusher_startup.log 2>&1 &
|
|
||||||
echo $! > /opt/splunk/etc/apps/pusher_app/bin/git_pusher.pid
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDCTCCAfGgAwIBAgIUCuKo8SLloS5cjBOR04+X6ayZ40cwDQYJKoZIhvcNAQEL
|
|
||||||
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDEyMzIyMTIxOFoXDTI3MDEy
|
|
||||||
MzIyMTIxOFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
|
|
||||||
AAOCAQ8AMIIBCgKCAQEAs0vF6sFTseKgZC1nZ6CVZdw45yk1Ni0W9Mc24KZ9NKCJ
|
|
||||||
rP0tHy0hs6mME/sq8DV1fh0YtqIvBCxcKEE84/cVXmUfZF9JRXO95734+JGPmo07
|
|
||||||
zpiu7p3r4WyIWmCXX5VB0UkMEXsPQmonqG1Kwtz+R1cfgis2lUk+xsC2zSjER8l4
|
|
||||||
2UODjHvtD25usgxKjpwPrCuZt43miArnVnwfB8OLbAqpwQeYIf18bPt/TrnQsdgd
|
|
||||||
ZZiQdE6UTaJ5xhqztwpYJO9pvZA24Bi3bGNfBciITds5RCGY2wQo8yxbeJsidTuW
|
|
||||||
7Z64DK9t33oVnB2PqlP6hVGD5Agthsv9ehRPxdd3MwIDAQABo1MwUTAdBgNVHQ4E
|
|
||||||
FgQUy0dni+ogqC7YuvfD/Pn0AuebsXQwHwYDVR0jBBgwFoAUy0dni+ogqC7YuvfD
|
|
||||||
/Pn0AuebsXQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAlyxg
|
|
||||||
vR15lsYp4TxJPi1WPzLZl1e6ewTl8GhyE1saxS8LRtyTyr8sa9EFRLQ0OIsqYrUw
|
|
||||||
zZi7FIDoDPZDKpd0/+U94UKlhUuPUyufQwl5vNu0A+SEpwKeznUMaj4Y98tHvVGd
|
|
||||||
1SCndZBWn/v2U4nXqHoTd6Y0xEOga0jUEsUMBckNC236BTo88Zk65/oa9Gncyb27
|
|
||||||
9vGVCbmPyzE70H4KFoVtxkoZrKywn+0ajHhgH5gqZNRPWpe6i8xTbMAeIXkCjmWL
|
|
||||||
LmOA7MkjeQBBEWewu4vMOXsvf+gCtxUj5owsAcOQlZ3g72Sng4MeMjuVx4ZRVxX9
|
|
||||||
fj+vCP9EFI8rX48tjQ==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQCzS8XqwVOx4qBk
|
|
||||||
LWdnoJVl3DjnKTU2LRb0xzbgpn00oIms/S0fLSGzqYwT+yrwNXV+HRi2oi8ELFwo
|
|
||||||
QTzj9xVeZR9kX0lFc73nvfj4kY+ajTvOmK7unevhbIhaYJdflUHRSQwRew9Caieo
|
|
||||||
bUrC3P5HVx+CKzaVST7GwLbNKMRHyXjZQ4OMe+0Pbm6yDEqOnA+sK5m3jeaICudW
|
|
||||||
fB8Hw4tsCqnBB5gh/Xxs+39OudCx2B1lmJB0TpRNonnGGrO3Clgk72m9kDbgGLds
|
|
||||||
Y18FyIhN2zlEIZjbBCjzLFt4myJ1O5btnrgMr23fehWcHY+qU/qFUYPkCC2Gy/16
|
|
||||||
FE/F13czAgMBAAECggEAMrEMrvej0xpQ4KHZp3nGY3sk9242JjAPWntsb42CvrtY
|
|
||||||
0XjvJe5bpfEcspWDqVBj/Jj7YL9v7Y0hLRxsu8Mi3oJWoskx7RnxKjES0CxPXpHp
|
|
||||||
w9p1Mu+hPiWyU2MVySdo6WPuro6NXOiod70WswtKNR9TwDi5gPGpdwYLaOvKusSp
|
|
||||||
Rncm0m0H3IBhgVA691X0AUIomAW3Wmh+5If1XHfjrNHTB8cjcNf6koPMkCqHCEZ9
|
|
||||||
wtINxOJior+gGkjMXaDszqzNlicVBXFEFjaXWcp38xAif1uimpqKsRzZEF6RAUzi
|
|
||||||
H7cI3aF2dXG3C9l6Byi7OSgd8X4JUnE0dlCpC7qweQKBgQDgvoavo8G0kYruCUIQ
|
|
||||||
6vcSs1YBByOkl6yZBCZWk10NgRpU1wyu9zmlvEwNVlUfALs5eoLxnhe8Wklq0ckQ
|
|
||||||
r/Rl+r/lj/MZUFn49TgUCsUOIi/G7nWQG0bPo4bCB2QXsAiKdY+KZeC56620uyom
|
|
||||||
1VY+nS3y8O4EP0YHX0qHFfmIZwKBgQDMOywO0DSrZMDyvmbwL0ISzHRcNpRn0jk7
|
|
||||||
pEtzM/VOx+v0O93E+5OygzmXlBKjF0MwMXBidf8IZu4xO8qWqAM4EP2DD0cpoS1Z
|
|
||||||
WiHHkc5NZhjgeG6C4XaCXR++7CuY25VKKe01yz/+j51linDD8OAibKUspkjVufEN
|
|
||||||
R/AT0GFLVQKBgBxMYTEkcXOHD/NA/yyaKVoVcrLWb0p+PqFVwG4OSB03MFWWbmZp
|
|
||||||
gry3pOvY/wbUVL68CljaCysQQ0ZL/AE55pAgrqD9KyL41xtd5R3A7WcGLvXheLQY
|
|
||||||
eyYR9RnhTF0fMTQd8WD/yvgeENU86+XP3vgrWmnIpG+sd+jdusifn7fpAn9QkwfO
|
|
||||||
0FX3SMjW/EegewSWZhOCTgY+77Gk1izuRpGBg16T/QqBrL+Yri0KoGC593OKj/bG
|
|
||||||
4ca8id9vjSdgSOj8NbfO/TgWNICvv9+T3PKHlsA5z0nKWSloRVVA/ew1YmyD1gbA
|
|
||||||
MnAM/pwac4QJyf6jljmUZAZYTAPOOZN+PbglAoGBAJ9cOGDgT+BCOoNc0T1GJDAk
|
|
||||||
xOR8d+tD+j4JH5IVxB51DXjJOZxw9U3XhNH1OcE0x3fRzKJOtlQLxP6fHYVtMVFq
|
|
||||||
VpeekmTtJ9OfMg68ELOlf7ykA3GhMJ3FarM6e8+X+KliGf6ND4HBMb112FlMgIi6
|
|
||||||
yYi7sfSL53Dzp1Q2DxXV
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
#
|
|
||||||
# Splunk app configuration file
|
|
||||||
#
|
|
||||||
|
|
||||||
[install]
|
|
||||||
is_configured = 0
|
|
||||||
|
|
||||||
[ui]
|
|
||||||
is_visible = true
|
|
||||||
label = Pusher Premium
|
|
||||||
|
|
||||||
[launcher]
|
|
||||||
author =
|
|
||||||
description =
|
|
||||||
version = 1.0.0
|
|
||||||
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
<nav search_view="search">
|
|
||||||
<view name="search" default='true' />
|
|
||||||
<view name="analytics_workspace" />
|
|
||||||
<view name="datasets" />
|
|
||||||
<view name="reports" />
|
|
||||||
<view name="alerts" />
|
|
||||||
<view name="dashboards" />
|
|
||||||
</nav>
|
|
||||||
@ -1 +0,0 @@
|
|||||||
Add all the views that your app needs in this directory
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
[ui]
|
|
||||||
|
|
||||||
[launcher]
|
|
||||||
@ -1,484 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<dashboard version="1.1" script="git_pusher.js">
|
|
||||||
<label>🚀 Git Pusher - Deploy Applications</label>
|
|
||||||
<description>Modern interface to push Splunk applications to Git repository</description>
|
|
||||||
|
|
||||||
<search id="dsearch">
|
|
||||||
<query>| rest /services/apps/local | search disabled=0 | fields name, label, description | sort label</query>
|
|
||||||
<earliest>-4h@h</earliest>
|
|
||||||
<latest>now</latest>
|
|
||||||
</search>
|
|
||||||
|
|
||||||
<row>
|
|
||||||
<panel>
|
|
||||||
<html>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 40px 20px;
|
|
||||||
text-align: center;
|
|
||||||
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4);
|
|
||||||
margin-bottom: 30px;
|
|
||||||
border-radius: 0 0 20px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header p {
|
|
||||||
font-size: 14px;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 30px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: white;
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 30px;
|
|
||||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
|
||||||
box-shadow: 0 15px 50px rgba(102, 126, 234, 0.2);
|
|
||||||
transform: translateY(-5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title::before {
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
width: 4px;
|
|
||||||
height: 24px;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input[type="text"],
|
|
||||||
.form-group input[type="password"],
|
|
||||||
.form-group textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border: 2px solid #e0e0e0;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
font-family: 'Segoe UI', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input[type="text"]:focus,
|
|
||||||
.form-group input[type="password"]:focus,
|
|
||||||
.form-group textarea:focus {
|
|
||||||
border-color: #667eea;
|
|
||||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group textarea {
|
|
||||||
resize: vertical;
|
|
||||||
min-height: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 10px;
|
|
||||||
padding: 12px;
|
|
||||||
background: #f5f7ff;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-group input[type="checkbox"] {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
accent-color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-group label {
|
|
||||||
margin: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #555;
|
|
||||||
font-size: 13px;
|
|
||||||
text-transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.apps-list {
|
|
||||||
border: 2px solid #e0e0e0;
|
|
||||||
border-radius: 12px;
|
|
||||||
max-height: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
background: #fafbfc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.apps-list::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.apps-list::-webkit-scrollbar-track {
|
|
||||||
background: #f1f1f1;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.apps-list::-webkit-scrollbar-thumb {
|
|
||||||
background: #667eea;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-header {
|
|
||||||
padding: 15px 16px;
|
|
||||||
border-bottom: 2px solid #e0e0e0;
|
|
||||||
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
|
|
||||||
font-weight: 600;
|
|
||||||
color: #667eea;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-count {
|
|
||||||
background: #667eea;
|
|
||||||
color: white;
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-item {
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-bottom: 1px solid #e0e0e0;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-item:hover {
|
|
||||||
background: #f0f4ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-item input[type="checkbox"] {
|
|
||||||
margin-right: 12px;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
accent-color: #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-item label {
|
|
||||||
margin: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
width: calc(100% - 40px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-badge {
|
|
||||||
display: inline-block;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 3px 10px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 700;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
padding: 12px 28px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover:not(:disabled) {
|
|
||||||
box-shadow: 0 6px 25px rgba(102, 126, 234, 0.5);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: #f5f7ff;
|
|
||||||
color: #667eea;
|
|
||||||
border: 2px solid #667eea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover:not(:disabled) {
|
|
||||||
background: #667eea;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
display: none;
|
|
||||||
text-align: center;
|
|
||||||
padding: 30px;
|
|
||||||
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
|
|
||||||
border-radius: 12px;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading.active {
|
|
||||||
display: block;
|
|
||||||
animation: fadeIn 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; }
|
|
||||||
to { opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
border: 3px solid #e0e0e0;
|
|
||||||
border-top: 3px solid #667eea;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
margin: 0 auto 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-text {
|
|
||||||
color: #667eea;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
padding: 16px 20px;
|
|
||||||
border-radius: 12px;
|
|
||||||
margin: 15px 0;
|
|
||||||
display: none;
|
|
||||||
animation: slideIn 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.success-message {
|
|
||||||
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
|
|
||||||
color: white;
|
|
||||||
border-left: 4px solid white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
background: linear-gradient(135deg, #f44336 0%, #da190b 100%);
|
|
||||||
color: white;
|
|
||||||
border-left: 4px solid white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-banner {
|
|
||||||
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
|
|
||||||
border: 2px solid #667eea;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 16px 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
color: #667eea;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
height: 1px;
|
|
||||||
background: linear-gradient(90deg, transparent, #e0e0e0, transparent);
|
|
||||||
margin: 30px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
text-align: center;
|
|
||||||
color: #999;
|
|
||||||
font-size: 12px;
|
|
||||||
margin-top: 40px;
|
|
||||||
padding-bottom: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="header">
|
|
||||||
<h1>🚀 Git Pusher</h1>
|
|
||||||
<p>Deploy your Splunk applications to Git with confidence</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<div class="info-banner">
|
|
||||||
✨ Configure your Git settings below and select the applications you want to deploy to your repository
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid">
|
|
||||||
<!-- Configuration Panel -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-title">⚙️ Configuration</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="git-url">Git Repository URL</label>
|
|
||||||
<input type="text" id="git-url" placeholder="https://github.com/username/repo.git" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="git-branch">Target Branch</label>
|
|
||||||
<input type="text" id="git-branch" placeholder="main" value="main" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="git-token">Git Token / Password</label>
|
|
||||||
<input type="password" id="git-token" placeholder="Enter your Git token or password" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="checkbox-group">
|
|
||||||
<input type="checkbox" id="save-credentials" />
|
|
||||||
<label for="save-credentials">Save credentials locally</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="commit-message">Commit Message</label>
|
|
||||||
<textarea id="commit-message" placeholder="Describe your changes..."></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="button-group">
|
|
||||||
<button class="btn btn-primary" id="push-btn" onclick="pushDashboards()">
|
|
||||||
✈️ Deploy to Git
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary" onclick="resetForm(true)">
|
|
||||||
🔄 Reset
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Applications Panel -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-title">📦 Applications</div>
|
|
||||||
|
|
||||||
<div class="apps-list" id="dashboard-list">
|
|
||||||
<div style="padding: 30px; text-align: center; color: #999;">
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<p>Loading applications...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="loading" id="loading">
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<div class="loading-text">Deploying applications to Git... Please wait</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="message success-message" id="success-msg">
|
|
||||||
✅ <span id="success-text">Applications successfully deployed to Git!</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="message error-message" id="error-msg">
|
|
||||||
❌ <span id="error-text">Error occurred while deploying applications</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
Made with ❤️ for Splunk • Git Pusher v2.0
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</html>
|
|
||||||
</panel>
|
|
||||||
</row>
|
|
||||||
|
|
||||||
</dashboard>
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
|
|
||||||
# Application-level permissions
|
|
||||||
|
|
||||||
[]
|
|
||||||
access = read : [ * ], write : [ admin, power ]
|
|
||||||
|
|
||||||
### EVENT TYPES
|
|
||||||
|
|
||||||
[eventtypes]
|
|
||||||
export = system
|
|
||||||
|
|
||||||
|
|
||||||
### PROPS
|
|
||||||
|
|
||||||
[props]
|
|
||||||
export = system
|
|
||||||
|
|
||||||
|
|
||||||
### TRANSFORMS
|
|
||||||
|
|
||||||
[transforms]
|
|
||||||
export = system
|
|
||||||
|
|
||||||
|
|
||||||
### LOOKUPS
|
|
||||||
|
|
||||||
[lookups]
|
|
||||||
export = system
|
|
||||||
|
|
||||||
|
|
||||||
### VIEWSTATES: even normal users should be able to create shared viewstates
|
|
||||||
|
|
||||||
[viewstates]
|
|
||||||
access = read : [ * ], write : [ * ]
|
|
||||||
export = system
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
[app/ui]
|
|
||||||
version = 10.0.2
|
|
||||||
modtime = 1769115948.043388000
|
|
||||||
|
|
||||||
[app/launcher]
|
|
||||||
version = 10.0.2
|
|
||||||
modtime = 1769115948.046389000
|
|
||||||
|
|
||||||
[views/git_pusher_-_push_dashboards_to_git]
|
|
||||||
access = read : [ admin ], write : [ admin ]
|
|
||||||
export = system
|
|
||||||
owner = admin
|
|
||||||
version = 10.0.2
|
|
||||||
modtime = 1769276443.812957000
|
|
||||||
|
|
||||||
[views/git_pusher_-_push_applications_to_git]
|
|
||||||
owner = admin
|
|
||||||
version = 10.0.2
|
|
||||||
modtime = 1769361925.808816000
|
|
||||||
|
|
||||||
[views/git_pusher_-_deploy_applications]
|
|
||||||
access = read : [ * ], write : [ * ]
|
|
||||||
export = none
|
|
||||||
owner = admin
|
|
||||||
version = 10.0.2
|
|
||||||
modtime = 1769368785.414181000
|
|
||||||
Loading…
Reference in new issue