CICD Sort Tool

i want sort!

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         CICD Sort Tool
// @namespace    lfyou
// @version      1.5
// @description  i want sort!
// @author       lfyou
// @include      /^https?:.*?\/console-.*?/
// @icon         https://cdn.icon-icons.com/icons2/2148/PNG/512/monkey_icon_132188.png
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';
    console.log('hello, sort monkey')


    class LocalConfig {
        storageKey = 'SORT_MONKEY'

        getSuffix(){
            return ''
        }

        constructor(getSuffix){
            if(getSuffix) {
                this.getSuffix = getSuffix
            }
            try{
                this.config = JSON.parse(localStorage.getItem(this.storageKey)) || {}
            }catch{
                 this.config = {}
            }
        }

        getPageKey(){
            const prefix = location.host + location.pathname

            return prefix + '?' + this.getSuffix()
        }

        getConfig(){
            return this.config[this.getPageKey()] || []
        }

        setConfig(key,index){
            const pageKey = this.getPageKey()
            const currentConfig = (this.config[pageKey] = this.config[pageKey] || [])

            const originIdx = currentConfig.indexOf(key)
            if(originIdx >= 0){
                currentConfig.splice(originIdx,1)
            }

            if(Number.isInteger(index)){
                currentConfig.splice(index,0,key)
            }else{
                currentConfig.push(key)
            }


            this.updateLocalStorage()
        }

        removeConfig(key){
            const currentConfig = this.config[this.getPageKey()]
            if(!currentConfig) return

            const idx = currentConfig.indexOf(key)
            if(idx < 0) return

            currentConfig.splice(idx,1)

            this.updateLocalStorage()
        }

        updateGoing = false
        updateLocalStorage(){
            if(!this.updateGoing){
                setTimeout(() => {
                    localStorage.setItem(this.storageKey, JSON.stringify(this.config))
                    this.updateGoing = false
                })
            }
            this.updateGoing = true
        }

    }

    class Configure {
        wrapper = document
        panel = null

        constructor(panel,getKey){
            this.panel = panel
            this.getKey = getKey
        }

        register(wrapper){
            if(!wrapper) return
            this.unRegisterEventListener()
            this.wrapper = wrapper
            this.registerEventListener()
        }

        getItem(t){
            while(t && t.parentNode !== this.wrapper){
                t = t.parentNode
            }
            return t
        }

        getActiveKey(wrapper){
            return wrapper ? this.getKey?.(wrapper) : null
        }

        registerEventListener(){
            this.wrapper.addEventListener('contextmenu',this.registedEvent)
        }

        unRegisterEventListener(){
            this.wrapper?.removeEventListener('contextmenu',this.registedEvent)
        }

        registedEvent = (e) => {
            if(e.ctrlKey) {
                return
            }
            e.preventDefault()

            const activeKey = this.getActiveKey(this.getItem(e.target))
            if(!activeKey) return
            this.panel?.create(e,activeKey)
        }
    }

    class Panel {
        constructor(localConfig,sort){
            this.localConfig = localConfig
            this.sort = sort
        }

        create(e, activeKey){
            if(!(this.activeKey = activeKey)) return

            const {pageX:x,pageY:y} = e
            const {body} = document

            const gp = this.generatePanel(x,y)
            body.appendChild(gp)

        }

        generatePanel(x,y){
            const generatePanel = document.createElement('div')
            generatePanel.style.zIndex = '100'
            generatePanel.style.width = '100px'
            generatePanel.style.height = '100px'
            generatePanel.style.backdropFilter = 'blur(3px)'
            generatePanel.style.borderRadius = '8px'
            generatePanel.style.boxShadow = 'rgba(0,0,0,.2) 0 1px 5px 0px'
            generatePanel.style.position = 'absolute'
            generatePanel.style.left = x - 10 + 'px'
            generatePanel.style.top = y - 5 + 'px'
            const content = this.generateContent()

            generatePanel.appendChild(content)


            this.removeListener(generatePanel)
            return generatePanel
        }

        generateContent(){
            const wrapper = document.createElement('div')
            wrapper.style.width = '80%'
            wrapper.style.height = '60%'
            wrapper.style.paddingLeft = '6px'
            wrapper.style.display = 'flex'
            wrapper.style.flexDirection = 'column'
            wrapper.style.justifyContent = 'space-around'
            wrapper.style.position = 'absolute'
            wrapper.style.top = '50%'
            wrapper.style.left = '50%'
            wrapper.style.transform = 'translate(-50%, -50%)'

            const collectionControl = this.generateControl(this.generateLabel('收藏'),this.generateCheckbox())
            wrapper.appendChild(collectionControl)

            const indexControl = this.generateControl(this.generateLabel('排序'),this.generateSelect())
            this.setIndexControlInitStatus(indexControl)

            wrapper.appendChild(indexControl)

            this.registerControlEvent(collectionControl,indexControl)

            return wrapper
        }

        setIndexControlInitStatus(indexControl){
            const config = this.localConfig.getConfig()
            if(config.includes(this.activeKey)){
                indexControl.hidden = false
            }else{
                indexControl.hidden = true
            }
        }

        generateControl(...args){
            const wrapper = document.createElement('div')
            wrapper.style.display = 'flex'
            args.forEach(a => wrapper.appendChild(a))

            return wrapper
        }

        generateLabel(text){
            const label = document.createElement('label')
            label.style.marginRight = '8px'
            label.innerText = text
            return label
        }

        generateCheckbox(){
            const checkbox = document.createElement('input')
            checkbox.type = 'checkbox'

            const config = this.localConfig.getConfig()
            if(config.includes(this.activeKey)){
                checkbox.checked = true
            }

            return checkbox
        }

        generateSelect(){
            const select = document.createElement('select')
            select.style.backgroundColor = 'inherit'


            const config = this.localConfig.getConfig()

            const length = config.length

            for(let i = 0; i < length; i++){
                select.appendChild(generateOption(i))
            }

            const index = config.indexOf(this.activeKey)

            if(index >=0){
                select.value = index
            }else{
                select.appendChild(generateOption(length))
                select.value = length
            }

            function generateOption(i){
                const option = document.createElement('option')
                option.value = i
                option.innerText = i
                return option
            }


            return select
        }

        registerControlEvent(collectionControl,indexControl){
            this.collectionControl = collectionControl
            this.indexControl = indexControl

            collectionControl.addEventListener('click',(e) => {
                if(e.target.checked){
                    this.localConfig.setConfig(this.activeKey)
                    this.sort.manualSort()
                    indexControl.hidden = false
                }else{
                    this.localConfig.removeConfig(this.activeKey)
                    this.sort.manualSort()
                    indexControl.hidden = true
                }
            })

            indexControl.addEventListener('change',(e) => {
                this.localConfig.setConfig(this.activeKey,Number(e.target.value))
                this.sort.manualSort()
            })
        }

        removeListener(panel){
            panel.addEventListener('mouseleave',(e)=>{
                // 修复firefox 的select下拉框回触发mouseleave的bug
                if(e.explicitOriginalTarget === panel){
                    return
                }
                document.body.removeChild(panel)
            })
        }
    }

    class Sort {
        canAutomaticallySelect(){
            return true
        }

        constructor(localConfig,markHelper,getKey,canAutomaticallySelect){
            this.localConfig = localConfig
            this.markHelper = markHelper
            this.getKey = getKey
            if(canAutomaticallySelect){
                this.canAutomaticallySelect = canAutomaticallySelect
            }
        }

        register(wrapper){
            if(!wrapper) return
            this.wrapper = wrapper
            this.sortGoing = false
            this.initializedSort()
            this.registerObservedSort()
        }

        initializedSort(){
            this._sort(true)
        }

        registerObservedSort(){
            const observe = new MutationObserver(() => {
                this._sort()
            })
            observe.observe(this.wrapper,{childList:true})

        }

        // 更新收藏后手动触发sort
        manualSort(){
            this._sort()
        }

        sortGoing = false
        _sort(selectFirst){
            if(this.sortGoing) return
            requestAnimationFrame(() => {
                const sorted = this._sortCore()

                if(selectFirst && this.canAutomaticallySelect()){
                    this.autoSelectFirst(sorted)
                }

                this.sortGoing = false
            })
            this.sortGoing = true
        }

        _sortCore(){
            const children = Array.from(this.wrapper.childNodes)
            const markedChild = []
            const config = this.localConfig.getConfig()

            // li未加载成功是,直接return
            if(children.map(child => this.getKey(child)).every(_ => !_)) return

            let empty = 0
            for(let i = 0,l = config.length; i < l; i++){
                for(let j = 0, ll = children.length; j < ll; j++){

                    const child = children[j]
                    if(config[i] === this.getKey(child)){
                        // 若key匹配,则更新标记
                        this.markHelper.mark(child,i)
                        markedChild.push(child)

                        // 若当前位置和预期位置不符,则更新dom位置
                        if(i - empty !== j){
                            this.wrapper.insertBefore(child,children[i - empty])
                            children.splice(j,1)
                            // (j < i ? -1 : 0),若当前位置在预期位置之前,则从children移除当前节点后,插入位置的index需要相应的 -1
                            children.splice(i - empty + (j < i ? -1 : 0),0,child)
                        }
                        break;
                    }
                    if(j === ll -1){
                        empty++
                    }
                }
            }

            // 移除样式还存在问题
            children.filter(child => !markedChild.includes(child)).forEach(item => this.markHelper.remove(item))

            return children
        }

        autoSelectFirst(children){
            // 自动选中第一项
            children.at(0).click()
        }
    }

    class MarkHelper {
        markClass = 'marked'
        constructor(showIndex){
            this.showIndex = showIndex
        }
        mark(wrapper,index){
            if(wrapper.marked) {
                const sign = wrapper.querySelector(`.${this.markClass}`)
                if(sign && sign.textContent !== index + ''){
                    sign.innerText = index
                }

                return
            }

            const sign = this.generateSign(index)
            this.toggleWrapperStyle(wrapper, sign, true)
        }

        toggleWrapperStyle(wrapper,sign,isMark){
            wrapper.marked = isMark
            wrapper.style.position = isMark ? 'relative' : ''
            wrapper.style.overflow = isMark ? 'hidden' : ''

            if(isMark){
                wrapper.appendChild(sign)
            }else{
                wrapper.removeChild(sign)
            }
        }

        generateSign(index){
            const sign = document.createElement('div')
            sign.style.textAlign = "center"
            sign.style.lineHeight = "14px"
            sign.style.color = "white"
            sign.style.fontSize = "12px"

            sign.style.backgroundColor = "#ebeb0080"
            sign.style.width = "66px"
            sign.style.height = "14px"
            sign.style.position = "absolute"
            sign.style.top = "10px"
            sign.style.right = "0px"
            sign.style.transform = "rotate(43deg) translate(11px, -22px)"
            sign.innerText = index
            if(!this.showIndex){
                sign.style.color = 'transparent'
            }
            sign.classList.add(this.markClass)
            return sign
        }

        remove(wrapper){
            if(wrapper.marked) {
                const sign = wrapper.querySelector(`.${this.markClass}`)
                if(!sign) return
                this.toggleWrapperStyle(wrapper, sign, false)

            }
        }
    }

    class Register {
        constructor({wrapperSelector,getKey,getSuffix,canAutomaticallySelect}){
            // 容器的选择器
            this.wrapperSelector = wrapperSelector
            // item生成key的回调函数
            this.getKey = getKey
            // url中需要保留的search参数
            this.getSuffix = getSuffix
            // 是否允许自动选中第一项
            this.canAutomaticallySelect = canAutomaticallySelect

            this.showIndex = false

            this.initPlugin()
            this.initObserve()
        }

        initPlugin(){
            this.markHelper = new MarkHelper(this.showIndex)

            this.localConfig = new LocalConfig(this.getSuffix)

            this.sort = new Sort(this.localConfig, this.markHelper, this.getKey, this.canAutomaticallySelect)

            this.panel = new Panel(this.localConfig, this.sort)

            this.configure = new Configure(this.panel, this.getKey)
        }

        initObserve(){
            this.registerObserve = new MutationObserver(() => {
                const wrapper = document.querySelector(this.wrapperSelector)
                if(!wrapper || this.wrapperCache === wrapper ) return

                this.wrapperCache = wrapper

                this.sort.register(wrapper)

                this.configure.register(wrapper)
            })
        }

        register(){
            this.registerObserve.observe(document.body,{childList:true, subtree:true})
            return this
        }
    }



    window.sortMonkey = new Register({
        wrapperSelector: 'alo-cicd-page-state-wapper > ul',// 列表容器选择器
        getKey: (item) => item?.firstChild?.firstChild?.textContent, // 列表项生成key的回调函数
        getSuffix:() => {
            const params= ['namespace','cluster']
            const search = location.search.slice(1).split('&')
            const res = []

            for(const p of params){
               const find = search.find(s => s.includes(p))
               if(find) res.push(find)
            }
            return res.join('&')
        },// 收藏列表的保存区域后缀,用来区分不同参数下的列表
        canAutomaticallySelect: () => !location.search.match(/buildName|delivery/) // 满足条件时,可允许自动选中列表第一项
    }).register()

})();