Adding Vue.js webui
Squashed commit of the following: commit 29571182b1ec3b5be2cec3212c2bea1121a3dac2 Author: Ian Roddis <tech@kinesin.ca> Date: Thu Feb 24 11:29:47 2022 -0400 Adding more elegant handling of tasks with no attempts commit 18c8ccb0863abbf6c9cc0efe5cc68df03a9eb80d Author: Ian Roddis <tech@kinesin.ca> Date: Thu Feb 24 11:18:59 2022 -0400 Better handling of no attempts at all commit 962f9f6e5e17f71bc3766553913774631f66e7ef Author: Ian Roddis <tech@kinesin.ca> Date: Thu Feb 24 11:10:28 2022 -0400 Adding fix for missing attempts commit 19b8203e952b3d21f4ff3f9b97a01c4d567ff1e7 Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 23 16:56:37 2022 -0400 Adding webui instructions to readme commit 81383c80f01101828c0c49868916a2712d140f42 Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 23 16:48:31 2022 -0400 Adding in route splatting to support static assets commit c9b39b307916c0fb1e88769d6986ddf7c3ba183a Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 23 12:11:11 2022 -0400 Cleanup commit 177819a1439cd1a0f32c652abf670f54457e105a Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 23 12:09:40 2022 -0400 Setting explicit url for extra CSS commit 78261129511c50657e7902934cee396eb1e4e3a8 Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 23 12:08:27 2022 -0400 Moving webui commit 9f8db6e2c2c8a231060217cb82f1b13aabe4eae2 Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 23 12:06:25 2022 -0400 Reorganizing elements, adding regex for run list commit f114250c9a506b2c0e9d642cc75749e99cc76cef Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 23 10:52:41 2022 -0400 Adding regex filtering to tasks commit 2de2f218416210443119aa88fa49c714197f4b16 Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 23 10:42:22 2022 -0400 Adding in task details and getting the plumbing working commit 660a2078e22799ba51b4b8bbe5c12cd0f9315b0a Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 23 09:38:13 2022 -0400 Fixing remaining settings commit 1aa0dfe1c971a12dfed183586ee5a3206d452409 Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 23 09:36:25 2022 -0400 Playing with settings commit 84cbd11c45651c7c6c96c16714e741b6aee10bc5 Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 23 08:52:52 2022 -0400 Removing extra code commit 6e31646b7c62368cab22b3844a70943e0149ddc7 Author: Ian Roddis <tech@kinesin.ca> Date: Tue Feb 22 17:29:47 2022 -0400 Adding linter, renaming components to meet standards, fixing some mixups in settings commit 225442ee5732d007867e485ccea05293e3e5e1b7 Author: Ian Roddis <tech@kinesin.ca> Date: Tue Feb 22 17:25:27 2022 -0400 Fixing sorters commit eb0d7a4c4c30d8e8b43b574ed0c2f97515bb9353 Author: Ian Roddis <tech@kinesin.ca> Date: Tue Feb 22 16:46:41 2022 -0400 Controls are coming together commit b1789d1cc3c0bae170e0ca1a47cccfd344197244 Author: Ian Roddis <tech@kinesin.ca> Date: Tue Feb 22 11:08:09 2022 -0400 More refactoring commit 6d0afce429aad00864482a2cc7dd731a53312e14 Author: Ian Roddis <tech@kinesin.ca> Date: Sun Feb 20 22:29:43 2022 -0400 figuring out layout commit 6af498f3aa7fe2f45121df2278cdfac297165c5c Author: Ian Roddis <tech@kinesin.ca> Date: Sun Feb 20 12:30:49 2022 -0400 Migrating to prop drilling / emiting commit dffe7059ce01209d2def6ef7c03bc750e31fe741 Author: Ian Roddis <tech@kinesin.ca> Date: Fri Feb 18 17:20:46 2022 -0400 Checkpointing work for now commit d6428ad59c9c05ab7fba82ce3c0441ac3f568796 Author: Ian Roddis <tech@kinesin.ca> Date: Fri Feb 18 17:05:37 2022 -0400 Adding in toggling for states commit b9a4f2dc02f327d3529821e217d3b6a00a84f202 Author: Ian Roddis <tech@kinesin.ca> Date: Fri Feb 18 16:43:01 2022 -0400 Reorganizing everything commit d33691d022597d1ff8f588450e147c72555be9f4 Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 16 17:04:54 2022 -0400 Removing console logging commit 4537376ccad6fc0c52f0a7cfd2b2bf23f708196c Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 16 17:04:27 2022 -0400 Refresh timer working now commit 213a3da4fd07c82cd18cd8c3b2422ddc78bd6fb4 Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 16 16:40:45 2022 -0400 Adding timer commit ff495ac69563689ff4fc07119936079e57608ea7 Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 16 16:02:53 2022 -0400 Refactoring some code, adding in endpoint to kill a running task commit 97ff28b9b1910e03e0f2725a3f54d2a07e53714c Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 16 14:56:15 2022 -0400 Renaming UI commit affab06ad657833b73588eac919250935b353f31 Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 16 13:29:31 2022 -0400 moving to bootstrap commit c40a2e58a86362863c905470f4417753aaf0dac2 Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 16 12:33:08 2022 -0400 adding task button commit 420463b8d7f964baa0dfc7c87c2e9024bc8284cc Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 16 10:51:11 2022 -0400 checkpoint commit a7aa3db731255e7e13bc58d901b8eb1e30ede39c Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 16 09:33:01 2022 -0400 Fixing up state commit 361b4cbcd8f1268eb9b494084d6862a6ab8f3a27 Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 16 09:29:14 2022 -0400 Fixing event callbacks commit 388cada692dc8d7e0eff611467d4c77ce897a54c Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 16 09:24:39 2022 -0400 Adding global state, task view and buttons commit cb5a3acef0bd982621678fbd44a133db56420871 Author: Ian Roddis <tech@kinesin.ca> Date: Wed Feb 16 07:49:30 2022 -0400 Adding RunView commit 4c78ef1250709e7c8f5ef3433640fd8d1d319a8d Author: Ian Roddis <tech@kinesin.ca> Date: Tue Feb 15 17:20:23 2022 -0400 checkpoint commit 2c5b610101e9c18ef1ad8f962d7309b63c80743c Author: Ian Roddis <tech@kinesin.ca> Date: Tue Feb 15 17:10:06 2022 -0400 Adding explicit payload headers, adding vue and react apps commit 95ac6c05903bc83c6934db58b48649eee2038c3d Author: Ian Roddis <tech@kinesin.ca> Date: Tue Feb 15 12:56:57 2022 -0400 Adding CORS support, rough-in of webui
28
webui/.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
webui/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["johnsoncodehk.volar", "johnsoncodehk.vscode-typescript-vue-plugin"]
|
||||
}
|
||||
19
webui/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Daggy Web UI
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev -- --host
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
3
webui/TODO.otl
Normal file
@@ -0,0 +1,3 @@
|
||||
- Change the daggyd return so that everything related to a task is at a single location
|
||||
- Add RunViewSortIndicators
|
||||
- Add TaskView logic
|
||||
20
webui/index.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Daggy</title>
|
||||
<!--<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous"> -->
|
||||
<!--<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous"> -->
|
||||
<link rel="stylesheet" href="/simple.min.css">
|
||||
<link rel="stylesheet" href="/daggyd.css">
|
||||
</head>
|
||||
<body width="100%">
|
||||
<!--<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>-->
|
||||
<main>
|
||||
<div id="app"></div>
|
||||
</main>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
35
webui/layout.otl
Normal file
@@ -0,0 +1,35 @@
|
||||
App
|
||||
: daggydURL
|
||||
: refreshInterval
|
||||
Settings
|
||||
: -> daggydURL
|
||||
: -> refreshInterval
|
||||
: <- update-refresh-interval (newInterval)
|
||||
: <- update-daggyd-url (newURL)
|
||||
Explorer
|
||||
: activeRunID
|
||||
RunListFilter
|
||||
: selectedStates
|
||||
: minTime
|
||||
: maxTime
|
||||
: <- update-active-runid
|
||||
RunList
|
||||
: -> selectedStates
|
||||
: -> minTime
|
||||
: -> maxTime
|
||||
: <- update-active-runid
|
||||
: runs
|
||||
SortedTable
|
||||
: -> tables
|
||||
: sortColumn
|
||||
: sortDirection
|
||||
RunButton
|
||||
RunViewPanel
|
||||
: -> activeRunID
|
||||
: activeRun
|
||||
RunViewFilter
|
||||
RunView
|
||||
TaskButton
|
||||
TaskViewPanel
|
||||
TaskView
|
||||
TaskButton
|
||||
4131
webui/package-lock.json
generated
Normal file
21
webui/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "daggyvue",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --port 5050"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.26.0",
|
||||
"vue": "^3.2.29"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^2.2.2",
|
||||
"eslint": "^8.9.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-vue": "^8.5.0",
|
||||
"vite": "^2.7.13"
|
||||
}
|
||||
}
|
||||
15
webui/public/daggyd.css
Normal file
@@ -0,0 +1,15 @@
|
||||
body {
|
||||
max-width: 100%;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
#app {
|
||||
flex-flow: column wrap;
|
||||
}
|
||||
|
||||
.svgicon {
|
||||
height: 1em;
|
||||
width: auto;
|
||||
padding: 2px;
|
||||
}
|
||||
BIN
webui/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
1
webui/public/icon-launch.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="icon-launch"><g><path class="primary" d="M14.57 6.96a2 2 0 0 1 2.47 2.47c.29.17.5.47.5.86v7.07a1 1 0 0 1-.3.71L13 22.31a1 1 0 0 1-1.7-.7v-3.58l-.49.19a1 1 0 0 1-1.17-.37 14.1 14.1 0 0 0-3.5-3.5 1 1 0 0 1-.36-1.16l.19-.48H2.39A1 1 0 0 1 1.7 11l4.24-4.24a1 1 0 0 1 .7-.3h7.08c.39 0 .7.21.86.5zM13.19 9.4l-2.15 2.15a3 3 0 0 1 .84.57 3 3 0 0 1 .57.84l2.15-2.15A2 2 0 0 1 13.2 9.4zm6.98-6.61a1 1 0 0 1 1.04 1.04c-.03.86-.13 1.71-.3 2.55-.47-.6-1.99-.19-2.55-.74-.55-.56-.14-2.08-.74-2.55.84-.17 1.7-.27 2.55-.3z"/><path class="secondary" d="M7.23 10.26A16.05 16.05 0 0 1 17.62 3.1a19.2 19.2 0 0 1 3.29 3.29 15.94 15.94 0 0 1-7.17 10.4 19.05 19.05 0 0 0-6.51-6.52zm-.86 5.5a16.2 16.2 0 0 1 1.87 1.87 1 1 0 0 1-.47 1.6c-.79.25-1.6.42-2.4.54a1 1 0 0 1-1.14-1.13c.12-.82.3-1.62.53-2.41a1 1 0 0 1 1.6-.47zm7.34-5.47a2 2 0 1 0 2.83-2.83 2 2 0 0 0-2.83 2.83z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 926 B |
1
webui/public/icon-order-vertical.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="icon-order-vertical"><path class="secondary" d="M7 18.59V9a1 1 0 0 1 2 0v9.59l2.3-2.3a1 1 0 0 1 1.4 1.42l-4 4a1 1 0 0 1-1.4 0l-4-4a1 1 0 1 1 1.4-1.42L7 18.6z"/><path class="primary" d="M17 5.41V15a1 1 0 1 1-2 0V5.41l-2.3 2.3a1 1 0 1 1-1.4-1.42l4-4a1 1 0 0 1 1.4 0l4 4a1 1 0 0 1-1.4 1.42L17 5.4z"/></svg>
|
||||
|
After Width: | Height: | Size: 370 B |
1
webui/public/icon-search.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="icon-search"><circle cx="10" cy="10" r="7" class="primary"/><path class="secondary" d="M16.32 14.9l1.1 1.1c.4-.02.83.13 1.14.44l3 3a1.5 1.5 0 0 1-2.12 2.12l-3-3a1.5 1.5 0 0 1-.44-1.14l-1.1-1.1a8 8 0 1 1 1.41-1.41zM10 16a6 6 0 1 0 0-12 6 6 0 0 0 0 12z"/></svg>
|
||||
|
After Width: | Height: | Size: 326 B |
1
webui/public/icon-sort-ascending.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="icon-sort-ascending"><path class="secondary" d="M18 13v7a1 1 0 0 1-2 0v-7h-3a1 1 0 0 1-.7-1.7l4-4a1 1 0 0 1 1.4 0l4 4A1 1 0 0 1 21 13h-3z"/><path class="primary" d="M3 3h13a1 1 0 0 1 0 2H3a1 1 0 1 1 0-2zm0 4h9a1 1 0 0 1 0 2H3a1 1 0 1 1 0-2zm0 4h5a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2z"/></svg>
|
||||
|
After Width: | Height: | Size: 353 B |
1
webui/public/icon-sort-decending.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="icon-sort-decending"><path class="secondary" d="M6 11V4a1 1 0 1 1 2 0v7h3a1 1 0 0 1 .7 1.7l-4 4a1 1 0 0 1-1.4 0l-4-4A1 1 0 0 1 3 11h3z"/><path class="primary" d="M21 21H8a1 1 0 0 1 0-2h13a1 1 0 0 1 0 2zm0-4h-9a1 1 0 0 1 0-2h9a1 1 0 0 1 0 2zm0-4h-5a1 1 0 0 1 0-2h5a1 1 0 0 1 0 2z"/></svg>
|
||||
|
After Width: | Height: | Size: 354 B |
1
webui/public/icon-trash.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="icon-trash"><path class="primary" d="M5 5h14l-.89 15.12a2 2 0 0 1-2 1.88H7.9a2 2 0 0 1-2-1.88L5 5zm5 5a1 1 0 0 0-1 1v6a1 1 0 0 0 2 0v-6a1 1 0 0 0-1-1zm4 0a1 1 0 0 0-1 1v6a1 1 0 0 0 2 0v-6a1 1 0 0 0-1-1z"/><path class="secondary" d="M8.59 4l1.7-1.7A1 1 0 0 1 11 2h2a1 1 0 0 1 .7.3L15.42 4H19a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2h3.59z"/></svg>
|
||||
|
After Width: | Height: | Size: 402 B |
1
webui/public/simple.min.css
vendored
Normal file
50
webui/src/App.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<script>
|
||||
import RunExplorer from './components/RunExplorer.vue'
|
||||
import GlobalSettings from './components/GlobalSettings.vue'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
refreshSeconds: 15, // How often to refresh
|
||||
daggydURL: window.location.origin,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateURL(url) {
|
||||
this.daggydURL = url;
|
||||
},
|
||||
updateRefreshInterval(interval) {
|
||||
this.refreshSeconds = interval;
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
GlobalSettings,
|
||||
RunExplorer
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
select { max-width: 25%; }
|
||||
input { max-width: 25%; }
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div id="settings">
|
||||
<GlobalSettings
|
||||
:daggydURL="daggydURL"
|
||||
:refreshSeconds="refreshSeconds"
|
||||
@update-refresh-interval="(interval) => this.updateRefreshInterval(interval)"
|
||||
@update-daggyd-url="(url) => this.updateURL(url)"
|
||||
/>
|
||||
</div>
|
||||
<div id="explorer">
|
||||
<RunExplorer
|
||||
:refreshSeconds="refreshSeconds"
|
||||
:daggydURL="daggydURL"
|
||||
@new-active-run="(runID) => this.activeRunID = runID"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
74
webui/src/assets/base.css
Normal file
@@ -0,0 +1,74 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition: color 0.5s, background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
||||
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
1
webui/src/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69" xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||
|
After Width: | Height: | Size: 308 B |
19
webui/src/components/.eslintrc.js
Normal file
@@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/essential",
|
||||
"airbnb-base"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"vue"
|
||||
],
|
||||
"rules": {
|
||||
}
|
||||
}
|
||||
40
webui/src/components/GlobalSettings.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script>
|
||||
export default {
|
||||
props: ['refreshSeconds', 'daggydURL'],
|
||||
data() {
|
||||
return {
|
||||
interval: this.refreshSeconds,
|
||||
url: this.daggydURL,
|
||||
};
|
||||
},
|
||||
emits: ['update-refresh-interval', 'update-daggyd-url'],
|
||||
computed: {
|
||||
validRefreshIntervals() {
|
||||
return [5, 10, 15, 30, 60, 300, 600];
|
||||
},
|
||||
isSelected(interval) {
|
||||
return (interval === this.refreshSeconds ? 'selected' : 'unselected');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<details>
|
||||
<summary>Global Settings</summary>
|
||||
<label>
|
||||
Daggy Base URL
|
||||
<input @change="$emit('update-daggyd-url', url)" v-model="url"/>
|
||||
</label>
|
||||
<label>Refresh Interval (seconds)
|
||||
<select @change="$emit('update-refresh-interval', interval)" v-model="interval">
|
||||
<option v-for="interval in validRefreshIntervals"
|
||||
:key="interval"
|
||||
:value="interval"
|
||||
>
|
||||
{{ interval }} Seconds
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</details>
|
||||
</template>
|
||||
245
webui/src/components/RunDetails.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<script>
|
||||
import { ALL_STATES } from '../defs.js'
|
||||
|
||||
import SortableTableHeader from './SortableTableHeader.vue';
|
||||
import TaskDetails from './TaskDetails.vue';
|
||||
|
||||
export default {
|
||||
props: ['daggydURL', 'refreshSeconds', 'activeRunID'],
|
||||
components: { SortableTableHeader, TaskDetails },
|
||||
data() {
|
||||
return {
|
||||
sortCol: 'lastUpdate',
|
||||
sortAscending: false,
|
||||
run: null,
|
||||
activeTaskName: null,
|
||||
filterStates: ALL_STATES.map((x) => x.name),
|
||||
filterMinTime: 0,
|
||||
filterMaxTime: 2000000000000000000,
|
||||
filterRegex: '.*',
|
||||
columns: [
|
||||
{ name: 'taskName', title: 'Name', sortable: true },
|
||||
{ name: 'taskState', title: 'State', sortable: true },
|
||||
{ name: 'startTime', title: 'Last Update', sortable: true },
|
||||
{ name: 'duration', title: 'Duration (s)', sortable: true },
|
||||
{ name: 'attempts', title: '# of Attempts', sortable: true },
|
||||
{ name: 'controls', title: 'Controls', sortable: false },
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
activeRunID() {
|
||||
this.fetchRun();
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
tasks() {
|
||||
if (this.run === null) {
|
||||
return [];
|
||||
}
|
||||
const tasks = Object.keys(this.run.tasks)
|
||||
.map((taskName) => {
|
||||
let startTime = 0;
|
||||
let stopTime = 0;
|
||||
let duration = 0;
|
||||
const attempts = (taskName in this.run.taskAttempts
|
||||
? this.run.taskAttempts[taskName]
|
||||
: []);
|
||||
if (attempts.length > 0) {
|
||||
const firstAttempt = attempts[0];
|
||||
const lastAttempt = attempts[attempts.length - 1];
|
||||
startTime = firstAttempt.startTime;
|
||||
stopTime = lastAttempt.stopTime;
|
||||
duration = lastAttempt.stopTime - firstAttempt.startTime;
|
||||
}
|
||||
|
||||
return {
|
||||
name: taskName,
|
||||
state: this.run.taskStates[taskName],
|
||||
startTime,
|
||||
lastUpdate: stopTime,
|
||||
attempts: attempts.length,
|
||||
duration: (duration / 1e9).toFixed(2),
|
||||
};
|
||||
});
|
||||
return tasks
|
||||
.filter(this.filter)
|
||||
.sort(this.sorter);
|
||||
},
|
||||
allStates() {
|
||||
return ALL_STATES;
|
||||
},
|
||||
activeTask() {
|
||||
if (this.activeTaskName === null) {
|
||||
return null;
|
||||
}
|
||||
const name = this.activeTaskName;
|
||||
const attempts = (name in this.run.taskAttempts ? this.run.taskAttempts[name] : []);
|
||||
const augAttempts = attempts
|
||||
.sort((a, b) => a.startTime - b.startTime)
|
||||
.map((a, i) => {
|
||||
a.id = i + 1;
|
||||
return a;
|
||||
});
|
||||
const obj = {
|
||||
name,
|
||||
task: this.run.tasks[name],
|
||||
attempts: augAttempts,
|
||||
state: this.run.taskStates[name],
|
||||
};
|
||||
return obj;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
isNumeric(x) {
|
||||
const p = parseFloat(x);
|
||||
return !Number.isNaN(p) && Number.isFinite(p);
|
||||
},
|
||||
|
||||
sorter(a, b) {
|
||||
const aa = a[this.sortCol];
|
||||
const bb = b[this.sortCol];
|
||||
|
||||
let ret = 0;
|
||||
if (this.isNumeric(aa) && this.isNumeric(bb)) {
|
||||
ret = aa - bb;
|
||||
} else if (aa < bb) {
|
||||
ret = -1;
|
||||
} else if (bb === aa) {
|
||||
ret = 0;
|
||||
} else {
|
||||
ret = 1;
|
||||
}
|
||||
|
||||
if (!this.sortAscending) {
|
||||
ret *= -1;
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
|
||||
filter(task) {
|
||||
const reFilter = new RegExp(this.filterRegex, '');
|
||||
return (this.filterStates.indexOf(task.state) > -1)
|
||||
&& (task.startTime >= this.filterMinTime)
|
||||
&& (task.lastUpdate <= this.filterMaxTime)
|
||||
&& (reFilter.test(task.name));
|
||||
},
|
||||
|
||||
setSortCol(name) {
|
||||
if (this.sortCol === name) {
|
||||
this.sortAscending = !this.sortAscending;
|
||||
} else {
|
||||
this.sortCol = name;
|
||||
this.sortAscending = true;
|
||||
}
|
||||
},
|
||||
|
||||
// Root tags
|
||||
// runID, tag, tasks, taskStates, taskAttempts
|
||||
|
||||
async fetchRun() {
|
||||
if (this.activeRunID === null) { return; }
|
||||
const resp = await fetch(`${this.daggydURL}/v1/dagrun/${this.activeRunID}`);
|
||||
this.run = await resp.json();
|
||||
},
|
||||
|
||||
killTask(taskName) {
|
||||
fetch(`${this.daggydURL}/v1/dagrun/${this.activeRunID}/task/${taskName}`, { method: 'delete' });
|
||||
},
|
||||
|
||||
retryTask(taskName) {
|
||||
fetch(`${this.daggydURL}/v1/dagrun/${this.activeRunID}/task/${taskName}/state/QUEUED`, { method: 'patch' });
|
||||
},
|
||||
|
||||
update() {
|
||||
this.fetchRun();
|
||||
setTimeout(() => {
|
||||
this.update();
|
||||
}, this.refreshSeconds * 1000);
|
||||
},
|
||||
setActiveTask(taskName) {
|
||||
this.activeTaskName = taskName;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.update();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
input {
|
||||
max-width: 25%;
|
||||
}
|
||||
label {
|
||||
margin: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="run-view">
|
||||
<TaskDetails :task="activeTask" />
|
||||
<div id="run-view-filter">
|
||||
<details>
|
||||
<summary>Task Filter</summary>
|
||||
<div>
|
||||
<label>
|
||||
Min Time
|
||||
<input v-model.lazy="filterMinTime"/>
|
||||
</label>
|
||||
<label>
|
||||
Max Time
|
||||
<input v-model.lazy="filterMaxTime"/>
|
||||
</label>
|
||||
<label>
|
||||
Task Name Regex
|
||||
<input v-model.lazy="filterRegex"/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label v-for="state in allStates" :key="state.name">
|
||||
{{ state.display }}
|
||||
<input type="checkbox" :value="state.name" v-model="filterStates">
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div id="run-view-data">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="col in columns" :key="col.name">
|
||||
<SortableTableHeader
|
||||
:title="col.title"
|
||||
:sorted="col.name == this.sortCol"
|
||||
:ascending="sortAscending"
|
||||
:sortable="col.sortable"
|
||||
@update-sort-column="setSortCol(col.name)"
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="task in tasks" :key="task.name">
|
||||
<td>{{task.name}}</td>
|
||||
<td>{{task.state}}</td>
|
||||
<td>{{task.startTime}}</td>
|
||||
<td>{{task.duration}}</td>
|
||||
<td>{{task.attempts}}</td>
|
||||
<td>
|
||||
<img class='svgicon'
|
||||
src='/icon-search.svg'
|
||||
@click="setActiveTask(task.name)"/>
|
||||
<img class='svgicon' src='/icon-trash.svg' @click="killTask(task.name)"/>
|
||||
<img class='svgicon' src='/icon-launch.svg' @click="retryTask(task.name)"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
38
webui/src/components/RunExplorer.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script>
|
||||
import RunList from './RunList.vue';
|
||||
import RunDetails from './RunDetails.vue';
|
||||
|
||||
export default {
|
||||
props: ['refreshSeconds', 'daggydURL'],
|
||||
data() {
|
||||
return {
|
||||
activeRunID: null,
|
||||
activeTaskName: null,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
RunList,
|
||||
RunDetails,
|
||||
},
|
||||
methods: {
|
||||
setActiveRunID(runID) {
|
||||
this.activeRunID = runID;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="explorer">
|
||||
<RunList
|
||||
:daggydURL="daggydURL"
|
||||
:refreshSeconds="refreshSeconds"
|
||||
@update-active-runid="(runID) => setActiveRunID(runID)"
|
||||
/>
|
||||
<RunDetails
|
||||
:daggydURL="daggydURL"
|
||||
:refreshSeconds="refreshSeconds"
|
||||
:activeRunID="activeRunID"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
208
webui/src/components/RunList.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<script>
|
||||
import { ALL_STATES, defaultCountHandler } from '../defs.js'
|
||||
|
||||
import SortableTableHeader from './SortableTableHeader.vue';
|
||||
|
||||
// import SortIndicator from './SortIndicator.vue';
|
||||
// import RunButton from './RunButton.vue';
|
||||
// components: { RunButton, SortIndicator },
|
||||
|
||||
export default {
|
||||
props: ['daggydURL', 'refreshSeconds'],
|
||||
components: { SortableTableHeader },
|
||||
data() {
|
||||
return {
|
||||
sortCol: 'lastUpdate', // Which column to sort view on
|
||||
sortAscending: false,
|
||||
runs: [],
|
||||
filterStates: ALL_STATES.map((x) => x.name),
|
||||
filterMinTime: 0,
|
||||
filterMaxTime: 2000000000000000000,
|
||||
filterRegex: '.*',
|
||||
columns: [
|
||||
{ name: 'runID', title: 'Run ID', sortable: true },
|
||||
{ name: 'tag', title: 'Tag', sortable: true },
|
||||
{ name: 'state', title: 'State', sortable: true },
|
||||
{ name: 'progress', title: 'Progress', sortable: true },
|
||||
{ name: 'startTime', title: 'Start Time', sortable: true },
|
||||
{ name: 'lastUpdate', title: 'LastUpdate', sortable: true },
|
||||
{ name: 'queued', title: 'Queued', sortable: true },
|
||||
{ name: 'running', title: 'Running', sortable: true },
|
||||
{ name: 'errored', title: 'Errored', sortable: true },
|
||||
{ name: 'completed', title: 'Completed', sortable: true },
|
||||
{ name: 'controls', title: 'Controls', sortable: false },
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
runList() {
|
||||
return this.runs
|
||||
.filter((run) => this.runFilter(run))
|
||||
.map((r) => {
|
||||
const run = r;
|
||||
run.nTasks = Object
|
||||
.values(run.taskCounts)
|
||||
.reduce((prev, cur) => prev + cur, 0);
|
||||
run.task_states = new Proxy(run.taskCounts, defaultCountHandler);
|
||||
run.progress = run.task_states.COMPLETED / run.nTasks;
|
||||
return run;
|
||||
})
|
||||
.sort((a, b) => this.sorter(a, b));
|
||||
},
|
||||
allStates() {
|
||||
return ALL_STATES;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
isNumeric(x) {
|
||||
const p = parseFloat(x);
|
||||
return !Number.isNaN(p) && Number.isFinite(p);
|
||||
},
|
||||
|
||||
sorter(a, b) {
|
||||
const aa = a[this.sortCol];
|
||||
const bb = b[this.sortCol];
|
||||
|
||||
let ret = 0;
|
||||
if (this.isNumeric(aa) && this.isNumeric(bb)) {
|
||||
ret = aa - bb;
|
||||
} else if (aa < bb) {
|
||||
ret = -1;
|
||||
} else if (bb === aa) {
|
||||
ret = 0;
|
||||
} else {
|
||||
ret = 1;
|
||||
}
|
||||
|
||||
if (!this.sortAscending) {
|
||||
ret *= -1;
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
|
||||
runFilter(run) {
|
||||
const reFilter = new RegExp(this.filterRegex, '');
|
||||
return (this.filterStates.indexOf(run.state) > -1)
|
||||
&& (run.startTime >= this.filterMinTime)
|
||||
&& (run.lastUpdate <= this.filterMaxTime)
|
||||
&& (reFilter.test(run.tag));
|
||||
},
|
||||
|
||||
setSortCol(name) {
|
||||
if (this.sortCol === name) {
|
||||
this.sortAscending = !this.sortAscending;
|
||||
} else {
|
||||
this.sortCol = name;
|
||||
this.sortAscending = true;
|
||||
}
|
||||
},
|
||||
|
||||
killRun(runID) {
|
||||
fetch(`${this.daggydURL}/v1/dagrun/${runID}`, { method: 'delete' });
|
||||
},
|
||||
|
||||
retryRun(runID) {
|
||||
fetch(`${this.daggydURL}/v1/dagrun/${runID}/state/QUEUED`, { method: 'patch' });
|
||||
},
|
||||
|
||||
async fetchRuns() {
|
||||
const res = await fetch(`${this.daggydURL}/v1/dagruns?all=1`);
|
||||
this.runs = await res.json();
|
||||
},
|
||||
|
||||
update() {
|
||||
this.fetchRuns();
|
||||
setTimeout(() => {
|
||||
this.update();
|
||||
}, this.refreshSeconds * 1000);
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.update();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="run-list">
|
||||
<div id="run-list-filter">
|
||||
<details>
|
||||
<summary>Run Filter</summary>
|
||||
<div>
|
||||
<label>
|
||||
Start Time
|
||||
<input v-model.lazy="filterMinTime"/>
|
||||
</label>
|
||||
<label>
|
||||
Time
|
||||
<input v-model.lazy="filterMaxTime"/>
|
||||
</label>
|
||||
<label>
|
||||
Task Name Regex
|
||||
<input v-model.lazy="filterRegex"/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label v-for="state in allStates" :key="state.name">
|
||||
{{ state.display }}
|
||||
<input type="checkbox" :value="state.name" v-model="filterStates">
|
||||
</label>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div id="run-list-data">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="col in columns" :key="col.name">
|
||||
<SortableTableHeader
|
||||
:title="col.title"
|
||||
:sorted="col.name == this.sortCol"
|
||||
:ascending="sortAscending"
|
||||
:sortable="col.sortable"
|
||||
@update-sort-column="setSortCol(col.name)"
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="run in runList" :key="run.runID">
|
||||
<td>{{run.runID}}</td>
|
||||
<td>{{run.tag}}</td>
|
||||
<td>{{run.state}}</td>
|
||||
<td><progress :value="run.progress"></progress></td>
|
||||
<td>{{run.startTime}}</td>
|
||||
<td>{{run.lastUpdate}}</td>
|
||||
<td>{{run.task_states["QUEUED"]}}</td>
|
||||
<td>{{run.task_states["RUNNING"]}}</td>
|
||||
<td>{{run.task_states["ERRORED"]}}</td>
|
||||
<td>{{run.task_states["COMPLETED"]}}</td>
|
||||
<td>
|
||||
<a href="#">
|
||||
<img
|
||||
class='svgicon'
|
||||
src='/icon-search.svg'
|
||||
@click="$emit('update-active-runid', run.runID)"/>
|
||||
</a>
|
||||
<a href="#">
|
||||
<img class='svgicon' src='/icon-trash.svg' @click="killRun(run.runID)"/>
|
||||
</a>
|
||||
<a href="#">
|
||||
<img class='svgicon' src='/icon-launch.svg' @click="retryRun(run.runID)"/>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.svgicon {
|
||||
height: 1em;
|
||||
width: auto;
|
||||
}
|
||||
</style>
|
||||
43
webui/src/components/SortableTableHeader.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script>
|
||||
export default {
|
||||
props: ['title', 'sorted', 'ascending', 'sortable'],
|
||||
emits: ['update-sort-column'],
|
||||
computed: {
|
||||
src() {
|
||||
let loc = '/icon-';
|
||||
if (this.sorted) {
|
||||
if (this.ascending) {
|
||||
loc += 'sort-ascending';
|
||||
} else {
|
||||
loc += 'sort-decending';
|
||||
}
|
||||
} else {
|
||||
loc += 'order-vertical';
|
||||
}
|
||||
loc += '.svg';
|
||||
return loc;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.svgicon {
|
||||
height: 1em;
|
||||
width: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
{{ title }}
|
||||
<a href="#">
|
||||
<img
|
||||
v-if="sortable"
|
||||
class='svgicon'
|
||||
:src="src"
|
||||
:alt="title"
|
||||
@click="$emit('update-sort-column')"/>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
60
webui/src/components/TaskDetails.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script>
|
||||
export default {
|
||||
props: ['task'],
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="task-details" v-if="task !== null">
|
||||
<details open>
|
||||
<summary>Task Details for {{ task.name }}</summary>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<td>{{ task.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>State</th>
|
||||
<td>{{ task.state }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Definition</th>
|
||||
<td><pre>{{ JSON.stringify(task.task, null, 2) }}</pre></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<details v-for="attempt in task.attempts" :key="attempt.id" open>
|
||||
<summary>Attempt {{attempt.id}}</summary>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Start Time</th>
|
||||
<td>{{ attempt.startTime }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Stop Time</th>
|
||||
<td>{{ attempt.stopTime }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Return Code</th>
|
||||
<td>{{ attempt.rc }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Standard Out</th>
|
||||
<td><pre>{{ attempt.outputLog }}</pre></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Standard Error</th>
|
||||
<td><pre>{{ attempt.errorLog }}</pre></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Executor Log</th>
|
||||
<td><pre>{{ attempt.ExecutorLog }}</pre></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
17
webui/src/defs.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { reactive } from 'vue';
|
||||
|
||||
export const ALL_STATES = [
|
||||
{ name: 'QUEUED', display: 'Queued' },
|
||||
{ name: 'RUNNING', display: 'Running' },
|
||||
{ name: 'ERRORED', display: 'Errored' },
|
||||
{ name: 'COMPLETED', display: 'Completed' },
|
||||
{ name: 'KILLED', display: 'Killed' },
|
||||
];
|
||||
|
||||
export const defaultCountHandler = {
|
||||
get(target, name) {
|
||||
return name in target ? target[name] : 0;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
4
webui/src/main.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
14
webui/vite.config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { fileURLToPath, URL } from 'url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
}
|
||||
})
|
||||