DevOps Projects Overview

Zeigt alle Projekte für alle Organisationen

As of 2024-10-02. See the latest version.

  1. // ==UserScript==
  2. // @name DevOps Projects Overview
  3. // @namespace Violentmonkey Scripts
  4. // @match https://dev.azure.com/*
  5. // @grant none
  6. // @version 1.0.7
  7. // @author Der_Floh
  8. // @description Zeigt alle Projekte für alle Organisationen
  9. // @license MIT
  10. // @icon https://www.svgrepo.com/show/448271/azure-devops.svg
  11. // ==/UserScript==
  12.  
  13. // jshint esversion: 8
  14.  
  15. let windowUrl = window.location.toString();
  16. if (windowUrl.endsWith('/'))
  17. windowUrl = windowUrl.slice(0, -1);
  18. const slashCount = windowUrl.match(/\//g).length;
  19.  
  20. if (window.location.toString().endsWith('?default')) {
  21. return;
  22. }
  23.  
  24. if (window.location.toString().endsWith('?projectonly')) {
  25. addErrorHideCss();
  26. convertToProjectOnly();
  27. } else if (slashCount == 3) {
  28. addErrorHideCss();
  29. showAllProjects();
  30. }
  31.  
  32. async function showAllProjects() {
  33. const navigation = await waitForElementToExistQuery(document.body, 'div[class*="top-level-navigation"][role="navigation"]');
  34. try {
  35. const showMoreButton = await waitForElementToExistQuery(navigation, 'span[class*="action-link top-navigation-item"][role="button"]');
  36. if (showMoreButton)
  37. showMoreButton.click();
  38. } catch { }
  39.  
  40. const projectsContainer = await waitForElementToExistId('skip-to-main-content');
  41. const currentProject = await waitForElementToExistQuery(projectsContainer, 'li[class="project-card flex-row flex-grow"]');
  42. const container = currentProject.parentNode;
  43. container.classList.add('wrap-ul');
  44. addWrapUlCss();
  45.  
  46. await addProjectCards(container);
  47. }
  48.  
  49. function addWrapUlCss() {
  50. const css = `
  51. ul.wrap-ul {
  52. display: flex;
  53. flex-wrap: wrap;
  54. padding: 0;
  55. list-style-type: none;
  56. margin: 0;
  57. }
  58.  
  59. ul.wrap-ul li,
  60. ul.wrap-ul iframe {
  61. flex: 1 1 auto;
  62. margin: 5px;
  63. box-sizing: border-box;
  64. }
  65. `;
  66. const style = document.createElement('style');
  67. style.type = 'text/css';
  68. style.innerHTML = css;
  69. document.head.appendChild(style);
  70. }
  71.  
  72. function addErrorHideCss() {
  73. const css = `
  74. .tfs-unhandled-error {
  75. display: none;
  76. }
  77. `;
  78. const style = document.createElement('style');
  79. style.type = 'text/css';
  80. style.innerHTML = css;
  81. document.head.appendChild(style);
  82. }
  83.  
  84. async function convertToProjectOnly() {
  85. const projectsContainer = await waitForElementToExistId('skip-to-main-content');
  86. const currentProject = await waitForElementToExistQuery(projectsContainer, 'li[class="project-card flex-row flex-grow"]');
  87. const container = currentProject.parentNode.parentNode;
  88. container.style.height = '100%';
  89. container.firstChild.style.height = '100%';
  90. container.firstChild.firstChild.style.height = '100%';
  91. container.firstChild.firstChild.firstChild.style.height = '100%';
  92. container.firstChild.firstChild.firstChild.firstChild.style.height = '100%';
  93. const projectName = currentProject.querySelector('div[class*="project-name"]').textContent;
  94. container.onclick = (event) => {
  95. event.preventDefault();
  96. const url = `https://dev.azure.com/${window.frameElement.getAttribute('name')}/${projectName}`;
  97. if (event.ctrlKey == true) {
  98. window.open(url, '_blank');
  99. } else {
  100. window.top.location = url;
  101. }
  102. };
  103. keepOnlyElementAndAncestors(container);
  104. }
  105.  
  106. function keepOnlyElementAndAncestors(element) {
  107. const elementsToKeep = new Set();
  108. let currentElement = element;
  109.  
  110. while (currentElement) {
  111. elementsToKeep.add(currentElement);
  112. currentElement = currentElement.parentElement;
  113. }
  114.  
  115. function addDescendantsToSet(element) {
  116. elementsToKeep.add(element);
  117. Array.from(element.children).forEach(child => {
  118. addDescendantsToSet(child);
  119. });
  120. }
  121. addDescendantsToSet(element);
  122.  
  123. const allElements = document.body.getElementsByTagName('*');
  124. Array.from(allElements).forEach(el => {
  125. if (!elementsToKeep.has(el)) {
  126. el.remove();
  127. }
  128. });
  129. }
  130.  
  131. async function addProjectCards(baseNode) {
  132. const projectCards = [];
  133. await waitForElementToExistQuery(document.body, 'a[class*="host-link navigation-link"][role="option"]');
  134. const projects = Array.from(document.body.querySelectorAll('a[class*="host-link navigation-link"][role="option"]'));
  135. projects.shift();
  136. for (const project of projects) {
  137. if (project.href.startsWith('https://dev.azure.com'))
  138. createIFrameForProject(baseNode, project);
  139. }
  140. }
  141.  
  142. function createIFrameForProject(baseNode, project) {
  143. const iframe = document.createElement('iframe');
  144. iframe.id = project.id.replace('__bolt-host-', 'project_');
  145. iframe.setAttribute('name', project.querySelector('span').textContent);
  146. iframe.src = project.href + '?projectonly';
  147. iframe.style.border = 'none';
  148. baseNode.appendChild(iframe);
  149. }
  150.  
  151. async function waitForElementToExistId(elementId) {
  152. return new Promise(async (resolve) => {
  153. async function checkElement() {
  154. const element = document.getElementById(elementId);
  155. if (element !== null)
  156. resolve(element);
  157. else
  158. setTimeout(checkElement, 100);
  159. }
  160. await checkElement();
  161. });
  162. }
  163.  
  164. async function waitForElementToExistQuery(baseNode, query, timeout = 3000) {
  165. return new Promise((resolve, reject) => {
  166. const startTime = Date.now();
  167. async function checkElement() {
  168. const element = baseNode.querySelector(query);
  169. if (element !== null) {
  170. resolve(element);
  171. } else {
  172. if (Date.now() - startTime > timeout) {
  173. reject(new Error(`Timeout: Element with query '${query}' did not appear within ${timeout}ms`));
  174. } else {
  175. setTimeout(checkElement, 100);
  176. }
  177. }
  178. }
  179. checkElement();
  180. });
  181. }