fix: Gitea Traefik routing and connection pool optimization
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
This commit is contained in:
256
resources/js/modules/livecomponent/ActionLoadingManager.js
Normal file
256
resources/js/modules/livecomponent/ActionLoadingManager.js
Normal file
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* Action Loading Manager for LiveComponents
|
||||
*
|
||||
* Provides skeleton loading states during component actions with:
|
||||
* - Automatic skeleton overlay during actions
|
||||
* - Configurable skeleton templates per component
|
||||
* - Smooth transitions
|
||||
* - Fragment-aware loading
|
||||
*/
|
||||
|
||||
export class ActionLoadingManager {
|
||||
constructor() {
|
||||
this.loadingStates = new Map(); // componentId → loading state
|
||||
this.skeletonTemplates = new Map(); // componentId → template
|
||||
this.config = {
|
||||
showDelay: 150, // ms before showing skeleton
|
||||
transitionDuration: 200, // ms for transitions
|
||||
preserveContent: true, // Keep content visible under skeleton
|
||||
opacity: 0.6 // Skeleton overlay opacity
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Show loading state for component
|
||||
*
|
||||
* @param {string} componentId - Component ID
|
||||
* @param {HTMLElement} element - Component element
|
||||
* @param {Object} options - Loading options
|
||||
*/
|
||||
showLoading(componentId, element, options = {}) {
|
||||
// Check if already loading
|
||||
if (this.loadingStates.has(componentId)) {
|
||||
return; // Already showing loading state
|
||||
}
|
||||
|
||||
const showDelay = options.showDelay ?? this.config.showDelay;
|
||||
const fragments = options.fragments || null;
|
||||
|
||||
// Delay showing skeleton (for fast responses)
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.createSkeletonOverlay(componentId, element, fragments, options);
|
||||
}, showDelay);
|
||||
|
||||
// Store loading state
|
||||
this.loadingStates.set(componentId, {
|
||||
timeoutId,
|
||||
element,
|
||||
fragments,
|
||||
options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create skeleton overlay
|
||||
*
|
||||
* @param {string} componentId - Component ID
|
||||
* @param {HTMLElement} element - Component element
|
||||
* @param {Array<string>|null} fragments - Fragment names to show skeleton for
|
||||
* @param {Object} options - Options
|
||||
*/
|
||||
createSkeletonOverlay(componentId, element, fragments, options) {
|
||||
// Get skeleton template
|
||||
const template = this.getSkeletonTemplate(componentId, element, fragments);
|
||||
|
||||
// Create overlay container
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'livecomponent-loading-overlay';
|
||||
overlay.setAttribute('data-component-id', componentId);
|
||||
overlay.setAttribute('aria-busy', 'true');
|
||||
overlay.setAttribute('aria-label', 'Loading...');
|
||||
|
||||
overlay.style.cssText = `
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, ${this.config.opacity});
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity ${this.config.transitionDuration}ms ease;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
// Add skeleton content
|
||||
overlay.innerHTML = template;
|
||||
|
||||
// Ensure element has relative positioning
|
||||
const originalPosition = element.style.position;
|
||||
if (getComputedStyle(element).position === 'static') {
|
||||
element.style.position = 'relative';
|
||||
}
|
||||
|
||||
// Append overlay
|
||||
element.appendChild(overlay);
|
||||
|
||||
// Animate in
|
||||
requestAnimationFrame(() => {
|
||||
overlay.style.opacity = '1';
|
||||
});
|
||||
|
||||
// Update loading state
|
||||
const state = this.loadingStates.get(componentId);
|
||||
if (state) {
|
||||
state.overlay = overlay;
|
||||
state.originalPosition = originalPosition;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get skeleton template for component
|
||||
*
|
||||
* @param {string} componentId - Component ID
|
||||
* @param {HTMLElement} element - Component element
|
||||
* @param {Array<string>|null} fragments - Fragment names
|
||||
* @returns {string} Skeleton HTML
|
||||
*/
|
||||
getSkeletonTemplate(componentId, element, fragments) {
|
||||
// Check for custom template
|
||||
const customTemplate = this.skeletonTemplates.get(componentId);
|
||||
if (customTemplate) {
|
||||
return typeof customTemplate === 'function'
|
||||
? customTemplate(element, fragments)
|
||||
: customTemplate;
|
||||
}
|
||||
|
||||
// If fragments specified, create fragment-specific skeletons
|
||||
if (fragments && fragments.length > 0) {
|
||||
return this.createFragmentSkeletons(fragments);
|
||||
}
|
||||
|
||||
// Default skeleton template
|
||||
return this.createDefaultSkeleton(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create default skeleton template
|
||||
*
|
||||
* @param {HTMLElement} element - Component element
|
||||
* @returns {string} Skeleton HTML
|
||||
*/
|
||||
createDefaultSkeleton(element) {
|
||||
const height = element.offsetHeight || 200;
|
||||
const width = element.offsetWidth || '100%';
|
||||
|
||||
return `
|
||||
<div class="skeleton-container" style="width: ${width}; height: ${height}px; padding: 1.5rem;">
|
||||
<div class="skeleton skeleton-text skeleton-text--full" style="margin-bottom: 1rem;"></div>
|
||||
<div class="skeleton skeleton-text skeleton-text--80" style="margin-bottom: 0.75rem;"></div>
|
||||
<div class="skeleton skeleton-text skeleton-text--60" style="margin-bottom: 0.75rem;"></div>
|
||||
<div class="skeleton skeleton-text skeleton-text--full" style="margin-bottom: 1rem;"></div>
|
||||
<div class="skeleton skeleton-text skeleton-text--80"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create fragment-specific skeletons
|
||||
*
|
||||
* @param {Array<string>} fragments - Fragment names
|
||||
* @returns {string} Skeleton HTML
|
||||
*/
|
||||
createFragmentSkeletons(fragments) {
|
||||
return fragments.map(fragmentName => `
|
||||
<div class="skeleton-fragment" data-fragment="${fragmentName}">
|
||||
<div class="skeleton skeleton-text skeleton-text--full" style="margin-bottom: 0.75rem;"></div>
|
||||
<div class="skeleton skeleton-text skeleton-text--80"></div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide loading state
|
||||
*
|
||||
* @param {string} componentId - Component ID
|
||||
*/
|
||||
hideLoading(componentId) {
|
||||
const state = this.loadingStates.get(componentId);
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear timeout if not yet shown
|
||||
if (state.timeoutId) {
|
||||
clearTimeout(state.timeoutId);
|
||||
}
|
||||
|
||||
// Remove overlay if exists
|
||||
if (state.overlay) {
|
||||
// Animate out
|
||||
state.overlay.style.opacity = '0';
|
||||
|
||||
setTimeout(() => {
|
||||
if (state.overlay && state.overlay.parentNode) {
|
||||
state.overlay.parentNode.removeChild(state.overlay);
|
||||
}
|
||||
}, this.config.transitionDuration);
|
||||
}
|
||||
|
||||
// Restore original position
|
||||
if (state.originalPosition !== undefined) {
|
||||
state.element.style.position = state.originalPosition;
|
||||
}
|
||||
|
||||
// Remove from map
|
||||
this.loadingStates.delete(componentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register custom skeleton template for component
|
||||
*
|
||||
* @param {string} componentId - Component ID
|
||||
* @param {string|Function} template - Template HTML or function that returns HTML
|
||||
*/
|
||||
registerTemplate(componentId, template) {
|
||||
this.skeletonTemplates.set(componentId, template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister skeleton template
|
||||
*
|
||||
* @param {string} componentId - Component ID
|
||||
*/
|
||||
unregisterTemplate(componentId) {
|
||||
this.skeletonTemplates.delete(componentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if component is loading
|
||||
*
|
||||
* @param {string} componentId - Component ID
|
||||
* @returns {boolean} True if loading
|
||||
*/
|
||||
isLoading(componentId) {
|
||||
return this.loadingStates.has(componentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update configuration
|
||||
*
|
||||
* @param {Object} newConfig - New configuration
|
||||
*/
|
||||
updateConfig(newConfig) {
|
||||
this.config = {
|
||||
...this.config,
|
||||
...newConfig
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const actionLoadingManager = new ActionLoadingManager();
|
||||
|
||||
Reference in New Issue
Block a user