From ce44f68ee7a940122580ccb11f7b1379ade89ab5 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 26 Dec 2023 15:41:25 +0800 Subject: [PATCH] feat: display job run results in page (#1116) * feat: display job run results in page * [autofix.ci] apply automated fixes * resolve comment --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- ee/tabby-webserver/graphql/schema.graphql | 21 ++++++ ee/tabby-webserver/src/schema/auth.rs | 64 +++++++++++++++- ee/tabby-webserver/src/schema/mod.rs | 73 ++++++++++--------- ee/tabby-webserver/src/service/auth.rs | 35 +++++++-- ee/tabby-webserver/src/service/db/job_runs.rs | 66 ++++++++++++++++- ee/tabby-webserver/src/service/db/users.rs | 6 +- 6 files changed, 218 insertions(+), 47 deletions(-) diff --git a/ee/tabby-webserver/graphql/schema.graphql b/ee/tabby-webserver/graphql/schema.graphql index 64ecc8c1cd7d..41e96c04411e 100644 --- a/ee/tabby-webserver/graphql/schema.graphql +++ b/ee/tabby-webserver/graphql/schema.graphql @@ -28,6 +28,16 @@ type JWTPayload { isAdmin: Boolean! } +type JobRun { + id: ID! + jobName: String! + startTime: DateTimeUtc! + finishTime: DateTimeUtc + exitCode: Int + stdout: String! + stderr: String! +} + type Query { workers: [Worker!]! registrationToken: String! @@ -37,6 +47,7 @@ type Query { users: [User!]! usersNext(after: String, before: String, first: Int, last: Int): UserConnection! invitationsNext(after: String, before: String, first: Int, last: Int): InvitationConnection! + jobRuns(after: String, before: String, first: Int, last: Int): JobRunConnection! } type UserEdge { @@ -44,6 +55,11 @@ type UserEdge { cursor: String! } +type JobRunConnection { + edges: [JobRunEdge!]! + pageInfo: PageInfo! +} + type RefreshTokenResponse { accessToken: String! refreshToken: String! @@ -114,6 +130,11 @@ type PageInfo { endCursor: String } +type JobRunEdge { + node: JobRun! + cursor: String! +} + type InvitationConnection { edges: [InvitationEdge!]! pageInfo: PageInfo! diff --git a/ee/tabby-webserver/src/schema/auth.rs b/ee/tabby-webserver/src/schema/auth.rs index 9709700345cc..ee7d043bcf37 100644 --- a/ee/tabby-webserver/src/schema/auth.rs +++ b/ee/tabby-webserver/src/schema/auth.rs @@ -13,7 +13,7 @@ use tracing::{error, warn}; use uuid::Uuid; use validator::ValidationErrors; -use super::{from_validation_errors, User}; +use super::from_validation_errors; use crate::schema::Context; lazy_static! { @@ -237,6 +237,32 @@ impl JWTPayload { } } +#[derive(Debug, GraphQLObject)] +#[graphql(context = Context)] +pub struct User { + pub id: juniper::ID, + pub email: String, + pub is_admin: bool, + pub auth_token: String, + pub created_at: DateTime, +} + +impl relay::NodeType for User { + type Cursor = String; + + fn cursor(&self) -> Self::Cursor { + self.id.to_string() + } + + fn connection_type_name() -> &'static str { + "UserConnection" + } + + fn edge_type_name() -> &'static str { + "UserEdge" + } +} + #[derive(Debug, Default, Serialize, Deserialize, GraphQLObject)] pub struct Invitation { pub id: i32, @@ -283,6 +309,34 @@ impl From for InvitationNext { } } +#[derive(Debug, GraphQLObject)] +#[graphql(context = Context)] +pub struct JobRun { + pub id: juniper::ID, + pub job_name: String, + pub start_time: DateTime, + pub finish_time: Option>, + pub exit_code: Option, + pub stdout: String, + pub stderr: String, +} + +impl relay::NodeType for JobRun { + type Cursor = String; + + fn cursor(&self) -> Self::Cursor { + self.id.to_string() + } + + fn connection_type_name() -> &'static str { + "JobRunConnection" + } + + fn edge_type_name() -> &'static str { + "JobRunEdge" + } +} + #[async_trait] pub trait AuthenticationService: Send + Sync { async fn register( @@ -330,6 +384,14 @@ pub trait AuthenticationService: Send + Sync { first: Option, last: Option, ) -> Result>; + + async fn list_job_runs( + &self, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result>; } #[cfg(test)] diff --git a/ee/tabby-webserver/src/schema/mod.rs b/ee/tabby-webserver/src/schema/mod.rs index a6fdd0b1ffd0..b61b3c6e9b72 100644 --- a/ee/tabby-webserver/src/schema/mod.rs +++ b/ee/tabby-webserver/src/schema/mod.rs @@ -4,25 +4,22 @@ pub mod worker; use std::sync::Arc; use auth::AuthenticationService; -use chrono::{DateTime, Utc}; use juniper::{ - graphql_object, graphql_value, EmptySubscription, FieldError, FieldResult, GraphQLObject, - IntoFieldError, Object, RootNode, ScalarValue, Value, + graphql_object, graphql_value, EmptySubscription, FieldError, FieldResult, IntoFieldError, + Object, RootNode, ScalarValue, Value, }; use juniper_axum::{relay, FromAuth}; use tabby_common::api::{code::CodeSearch, event::RawEventLogger}; +use tracing::error; use validator::ValidationErrors; -use self::{ - auth::{validate_jwt, Invitation, RegisterError, TokenAuthError}, - worker::WorkerService, -}; use crate::schema::{ auth::{ - InvitationNext, RefreshTokenError, RefreshTokenResponse, RegisterResponse, - TokenAuthResponse, VerifyTokenResponse, + validate_jwt, Invitation, InvitationNext, JobRun, RefreshTokenError, RefreshTokenResponse, + RegisterError, RegisterResponse, TokenAuthError, TokenAuthResponse, User, + VerifyTokenResponse, }, - worker::Worker, + worker::{Worker, WorkerService}, }; pub trait ServiceLocator: Send + Sync { @@ -196,31 +193,39 @@ impl Query { "Only admin is able to query users", ))) } -} - -#[derive(Debug, GraphQLObject)] -#[graphql(context = Context)] -pub struct User { - pub id: juniper::ID, - pub email: String, - pub is_admin: bool, - pub auth_token: String, - pub created_at: DateTime, -} - -impl relay::NodeType for User { - type Cursor = String; - - fn cursor(&self) -> Self::Cursor { - self.id.to_string() - } - - fn connection_type_name() -> &'static str { - "UserConnection" - } - fn edge_type_name() -> &'static str { - "UserEdge" + async fn job_runs( + ctx: &Context, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> FieldResult> { + if let Some(claims) = &ctx.claims { + if claims.is_admin { + return relay::query_async( + after, + before, + first, + last, + |after, before, first, last| async move { + match ctx + .locator + .auth() + .list_job_runs(after, before, first, last) + .await + { + Ok(job_runs) => Ok(job_runs), + Err(err) => Err(FieldError::from(err)), + } + }, + ) + .await; + } + } + Err(FieldError::from(CoreError::Unauthorized( + "Only admin is able to query job runs", + ))) } } diff --git a/ee/tabby-webserver/src/service/auth.rs b/ee/tabby-webserver/src/service/auth.rs index 6a12a94ffbb9..024c1905903f 100644 --- a/ee/tabby-webserver/src/service/auth.rs +++ b/ee/tabby-webserver/src/service/auth.rs @@ -10,13 +10,10 @@ use async_trait::async_trait; use validator::{Validate, ValidationError}; use super::db::DbConn; -use crate::schema::{ - auth::{ - generate_jwt, generate_refresh_token, validate_jwt, AuthenticationService, Invitation, - InvitationNext, JWTPayload, RefreshTokenError, RefreshTokenResponse, RegisterError, - RegisterResponse, TokenAuthError, TokenAuthResponse, VerifyTokenResponse, - }, - User, +use crate::schema::auth::{ + generate_jwt, generate_refresh_token, validate_jwt, AuthenticationService, Invitation, + InvitationNext, JWTPayload, JobRun, RefreshTokenError, RefreshTokenResponse, RegisterError, + RegisterResponse, TokenAuthError, TokenAuthResponse, User, VerifyTokenResponse, }; /// Input parameters for register mutation @@ -349,6 +346,30 @@ impl AuthenticationService for DbConn { Ok(invitations.into_iter().map(|x| x.into()).collect()) } + + async fn list_job_runs( + &self, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result> { + let runs = match (first, last) { + (Some(first), None) => { + let after = after.map(|x| x.parse::()).transpose()?; + self.list_job_runs_with_filter(Some(first), after, false) + .await? + } + (None, Some(last)) => { + let before = before.map(|x| x.parse::()).transpose()?; + self.list_job_runs_with_filter(Some(last), before, true) + .await? + } + _ => self.list_job_runs_with_filter(None, None, false).await?, + }; + + Ok(runs.into_iter().map(|x| x.into()).collect()) + } } fn password_hash(raw: &str) -> password_hash::Result { diff --git a/ee/tabby-webserver/src/service/db/job_runs.rs b/ee/tabby-webserver/src/service/db/job_runs.rs index 1dcbb543a68b..16e9269b40ae 100644 --- a/ee/tabby-webserver/src/service/db/job_runs.rs +++ b/ee/tabby-webserver/src/service/db/job_runs.rs @@ -1,10 +1,10 @@ use anyhow::Result; use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; use super::DbConn; +use crate::schema::auth; -#[derive(Default, Clone, Serialize, Deserialize)] +#[derive(Default, Clone)] pub struct JobRun { pub id: i32, pub job_name: String, @@ -15,6 +15,34 @@ pub struct JobRun { pub stderr: String, } +impl JobRun { + fn from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result { + Ok(Self { + id: row.get(0)?, + job_name: row.get(1)?, + start_time: row.get(2)?, + finish_time: row.get(3)?, + exit_code: row.get(4)?, + stdout: row.get(5)?, + stderr: row.get(6)?, + }) + } +} + +impl From for auth::JobRun { + fn from(run: JobRun) -> Self { + Self { + id: juniper::ID::new(run.id.to_string()), + job_name: run.job_name, + start_time: run.start_time, + finish_time: run.finish_time, + exit_code: run.exit_code, + stdout: run.stdout, + stderr: run.stderr, + } + } +} + /// db read/write operations for `job_runs` table impl DbConn { pub async fn create_job_run(&self, run: JobRun) -> Result { @@ -81,6 +109,40 @@ impl DbConn { .await?; Ok(()) } + + pub async fn list_job_runs_with_filter( + &self, + limit: Option, + skip_id: Option, + backwards: bool, + ) -> Result> { + let query = Self::make_pagination_query( + "job_runs", + &[ + "id", + "job", + "start_ts", + "end_ts", + "exit_code", + "stdout", + "stderr", + ], + limit, + skip_id, + backwards, + ); + + let runs = self + .conn + .call(move |c| { + let mut stmt = c.prepare(&query)?; + let run_iter = stmt.query_map([], JobRun::from_row)?; + Ok(run_iter.filter_map(|x| x.ok()).collect::>()) + }) + .await?; + + Ok(runs) + } } #[cfg(test)] diff --git a/ee/tabby-webserver/src/service/db/users.rs b/ee/tabby-webserver/src/service/db/users.rs index b3833065fb39..007c69238010 100644 --- a/ee/tabby-webserver/src/service/db/users.rs +++ b/ee/tabby-webserver/src/service/db/users.rs @@ -4,7 +4,7 @@ use rusqlite::{params, OptionalExtension, Row}; use uuid::Uuid; use super::DbConn; -use crate::schema; +use crate::schema::auth; #[allow(unused)] pub struct User { @@ -40,9 +40,9 @@ impl User { } } -impl From for schema::User { +impl From for auth::User { fn from(val: User) -> Self { - schema::User { + auth::User { id: juniper::ID::new(val.id.to_string()), email: val.email, is_admin: val.is_admin,