Skip to content

Commit

Permalink
feat: display job run results in page (#1116)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
darknight and autofix-ci[bot] authored Dec 26, 2023
1 parent 3f1fefb commit ce44f68
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 47 deletions.
21 changes: 21 additions & 0 deletions ee/tabby-webserver/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand All @@ -37,13 +47,19 @@ 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 {
node: User!
cursor: String!
}

type JobRunConnection {
edges: [JobRunEdge!]!
pageInfo: PageInfo!
}

type RefreshTokenResponse {
accessToken: String!
refreshToken: String!
Expand Down Expand Up @@ -114,6 +130,11 @@ type PageInfo {
endCursor: String
}

type JobRunEdge {
node: JobRun!
cursor: String!
}

type InvitationConnection {
edges: [InvitationEdge!]!
pageInfo: PageInfo!
Expand Down
64 changes: 63 additions & 1 deletion ee/tabby-webserver/src/schema/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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! {
Expand Down Expand Up @@ -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<Utc>,
}

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,
Expand Down Expand Up @@ -283,6 +309,34 @@ impl From<Invitation> for InvitationNext {
}
}

#[derive(Debug, GraphQLObject)]
#[graphql(context = Context)]
pub struct JobRun {
pub id: juniper::ID,
pub job_name: String,
pub start_time: DateTime<Utc>,
pub finish_time: Option<DateTime<Utc>>,
pub exit_code: Option<i32>,
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(
Expand Down Expand Up @@ -330,6 +384,14 @@ pub trait AuthenticationService: Send + Sync {
first: Option<usize>,
last: Option<usize>,
) -> Result<Vec<InvitationNext>>;

async fn list_job_runs(
&self,
after: Option<String>,
before: Option<String>,
first: Option<usize>,
last: Option<usize>,
) -> Result<Vec<JobRun>>;
}

#[cfg(test)]
Expand Down
73 changes: 39 additions & 34 deletions ee/tabby-webserver/src/schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Utc>,
}

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<String>,
before: Option<String>,
first: Option<i32>,
last: Option<i32>,
) -> FieldResult<relay::Connection<JobRun>> {
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",
)))
}
}

Expand Down
35 changes: 28 additions & 7 deletions ee/tabby-webserver/src/service/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String>,
before: Option<String>,
first: Option<usize>,
last: Option<usize>,
) -> Result<Vec<JobRun>> {
let runs = match (first, last) {
(Some(first), None) => {
let after = after.map(|x| x.parse::<i32>()).transpose()?;
self.list_job_runs_with_filter(Some(first), after, false)
.await?
}
(None, Some(last)) => {
let before = before.map(|x| x.parse::<i32>()).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<String> {
Expand Down
66 changes: 64 additions & 2 deletions ee/tabby-webserver/src/service/db/job_runs.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -15,6 +15,34 @@ pub struct JobRun {
pub stderr: String,
}

impl JobRun {
fn from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<Self> {
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<JobRun> 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<i32> {
Expand Down Expand Up @@ -81,6 +109,40 @@ impl DbConn {
.await?;
Ok(())
}

pub async fn list_job_runs_with_filter(
&self,
limit: Option<usize>,
skip_id: Option<i32>,
backwards: bool,
) -> Result<Vec<JobRun>> {
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::<Vec<_>>())
})
.await?;

Ok(runs)
}
}

#[cfg(test)]
Expand Down
6 changes: 3 additions & 3 deletions ee/tabby-webserver/src/service/db/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -40,9 +40,9 @@ impl User {
}
}

impl From<User> for schema::User {
impl From<User> 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,
Expand Down

0 comments on commit ce44f68

Please sign in to comment.