Преглед изворни кода

feat: Clickable links in outout (#900)

jamesread пре 4 месеци
родитељ
комит
7051aad599

+ 13 - 1
frontend/js/OutputTerminal.js

@@ -1,5 +1,6 @@
 import { Terminal } from '@xterm/xterm'
 import { Terminal } from '@xterm/xterm'
 import { FitAddon } from '@xterm/addon-fit'
 import { FitAddon } from '@xterm/addon-fit'
+import { WebLinksAddon } from '@xterm/addon-web-links'
 import { Mutex } from './Mutex.js'
 import { Mutex } from './Mutex.js'
 
 
 /**
 /**
@@ -18,13 +19,24 @@ export class OutputTerminal {
   constructor (executionTrackingId) {
   constructor (executionTrackingId) {
     this.executionTrackingId = executionTrackingId
     this.executionTrackingId = executionTrackingId
     this.writeMutex = new Mutex()
     this.writeMutex = new Mutex()
+    const linkHandler = {
+      activate (event, text, _range) {
+        event.preventDefault()
+        window.open(text, '_blank')
+      }
+    }
+
     this.terminal = new Terminal({
     this.terminal = new Terminal({
-      convertEol: true
+      convertEol: true,
+      linkHandler
     })
     })
 
 
     const fitAddon = new FitAddon()
     const fitAddon = new FitAddon()
     this.terminal.loadAddon(fitAddon)
     this.terminal.loadAddon(fitAddon)
     this.terminal.fit = fitAddon
     this.terminal.fit = fitAddon
+
+    this.terminal.loadAddon(new WebLinksAddon((event, uri) => linkHandler.activate(event, uri)))
+    this.linkHandlerConfigured = true
   }
   }
 
 
   async write (out, then) {
   async write (out, then) {

+ 7 - 0
frontend/package-lock.json

@@ -15,6 +15,7 @@
 				"@hugeicons/vue": "^1.0.4",
 				"@hugeicons/vue": "^1.0.4",
 				"@vitejs/plugin-vue": "^6.0.4",
 				"@vitejs/plugin-vue": "^6.0.4",
 				"@xterm/addon-fit": "^0.11.0",
 				"@xterm/addon-fit": "^0.11.0",
+				"@xterm/addon-web-links": "^0.12.0",
 				"@xterm/xterm": "^6.0.0",
 				"@xterm/xterm": "^6.0.0",
 				"iconify-icon": "^3.0.2",
 				"iconify-icon": "^3.0.2",
 				"picocrank": "^1.14.0",
 				"picocrank": "^1.14.0",
@@ -1570,6 +1571,12 @@
 			"integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
 			"integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
 			"license": "MIT"
 			"license": "MIT"
 		},
 		},
+		"node_modules/@xterm/addon-web-links": {
+			"version": "0.12.0",
+			"resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.12.0.tgz",
+			"integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==",
+			"license": "MIT"
+		},
 		"node_modules/@xterm/xterm": {
 		"node_modules/@xterm/xterm": {
 			"version": "6.0.0",
 			"version": "6.0.0",
 			"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",
 			"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",

+ 2 - 1
frontend/package.json

@@ -28,13 +28,14 @@
 		"@hugeicons/vue": "^1.0.4",
 		"@hugeicons/vue": "^1.0.4",
 		"@vitejs/plugin-vue": "^6.0.4",
 		"@vitejs/plugin-vue": "^6.0.4",
 		"@xterm/addon-fit": "^0.11.0",
 		"@xterm/addon-fit": "^0.11.0",
+		"@xterm/addon-web-links": "^0.12.0",
 		"@xterm/xterm": "^6.0.0",
 		"@xterm/xterm": "^6.0.0",
 		"iconify-icon": "^3.0.2",
 		"iconify-icon": "^3.0.2",
 		"picocrank": "^1.14.0",
 		"picocrank": "^1.14.0",
 		"standard": "^17.1.2",
 		"standard": "^17.1.2",
 		"unplugin-vue-components": "^31.0.0",
 		"unplugin-vue-components": "^31.0.0",
 		"vite": "^7.3.1",
 		"vite": "^7.3.1",
-    "vue": "^3.5.29",
+		"vue": "^3.5.29",
 		"vue-i18n": "^11.2.8",
 		"vue-i18n": "^11.2.8",
 		"vue-router": "^5.0.3"
 		"vue-router": "^5.0.3"
 	}
 	}

+ 13 - 0
integration-tests/tests/xtermLinkHandling/config.yaml

@@ -0,0 +1,13 @@
+#
+# Integration Test Config: xterm link handling
+#
+
+listenAddressSingleHTTPFrontend: 0.0.0.0:1337
+
+logLevel: "DEBUG"
+checkForUpdates: false
+
+actions:
+  - title: Echo URL
+    shell: echo "See https://example.com for more info"
+    popupOnStart: execution-dialog-stdout-only

+ 69 - 0
integration-tests/tests/xtermLinkHandling/xtermLinkHandling.mjs

@@ -0,0 +1,69 @@
+import { describe, it, before, after } from 'mocha'
+import { expect } from 'chai'
+import { By, Condition } from 'selenium-webdriver'
+import {
+  getRootAndWait,
+  takeScreenshotOnFailure,
+  getTerminalBuffer,
+} from '../../lib/elements.js'
+
+describe('config: xtermLinkHandling', function () {
+  before(async function () {
+    await runner.start('xtermLinkHandling')
+  })
+
+  after(async () => {
+    await runner.stop()
+  })
+
+  afterEach(function () {
+    takeScreenshotOnFailure(this.currentTest, webdriver)
+  })
+
+  it('xterm output shows URL and link handling is configured', async function () {
+    await getRootAndWait()
+
+    await webdriver.wait(new Condition('wait for Echo URL button', async () => {
+      const btns = await webdriver.findElements(By.css('[title="Echo URL"]'))
+      return btns.length === 1
+    }), 10000)
+
+    const echoUrlButton = await webdriver.findElement(By.css('[title="Echo URL"]'))
+    await echoUrlButton.click()
+
+    await webdriver.wait(new Condition('wait for execution view', async () => {
+      const url = await webdriver.getCurrentUrl()
+      return url.includes('/logs/') && !url.endsWith('/logs')
+    }), 10000)
+
+    await webdriver.wait(new Condition('wait for execution status', async () => {
+      const statusElements = await webdriver.findElements(By.id('execution-dialog-status'))
+      return statusElements.length > 0
+    }), 5000)
+
+    await webdriver.wait(new Condition('wait for execution to finish', async () => {
+      try {
+        const statusElement = await webdriver.findElement(By.id('execution-dialog-status'))
+        const statusText = await statusElement.getText()
+        return !statusText.includes('Executing')
+      } catch (e) {
+        return false
+      }
+    }), 5000)
+
+    await webdriver.sleep(500)
+
+    const bufferText = await getTerminalBuffer()
+    expect(bufferText).to.not.be.null
+    expect(bufferText).to.include('https://example.com')
+
+    const linkHandlerSet = await webdriver.executeScript(`
+      try {
+        return !!(window.terminal && window.terminal.linkHandlerConfigured === true)
+      } catch (e) {
+        return false
+      }
+    `)
+    expect(linkHandlerSet).to.equal(true)
+  })
+})