DevOps Projects Overview

Zeigt alle Projekte für alle Organisationen

  1. // ==UserScript==
  2. // @name DevOps Projects Overview
  3. // @namespace Violentmonkey Scripts
  4. // @match https://dev.azure.com/*
  5. // @grant none
  6. // @version 1.1.0
  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. let url = window.frameElement.src;
  97. const questionMarkIndex = url.indexOf('?');
  98. if (questionMarkIndex !== -1) {
  99. url = url.substring(0, questionMarkIndex);
  100. }
  101. if (!url.endsWith("/"))
  102. url += "/";
  103. url = url + projectName;
  104. console.log(url);
  105. if (event.ctrlKey == true) {
  106. window.open(url, '_blank');
  107. } else {
  108. window.top.location = url;
  109. }
  110. };
  111. keepOnlyElementAndAncestors(container);
  112. }
  113.  
  114. function keepOnlyElementAndAncestors(element) {
  115. const elementsToKeep = new Set();
  116. let currentElement = element;
  117.  
  118. while (currentElement) {
  119. elementsToKeep.add(currentElement);
  120. currentElement = currentElement.parentElement;
  121. }
  122.  
  123. function addDescendantsToSet(element) {
  124. elementsToKeep.add(element);
  125. Array.from(element.children).forEach(child => {
  126. addDescendantsToSet(child);
  127. });
  128. }
  129. addDescendantsToSet(element);
  130.  
  131. const allElements = document.body.getElementsByTagName('*');
  132. Array.from(allElements).forEach(el => {
  133. if (!elementsToKeep.has(el)) {
  134. el.remove();
  135. }
  136. });
  137. }
  138.  
  139. async function addProjectCards(baseNode) {
  140. const projectCards = [];
  141. await waitForElementToExistQuery(document.body, 'a[class*="host-link navigation-link"][role="option"]');
  142. const projects = Array.from(document.body.querySelectorAll('a[class*="host-link navigation-link"][role="option"]'));
  143. projects.shift();
  144. const sortedProjects = projects.sort((a, b) => {
  145. const textA = a.querySelector('span').textContent.toLowerCase();
  146. const textB = b.querySelector('span').textContent.toLowerCase();
  147. return textA.localeCompare(textB);
  148. });
  149. for (const project of sortedProjects) {
  150. if (project.href.startsWith('https://dev.azure.com'))
  151. createIFrameForProject(baseNode, project);
  152. }
  153. }
  154.  
  155. function createIFrameForProject(baseNode, project) {
  156. const iframe = document.createElement('iframe');
  157. iframe.id = project.id.replace('__bolt-host-', 'project_');
  158. iframe.setAttribute('name', project.querySelector('span').textContent);
  159. iframe.src = project.href + '?projectonly';
  160. iframe.style.border = 'none';
  161. baseNode.appendChild(iframe);
  162. }
  163.  
  164. async function waitForElementToExistId(elementId) {
  165. return new Promise(async (resolve) => {
  166. async function checkElement() {
  167. const element = document.getElementById(elementId);
  168. if (element !== null)
  169. resolve(element);
  170. else
  171. setTimeout(checkElement, 100);
  172. }
  173. await checkElement();
  174. });
  175. }
  176.  
  177. async function waitForElementToExistQuery(baseNode, query, timeout = 3000) {
  178. return new Promise((resolve, reject) => {
  179. const startTime = Date.now();
  180. async function checkElement() {
  181. const element = baseNode.querySelector(query);
  182. if (element !== null) {
  183. resolve(element);
  184. } else {
  185. if (Date.now() - startTime > timeout) {
  186. reject(new Error(`Timeout: Element with query '${query}' did not appear within ${timeout}ms`));
  187. } else {
  188. setTimeout(checkElement, 100);
  189. }
  190. }
  191. }
  192. checkElement();
  193. });
  194. }