birjj / azdo-enhancer

Chrome Extension for adapting Azure DevOps
https://azdo-enhancer.netlify.com
1 stars 1 forks source link

Feature request - automatically untick specific stages #5

Open chaoscreater opened 7 months ago

chaoscreater commented 7 months ago

Hi again,

I'd like to make another feature request please. When I run a pipeline, I get to choose the stage I want to run and by default, all stages are ticked, which isn't what I want. Any stage that has the "apply" name should be unticked and users can manually tick it if they choose to run it. Here's an example of what I mean:

image

You can run a pipeline at the root level: image

OR you can drill down into a specific pipeline run and do a new run: image

Either of those will present the same panel as the first screenshot.

I tried to write a userscript for this and it's not very consistent. It will always untick the tickbox for the panel that is open under the specific pipeline run, but will only untick the tickbox for the panel opened under the pipeline root ONLY when the page is refreshed. Also, I can't figure out how to get it to untick the tickbox just once, as I need to be able to manually tick it if I want to run that stage. I'm terrible with Javascript and this is what I've got, maybe it might help?


    function executeScriptLogic() {
        console.log("Rickscript --- Executing script logic...");
        let checkboxes = document.querySelectorAll('.primary-text');

        checkboxes.forEach(function(checkbox) {
            console.log("Rickscript --- Checking checkbox:", checkbox.textContent);
            if (checkbox.textContent.includes('apply')) {
                console.log("Rickscript --- Found checkbox with 'apply' in text:", checkbox);
                let checkboxParent = checkbox.closest('.queue-panel-list-row');
                let checkboxElement = checkboxParent.querySelector('.bolt-checkbox');

                if (checkboxElement) {
                    console.log("Rickscript --- Checkbox element found:", checkboxElement);
                    if (checkboxElement.getAttribute('aria-checked') === 'true') {
                        console.log("Rickscript --- Checkbox is checked, unticking...");
                        checkboxElement.click();
                    } else {
                        console.log("Rickscript --- Checkbox is already unchecked.");
                    }
                } else {
                    console.log("Rickscript --- Checkbox element not found!");
                }
            }
        });
    }

    // Function to wait for the panel "Stages to run" to be opened
    function waitForPanelToOpen() {
        const panelObserver = new MutationObserver(function(mutationsList, observer) {
            mutationsList.forEach(function(mutation) {
                // Check if the panel with the name "Stages to run" is added to the DOM
                if (mutation.addedNodes.length > 0) {
                    const panels = document.querySelectorAll('.bolt-header-title.title-m');
                    panels.forEach(function(panel) {
                        if (panel.textContent === 'Stages to run') {
                            // Panel is found, now execute the necessary script logic
                            console.log("Rickscript --- blah panel found");

                            setTimeout(function() {
                                executeScriptLogic();
                            }, 500);

                            // Disconnect the observer as it's no longer needed
                            observer.disconnect();
                        }
                    });
                }
            });
        });

        // Start observing the document for changes in the DOM
        panelObserver.observe(document.documentElement, { childList: true, subtree: true });

        // Set a timeout to ensure the observer is disconnected if the panel is not found
        setTimeout(function() {
            panelObserver.disconnect();
        }, 5000); // Adjust timeout as needed (in milliseconds)
    }

    // Function to handle URL changes
    function handleUrlChange(mutationsList, observer) {
        for (let mutation of mutationsList) {

            if (mutation.type === 'childList' || mutation.type === 'attributes') {

                const currentUrl = window.location.href;

                if (currentUrl !== observer.previousUrl)
                {

                    // console.log("Rickscript --- currentUrl --- ", currentUrl);

                    // console.log("Rickscript --- observer.previousUrl --- ", observer.previousUrl);

                    if (currentUrl.includes('a=summary')) {

                        console.log("Rickscript - _a=summary");
                        waitForPanelToOpen();

                    } else if (currentUrl.includes('view=results')) {

                        console.log("Rickscript - view=results");
                        waitForPanelToOpen();
                    }

                }
                break;
            }
        }
    }

    const observer = new MutationObserver(handleUrlChange);
    observer.observe(document.documentElement, { subtree: true, childList: true, attributes: true });
    observer.previousUrl = window.location.href;
birjj commented 7 months ago

This is another case where we run into problems because of the limited information we have as a browser extension.

It's a slightly complicated process that'll unfortunately cause a bit of UI flashing (see below), but I see the value. I'm not sure if I'll get around to implementing it, but until then, a workaround would be to use a parameter to toggle the stages instead - parameters are cached in AZDO natively:

Show example pipeline with parameter toggling stages (written from memory, so test first 😉) ```yaml parameters: - name: skip_apply displayName: "Skip apply stages?" type: boolean default: true stages: - stage: some_other_stage displayName: This will run normally jobs: "..." - stage: apply_some_other_stage displayName: This will be skipped if skip_apply is true condition: and(succeeded(), eq(parameters.skip_apply, false)) jobs: "..." ```

Implementation thoughts

(not particularly interesting for anyone who isn't trying to implement this)

It looks like AZDO handles the stage selection by adding a stagesToSkip: ["stage_id_1", "stage_id_2", ...] entry to the POST payload when starting the run.
This means we'll either have to i) re-implement the "Run" button ourselves (a non-starter, since we don't have access to the necessary information), ii) modify the POST request after it's been sent (no longer possible in Manifest v3, thanks to Google), iii) hook into the client-side data AZDO uses to track which stages to skip (non-starter since it's handled entirely within AZDO's React, not using DOM inputs), or iv) emulate the UI interactions that would set the given stages (will cause UI flashes, but looks like the only possible solution).

Emulating the UI is possible, but a bit complicated because it's purely a loop of "observe DOM changes, interact as needed, hope nothing unexpected happens". Specifically it'd probably be a process of:

  1. Observe the .bolt-portal-host element for new direct descendants. If it's a pipeline run popup, continue.
  2. Check if "Stages to run" can be clicked (AZDO doesn't use standard interaction elements, so probably check the classes for some AZDO-specific indicator?) - it'll be disabled if the pipeline parameters aren't yet valid. If it isn't valid:
    • Pause the process. Add an observer to the "Stages to run" element, observing class changes to detect when it becomes available.
    • Once that observer triggers, remove it and continue the process.
  3. Click the "Stages to run". Add an observer to the popup to detect when the stages have been loaded.
    • Once that observer triggers, remove it and continue the process.
  4. Click the relevant checkmarks (detect by stage name, since AZDO doesn't provide the AZDO ID in the DOM) - AZDO doesn't use standard interaction elements, so probably emulate a click event and hope it triggers the relevant React code?.
  5. Click "Use selected stages" and mark the popup as processed.

The most complicated part is the repeated use of observers pausing the process, since that's not a pattern that's generally supported (but isn't incredibly hard to implement), and it requires a lot of cleanup in case the user closes the popup at some arbitrary point in the process.