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
11
README.md
@@ -44,9 +44,10 @@ Building
|
|||||||
- cmake >= 3.14
|
- cmake >= 3.14
|
||||||
- gcc >= 8
|
- gcc >= 8
|
||||||
|
|
||||||
|
- npm (if you want the webui)
|
||||||
- libslurm (if needed)
|
- libslurm (if needed)
|
||||||
|
|
||||||
```
|
```sh
|
||||||
git clone https://gitlab.com/iroddis/daggy
|
git clone https://gitlab.com/iroddis/daggy
|
||||||
cd daggy
|
cd daggy
|
||||||
mkdir build
|
mkdir build
|
||||||
@@ -55,6 +56,14 @@ cmake [-DDAGGY_ENABLE_SLURM=ON] ..
|
|||||||
make
|
make
|
||||||
|
|
||||||
tests/tests # for unit tests
|
tests/tests # for unit tests
|
||||||
|
|
||||||
|
# Web UI
|
||||||
|
cd webui
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Lauching daggyd with the web UI
|
||||||
|
build/bin/daggyd -v --assets-dir webui/dist
|
||||||
```
|
```
|
||||||
|
|
||||||
DAG Run Definition
|
DAG Run Definition
|
||||||
|
|||||||
@@ -264,6 +264,7 @@ int main(int argc, char **argv)
|
|||||||
.default_value(false)
|
.default_value(false)
|
||||||
.implicit_value(true);
|
.implicit_value(true);
|
||||||
args.add_argument("-d", "--daemon").default_value(false).implicit_value(true);
|
args.add_argument("-d", "--daemon").default_value(false).implicit_value(true);
|
||||||
|
args.add_argument("--assets-dir").default_value(std::string{});
|
||||||
args.add_argument("--config").default_value(std::string{});
|
args.add_argument("--config").default_value(std::string{});
|
||||||
args.add_argument("--ip").default_value(std::string{"127.0.0.1"});
|
args.add_argument("--ip").default_value(std::string{"127.0.0.1"});
|
||||||
args.add_argument("--port").default_value(2503u).action(
|
args.add_argument("--port").default_value(2503u).action(
|
||||||
@@ -281,6 +282,7 @@ int main(int argc, char **argv)
|
|||||||
bool verbose = args.get<bool>("--verbose");
|
bool verbose = args.get<bool>("--verbose");
|
||||||
bool asDaemon = args.get<bool>("--daemon");
|
bool asDaemon = args.get<bool>("--daemon");
|
||||||
auto configFile = args.get<std::string>("--config");
|
auto configFile = args.get<std::string>("--config");
|
||||||
|
auto staticAssetsDir = args.get<std::string>("--assets-dir");
|
||||||
std::string listenIP = args.get<std::string>("--ip");
|
std::string listenIP = args.get<std::string>("--ip");
|
||||||
auto listenPort = args.get<unsigned>("--port");
|
auto listenPort = args.get<unsigned>("--port");
|
||||||
size_t webThreads = 50;
|
size_t webThreads = 50;
|
||||||
@@ -301,6 +303,8 @@ int main(int argc, char **argv)
|
|||||||
|
|
||||||
if (doc.HasMember("ip"))
|
if (doc.HasMember("ip"))
|
||||||
listenIP = doc["ip"].GetString();
|
listenIP = doc["ip"].GetString();
|
||||||
|
if (doc.HasMember("assets-dir"))
|
||||||
|
staticAssetsDir = doc["assets-dir"].GetString();
|
||||||
if (doc.HasMember("port"))
|
if (doc.HasMember("port"))
|
||||||
listenPort = doc["port"].GetInt();
|
listenPort = doc["port"].GetInt();
|
||||||
if (doc.HasMember("web-threads"))
|
if (doc.HasMember("web-threads"))
|
||||||
@@ -331,7 +335,8 @@ int main(int argc, char **argv)
|
|||||||
|
|
||||||
Pistache::Address listenSpec(listenIP, listenPort);
|
Pistache::Address listenSpec(listenIP, listenPort);
|
||||||
|
|
||||||
daggy::daggyd::Server server(listenSpec, *logger, *executor, dagThreads);
|
daggy::daggyd::Server server(listenSpec, *logger, *executor, dagThreads,
|
||||||
|
staticAssetsDir);
|
||||||
server.init(webThreads);
|
server.init(webThreads);
|
||||||
server.start();
|
server.start();
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ namespace daggy::daggyd {
|
|||||||
public:
|
public:
|
||||||
Server(const Pistache::Address &listenSpec,
|
Server(const Pistache::Address &listenSpec,
|
||||||
loggers::dag_run::DAGRunLogger &logger,
|
loggers::dag_run::DAGRunLogger &logger,
|
||||||
executors::task::TaskExecutor &executor, size_t nDAGRunners);
|
executors::task::TaskExecutor &executor, size_t nDAGRunners,
|
||||||
|
const fs::path &staticAssetsDir);
|
||||||
~Server();
|
~Server();
|
||||||
|
|
||||||
Server &setSSLCertificates(const fs::path &cert, const fs::path &key);
|
Server &setSSLCertificates(const fs::path &cert, const fs::path &key);
|
||||||
@@ -40,7 +41,7 @@ namespace daggy::daggyd {
|
|||||||
void queueDAG_(DAGRunID runID, const TaskDAG &dag,
|
void queueDAG_(DAGRunID runID, const TaskDAG &dag,
|
||||||
const TaskParameters &taskParameters);
|
const TaskParameters &taskParameters);
|
||||||
|
|
||||||
DAGGY_REST_HANDLER(handleRoot); // X
|
DAGGY_REST_HANDLER(handleStatic); // X
|
||||||
DAGGY_REST_HANDLER(handleReady); // X
|
DAGGY_REST_HANDLER(handleReady); // X
|
||||||
DAGGY_REST_HANDLER(handleQueryDAGs); // X
|
DAGGY_REST_HANDLER(handleQueryDAGs); // X
|
||||||
DAGGY_REST_HANDLER(handleRunDAG); // X
|
DAGGY_REST_HANDLER(handleRunDAG); // X
|
||||||
@@ -53,6 +54,7 @@ namespace daggy::daggyd {
|
|||||||
DAGGY_REST_HANDLER(handleStopTask); // X
|
DAGGY_REST_HANDLER(handleStopTask); // X
|
||||||
DAGGY_REST_HANDLER(handleGetTaskState); // X
|
DAGGY_REST_HANDLER(handleGetTaskState); // X
|
||||||
DAGGY_REST_HANDLER(handleSetTaskState); // X
|
DAGGY_REST_HANDLER(handleSetTaskState); // X
|
||||||
|
DAGGY_REST_HANDLER(handleCORS);
|
||||||
|
|
||||||
bool handleAuth(const Pistache::Rest::Request &request);
|
bool handleAuth(const Pistache::Rest::Request &request);
|
||||||
|
|
||||||
@@ -63,6 +65,7 @@ namespace daggy::daggyd {
|
|||||||
loggers::dag_run::DAGRunLogger &logger_;
|
loggers::dag_run::DAGRunLogger &logger_;
|
||||||
executors::task::TaskExecutor &executor_;
|
executors::task::TaskExecutor &executor_;
|
||||||
ThreadPool runnerPool_;
|
ThreadPool runnerPool_;
|
||||||
|
fs::path staticAssetsDir_;
|
||||||
|
|
||||||
std::mutex runnerGuard_;
|
std::mutex runnerGuard_;
|
||||||
std::unordered_map<DAGRunID, std::shared_ptr<DAGRunner>> runners_;
|
std::unordered_map<DAGRunID, std::shared_ptr<DAGRunner>> runners_;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <daggy/Serialization.hpp>
|
#include <daggy/Serialization.hpp>
|
||||||
#include <daggy/Utilities.hpp>
|
#include <daggy/Utilities.hpp>
|
||||||
#include <daggyd/Server.hpp>
|
#include <daggyd/Server.hpp>
|
||||||
|
#include <fstream>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
@@ -20,14 +21,20 @@ using namespace Pistache;
|
|||||||
|
|
||||||
namespace daggy::daggyd {
|
namespace daggy::daggyd {
|
||||||
|
|
||||||
bool requestIsForJSON(const Pistache::Rest::Request &request)
|
void addResponseHeaders(Pistache::Http::ResponseWriter &response)
|
||||||
{
|
{
|
||||||
auto acceptedMimeTypes =
|
response.headers().add(
|
||||||
request.headers().get<Pistache::Http::Header::Accept>()->media();
|
std::make_shared<Pistache::Http::Header::AccessControlAllowOrigin>(
|
||||||
auto fit =
|
"*"));
|
||||||
std::find(acceptedMimeTypes.begin(), acceptedMimeTypes.end(),
|
response.headers().add(
|
||||||
Pistache::Http::Mime::MediaType::fromString("text/html"));
|
std::make_shared<Pistache::Http::Header::AccessControlAllowHeaders>(
|
||||||
return fit == acceptedMimeTypes.end();
|
"content-type"));
|
||||||
|
response.headers().add(
|
||||||
|
std::make_shared<Pistache::Http::Header::AccessControlAllowMethods>(
|
||||||
|
"PUT, OPTIONS, PATCH, GET, HEAD, CONNECT, DELETE, POST, TRACE"));
|
||||||
|
response.headers().add(
|
||||||
|
std::make_shared<Pistache::Http::Header::ContentType>(
|
||||||
|
"application/javascript"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Server::init(size_t threads)
|
void Server::init(size_t threads)
|
||||||
@@ -44,12 +51,14 @@ namespace daggy::daggyd {
|
|||||||
|
|
||||||
Server::Server(const Pistache::Address &listenSpec,
|
Server::Server(const Pistache::Address &listenSpec,
|
||||||
loggers::dag_run::DAGRunLogger &logger,
|
loggers::dag_run::DAGRunLogger &logger,
|
||||||
executors::task::TaskExecutor &executor, size_t nDAGRunners)
|
executors::task::TaskExecutor &executor, size_t nDAGRunners,
|
||||||
|
const fs::path &staticAssetsDir)
|
||||||
: endpoint_(listenSpec)
|
: endpoint_(listenSpec)
|
||||||
, desc_("Daggy API", "0.1")
|
, desc_("Daggy API", "0.1")
|
||||||
, logger_(logger)
|
, logger_(logger)
|
||||||
, executor_(executor)
|
, executor_(executor)
|
||||||
, runnerPool_(nDAGRunners)
|
, runnerPool_(nDAGRunners)
|
||||||
|
, staticAssetsDir_(staticAssetsDir)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +100,19 @@ namespace daggy::daggyd {
|
|||||||
desc_.response(Http::Code::Internal_Server_Error,
|
desc_.response(Http::Code::Internal_Server_Error,
|
||||||
R"({"error": "An error occurred with the backend"})");
|
R"({"error": "An error occurred with the backend"})");
|
||||||
|
|
||||||
|
desc_.route(desc_.get("/"))
|
||||||
|
.bind(&Server::handleStatic, this)
|
||||||
|
.response(Http::Code::Ok, "Serve static assets")
|
||||||
|
.hide();
|
||||||
|
desc_.route(desc_.get("/*"))
|
||||||
|
.bind(&Server::handleStatic, this)
|
||||||
|
.response(Http::Code::Ok, "Serve static assets")
|
||||||
|
.hide();
|
||||||
|
desc_.route(desc_.get("/*/*"))
|
||||||
|
.bind(&Server::handleStatic, this)
|
||||||
|
.response(Http::Code::Ok, "Serve static assets")
|
||||||
|
.hide();
|
||||||
|
|
||||||
desc_.schemes(Rest::Scheme::Http)
|
desc_.schemes(Rest::Scheme::Http)
|
||||||
.basePath("/v1")
|
.basePath("/v1")
|
||||||
.produces(MIME(Application, Json))
|
.produces(MIME(Application, Json))
|
||||||
@@ -101,11 +123,6 @@ namespace daggy::daggyd {
|
|||||||
.response(Http::Code::Ok, "Response to the /ready call")
|
.response(Http::Code::Ok, "Response to the /ready call")
|
||||||
.hide();
|
.hide();
|
||||||
|
|
||||||
desc_.route(desc_.get("/"))
|
|
||||||
.bind(&Server::handleRoot, this)
|
|
||||||
.response(Http::Code::Ok, "Response to the /ready call")
|
|
||||||
.hide();
|
|
||||||
|
|
||||||
auto versionPath = desc_.path("/v1");
|
auto versionPath = desc_.path("/v1");
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -113,6 +130,7 @@ namespace daggy::daggyd {
|
|||||||
*/
|
*/
|
||||||
auto dagRunsPath = versionPath.path("/dagruns");
|
auto dagRunsPath = versionPath.path("/dagruns");
|
||||||
|
|
||||||
|
dagRunsPath.route(desc_.options("/")).bind(&Server::handleCORS, this);
|
||||||
dagRunsPath.route(desc_.get("/"))
|
dagRunsPath.route(desc_.get("/"))
|
||||||
.bind(&Server::handleQueryDAGs, this)
|
.bind(&Server::handleQueryDAGs, this)
|
||||||
.produces(MIME(Application, Json))
|
.produces(MIME(Application, Json))
|
||||||
@@ -123,11 +141,14 @@ namespace daggy::daggyd {
|
|||||||
*/
|
*/
|
||||||
auto dagRunPath = versionPath.path("/dagrun");
|
auto dagRunPath = versionPath.path("/dagrun");
|
||||||
|
|
||||||
|
dagRunPath.route(desc_.options("/")).bind(&Server::handleCORS, this);
|
||||||
dagRunPath.route(desc_.post("/"))
|
dagRunPath.route(desc_.post("/"))
|
||||||
.bind(&Server::handleRunDAG, this)
|
.bind(&Server::handleRunDAG, this)
|
||||||
.produces(MIME(Application, Json))
|
.produces(MIME(Application, Json))
|
||||||
.response(Http::Code::Ok, "Run a DAG");
|
.response(Http::Code::Ok, "Run a DAG");
|
||||||
|
|
||||||
|
dagRunPath.route(desc_.options("/validate"))
|
||||||
|
.bind(&Server::handleCORS, this);
|
||||||
dagRunPath.route(desc_.post("/validate"))
|
dagRunPath.route(desc_.post("/validate"))
|
||||||
.bind(&Server::handleValidateDAG, this)
|
.bind(&Server::handleValidateDAG, this)
|
||||||
.produces(MIME(Application, Json))
|
.produces(MIME(Application, Json))
|
||||||
@@ -138,6 +159,8 @@ namespace daggy::daggyd {
|
|||||||
*/
|
*/
|
||||||
auto specificDAGRunPath = dagRunPath.path("/:runID");
|
auto specificDAGRunPath = dagRunPath.path("/:runID");
|
||||||
|
|
||||||
|
specificDAGRunPath.route(desc_.options("/"))
|
||||||
|
.bind(&Server::handleCORS, this);
|
||||||
specificDAGRunPath.route(desc_.del("/"))
|
specificDAGRunPath.route(desc_.del("/"))
|
||||||
.bind(&Server::handleStopDAGRun, this)
|
.bind(&Server::handleStopDAGRun, this)
|
||||||
.produces(MIME(Application, Json))
|
.produces(MIME(Application, Json))
|
||||||
@@ -148,12 +171,16 @@ namespace daggy::daggyd {
|
|||||||
.produces(MIME(Application, Json))
|
.produces(MIME(Application, Json))
|
||||||
.response(Http::Code::Ok, "Full DAG Run");
|
.response(Http::Code::Ok, "Full DAG Run");
|
||||||
|
|
||||||
|
specificDAGRunPath.route(desc_.options("/state"))
|
||||||
|
.bind(&Server::handleCORS, this);
|
||||||
specificDAGRunPath.route(desc_.get("/state"))
|
specificDAGRunPath.route(desc_.get("/state"))
|
||||||
.bind(&Server::handleGetDAGRunState, this)
|
.bind(&Server::handleGetDAGRunState, this)
|
||||||
.produces(MIME(Application, Json))
|
.produces(MIME(Application, Json))
|
||||||
.response(Http::Code::Ok,
|
.response(Http::Code::Ok,
|
||||||
"Structure of a DAG and DAG and Task run states");
|
"Structure of a DAG and DAG and Task run states");
|
||||||
|
|
||||||
|
specificDAGRunPath.route(desc_.options("/state/:state"))
|
||||||
|
.bind(&Server::handleCORS, this);
|
||||||
specificDAGRunPath.route(desc_.patch("/state/:state"))
|
specificDAGRunPath.route(desc_.patch("/state/:state"))
|
||||||
.bind(&Server::handleSetDAGRunState, this)
|
.bind(&Server::handleSetDAGRunState, this)
|
||||||
.produces(MIME(Application, Json))
|
.produces(MIME(Application, Json))
|
||||||
@@ -163,6 +190,7 @@ namespace daggy::daggyd {
|
|||||||
Task paths
|
Task paths
|
||||||
*/
|
*/
|
||||||
auto taskPath = specificDAGRunPath.path("/task/:taskName");
|
auto taskPath = specificDAGRunPath.path("/task/:taskName");
|
||||||
|
taskPath.route(desc_.options("/")).bind(&Server::handleCORS, this);
|
||||||
taskPath.route(desc_.get("/"))
|
taskPath.route(desc_.get("/"))
|
||||||
.bind(&Server::handleGetTask, this)
|
.bind(&Server::handleGetTask, this)
|
||||||
.produces(MIME(Application, Json))
|
.produces(MIME(Application, Json))
|
||||||
@@ -177,22 +205,33 @@ namespace daggy::daggyd {
|
|||||||
*/
|
*/
|
||||||
auto taskStatePath = taskPath.path("/state");
|
auto taskStatePath = taskPath.path("/state");
|
||||||
|
|
||||||
|
taskStatePath.route(desc_.options("/")).bind(&Server::handleCORS, this);
|
||||||
taskStatePath.route(desc_.get("/"))
|
taskStatePath.route(desc_.get("/"))
|
||||||
.bind(&Server::handleGetTaskState, this)
|
.bind(&Server::handleGetTaskState, this)
|
||||||
.produces(MIME(Application, Json))
|
.produces(MIME(Application, Json))
|
||||||
.response(Http::Code::Ok, "Get a task state");
|
.response(Http::Code::Ok, "Get a task state");
|
||||||
|
|
||||||
|
taskStatePath.route(desc_.options("/:state"))
|
||||||
|
.bind(&Server::handleCORS, this);
|
||||||
taskStatePath.route(desc_.patch("/:state"))
|
taskStatePath.route(desc_.patch("/:state"))
|
||||||
.bind(&Server::handleSetTaskState, this)
|
.bind(&Server::handleSetTaskState, this)
|
||||||
.produces(MIME(Application, Json))
|
.produces(MIME(Application, Json))
|
||||||
.response(Http::Code::Ok, "Set a task state");
|
.response(Http::Code::Ok, "Set a task state");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Server::handleCORS(const Pistache::Rest::Request &request,
|
||||||
|
Pistache::Http::ResponseWriter response)
|
||||||
|
{
|
||||||
|
addResponseHeaders(response);
|
||||||
|
response.send(Pistache::Http::Code::Ok, "");
|
||||||
|
}
|
||||||
|
|
||||||
void Server::handleRunDAG(const Pistache::Rest::Request &request,
|
void Server::handleRunDAG(const Pistache::Rest::Request &request,
|
||||||
Pistache::Http::ResponseWriter response)
|
Pistache::Http::ResponseWriter response)
|
||||||
{
|
{
|
||||||
if (!handleAuth(request))
|
if (!handleAuth(request))
|
||||||
return;
|
return;
|
||||||
|
addResponseHeaders(response);
|
||||||
|
|
||||||
DAGRunID runID = 0;
|
DAGRunID runID = 0;
|
||||||
try {
|
try {
|
||||||
@@ -216,6 +255,7 @@ namespace daggy::daggyd {
|
|||||||
void Server::handleValidateDAG(const Pistache::Rest::Request &request,
|
void Server::handleValidateDAG(const Pistache::Rest::Request &request,
|
||||||
Pistache::Http::ResponseWriter response)
|
Pistache::Http::ResponseWriter response)
|
||||||
{
|
{
|
||||||
|
addResponseHeaders(response);
|
||||||
try {
|
try {
|
||||||
dagFromJSON(request.body());
|
dagFromJSON(request.body());
|
||||||
response.send(Pistache::Http::Code::Ok, R"({"valid": true}\n)");
|
response.send(Pistache::Http::Code::Ok, R"({"valid": true}\n)");
|
||||||
@@ -231,6 +271,7 @@ namespace daggy::daggyd {
|
|||||||
void Server::handleQueryDAGs(const Pistache::Rest::Request &request,
|
void Server::handleQueryDAGs(const Pistache::Rest::Request &request,
|
||||||
Pistache::Http::ResponseWriter response)
|
Pistache::Http::ResponseWriter response)
|
||||||
{
|
{
|
||||||
|
addResponseHeaders(response);
|
||||||
if (!handleAuth(request))
|
if (!handleAuth(request))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -241,116 +282,63 @@ namespace daggy::daggyd {
|
|||||||
tag = request.query().get("tag").value();
|
tag = request.query().get("tag").value();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isJSON = requestIsForJSON(request);
|
if (request.query().has("all")) {
|
||||||
|
auto val = request.query().get("all").value();
|
||||||
if (request.hasParam(":all")) {
|
|
||||||
auto val = request.query().get(":all").value();
|
|
||||||
if (val == "true" or val == "1") {
|
if (val == "true" or val == "1") {
|
||||||
all = true;
|
all = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (!isJSON) {
|
|
||||||
all = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto dagRuns = logger_.queryDAGRuns(tag, all);
|
auto dagRuns = logger_.queryDAGRuns(tag, all);
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
if (isJSON) {
|
// default to json
|
||||||
// default to json
|
ss << '[';
|
||||||
ss << '[';
|
|
||||||
|
|
||||||
bool first = true;
|
bool first = true;
|
||||||
for (const auto &run : dagRuns) {
|
for (const auto &run : dagRuns) {
|
||||||
if (first) {
|
if (first) {
|
||||||
first = false;
|
first = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ss << ", ";
|
||||||
|
}
|
||||||
|
|
||||||
|
ss << " {"
|
||||||
|
<< R"("runID": )" << run.runID << ',' << R"("tag": )"
|
||||||
|
<< std::quoted(run.tag) << ","
|
||||||
|
<< R"("state": )" << std::quoted(run.runState._to_string()) << ","
|
||||||
|
<< R"("startTime": )" << std::quoted(timePointToString(run.startTime))
|
||||||
|
<< ',' << R"("lastUpdate": )"
|
||||||
|
<< std::quoted(timePointToString(run.lastUpdate)) << ','
|
||||||
|
<< R"("taskCounts": {)";
|
||||||
|
bool firstState = true;
|
||||||
|
for (const auto &[state, count] : run.taskStateCounts) {
|
||||||
|
if (firstState) {
|
||||||
|
firstState = false;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
ss << ", ";
|
ss << ", ";
|
||||||
}
|
}
|
||||||
|
ss << std::quoted(state._to_string()) << ':' << count;
|
||||||
ss << " {"
|
|
||||||
<< R"("runID": )" << run.runID << ',' << R"("tag": )"
|
|
||||||
<< std::quoted(run.tag) << ","
|
|
||||||
<< R"("startTime": )"
|
|
||||||
<< std::quoted(timePointToString(run.startTime)) << ','
|
|
||||||
<< R"("lastUpdate": )"
|
|
||||||
<< std::quoted(timePointToString(run.lastUpdate)) << ','
|
|
||||||
<< R"("taskCounts": {)";
|
|
||||||
bool firstState = true;
|
|
||||||
for (const auto &[state, count] : run.taskStateCounts) {
|
|
||||||
if (firstState) {
|
|
||||||
firstState = false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
ss << ", ";
|
|
||||||
}
|
|
||||||
ss << std::quoted(state._to_string()) << ':' << count;
|
|
||||||
}
|
|
||||||
ss << '}' // end of taskCounts
|
|
||||||
<< '}'; // end of item
|
|
||||||
}
|
}
|
||||||
ss << "]\n";
|
ss << '}' // end of taskCounts
|
||||||
}
|
<< '}'; // end of item
|
||||||
else {
|
|
||||||
// HTML
|
|
||||||
ss << "<html><head><title>Daggy "
|
|
||||||
"Runs</title><meta http-equiv=\"refresh\" "
|
|
||||||
"content=\"10\"></head><body><center><h2>Current Runs</h2><br>";
|
|
||||||
if (!dagRuns.empty()) {
|
|
||||||
std::sort(dagRuns.begin(), dagRuns.end(),
|
|
||||||
[](const auto &a, const auto &b) {
|
|
||||||
return a.startTime > b.startTime;
|
|
||||||
});
|
|
||||||
ss << "<table><tr><th>Run ID</th><th>Tag</th><th>State</th><th># "
|
|
||||||
"Tasks</th><th>Start Time</th><th>Last "
|
|
||||||
"Update</th><th>Queued</th><th>Running</th><th>Retry</"
|
|
||||||
"th><th>Errored</th><th>Completed</th></tr>";
|
|
||||||
for (auto &ds : dagRuns) {
|
|
||||||
size_t nTasks = 0;
|
|
||||||
for (const auto &[k, cnt] : ds.taskStateCounts)
|
|
||||||
nTasks += cnt;
|
|
||||||
|
|
||||||
auto stateURL = [&](RunState state) {
|
|
||||||
std::stringstream ss;
|
|
||||||
ss << R"(<a href="/v1/dagrun/)" << ds.runID
|
|
||||||
<< "/?state=" << state._to_string() << "\">"
|
|
||||||
<< ds.taskStateCounts[state] << "</a>";
|
|
||||||
return ss.str();
|
|
||||||
};
|
|
||||||
|
|
||||||
ss << "<tr>"
|
|
||||||
<< R"(<td><a href="/v1/dagrun/)" << ds.runID << R"(">)" << ds.runID
|
|
||||||
<< "</a></td>"
|
|
||||||
<< "<td>" << ds.tag << "</td>"
|
|
||||||
<< "<td>" << ds.runState << "</td>"
|
|
||||||
<< "<td>" << nTasks << "</td>"
|
|
||||||
<< "<td>" << timePointToString(ds.startTime) << "</td>"
|
|
||||||
<< "<td>" << timePointToString(ds.lastUpdate) << "</td>"
|
|
||||||
<< "<td>" << stateURL(RunState::QUEUED) << "</td>"
|
|
||||||
<< "<td>" << stateURL(RunState::RUNNING) << "</td>"
|
|
||||||
<< "<td>" << stateURL(RunState::RETRY) << "</td>"
|
|
||||||
<< "<td>" << stateURL(RunState::ERRORED) << "</td>"
|
|
||||||
<< "<td>" << stateURL(RunState::COMPLETED) << "</td>"
|
|
||||||
<< "</tr>";
|
|
||||||
}
|
|
||||||
ss << "</table>";
|
|
||||||
}
|
|
||||||
ss << "</body></html>\n";
|
|
||||||
}
|
}
|
||||||
|
ss << "]\n";
|
||||||
response.send(Pistache::Http::Code::Ok, ss.str());
|
response.send(Pistache::Http::Code::Ok, ss.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void Server::handleGetDAGRun(const Pistache::Rest::Request &request,
|
void Server::handleGetDAGRun(const Pistache::Rest::Request &request,
|
||||||
Pistache::Http::ResponseWriter response)
|
Pistache::Http::ResponseWriter response)
|
||||||
{
|
{
|
||||||
|
addResponseHeaders(response);
|
||||||
if (!handleAuth(request))
|
if (!handleAuth(request))
|
||||||
return;
|
return;
|
||||||
if (!request.hasParam(":runID")) {
|
if (!request.hasParam(":runID")) {
|
||||||
REQ_RESPONSE(Not_Found, "No runID provided in URL");
|
REQ_RESPONSE(Not_Found, "No runID provided in URL");
|
||||||
}
|
}
|
||||||
auto runID = request.param(":runID").as<size_t>();
|
auto runID = request.param(":runID").as<size_t>();
|
||||||
auto run = logger_.getDAGRun(runID);
|
auto run = logger_.getDAGRun(runID);
|
||||||
bool isJSON = requestIsForJSON(request);
|
|
||||||
|
|
||||||
std::optional<RunState> filterState;
|
std::optional<RunState> filterState;
|
||||||
if (request.query().has("state")) {
|
if (request.query().has("state")) {
|
||||||
@@ -359,136 +347,72 @@ namespace daggy::daggyd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
if (isJSON) {
|
bool first = true;
|
||||||
bool first = true;
|
ss << "{"
|
||||||
ss << "{"
|
<< R"("runID": )" << runID << ',' << R"("tag": )"
|
||||||
<< R"("runID": )" << runID << ',' << R"("tag": )"
|
<< std::quoted(run.dagSpec.tag) << ',' << R"("tasks": )"
|
||||||
<< std::quoted(run.dagSpec.tag) << ',' << R"("tasks": )"
|
<< tasksToJSON(run.dagSpec.tasks) << ',';
|
||||||
<< tasksToJSON(run.dagSpec.tasks) << ',';
|
|
||||||
|
|
||||||
// task run states
|
// task run states
|
||||||
ss << R"("taskStates": { )";
|
ss << R"("taskStates": { )";
|
||||||
first = true;
|
first = true;
|
||||||
for (const auto &[name, state] : run.taskRunStates) {
|
for (const auto &[name, state] : run.taskRunStates) {
|
||||||
if (first) {
|
if (first) {
|
||||||
first = false;
|
first = false;
|
||||||
}
|
|
||||||
else {
|
|
||||||
ss << ',';
|
|
||||||
}
|
|
||||||
ss << std::quoted(name) << ": " << std::quoted(state._to_string());
|
|
||||||
}
|
}
|
||||||
ss << "},";
|
else {
|
||||||
|
ss << ',';
|
||||||
// Attempt records
|
|
||||||
first = true;
|
|
||||||
ss << R"("taskAttempts": { )";
|
|
||||||
for (const auto &[taskName, attempts] : run.taskAttempts) {
|
|
||||||
if (first) {
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
ss << ',';
|
|
||||||
}
|
|
||||||
ss << std::quoted(taskName) << ": [";
|
|
||||||
bool firstAttempt = true;
|
|
||||||
for (const auto &attempt : attempts) {
|
|
||||||
if (firstAttempt) {
|
|
||||||
firstAttempt = false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
ss << ',';
|
|
||||||
}
|
|
||||||
ss << attemptRecordToJSON(attempt);
|
|
||||||
}
|
|
||||||
ss << ']';
|
|
||||||
}
|
}
|
||||||
ss << "},";
|
ss << std::quoted(name) << ": " << std::quoted(state._to_string());
|
||||||
|
|
||||||
// DAG state changes
|
|
||||||
first = true;
|
|
||||||
ss << R"("dagStateChanges": [ )";
|
|
||||||
for (const auto &change : run.dagStateChanges) {
|
|
||||||
if (first) {
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
ss << ',';
|
|
||||||
}
|
|
||||||
ss << stateUpdateRecordToJSON(change);
|
|
||||||
}
|
|
||||||
ss << "]";
|
|
||||||
ss << "}\n";
|
|
||||||
}
|
}
|
||||||
else {
|
ss << "},";
|
||||||
std::unordered_map<RunState, size_t> stateCounts;
|
|
||||||
for (const auto &[_, state] : run.taskRunStates) {
|
// Attempt records
|
||||||
stateCounts[state]++;
|
first = true;
|
||||||
|
ss << R"("taskAttempts": { )";
|
||||||
|
for (const auto &[taskName, attempts] : run.taskAttempts) {
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
ss << R"(<html>
|
ss << ',';
|
||||||
<head>
|
}
|
||||||
<title>Details for RunID )"
|
ss << std::quoted(taskName) << ": [";
|
||||||
<< runID << R"(</title>
|
bool firstAttempt = true;
|
||||||
<meta http-equiv="refresh" "content="10">
|
for (const auto &attempt : attempts) {
|
||||||
<script>
|
if (firstAttempt) {
|
||||||
function resubmit(run_id, task_name) {
|
firstAttempt = false;
|
||||||
let url = "/v1/dagrun/" + run_id + "/task/" + task_name + "/state/QUEUED";
|
|
||||||
fetch(url, { method: 'PATCH' })
|
|
||||||
.then(res => console.log('Resubmitted task'))
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<center>
|
|
||||||
<h2>Summary</h2>
|
|
||||||
<table><tr><th>Run ID</th><th>Tag</th><th>State</th>
|
|
||||||
<th>#Tasks</th>
|
|
||||||
<th>Queued</th><th>Running</th><th>Retry</th>
|
|
||||||
<th>Errored</th><th>Completed</th></tr>
|
|
||||||
<tr>)"
|
|
||||||
<< "<td>" << runID << "</td>"
|
|
||||||
<< "<td>" << run.dagSpec.tag << "</td>"
|
|
||||||
<< "<td>" << run.dagStateChanges.back().state << "</td>"
|
|
||||||
<< "<td>" << run.dagSpec.tasks.size() << "</td>"
|
|
||||||
<< "<td>" << stateCounts[RunState::QUEUED] << "</td>"
|
|
||||||
<< "<td>" << stateCounts[RunState::RUNNING] << "</td>"
|
|
||||||
<< "<td>" << stateCounts[RunState::RETRY] << "</td>"
|
|
||||||
<< "<td>" << stateCounts[RunState::ERRORED] << "</td>"
|
|
||||||
<< "<td>" << stateCounts[RunState::COMPLETED] << "</td>"
|
|
||||||
<< "</tr></table>"
|
|
||||||
<< "<h2>Task Details</h2>"
|
|
||||||
<< "<table><tr><th>Task Name</th><th> State</th><th>Last "
|
|
||||||
"Update</th><th> Logs</th></tr>";
|
|
||||||
|
|
||||||
for (const auto &[taskName, task] : run.dagSpec.tasks) {
|
|
||||||
auto taskState = run.taskRunStates.at(taskName);
|
|
||||||
if (filterState and taskState != *filterState)
|
|
||||||
continue;
|
|
||||||
std::string retryButton = "";
|
|
||||||
if (taskState == +RunState::ERRORED) {
|
|
||||||
retryButton = " <a href=\"#\" onclick=\"resubmit(" +
|
|
||||||
std::to_string(runID) + ", '" + taskName +
|
|
||||||
"');\">Retry</a>";
|
|
||||||
}
|
}
|
||||||
ss << "<tr>"
|
else {
|
||||||
<< "<td>" << taskName << "</td>"
|
ss << ',';
|
||||||
<< "<td>" << run.taskRunStates.at(taskName) << "</td>"
|
}
|
||||||
<< "<td>"
|
ss << attemptRecordToJSON(attempt);
|
||||||
<< timePointToString(run.taskStateChanges.at(taskName).back().time)
|
|
||||||
<< "</td>"
|
|
||||||
<< "<td><a href=\"/v1/dagrun/" << runID << "/task/" << taskName
|
|
||||||
<< "\">Logs</a>" << retryButton << "</td>"
|
|
||||||
<< "</tr>";
|
|
||||||
}
|
}
|
||||||
ss << "</table></center></body></html>";
|
ss << ']';
|
||||||
}
|
}
|
||||||
|
ss << "},";
|
||||||
|
|
||||||
|
// DAG state changes
|
||||||
|
first = true;
|
||||||
|
ss << R"("dagStateChanges": [ )";
|
||||||
|
for (const auto &change : run.dagStateChanges) {
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ss << ',';
|
||||||
|
}
|
||||||
|
ss << stateUpdateRecordToJSON(change);
|
||||||
|
}
|
||||||
|
ss << "]";
|
||||||
|
ss << "}\n";
|
||||||
response.send(Pistache::Http::Code::Ok, ss.str());
|
response.send(Pistache::Http::Code::Ok, ss.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void Server::handleStopDAGRun(const Pistache::Rest::Request &request,
|
void Server::handleStopDAGRun(const Pistache::Rest::Request &request,
|
||||||
Pistache::Http::ResponseWriter response)
|
Pistache::Http::ResponseWriter response)
|
||||||
{
|
{
|
||||||
|
addResponseHeaders(response);
|
||||||
if (!handleAuth(request))
|
if (!handleAuth(request))
|
||||||
return;
|
return;
|
||||||
if (!request.hasParam(":runID")) {
|
if (!request.hasParam(":runID")) {
|
||||||
@@ -556,6 +480,7 @@ namespace daggy::daggyd {
|
|||||||
{
|
{
|
||||||
if (!handleAuth(request))
|
if (!handleAuth(request))
|
||||||
return;
|
return;
|
||||||
|
addResponseHeaders(response);
|
||||||
|
|
||||||
// TODO handle state transition
|
// TODO handle state transition
|
||||||
DAGRunID runID = request.param(":runID").as<DAGRunID>();
|
DAGRunID runID = request.param(":runID").as<DAGRunID>();
|
||||||
@@ -609,63 +534,54 @@ namespace daggy::daggyd {
|
|||||||
void Server::handleGetTask(const Pistache::Rest::Request &request,
|
void Server::handleGetTask(const Pistache::Rest::Request &request,
|
||||||
Pistache::Http::ResponseWriter response)
|
Pistache::Http::ResponseWriter response)
|
||||||
{
|
{
|
||||||
|
addResponseHeaders(response);
|
||||||
if (!handleAuth(request))
|
if (!handleAuth(request))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
auto runID = request.param(":runID").as<DAGRunID>();
|
auto runID = request.param(":runID").as<DAGRunID>();
|
||||||
auto taskName = request.param(":taskName").as<std::string>();
|
auto taskName = request.param(":taskName").as<std::string>();
|
||||||
bool isJSON = requestIsForJSON(request);
|
|
||||||
|
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
if (isJSON) {
|
Task task;
|
||||||
Task task;
|
try {
|
||||||
try {
|
task = logger_.getTask(runID, taskName);
|
||||||
task = logger_.getTask(runID, taskName);
|
|
||||||
}
|
|
||||||
catch (std::exception &e) {
|
|
||||||
REQ_RESPONSE(Not_Found, e.what());
|
|
||||||
}
|
|
||||||
ss << taskToJSON(task);
|
|
||||||
}
|
}
|
||||||
else {
|
catch (std::exception &e) {
|
||||||
std::optional<loggers::dag_run::TaskRecord> tr;
|
REQ_RESPONSE(Not_Found, e.what());
|
||||||
try {
|
|
||||||
tr.emplace(logger_.getTaskRecord(runID, taskName));
|
|
||||||
}
|
|
||||||
catch (std::exception &e) {
|
|
||||||
REQ_RESPONSE(Not_Found, e.what());
|
|
||||||
}
|
|
||||||
ss << "<html><title>Task Details for " << runID << " / " << taskName
|
|
||||||
<< "</title><body>"
|
|
||||||
<< "<table>"
|
|
||||||
<< "<tr><th>Name</th><td>" << taskName << "</td></tr>"
|
|
||||||
<< "<tr><th>State</th><td>" << tr->state << "</td></tr>"
|
|
||||||
<< "<tr><th>Definition</th><td>" << taskToJSON(tr->task)
|
|
||||||
<< "</td></tr>"
|
|
||||||
<< "<tr><th colspan=2>Attempts</th></tr>";
|
|
||||||
|
|
||||||
std::sort(tr->attempts.begin(), tr->attempts.end(),
|
|
||||||
[](const auto &a, const auto &b) {
|
|
||||||
return a.startTime < b.startTime;
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const auto &attempt : tr->attempts) {
|
|
||||||
ss << "<tr><td valign=top>" << timePointToString(attempt.startTime)
|
|
||||||
<< "</td><td><pre>rc: " << attempt.rc
|
|
||||||
<< "\n\nstdout:\n--------------\n"
|
|
||||||
<< attempt.outputLog << "\n\nstderr:\n--------------\n"
|
|
||||||
<< attempt.errorLog << "\n\nexecutor:\n--------------\n"
|
|
||||||
<< attempt.executorLog << "</pre></td></tr>";
|
|
||||||
}
|
|
||||||
|
|
||||||
ss << "</table></body></html>\n";
|
|
||||||
}
|
}
|
||||||
|
ss << taskToJSON(task);
|
||||||
response.send(Pistache::Http::Code::Ok, ss.str());
|
response.send(Pistache::Http::Code::Ok, ss.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Server::handleStopTask(const Pistache::Rest::Request &request,
|
||||||
|
Pistache::Http::ResponseWriter response)
|
||||||
|
{
|
||||||
|
addResponseHeaders(response);
|
||||||
|
if (!handleAuth(request))
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto runID = request.param(":runID").as<DAGRunID>();
|
||||||
|
auto taskName = request.param(":taskName").as<std::string>();
|
||||||
|
|
||||||
|
std::shared_ptr<DAGRunner> runner{nullptr};
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(runnerGuard_);
|
||||||
|
auto it = runners_.find(runID);
|
||||||
|
if (runners_.find(runID) != runners_.end()) {
|
||||||
|
runner = it->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runner) {
|
||||||
|
runner->stopTask(taskName);
|
||||||
|
}
|
||||||
|
REQ_RESPONSE(Ok, "");
|
||||||
|
}
|
||||||
|
|
||||||
void Server::handleGetTaskState(const Pistache::Rest::Request &request,
|
void Server::handleGetTaskState(const Pistache::Rest::Request &request,
|
||||||
Pistache::Http::ResponseWriter response)
|
Pistache::Http::ResponseWriter response)
|
||||||
{
|
{
|
||||||
|
addResponseHeaders(response);
|
||||||
if (!handleAuth(request))
|
if (!handleAuth(request))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -685,28 +601,10 @@ namespace daggy::daggyd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Server::handleStopTask(const Pistache::Rest::Request &request,
|
|
||||||
Pistache::Http::ResponseWriter response)
|
|
||||||
{
|
|
||||||
if (!handleAuth(request))
|
|
||||||
return;
|
|
||||||
|
|
||||||
auto runID = request.param(":runID").as<DAGRunID>();
|
|
||||||
auto taskName = request.param(":taskName").as<std::string>();
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(runnerGuard_);
|
|
||||||
auto it = runners_.find(runID);
|
|
||||||
if (runners_.find(runID) != runners_.end()) {
|
|
||||||
it->second->stopTask(taskName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
response.send(Pistache::Http::Code::Ok, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
void Server::handleSetTaskState(const Pistache::Rest::Request &request,
|
void Server::handleSetTaskState(const Pistache::Rest::Request &request,
|
||||||
Pistache::Http::ResponseWriter response)
|
Pistache::Http::ResponseWriter response)
|
||||||
{
|
{
|
||||||
|
addResponseHeaders(response);
|
||||||
if (!handleAuth(request))
|
if (!handleAuth(request))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -728,16 +626,59 @@ namespace daggy::daggyd {
|
|||||||
void Server::handleReady(const Pistache::Rest::Request &request,
|
void Server::handleReady(const Pistache::Rest::Request &request,
|
||||||
Pistache::Http::ResponseWriter response)
|
Pistache::Http::ResponseWriter response)
|
||||||
{
|
{
|
||||||
response.send(Pistache::Http::Code::Ok, R"({ "msg": "Ya like DAGs?"}\n)");
|
addResponseHeaders(response);
|
||||||
|
REQ_RESPONSE(Ok, "Ya like DAGs?");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Server::handleRoot(const Pistache::Rest::Request &request,
|
void Server::handleStatic(const Pistache::Rest::Request &request,
|
||||||
Pistache::Http::ResponseWriter response)
|
Pistache::Http::ResponseWriter response)
|
||||||
{
|
{
|
||||||
|
addResponseHeaders(response);
|
||||||
|
std::string file = "index.html";
|
||||||
|
|
||||||
|
auto splats = request.splat();
|
||||||
|
if (!splats.empty()) {
|
||||||
|
file = splats[0].as<std::string>();
|
||||||
|
for (size_t i = 1; i < splats.size(); ++i)
|
||||||
|
file += "/" + splats[i].as<std::string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto fn = staticAssetsDir_ / file;
|
||||||
|
|
||||||
|
auto ext = fn.extension();
|
||||||
|
|
||||||
|
if (!fs::exists(fn)) {
|
||||||
|
std::cout << "Can't find " << fn << std::endl;
|
||||||
|
REQ_RESPONSE(Not_Found, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string contentType;
|
||||||
|
if (ext == ".svg") {
|
||||||
|
contentType = "image/svg+xml";
|
||||||
|
}
|
||||||
|
else if (ext == ".html") {
|
||||||
|
contentType = "text/html";
|
||||||
|
}
|
||||||
|
else if (ext == ".css") {
|
||||||
|
contentType = "text/css";
|
||||||
|
}
|
||||||
|
else if (ext == ".js") {
|
||||||
|
contentType = "text/javascript";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
REQ_RESPONSE(Bad_Request, "I don't know how to serve that kind of file");
|
||||||
|
}
|
||||||
|
|
||||||
|
response.headers().remove<Pistache::Http::Header::ContentType>();
|
||||||
response.headers().add(
|
response.headers().add(
|
||||||
std::make_shared<Pistache::Http::Header::Location>("/v1/dagruns"));
|
std::make_shared<Pistache::Http::Header::ContentType>(contentType));
|
||||||
response.send(Pistache::Http::Code::Moved_Permanently,
|
|
||||||
R"({ "msg": "These are the dags you are looking for"}\n)");
|
std::stringstream ss;
|
||||||
|
std::ifstream ifh;
|
||||||
|
ifh.open(fn, std::ios::binary);
|
||||||
|
ss << ifh.rdbuf();
|
||||||
|
|
||||||
|
response.send(Pistache::Http::Code::Ok, ss.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ TEST_CASE("rest_endpoint", "[server_basic]")
|
|||||||
|
|
||||||
const size_t nDAGRunners = 10, nWebThreads = 10;
|
const size_t nDAGRunners = 10, nWebThreads = 10;
|
||||||
|
|
||||||
daggy::daggyd::Server server(listenSpec, logger, executor, nDAGRunners);
|
daggy::daggyd::Server server(listenSpec, logger, executor, nDAGRunners,
|
||||||
|
"/dev/null");
|
||||||
server.init(nWebThreads);
|
server.init(nWebThreads);
|
||||||
server.start();
|
server.start();
|
||||||
|
|
||||||
@@ -165,12 +166,13 @@ TEST_CASE("Server cancels and resumes execution", "[server_resume]")
|
|||||||
{
|
{
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
daggy::executors::task::ForkingTaskExecutor executor(10);
|
daggy::executors::task::ForkingTaskExecutor executor(10);
|
||||||
daggy::loggers::dag_run::OStreamLogger logger(ss);
|
daggy::loggers::dag_run::OStreamLogger logger(std::cout);
|
||||||
Pistache::Address listenSpec("localhost", Pistache::Port(0));
|
Pistache::Address listenSpec("localhost", Pistache::Port(0));
|
||||||
|
|
||||||
const size_t nDAGRunners = 10, nWebThreads = 10;
|
const size_t nDAGRunners = 10, nWebThreads = 10;
|
||||||
|
|
||||||
daggy::daggyd::Server server(listenSpec, logger, executor, nDAGRunners);
|
daggy::daggyd::Server server(listenSpec, logger, executor, nDAGRunners,
|
||||||
|
"/dev/null");
|
||||||
server.init(nWebThreads);
|
server.init(nWebThreads);
|
||||||
server.start();
|
server.start();
|
||||||
|
|
||||||
@@ -249,7 +251,7 @@ TEST_CASE("Server cancels and resumes execution", "[server_resume]")
|
|||||||
"PATCH");
|
"PATCH");
|
||||||
|
|
||||||
// Wait for run to complete
|
// Wait for run to complete
|
||||||
std::this_thread::sleep_for(5s);
|
std::this_thread::sleep_for(3s);
|
||||||
REQUIRE(logger.getDAGRunState(runID) == +daggy::RunState::COMPLETED);
|
REQUIRE(logger.getDAGRunState(runID) == +daggy::RunState::COMPLETED);
|
||||||
|
|
||||||
REQUIRE(fs::exists("resume_touch_c"));
|
REQUIRE(fs::exists("resume_touch_c"));
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ namespace daggy::daggyr {
|
|||||||
runningTasks_;
|
runningTasks_;
|
||||||
|
|
||||||
std::mutex resultsGuard_;
|
std::mutex resultsGuard_;
|
||||||
std::unordered_map<TaskID, Future<std::string>>
|
std::unordered_map<TaskID, Future<std::string>> results_;
|
||||||
results_;
|
|
||||||
};
|
};
|
||||||
} // namespace daggy::daggyr
|
} // namespace daggy::daggyr
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ namespace daggy {
|
|||||||
~DAGRunner();
|
~DAGRunner();
|
||||||
|
|
||||||
TaskDAG run();
|
TaskDAG run();
|
||||||
|
void stopTask(const std::string &taskName);
|
||||||
void resetRunning();
|
void resetRunning();
|
||||||
void stop(bool kill = false, bool blocking = false);
|
void stop(bool kill = false, bool blocking = false);
|
||||||
void stopTask(const std::string &taskName);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void collectFinished();
|
void collectFinished();
|
||||||
|
|||||||
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||