您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enhance task instance status visualization in Airflow for colorblind users with class transition tracking
// ==UserScript== // @name Airflow Task Instance Status Enhancer // @namespace namilink.airflow.colorblind-status // @version 0.6 // @description Enhance task instance status visualization in Airflow for colorblind users with class transition tracking // @author Mate Valko - Namilink.com // @match *://*/*dags* // @match *://*/*airflow* // @grant none // ==/UserScript== (function() { 'use strict'; // Configuration const CONFIG = { DEBUG: false, THROTTLE_INTERVAL: 1000, TASK_INSTANCE_SELECTOR: '[data-testid="task-instance"]' }; const STATE_MAPPINGS = { 'rgb(128, 128, 128)': { symbol: '⌛', label: 'Queued' }, // gray 'rgb(0, 255, 0)': { symbol: '⚙️', label: 'Running' }, // lime 'rgb(0, 128, 0)': { symbol: '✅', label: 'Success' }, // green 'rgb(238, 130, 238)': { symbol: '🔄', label: 'Restarting' }, // violet 'rgb(255, 0, 0)': { symbol: '❌', label: 'Failed' }, // red 'rgb(255, 215, 0)': { symbol: '🔁', label: 'Up for retry' }, // gold 'rgb(64, 224, 208)': { symbol: '⏳', label: 'Reschedule' }, // turquoise 'rgb(255, 165, 0)': { symbol: '⚠️', label: 'Upstream failed' }, // orange 'rgb(255, 105, 180)': { symbol: '⤵️', label: 'Skipped' }, // hotpink 'rgb(211, 211, 211)': { symbol: '🗑️', label: 'Removed' }, // lightgrey 'rgb(210, 180, 140)': { symbol: '⏰', label: 'Scheduled' }, // tan 'rgb(147, 112, 219)': { symbol: '⏸️', label: 'Deferred' } // mediumpurple }; const ClassTransitionStore = { classToState: new Map(), updateMapping(className, color) { if (!this.classToState.has(className)) { const state = STATE_MAPPINGS[color]; if (state) { this.classToState.set(className, state); debugLog('New class mapping:', className, state, color); } } return this.classToState.get(className); }, getStateForClass(className) { return this.classToState.get(className); }, debug() { console.log('Class to State Mappings:', Object.fromEntries(this.classToState)); } }; let lastExecutionTime = 0; function debugLog(...args) { if (CONFIG.DEBUG) console.log('[AirflowEnhancer]', ...args); } function createStatusIndicator(state) { const container = document.createElement('div'); container.innerHTML = `<div class="status-symbol">${state.symbol}</div>`; container.style.cssText = ` display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; height: 100%; font-size: 14px; font-weight: bold; `; return container; } function findElementsInShadowDOM(root, selector) { const elements = new Set(); function traverse(node) { if (!node) return; if (node.matches && node.matches(selector)) { elements.add(node); } if (node.shadowRoot) { Array.from(node.shadowRoot.querySelectorAll('*')).forEach(traverse); } if (node.children) { Array.from(node.children).forEach(traverse); } } traverse(root); return Array.from(elements); } function modifyElement(element) { if (!element?.isConnected) return; const reactClass = Array.from(element.classList) .find(cls => cls.startsWith('c-')); if (!reactClass) { debugLog('No React class found for element'); return; } let state = ClassTransitionStore.getStateForClass(reactClass); if (!state) { const backgroundColor = window.getComputedStyle(element).backgroundColor; state = ClassTransitionStore.updateMapping(reactClass, backgroundColor); if (!state) { if (backgroundColor !== '' && backgroundColor !== 'inherit' && backgroundColor !== 'transparent') { debugLog('Unable to map new class:', reactClass, 'with color:', backgroundColor); } return; } } debugLog('Applying state:', { class: reactClass, state }); element.style.setProperty('background', 'none', 'important'); element.innerHTML = ''; element.appendChild(createStatusIndicator(state)); } async function modifyElements() { const rootElements = document.querySelectorAll('#root, #react-container, [id*="react"]'); const taskInstances = new Set(); [...rootElements, document.body].forEach(root => { findElementsInShadowDOM(root, CONFIG.TASK_INSTANCE_SELECTOR) .forEach(element => taskInstances.add(element)); }); if (taskInstances.size === 0) { debugLog('No task instances found, retrying...'); await new Promise(resolve => setTimeout(resolve, 300)); return modifyElements(); } debugLog(`Found ${taskInstances.size} task instances`); taskInstances.forEach(modifyElement); } function throttledModifyElements() { const now = Date.now(); if (now - lastExecutionTime >= CONFIG.THROTTLE_INTERVAL) { lastExecutionTime = now; modifyElements(); } } function initialize() { debugLog('Initializing script'); window._airflowEnhancerStore = window._airflowEnhancerStore || ClassTransitionStore; modifyElements(); // Create and configure MutationObserver const observer = new MutationObserver((mutations) => { mutations.forEach(mutation => { if (mutation.addedNodes.length || (mutation.type === 'attributes' && (mutation.attributeName === 'style' || mutation.attributeName === 'class'))) { throttledModifyElements(); } }); }); // Start observing the document observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); // Periodic debug output if (CONFIG.DEBUG) { setInterval(() => { ClassTransitionStore.debug(); }, 10000); } // Event listeners for dynamic content ['load', 'urlchange'].forEach(event => window.addEventListener(event, throttledModifyElements)); // Handle history state changes ['pushState', 'replaceState'].forEach(method => { const original = history[method]; history[method] = function() { original.apply(history, arguments); throttledModifyElements(); }; }); window.addEventListener('popstate', throttledModifyElements); // Cleanup on page unload window.addEventListener('unload', () => { observer.disconnect(); }); } // Start the script if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { initialize(); } })();