diff --git a/apps/recorder/src/graphql/views/credential_3rd.rs b/apps/recorder/src/graphql/domains/credential_3rd.rs similarity index 97% rename from apps/recorder/src/graphql/views/credential_3rd.rs rename to apps/recorder/src/graphql/domains/credential_3rd.rs index 51fabff..872134e 100644 --- a/apps/recorder/src/graphql/views/credential_3rd.rs +++ b/apps/recorder/src/graphql/domains/credential_3rd.rs @@ -63,7 +63,9 @@ impl Credential3rdCheckAvailableInfo { } } -pub fn register_credential3rd_to_schema(mut builder: SeaographyBuilder) -> SeaographyBuilder { +pub fn register_credential3rd_to_schema_builder( + mut builder: SeaographyBuilder, +) -> SeaographyBuilder { builder.schema = builder .schema .register(Credential3rdCheckAvailableInput::generate_input_object()); diff --git a/apps/recorder/src/graphql/domains/crypto.rs b/apps/recorder/src/graphql/domains/crypto.rs new file mode 100644 index 0000000..6806cff --- /dev/null +++ b/apps/recorder/src/graphql/domains/crypto.rs @@ -0,0 +1,102 @@ +use std::sync::Arc; + +use async_graphql::dynamic::ValueAccessor; +use sea_orm::{EntityTrait, Value as SeaValue}; +use seaography::{BuilderContext, SeaResult}; + +use crate::{ + app::AppContextTrait, + graphql::infra::util::{get_column_key, get_entity_key}, + models::credential_3rd, +}; + +fn register_crypto_column_input_conversion_to_schema_context( + context: &mut BuilderContext, + ctx: Arc, + column: &T::Column, +) where + T: EntityTrait, + ::Model: Sync, +{ + let entity_key = get_entity_key::(context); + let column_name = get_column_key::(context, column); + let entity_name = context.entity_object.type_name.as_ref()(&entity_key); + let column_name = context.entity_object.column_name.as_ref()(&entity_key, &column_name); + + context.types.input_conversions.insert( + format!("{entity_name}.{column_name}"), + Box::new(move |value: &ValueAccessor| -> SeaResult { + let source = value.string()?; + let encrypted = ctx.crypto().encrypt_string(source.into())?; + Ok(encrypted.into()) + }), + ); +} + +fn register_crypto_column_output_conversion_to_schema_context( + context: &mut BuilderContext, + ctx: Arc, + column: &T::Column, +) where + T: EntityTrait, + ::Model: Sync, +{ + let entity_key = get_entity_key::(context); + let column_name = get_column_key::(context, column); + let entity_name = context.entity_object.type_name.as_ref()(&entity_key); + let column_name = context.entity_object.column_name.as_ref()(&entity_key, &column_name); + + context.types.output_conversions.insert( + format!("{entity_name}.{column_name}"), + Box::new( + move |value: &sea_orm::Value| -> SeaResult { + if let SeaValue::String(s) = value { + if let Some(s) = s { + let decrypted = ctx.crypto().decrypt_string(s)?; + Ok(async_graphql::Value::String(decrypted)) + } else { + Ok(async_graphql::Value::Null) + } + } else { + Err(async_graphql::Error::new("crypto column must be string column").into()) + } + }, + ), + ); +} + +pub fn register_crypto_to_schema_context( + context: &mut BuilderContext, + ctx: Arc, +) { + register_crypto_column_input_conversion_to_schema_context::( + context, + ctx.clone(), + &credential_3rd::Column::Cookies, + ); + register_crypto_column_input_conversion_to_schema_context::( + context, + ctx.clone(), + &credential_3rd::Column::Username, + ); + register_crypto_column_input_conversion_to_schema_context::( + context, + ctx.clone(), + &credential_3rd::Column::Password, + ); + register_crypto_column_output_conversion_to_schema_context::( + context, + ctx.clone(), + &credential_3rd::Column::Cookies, + ); + register_crypto_column_output_conversion_to_schema_context::( + context, + ctx.clone(), + &credential_3rd::Column::Username, + ); + register_crypto_column_output_conversion_to_schema_context::( + context, + ctx, + &credential_3rd::Column::Password, + ); +} diff --git a/apps/recorder/src/graphql/domains/mod.rs b/apps/recorder/src/graphql/domains/mod.rs new file mode 100644 index 0000000..0f1aaf6 --- /dev/null +++ b/apps/recorder/src/graphql/domains/mod.rs @@ -0,0 +1,5 @@ +pub mod credential_3rd; +pub mod crypto; +pub mod subscriber_tasks; +pub mod subscribers; +pub mod subscriptions; diff --git a/apps/recorder/src/graphql/domains/subscriber_tasks.rs b/apps/recorder/src/graphql/domains/subscriber_tasks.rs new file mode 100644 index 0000000..006afd0 --- /dev/null +++ b/apps/recorder/src/graphql/domains/subscriber_tasks.rs @@ -0,0 +1,42 @@ +use async_graphql::dynamic::Scalar; +use seaography::{Builder as SeaographyBuilder, BuilderContext, ConvertedType}; + +use crate::{ + graphql::infra::{ + json::restrict_jsonb_filter_input_for_entity, + util::{get_column_key, get_entity_key}, + }, + models::subscriber_tasks::{self, SubscriberTask}, +}; + +pub fn register_subscriber_tasks_to_schema_context(context: &mut BuilderContext) { + let entity_key = get_entity_key::(context); + let column_name = + get_column_key::(context, &subscriber_tasks::Column::Job); + let column_name = context.entity_object.column_name.as_ref()(&entity_key, &column_name); + context.types.overwrites.insert( + column_name, + ConvertedType::Custom(String::from("SubscriberTask")), + ); + restrict_jsonb_filter_input_for_entity::( + context, + &subscriber_tasks::Column::Job, + ); +} + +pub fn register_subscriber_tasks_to_schema_builder( + mut builder: SeaographyBuilder, +) -> SeaographyBuilder { + let subscriber_tasks_scalar = Scalar::new("SubscriberTasks") + .description("The subscriber tasks") + .validator(|value| -> bool { + if let Ok(json) = value.clone().into_json() { + serde_json::from_value::(json).is_ok() + } else { + false + } + }); + + builder.schema = builder.schema.register(subscriber_tasks_scalar); + builder +} diff --git a/apps/recorder/src/graphql/infra/filter/subscriber.rs b/apps/recorder/src/graphql/domains/subscribers/filter.rs similarity index 52% rename from apps/recorder/src/graphql/infra/filter/subscriber.rs rename to apps/recorder/src/graphql/domains/subscribers/filter.rs index b8e8a4b..3e3ffcc 100644 --- a/apps/recorder/src/graphql/infra/filter/subscriber.rs +++ b/apps/recorder/src/graphql/domains/subscribers/filter.rs @@ -1,16 +1,20 @@ -use async_graphql::dynamic::ObjectAccessor; -use once_cell::sync::OnceCell; -use sea_orm::{ColumnTrait, Condition, EntityTrait}; -use seaography::{ - BuilderContext, FilterInfo, FilterOperation as SeaographqlFilterOperation, SeaResult, -}; +use async_graphql::dynamic::TypeRef; +use lazy_static::lazy_static; +use maplit::btreeset; +use sea_orm::{ColumnTrait, EntityTrait}; +use seaography::{BuilderContext, FilterInfo, FilterOperation as SeaographqlFilterOperation}; -pub static SUBSCRIBER_ID_FILTER_INFO: OnceCell = OnceCell::new(); +use crate::graphql::infra::filter::FnFilterCondition; -pub type FnFilterCondition = - Box SeaResult + Send + Sync>; +lazy_static! { + pub static ref SUBSCRIBER_ID_FILTER_INFO: FilterInfo = FilterInfo { + type_name: String::from("SubscriberIdFilterInput"), + base_type: TypeRef::INT.into(), + supported_operations: btreeset! { SeaographqlFilterOperation::Equals }, + }; +} -pub fn subscriber_id_condition_function( +pub fn generate_subscriber_id_condition_function( _context: &BuilderContext, column: &T::Column, ) -> FnFilterCondition @@ -20,9 +24,7 @@ where { let column = *column; Box::new(move |mut condition, filter| { - let subscriber_id_filter_info = SUBSCRIBER_ID_FILTER_INFO.get().unwrap(); - let operations = &subscriber_id_filter_info.supported_operations; - for operation in operations { + for operation in &SUBSCRIBER_ID_FILTER_INFO.supported_operations { match operation { SeaographqlFilterOperation::Equals => { if let Some(value) = filter.get("eq") { diff --git a/apps/recorder/src/graphql/infra/guard.rs b/apps/recorder/src/graphql/domains/subscribers/guard.rs similarity index 100% rename from apps/recorder/src/graphql/infra/guard.rs rename to apps/recorder/src/graphql/domains/subscribers/guard.rs diff --git a/apps/recorder/src/graphql/domains/subscribers/mod.rs b/apps/recorder/src/graphql/domains/subscribers/mod.rs new file mode 100644 index 0000000..0c9a18f --- /dev/null +++ b/apps/recorder/src/graphql/domains/subscribers/mod.rs @@ -0,0 +1,94 @@ +use sea_orm::{EntityTrait, Iterable}; +use seaography::{Builder as SeaographyBuilder, BuilderContext, FilterType, FilterTypesMapHelper}; + +mod filter; +mod guard; +mod transformer; + +use filter::{SUBSCRIBER_ID_FILTER_INFO, generate_subscriber_id_condition_function}; +use guard::{guard_entity_with_subscriber_id, guard_field_with_subscriber_id}; +use transformer::{ + generate_subscriber_id_filter_condition_transformer, + generate_subscriber_id_mutation_input_object_transformer, +}; + +use crate::{ + graphql::infra::util::{get_entity_column_key, get_entity_key}, + models::subscribers, +}; + +pub fn restrict_subscriber_for_entity(context: &mut BuilderContext, column: &T::Column) +where + T: EntityTrait, + ::Model: Sync, +{ + let entity_key = get_entity_key::(context); + let entity_column_key = get_entity_column_key::(context, column); + context.guards.entity_guards.insert( + entity_key.clone(), + guard_entity_with_subscriber_id::(context, column), + ); + context.guards.field_guards.insert( + entity_column_key.clone(), + guard_field_with_subscriber_id::(context, column), + ); + context.filter_types.overwrites.insert( + entity_column_key.clone(), + Some(FilterType::Custom( + SUBSCRIBER_ID_FILTER_INFO.type_name.clone(), + )), + ); + context.filter_types.condition_functions.insert( + entity_column_key.clone(), + generate_subscriber_id_condition_function::(context, column), + ); + context.transformers.filter_conditions_transformers.insert( + entity_key.clone(), + generate_subscriber_id_filter_condition_transformer::(context, column), + ); + context + .transformers + .mutation_input_object_transformers + .insert( + entity_key, + generate_subscriber_id_mutation_input_object_transformer::(context, column), + ); + context + .entity_input + .insert_skips + .push(entity_column_key.clone()); + context.entity_input.update_skips.push(entity_column_key); +} + +pub fn register_subscribers_to_schema_context(context: &mut BuilderContext) { + for column in subscribers::Column::iter() { + if !matches!(column, subscribers::Column::Id) { + let key = get_entity_column_key::(context, &column); + context.filter_types.overwrites.insert(key, None); + } + } +} + +pub fn register_subscribers_to_schema_builder(mut builder: SeaographyBuilder) -> SeaographyBuilder { + { + let filter_types_map_helper = FilterTypesMapHelper { + context: builder.context, + }; + + builder.schema = builder + .schema + .register(filter_types_map_helper.generate_filter_input(&SUBSCRIBER_ID_FILTER_INFO)); + } + + { + builder.register_entity::( + ::iter() + .map(|rel| seaography::RelationBuilder::get_relation(&rel, builder.context)) + .collect(), + ); + builder = builder.register_entity_dataloader_one_to_one(subscribers::Entity, tokio::spawn); + builder = builder.register_entity_dataloader_one_to_many(subscribers::Entity, tokio::spawn); + } + + builder +} diff --git a/apps/recorder/src/graphql/domains/subscribers/transformer.rs b/apps/recorder/src/graphql/domains/subscribers/transformer.rs new file mode 100644 index 0000000..1c25ff2 --- /dev/null +++ b/apps/recorder/src/graphql/domains/subscribers/transformer.rs @@ -0,0 +1,85 @@ +use std::{collections::BTreeMap, sync::Arc}; + +use async_graphql::dynamic::ResolverContext; +use sea_orm::{ColumnTrait, Condition, EntityTrait, Value as SeaValue}; +use seaography::{BuilderContext, FnFilterConditionsTransformer, FnMutationInputObjectTransformer}; + +use crate::{ + auth::AuthUserInfo, + graphql::infra::util::{get_column_key, get_entity_key}, +}; + +pub fn generate_subscriber_id_filter_condition_transformer( + _context: &BuilderContext, + column: &T::Column, +) -> FnFilterConditionsTransformer +where + T: EntityTrait, + ::Model: Sync, +{ + let column = *column; + Box::new( + move |context: &ResolverContext, condition: Condition| -> Condition { + match context.ctx.data::() { + Ok(user_info) => { + let subscriber_id = user_info.subscriber_auth.subscriber_id; + condition.add(column.eq(subscriber_id)) + } + Err(err) => unreachable!("auth user info must be guarded: {:?}", err), + } + }, + ) +} + +pub fn generate_subscriber_id_mutation_input_object_transformer( + context: &BuilderContext, + column: &T::Column, +) -> FnMutationInputObjectTransformer +where + T: EntityTrait, + ::Model: Sync, +{ + let entity_key = get_entity_key::(context); + let entity_name = context.entity_query_field.type_name.as_ref()(&entity_key); + let column_key = get_column_key::(context, column); + let column_name = Arc::new(context.entity_object.column_name.as_ref()( + &entity_key, + &column_key, + )); + let entity_create_one_mutation_field_name = Arc::new(format!( + "{}{}", + entity_name, context.entity_create_one_mutation.mutation_suffix + )); + let entity_create_batch_mutation_field_name = Arc::new(format!( + "{}{}", + entity_name, + context.entity_create_batch_mutation.mutation_suffix.clone() + )); + Box::new( + move |context: &ResolverContext, + mut input: BTreeMap| + -> BTreeMap { + let field_name = context.field().name(); + if field_name == entity_create_one_mutation_field_name.as_str() + || field_name == entity_create_batch_mutation_field_name.as_str() + { + match context.ctx.data::() { + Ok(user_info) => { + let subscriber_id = user_info.subscriber_auth.subscriber_id; + let value = input.get_mut(column_name.as_str()); + if value.is_none() { + input.insert( + column_name.as_str().to_string(), + SeaValue::Int(Some(subscriber_id)), + ); + } + input + } + Err(err) => unreachable!("auth user info must be guarded: {:?}", err), + } + } else { + input + } + }, + ) +} diff --git a/apps/recorder/src/graphql/views/subscription.rs b/apps/recorder/src/graphql/domains/subscriptions.rs similarity index 98% rename from apps/recorder/src/graphql/views/subscription.rs rename to apps/recorder/src/graphql/domains/subscriptions.rs index 36ab4a2..25b7fcf 100644 --- a/apps/recorder/src/graphql/views/subscription.rs +++ b/apps/recorder/src/graphql/domains/subscriptions.rs @@ -66,7 +66,9 @@ impl SyncOneSubscriptionInfo { } } -pub fn register_subscriptions_to_schema(mut builder: SeaographyBuilder) -> SeaographyBuilder { +pub fn register_subscriptions_to_schema_builder( + mut builder: SeaographyBuilder, +) -> SeaographyBuilder { builder.schema = builder .schema .register(SyncOneSubscriptionFilterInput::generate_input_object()); diff --git a/apps/recorder/src/graphql/infra/filter.rs b/apps/recorder/src/graphql/infra/filter.rs new file mode 100644 index 0000000..7ea9e06 --- /dev/null +++ b/apps/recorder/src/graphql/infra/filter.rs @@ -0,0 +1,6 @@ +use async_graphql::dynamic::ObjectAccessor; +use sea_orm::Condition; +use seaography::SeaResult; + +pub type FnFilterCondition = + Box SeaResult + Send + Sync>; diff --git a/apps/recorder/src/graphql/infra/filter/mod.rs b/apps/recorder/src/graphql/infra/filter/mod.rs deleted file mode 100644 index 1f64d66..0000000 --- a/apps/recorder/src/graphql/infra/filter/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -mod json; -mod subscriber; - -use async_graphql::dynamic::TypeRef; -pub use json::{ - JSONB_FILTER_NAME, jsonb_filter_condition_function, - register_jsonb_input_filter_to_dynamic_schema, -}; -use maplit::btreeset; -use seaography::{FilterInfo, FilterOperation as SeaographqlFilterOperation}; -pub use subscriber::{SUBSCRIBER_ID_FILTER_INFO, subscriber_id_condition_function}; - -pub fn init_custom_filter_info() { - SUBSCRIBER_ID_FILTER_INFO.get_or_init(|| FilterInfo { - type_name: String::from("SubscriberIdFilterInput"), - base_type: TypeRef::INT.into(), - supported_operations: btreeset! { SeaographqlFilterOperation::Equals }, - }); -} diff --git a/apps/recorder/src/graphql/infra/filter/json.rs b/apps/recorder/src/graphql/infra/json.rs similarity index 98% rename from apps/recorder/src/graphql/infra/filter/json.rs rename to apps/recorder/src/graphql/infra/json.rs index e0e5835..bebe97d 100644 --- a/apps/recorder/src/graphql/infra/filter/json.rs +++ b/apps/recorder/src/graphql/infra/json.rs @@ -1,6 +1,6 @@ use async_graphql::{ Error as GraphqlError, - dynamic::{Scalar, SchemaBuilder, SchemaError}, + dynamic::{Scalar, SchemaError}, to_value, }; use itertools::Itertools; @@ -9,10 +9,13 @@ use sea_orm::{ Condition, EntityTrait, sea_query::{ArrayType, Expr, ExprTrait, IntoLikeExpr, SimpleExpr, Value as DbValue}, }; -use seaography::{BuilderContext, SeaographyError}; +use seaography::{Builder as SeaographyBuilder, BuilderContext, FilterType, SeaographyError}; use serde_json::Value as JsonValue; -use crate::{errors::RecorderResult, graphql::infra::filter::subscriber::FnFilterCondition}; +use crate::{ + errors::RecorderResult, + graphql::infra::{filter::FnFilterCondition, util::get_entity_column_key}, +}; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy)] pub enum JsonbFilterOperation { @@ -892,7 +895,7 @@ where pub const JSONB_FILTER_NAME: &str = "JsonbFilterInput"; -pub fn jsonb_filter_condition_function( +pub fn generate_jsonb_filter_condition_function( _context: &BuilderContext, column: &T::Column, ) -> FnFilterCondition @@ -917,11 +920,24 @@ where }) } -pub fn register_jsonb_input_filter_to_dynamic_schema( - schema_builder: SchemaBuilder, -) -> SchemaBuilder { +pub fn register_jsonb_input_filter_to_schema_builder( + mut builder: SeaographyBuilder, +) -> SeaographyBuilder { let json_filter_input_type = Scalar::new(JSONB_FILTER_NAME); - schema_builder.register(json_filter_input_type) + builder.schema = builder.schema.register(json_filter_input_type); + builder +} + +pub fn restrict_jsonb_filter_input_for_entity(context: &mut BuilderContext, column: &T::Column) +where + T: EntityTrait, + ::Model: Sync, +{ + let entity_column_key = get_entity_column_key::(context, column); + context.filter_types.overwrites.insert( + entity_column_key.clone(), + Some(FilterType::Custom(JSONB_FILTER_NAME.to_string())), + ); } #[cfg(test)] diff --git a/apps/recorder/src/graphql/infra/mod.rs b/apps/recorder/src/graphql/infra/mod.rs index 8d9170f..921c535 100644 --- a/apps/recorder/src/graphql/infra/mod.rs +++ b/apps/recorder/src/graphql/infra/mod.rs @@ -1,6 +1,3 @@ -pub mod filter; -pub mod guard; -pub mod order; -pub mod pagination; -pub mod transformer; +pub mod json; pub mod util; +pub mod filter; diff --git a/apps/recorder/src/graphql/infra/order.rs b/apps/recorder/src/graphql/infra/order.rs deleted file mode 100644 index e69de29..0000000 diff --git a/apps/recorder/src/graphql/infra/pagination.rs b/apps/recorder/src/graphql/infra/pagination.rs deleted file mode 100644 index c590f8e..0000000 --- a/apps/recorder/src/graphql/infra/pagination.rs +++ /dev/null @@ -1,36 +0,0 @@ -use async_graphql::{InputObject, SimpleObject}; - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, InputObject)] -pub struct CursorInput { - pub cursor: Option, - pub limit: u64, -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, InputObject)] -pub struct PageInput { - pub page: u64, - pub limit: u64, -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, InputObject)] -pub struct OffsetInput { - pub offset: u64, - pub limit: u64, -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, InputObject)] -pub struct PaginationInput { - pub cursor: Option, - pub page: Option, - pub offset: Option, -} - -pub type PageInfo = async_graphql::connection::PageInfo; - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, SimpleObject)] -pub struct PaginationInfo { - pub pages: u64, - pub current: u64, - pub offset: u64, - pub total: u64, -} diff --git a/apps/recorder/src/graphql/infra/transformer.rs b/apps/recorder/src/graphql/infra/transformer.rs deleted file mode 100644 index 619bba3..0000000 --- a/apps/recorder/src/graphql/infra/transformer.rs +++ /dev/null @@ -1,173 +0,0 @@ -use std::{collections::BTreeMap, sync::Arc}; - -use async_graphql::dynamic::{ResolverContext, ValueAccessor}; -use sea_orm::{ColumnTrait, Condition, EntityTrait, Value as SeaValue}; -use seaography::{ - BuilderContext, FnFilterConditionsTransformer, FnMutationInputObjectTransformer, SeaResult, -}; - -use super::util::{get_column_key, get_entity_key}; -use crate::{app::AppContextTrait, auth::AuthUserInfo, models::credential_3rd}; - -pub fn build_filter_condition_transformer( - _context: &BuilderContext, - column: &T::Column, -) -> FnFilterConditionsTransformer -where - T: EntityTrait, - ::Model: Sync, -{ - let column = *column; - Box::new( - move |context: &ResolverContext, condition: Condition| -> Condition { - match context.ctx.data::() { - Ok(user_info) => { - let subscriber_id = user_info.subscriber_auth.subscriber_id; - condition.add(column.eq(subscriber_id)) - } - Err(err) => unreachable!("auth user info must be guarded: {:?}", err), - } - }, - ) -} - -pub fn build_mutation_input_object_transformer( - context: &BuilderContext, - column: &T::Column, -) -> FnMutationInputObjectTransformer -where - T: EntityTrait, - ::Model: Sync, -{ - let entity_key = get_entity_key::(context); - let entity_name = context.entity_query_field.type_name.as_ref()(&entity_key); - let column_key = get_column_key::(context, column); - let column_name = Arc::new(context.entity_object.column_name.as_ref()( - &entity_key, - &column_key, - )); - let entity_create_one_mutation_field_name = Arc::new(format!( - "{}{}", - entity_name, context.entity_create_one_mutation.mutation_suffix - )); - let entity_create_batch_mutation_field_name = Arc::new(format!( - "{}{}", - entity_name, - context.entity_create_batch_mutation.mutation_suffix.clone() - )); - Box::new( - move |context: &ResolverContext, - mut input: BTreeMap| - -> BTreeMap { - let field_name = context.field().name(); - if field_name == entity_create_one_mutation_field_name.as_str() - || field_name == entity_create_batch_mutation_field_name.as_str() - { - match context.ctx.data::() { - Ok(user_info) => { - let subscriber_id = user_info.subscriber_auth.subscriber_id; - let value = input.get_mut(column_name.as_str()); - if value.is_none() { - input.insert( - column_name.as_str().to_string(), - SeaValue::Int(Some(subscriber_id)), - ); - } - input - } - Err(err) => unreachable!("auth user info must be guarded: {:?}", err), - } - } else { - input - } - }, - ) -} - -fn add_crypto_column_input_conversion( - context: &mut BuilderContext, - ctx: Arc, - column: &T::Column, -) where - T: EntityTrait, - ::Model: Sync, -{ - let entity_key = get_entity_key::(context); - let column_name = get_column_key::(context, column); - let entity_name = context.entity_object.type_name.as_ref()(&entity_key); - let column_name = context.entity_object.column_name.as_ref()(&entity_key, &column_name); - - context.types.input_conversions.insert( - format!("{entity_name}.{column_name}"), - Box::new(move |value: &ValueAccessor| -> SeaResult { - let source = value.string()?; - let encrypted = ctx.crypto().encrypt_string(source.into())?; - Ok(encrypted.into()) - }), - ); -} - -fn add_crypto_column_output_conversion( - context: &mut BuilderContext, - ctx: Arc, - column: &T::Column, -) where - T: EntityTrait, - ::Model: Sync, -{ - let entity_key = get_entity_key::(context); - let column_name = get_column_key::(context, column); - let entity_name = context.entity_object.type_name.as_ref()(&entity_key); - let column_name = context.entity_object.column_name.as_ref()(&entity_key, &column_name); - - context.types.output_conversions.insert( - format!("{entity_name}.{column_name}"), - Box::new( - move |value: &sea_orm::Value| -> SeaResult { - if let SeaValue::String(s) = value { - if let Some(s) = s { - let decrypted = ctx.crypto().decrypt_string(s)?; - Ok(async_graphql::Value::String(decrypted)) - } else { - Ok(async_graphql::Value::Null) - } - } else { - Err(async_graphql::Error::new("crypto column must be string column").into()) - } - }, - ), - ); -} - -pub fn add_crypto_transformers(context: &mut BuilderContext, ctx: Arc) { - add_crypto_column_input_conversion::( - context, - ctx.clone(), - &credential_3rd::Column::Cookies, - ); - add_crypto_column_input_conversion::( - context, - ctx.clone(), - &credential_3rd::Column::Username, - ); - add_crypto_column_input_conversion::( - context, - ctx.clone(), - &credential_3rd::Column::Password, - ); - add_crypto_column_output_conversion::( - context, - ctx.clone(), - &credential_3rd::Column::Cookies, - ); - add_crypto_column_output_conversion::( - context, - ctx.clone(), - &credential_3rd::Column::Username, - ); - add_crypto_column_output_conversion::( - context, - ctx, - &credential_3rd::Column::Password, - ); -} diff --git a/apps/recorder/src/graphql/mod.rs b/apps/recorder/src/graphql/mod.rs index c27728f..a84db2a 100644 --- a/apps/recorder/src/graphql/mod.rs +++ b/apps/recorder/src/graphql/mod.rs @@ -1,8 +1,8 @@ pub mod config; +pub mod domains; pub mod infra; mod schema; pub mod service; -pub mod views; pub use config::GraphQLConfig; pub use schema::build_schema; diff --git a/apps/recorder/src/graphql/schema.rs b/apps/recorder/src/graphql/schema.rs index f494c22..9e5a6c8 100644 --- a/apps/recorder/src/graphql/schema.rs +++ b/apps/recorder/src/graphql/schema.rs @@ -2,97 +2,30 @@ use std::sync::Arc; use async_graphql::dynamic::*; use once_cell::sync::OnceCell; -use sea_orm::{EntityTrait, Iterable}; -use seaography::{Builder, BuilderContext, FilterType, FilterTypesMapHelper}; +use seaography::{Builder, BuilderContext}; use crate::{ app::AppContextTrait, graphql::{ - infra::{ - filter::{ - JSONB_FILTER_NAME, SUBSCRIBER_ID_FILTER_INFO, init_custom_filter_info, - register_jsonb_input_filter_to_dynamic_schema, subscriber_id_condition_function, + domains::{ + credential_3rd::register_credential3rd_to_schema_builder, + crypto::register_crypto_to_schema_context, + subscriber_tasks::{ + register_subscriber_tasks_to_schema_builder, + register_subscriber_tasks_to_schema_context, }, - guard::{guard_entity_with_subscriber_id, guard_field_with_subscriber_id}, - transformer::{ - add_crypto_transformers, build_filter_condition_transformer, - build_mutation_input_object_transformer, + subscribers::{ + register_subscribers_to_schema_builder, register_subscribers_to_schema_context, + restrict_subscriber_for_entity, }, - util::{get_entity_column_key, get_entity_key}, + subscriptions::register_subscriptions_to_schema_builder, }, - views::{register_credential3rd_to_schema, register_subscriptions_to_schema}, + infra::json::register_jsonb_input_filter_to_schema_builder, }, }; pub static CONTEXT: OnceCell = OnceCell::new(); -fn restrict_filter_input_for_entity( - context: &mut BuilderContext, - column: &T::Column, - filter_type: Option, -) where - T: EntityTrait, - ::Model: Sync, -{ - let key = get_entity_column_key::(context, column); - context.filter_types.overwrites.insert(key, filter_type); -} - -fn restrict_jsonb_filter_input_for_entity(context: &mut BuilderContext, column: &T::Column) -where - T: EntityTrait, - ::Model: Sync, -{ - let entity_column_key = get_entity_column_key::(context, column); - context.filter_types.overwrites.insert( - entity_column_key.clone(), - Some(FilterType::Custom(JSONB_FILTER_NAME.to_string())), - ); -} - -fn restrict_subscriber_for_entity(context: &mut BuilderContext, column: &T::Column) -where - T: EntityTrait, - ::Model: Sync, -{ - let entity_key = get_entity_key::(context); - let entity_column_key = get_entity_column_key::(context, column); - context.guards.entity_guards.insert( - entity_key.clone(), - guard_entity_with_subscriber_id::(context, column), - ); - context.guards.field_guards.insert( - entity_column_key.clone(), - guard_field_with_subscriber_id::(context, column), - ); - context.filter_types.overwrites.insert( - entity_column_key.clone(), - Some(FilterType::Custom( - SUBSCRIBER_ID_FILTER_INFO.get().unwrap().type_name.clone(), - )), - ); - context.filter_types.condition_functions.insert( - entity_column_key.clone(), - subscriber_id_condition_function::(context, column), - ); - context.transformers.filter_conditions_transformers.insert( - entity_key.clone(), - build_filter_condition_transformer::(context, column), - ); - context - .transformers - .mutation_input_object_transformers - .insert( - entity_key, - build_mutation_input_object_transformer::(context, column), - ); - context - .entity_input - .insert_skips - .push(entity_column_key.clone()); - context.entity_input.update_skips.push(entity_column_key); -} - pub fn build_schema( app_ctx: Arc, depth: Option, @@ -101,70 +34,58 @@ pub fn build_schema( use crate::models::*; let database = app_ctx.db().as_ref().clone(); - init_custom_filter_info(); let context = CONTEXT.get_or_init(|| { let mut context = BuilderContext::default(); - context.pagination_input.type_name = "PaginationInput".to_string(); - context.pagination_info_object.type_name = "PaginationInfo".to_string(); - context.cursor_input.type_name = "CursorInput".to_string(); - context.offset_input.type_name = "OffsetInput".to_string(); - context.page_input.type_name = "PageInput".to_string(); - context.page_info_object.type_name = "PageInfo".to_string(); + { + // domains + register_subscribers_to_schema_context(&mut context); - restrict_subscriber_for_entity::( - &mut context, - &bangumi::Column::SubscriberId, - ); - restrict_subscriber_for_entity::( - &mut context, - &downloaders::Column::SubscriberId, - ); - restrict_subscriber_for_entity::( - &mut context, - &downloads::Column::SubscriberId, - ); - restrict_subscriber_for_entity::( - &mut context, - &episodes::Column::SubscriberId, - ); - restrict_subscriber_for_entity::( - &mut context, - &subscriptions::Column::SubscriberId, - ); - restrict_subscriber_for_entity::( - &mut context, - &subscribers::Column::Id, - ); - restrict_subscriber_for_entity::( - &mut context, - &subscription_bangumi::Column::SubscriberId, - ); - restrict_subscriber_for_entity::( - &mut context, - &subscription_episode::Column::SubscriberId, - ); - restrict_subscriber_for_entity::( - &mut context, - &subscriber_tasks::Column::SubscriberId, - ); - restrict_subscriber_for_entity::( - &mut context, - &credential_3rd::Column::SubscriberId, - ); - restrict_jsonb_filter_input_for_entity::( - &mut context, - &subscriber_tasks::Column::Job, - ); - add_crypto_transformers(&mut context, app_ctx.clone()); - for column in subscribers::Column::iter() { - if !matches!(column, subscribers::Column::Id) { - restrict_filter_input_for_entity::( + { + restrict_subscriber_for_entity::( &mut context, - &column, - None, + &bangumi::Column::SubscriberId, + ); + restrict_subscriber_for_entity::( + &mut context, + &downloaders::Column::SubscriberId, + ); + restrict_subscriber_for_entity::( + &mut context, + &downloads::Column::SubscriberId, + ); + restrict_subscriber_for_entity::( + &mut context, + &episodes::Column::SubscriberId, + ); + restrict_subscriber_for_entity::( + &mut context, + &subscriptions::Column::SubscriberId, + ); + restrict_subscriber_for_entity::( + &mut context, + &subscribers::Column::Id, + ); + restrict_subscriber_for_entity::( + &mut context, + &subscription_bangumi::Column::SubscriberId, + ); + restrict_subscriber_for_entity::( + &mut context, + &subscription_episode::Column::SubscriberId, + ); + restrict_subscriber_for_entity::( + &mut context, + &subscriber_tasks::Column::SubscriberId, + ); + restrict_subscriber_for_entity::( + &mut context, + &credential_3rd::Column::SubscriberId, ); } + + register_crypto_to_schema_context(&mut context, app_ctx.clone()); + register_subscriber_tasks_to_schema_context(&mut context); } context }); @@ -172,50 +93,39 @@ pub fn build_schema( let mut builder = Builder::new(context, database.clone()); { - let filter_types_map_helper = FilterTypesMapHelper { context }; + // infra + builder = register_jsonb_input_filter_to_schema_builder(builder); + } + { + // domains + builder = register_subscribers_to_schema_builder(builder); - builder.schema = builder.schema.register( - filter_types_map_helper.generate_filter_input(SUBSCRIBER_ID_FILTER_INFO.get().unwrap()), + seaography::register_entities!( + builder, + [ + bangumi, + downloaders, + downloads, + episodes, + subscription_bangumi, + subscription_episode, + subscriptions, + subscriber_tasks, + credential_3rd + ] ); - builder.schema = register_jsonb_input_filter_to_dynamic_schema(builder.schema); - } - { - builder.register_entity::( - ::iter() - .map(|rel| seaography::RelationBuilder::get_relation(&rel, builder.context)) - .collect(), - ); - builder = builder.register_entity_dataloader_one_to_one(subscribers::Entity, tokio::spawn); - builder = builder.register_entity_dataloader_one_to_many(subscribers::Entity, tokio::spawn); - } + { + builder.register_enumeration::(); + builder.register_enumeration::(); + builder.register_enumeration::(); + builder.register_enumeration::(); + builder.register_enumeration::(); + } - seaography::register_entities!( - builder, - [ - bangumi, - downloaders, - downloads, - episodes, - subscription_bangumi, - subscription_episode, - subscriptions, - subscriber_tasks, - credential_3rd - ] - ); - - { - builder.register_enumeration::(); - builder.register_enumeration::(); - builder.register_enumeration::(); - builder.register_enumeration::(); - builder.register_enumeration::(); - } - - { - builder = register_subscriptions_to_schema(builder); - builder = register_credential3rd_to_schema(builder); + builder = register_subscriptions_to_schema_builder(builder); + builder = register_credential3rd_to_schema_builder(builder); + builder = register_subscriber_tasks_to_schema_builder(builder); } let schema = builder.schema_builder(); diff --git a/apps/recorder/src/graphql/views/mod.rs b/apps/recorder/src/graphql/views/mod.rs deleted file mode 100644 index 07a4c82..0000000 --- a/apps/recorder/src/graphql/views/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod credential_3rd; -mod subscription; - -pub use credential_3rd::register_credential3rd_to_schema; -pub use subscription::register_subscriptions_to_schema; diff --git a/apps/recorder/src/models/subscriber_tasks.rs b/apps/recorder/src/models/subscriber_tasks.rs index 8b7de33..bf20822 100644 --- a/apps/recorder/src/models/subscriber_tasks.rs +++ b/apps/recorder/src/models/subscriber_tasks.rs @@ -1,6 +1,6 @@ use sea_orm::entity::prelude::*; -use crate::task::SubscriberTask; +pub use crate::task::SubscriberTask; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "subscriber_tasks")] diff --git a/apps/webui/src/app.css b/apps/webui/src/app.css index cafedda..b34cddb 100644 --- a/apps/webui/src/app.css +++ b/apps/webui/src/app.css @@ -137,6 +137,7 @@ } body { @apply bg-background text-foreground; + pointer-events: auto !important; } button:not(:disabled), diff --git a/apps/webui/src/app/config/nav.ts b/apps/webui/src/app/config/nav.ts new file mode 100644 index 0000000..2935de4 --- /dev/null +++ b/apps/webui/src/app/config/nav.ts @@ -0,0 +1,136 @@ +import type { NavMainGroup } from '@/infra/routes/nav'; +import { + BookOpen, + Folders, + KeyRound, + ListTodo, + Settings2, + SquareTerminal, + Telescope, + Tv, +} from 'lucide-react'; + +export const AppNavMainData: NavMainGroup[] = [ + { + group: 'Dashboard', + items: [ + { + title: 'Explore', + icon: Telescope, + link: { + to: '/explore', + }, + }, + { + title: 'Subscriptions', + link: { + to: '/subscriptions/manage', + }, + icon: Folders, + children: [ + { + title: 'Manage', + link: { + to: '/subscriptions/manage', + }, + }, + { + title: 'Create', + link: { + to: '/subscriptions/create', + }, + }, + ], + }, + { + title: 'Bangumi', + icon: Tv, + children: [ + { + title: 'Manage', + link: { + to: '/bangumi/recorder', + }, + }, + { + title: 'Feed', + link: { + to: '/bangumi/feed', + }, + }, + ], + }, + { + title: 'Tasks', + icon: ListTodo, + children: [ + { + title: 'Manage', + link: { + to: '/task/manage', + }, + }, + ], + }, + { + title: 'Credential 3rd', + link: { + to: '/credential3rd/manage', + }, + icon: KeyRound, + children: [ + { + title: 'Manage', + link: { + to: '/credential3rd/manage', + }, + }, + { + title: 'Create', + link: { + to: '/credential3rd/create', + }, + }, + ], + }, + { + title: 'Playground', + icon: SquareTerminal, + link: { + to: '/playground', + }, + children: [ + { + title: 'GraphQL Api', + link: { + to: '/playground/graphql-api', + }, + }, + ], + }, + { + title: 'Documentation', + link: { + href: 'https://github.com/dumtruck/konobangu/wiki', + target: '_blank', + }, + icon: BookOpen, + }, + { + title: 'Settings', + link: { + to: '/settings', + }, + icon: Settings2, + children: [ + { + title: 'Downloader', + link: { + to: '/settings/downloader', + }, + }, + ], + }, + ], + }, +]; diff --git a/apps/webui/src/components/layout/app-sidebar.tsx b/apps/webui/src/components/layout/app-sidebar.tsx index 4d23a44..4e59c01 100644 --- a/apps/webui/src/components/layout/app-sidebar.tsx +++ b/apps/webui/src/components/layout/app-sidebar.tsx @@ -1,3 +1,4 @@ +import { AppNavMainData } from '@/app/config/nav'; import { Sidebar, SidebarContent, @@ -5,7 +6,6 @@ import { SidebarHeader, SidebarRail, } from '@/components/ui/sidebar'; -import { AppNavMainData } from '@/infra/routes/nav'; import type { ComponentPropsWithoutRef } from 'react'; import { AppIcon } from './app-icon'; import { NavMain } from './nav-main'; diff --git a/apps/webui/src/components/layout/nav-main.tsx b/apps/webui/src/components/layout/nav-main.tsx index 475de0b..6cd7032 100644 --- a/apps/webui/src/components/layout/nav-main.tsx +++ b/apps/webui/src/components/layout/nav-main.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ChevronRight, type LucideIcon } from 'lucide-react'; +import { ChevronRight } from 'lucide-react'; import { Collapsible, @@ -26,20 +26,9 @@ import { SidebarMenuSubItem, useSidebar, } from '@/components/ui/sidebar'; +import type { NavMainGroup, NavMainItem } from '@/infra/routes/nav'; import { useMatches } from '@tanstack/react-router'; -export interface NavMainItem { - link?: ProLinkProps; - title: string; - icon?: LucideIcon; - children?: { title: string; link: ProLinkProps }[]; -} - -export interface NavMainGroup { - group: string; - items: NavMainItem[]; -} - export function NavMain({ groups, }: { diff --git a/apps/webui/src/components/ui/data-table-pagination.tsx b/apps/webui/src/components/ui/data-table-pagination.tsx index 9659b46..9ebd829 100644 --- a/apps/webui/src/components/ui/data-table-pagination.tsx +++ b/apps/webui/src/components/ui/data-table-pagination.tsx @@ -14,7 +14,6 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { useMemo } from "react"; interface DataTablePaginationProps { table: Table; diff --git a/apps/webui/src/domains/recorder/schema/subscriptions.ts b/apps/webui/src/domains/recorder/schema/subscriptions.ts index 19013e8..b36808b 100644 --- a/apps/webui/src/domains/recorder/schema/subscriptions.ts +++ b/apps/webui/src/domains/recorder/schema/subscriptions.ts @@ -8,7 +8,6 @@ import { type } from 'arktype'; import { MikanSubscriptionSeasonSourceUrlSchema, extractMikanSubscriptionBangumiSourceUrl, - extractMikanSubscriptionSeasonSourceUrl, extractMikanSubscriptionSubscriberSourceUrl, } from './mikan'; diff --git a/apps/webui/src/domains/recorder/schema/tasks.ts b/apps/webui/src/domains/recorder/schema/tasks.ts new file mode 100644 index 0000000..376cf0b --- /dev/null +++ b/apps/webui/src/domains/recorder/schema/tasks.ts @@ -0,0 +1,28 @@ +import { gql } from '@apollo/client'; + +export const GET_TASKS = gql` + query GetTasks($filters: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) { + subscriberTasks( + pagination: $pagination + filters: $filters + orderBy: $orderBy + ) { + nodes { + id, + status, + attempts, + maxAttempts, + runAt, + lastError, + lockAt, + lockBy, + doneAt, + priority + } + paginationInfo { + total + pages + } + } + } +`; diff --git a/apps/webui/src/infra/graphql/gql/gql.ts b/apps/webui/src/infra/graphql/gql/gql.ts index d9c6a9b..2ee1e55 100644 --- a/apps/webui/src/infra/graphql/gql/gql.ts +++ b/apps/webui/src/infra/graphql/gql/gql.ts @@ -28,6 +28,7 @@ type Documents = { "\n mutation SyncSubscriptionFeedsIncremental($id: Int!) {\n subscriptionSyncOneFeedsIncremental(filter: { id: $id }) {\n taskId\n }\n }\n": typeof types.SyncSubscriptionFeedsIncrementalDocument, "\n mutation SyncSubscriptionFeedsFull($id: Int!) {\n subscriptionSyncOneFeedsFull(filter: { id: $id }) {\n taskId\n }\n }\n": typeof types.SyncSubscriptionFeedsFullDocument, "\n mutation SyncSubscriptionSources($id: Int!) {\n subscriptionSyncOneSources(filter: { id: $id }) {\n taskId\n }\n }\n": typeof types.SyncSubscriptionSourcesDocument, + "\n query GetTasks($filters: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetTasksDocument, }; const documents: Documents = { "\n query GetCredential3rd($filters: Credential3rdFilterInput!, $orderBy: Credential3rdOrderInput, $pagination: PaginationInput) {\n credential3rd(filters: $filters, orderBy: $orderBy, pagination: $pagination) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetCredential3rdDocument, @@ -44,6 +45,7 @@ const documents: Documents = { "\n mutation SyncSubscriptionFeedsIncremental($id: Int!) {\n subscriptionSyncOneFeedsIncremental(filter: { id: $id }) {\n taskId\n }\n }\n": types.SyncSubscriptionFeedsIncrementalDocument, "\n mutation SyncSubscriptionFeedsFull($id: Int!) {\n subscriptionSyncOneFeedsFull(filter: { id: $id }) {\n taskId\n }\n }\n": types.SyncSubscriptionFeedsFullDocument, "\n mutation SyncSubscriptionSources($id: Int!) {\n subscriptionSyncOneSources(filter: { id: $id }) {\n taskId\n }\n }\n": types.SyncSubscriptionSourcesDocument, + "\n query GetTasks($filters: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetTasksDocument, }; /** @@ -116,6 +118,10 @@ export function gql(source: "\n mutation SyncSubscriptionFeedsFull($id: Int!) { * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function gql(source: "\n mutation SyncSubscriptionSources($id: Int!) {\n subscriptionSyncOneSources(filter: { id: $id }) {\n taskId\n }\n }\n"): (typeof documents)["\n mutation SyncSubscriptionSources($id: Int!) {\n subscriptionSyncOneSources(filter: { id: $id }) {\n taskId\n }\n }\n"]; +/** + * The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function gql(source: "\n query GetTasks($filters: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"): (typeof documents)["\n query GetTasks($filters: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"]; export function gql(source: string) { return (documents as any)[source] ?? {}; diff --git a/apps/webui/src/infra/graphql/gql/graphql.ts b/apps/webui/src/infra/graphql/gql/graphql.ts index f8365e3..20ef0fc 100644 --- a/apps/webui/src/infra/graphql/gql/graphql.ts +++ b/apps/webui/src/infra/graphql/gql/graphql.ts @@ -1759,6 +1759,15 @@ export type SyncSubscriptionSourcesMutationVariables = Exact<{ export type SyncSubscriptionSourcesMutation = { __typename?: 'Mutation', subscriptionSyncOneSources: { __typename?: 'SyncOneSubscriptionInfo', taskId: string } }; +export type GetTasksQueryVariables = Exact<{ + filters: SubscriberTasksFilterInput; + orderBy: SubscriberTasksOrderInput; + pagination: PaginationInput; +}>; + + +export type GetTasksQuery = { __typename?: 'Query', subscriberTasks: { __typename?: 'SubscriberTasksConnection', nodes: Array<{ __typename?: 'SubscriberTasks', id: string, status: string, attempts: number, maxAttempts: number, runAt: string, lastError?: string | null, lockAt?: string | null, lockBy?: string | null, doneAt?: string | null, priority: number }>, paginationInfo?: { __typename?: 'PaginationInfo', total: number, pages: number } | null } }; + export const GetCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdFilterInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdOrderInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"paginationInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"pages"}}]}}]}}]}}]} as unknown as DocumentNode; export const InsertCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rdCreateOne"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}}]}}]} as unknown as DocumentNode; @@ -1773,4 +1782,5 @@ export const DeleteSubscriptionsDocument = {"kind":"Document","definitions":[{"k export const GetSubscriptionDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptionDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"eq"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"bangumi"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"mikanBangumiId"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"rawName"}},{"kind":"Field","name":{"kind":"Name","value":"season"}},{"kind":"Field","name":{"kind":"Name","value":"seasonRaw"}},{"kind":"Field","name":{"kind":"Name","value":"fansub"}},{"kind":"Field","name":{"kind":"Name","value":"mikanFansubId"}},{"kind":"Field","name":{"kind":"Name","value":"rssLink"}},{"kind":"Field","name":{"kind":"Name","value":"posterLink"}},{"kind":"Field","name":{"kind":"Name","value":"savePath"}},{"kind":"Field","name":{"kind":"Name","value":"homepage"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const SyncSubscriptionFeedsIncrementalDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionFeedsIncremental"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionSyncOneFeedsIncremental"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"taskId"}}]}}]}}]} as unknown as DocumentNode; export const SyncSubscriptionFeedsFullDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionFeedsFull"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionSyncOneFeedsFull"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"taskId"}}]}}]}}]} as unknown as DocumentNode; -export const SyncSubscriptionSourcesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionSources"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionSyncOneSources"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"taskId"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const SyncSubscriptionSourcesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionSources"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionSyncOneSources"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"taskId"}}]}}]}}]} as unknown as DocumentNode; +export const GetTasksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetTasks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriberTasksFilterInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriberTasksOrderInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriberTasks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"attempts"}},{"kind":"Field","name":{"kind":"Name","value":"maxAttempts"}},{"kind":"Field","name":{"kind":"Name","value":"runAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastError"}},{"kind":"Field","name":{"kind":"Name","value":"lockAt"}},{"kind":"Field","name":{"kind":"Name","value":"lockBy"}},{"kind":"Field","name":{"kind":"Name","value":"doneAt"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}}]}},{"kind":"Field","name":{"kind":"Name","value":"paginationInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"pages"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/apps/webui/src/infra/routes/nav.ts b/apps/webui/src/infra/routes/nav.ts index b2d07af..04705ca 100644 --- a/apps/webui/src/infra/routes/nav.ts +++ b/apps/webui/src/infra/routes/nav.ts @@ -1,118 +1,26 @@ +import type { ProLinkProps } from '@/components/ui/pro-link'; import { type } from 'arktype'; import { BookOpen, Folders, KeyRound, + type LucideIcon, Settings2, SquareTerminal, Telescope, } from 'lucide-react'; -export const AppNavMainData = [ - { - group: 'Dashboard', - items: [ - { - title: 'Explore', - icon: Telescope, - children: [ - { - title: 'Feed', - link: { - to: '/feed', - }, - }, - { - title: 'Explore', - link: { - to: '/explore', - }, - }, - ], - }, - { - title: 'Subscriptions', - link: { - to: '/subscriptions/manage', - }, - icon: Folders, - children: [ - { - title: 'Manage', - link: { - to: '/subscriptions/manage', - }, - }, - { - title: 'Create', - link: { - to: '/subscriptions/create', - }, - }, - ], - }, - { - title: 'Credential', - link: { - to: '/credential3rd/manage', - }, - icon: KeyRound, - children: [ - { - title: 'Manage', - link: { - to: '/credential3rd/manage', - }, - }, - { - title: 'Create', - link: { - to: '/credential3rd/create', - }, - }, - ], - }, - { - title: 'Playground', - icon: SquareTerminal, - link: { - to: '/playground', - }, - children: [ - { - title: 'GraphQL Api', - link: { - to: '/playground/graphql-api', - }, - }, - ], - }, - { - title: 'Documentation', - link: { - href: 'https://github.com/dumtruck/konobangu/wiki', - target: '_blank', - }, - icon: BookOpen, - }, - { - title: 'Settings', - link: { - to: '/settings', - }, - icon: Settings2, - children: [ - { - title: 'Downloader', - link: { - to: '/settings/downloader', - }, - }, - ], - }, - ], - }, -]; +export interface NavMainItem { + link?: ProLinkProps; + title: string; + icon?: LucideIcon; + children?: { title: string; link: ProLinkProps }[]; +} + +export interface NavMainGroup { + group: string; + items: NavMainItem[]; +} export const CreateCompleteAction = { Back: 'back', diff --git a/apps/webui/src/presentation/hooks/use-debounded-skeleton.ts b/apps/webui/src/presentation/hooks/use-debounded-skeleton.ts index 0563727..bdeb39c 100644 --- a/apps/webui/src/presentation/hooks/use-debounded-skeleton.ts +++ b/apps/webui/src/presentation/hooks/use-debounded-skeleton.ts @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { useStateRef } from './use-state-ref.ts'; +import { useStateRef } from './use-state-ref'; export interface UseDebouncedSkeletonProps { minSkeletonDuration?: number; loading?: boolean; diff --git a/apps/webui/src/presentation/routeTree.gen.ts b/apps/webui/src/presentation/routeTree.gen.ts index cd916fd..a033c3a 100644 --- a/apps/webui/src/presentation/routeTree.gen.ts +++ b/apps/webui/src/presentation/routeTree.gen.ts @@ -32,7 +32,6 @@ import { Route as AppPlaygroundGraphqlApiImport } from './routes/_app/playground import { Route as AppCredential3rdManageImport } from './routes/_app/credential3rd/manage' import { Route as AppCredential3rdCreateImport } from './routes/_app/credential3rd/create' import { Route as AppBangumiManageImport } from './routes/_app/bangumi/manage' -import { Route as AppExploreFeedImport } from './routes/_app/_explore/feed' import { Route as AppExploreExploreImport } from './routes/_app/_explore/explore' import { Route as AppTasksDetailIdImport } from './routes/_app/tasks/detail.$id' import { Route as AppSubscriptionsEditIdImport } from './routes/_app/subscriptions/edit.$id' @@ -169,12 +168,6 @@ const AppBangumiManageRoute = AppBangumiManageImport.update({ getParentRoute: () => AppBangumiRouteRoute, } as any) -const AppExploreFeedRoute = AppExploreFeedImport.update({ - id: '/_explore/feed', - path: '/feed', - getParentRoute: () => AppRouteRoute, -} as any) - const AppExploreExploreRoute = AppExploreExploreImport.update({ id: '/_explore/explore', path: '/explore', @@ -306,13 +299,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AppExploreExploreImport parentRoute: typeof AppRouteImport } - '/_app/_explore/feed': { - id: '/_app/_explore/feed' - path: '/feed' - fullPath: '/feed' - preLoaderRoute: typeof AppExploreFeedImport - parentRoute: typeof AppRouteImport - } '/_app/bangumi/manage': { id: '/_app/bangumi/manage' path: '/manage' @@ -510,7 +496,6 @@ interface AppRouteRouteChildren { AppSubscriptionsRouteRoute: typeof AppSubscriptionsRouteRouteWithChildren AppTasksRouteRoute: typeof AppTasksRouteRouteWithChildren AppExploreExploreRoute: typeof AppExploreExploreRoute - AppExploreFeedRoute: typeof AppExploreFeedRoute } const AppRouteRouteChildren: AppRouteRouteChildren = { @@ -521,7 +506,6 @@ const AppRouteRouteChildren: AppRouteRouteChildren = { AppSubscriptionsRouteRoute: AppSubscriptionsRouteRouteWithChildren, AppTasksRouteRoute: AppTasksRouteRouteWithChildren, AppExploreExploreRoute: AppExploreExploreRoute, - AppExploreFeedRoute: AppExploreFeedRoute, } const AppRouteRouteWithChildren = AppRouteRoute._addFileChildren( @@ -542,7 +526,6 @@ export interface FileRoutesByFullPath { '/auth/sign-in': typeof AuthSignInRoute '/auth/sign-up': typeof AuthSignUpRoute '/explore': typeof AppExploreExploreRoute - '/feed': typeof AppExploreFeedRoute '/bangumi/manage': typeof AppBangumiManageRoute '/credential3rd/create': typeof AppCredential3rdCreateRoute '/credential3rd/manage': typeof AppCredential3rdManageRoute @@ -573,7 +556,6 @@ export interface FileRoutesByTo { '/auth/sign-in': typeof AuthSignInRoute '/auth/sign-up': typeof AuthSignUpRoute '/explore': typeof AppExploreExploreRoute - '/feed': typeof AppExploreFeedRoute '/bangumi/manage': typeof AppBangumiManageRoute '/credential3rd/create': typeof AppCredential3rdCreateRoute '/credential3rd/manage': typeof AppCredential3rdManageRoute @@ -605,7 +587,6 @@ export interface FileRoutesById { '/auth/sign-in': typeof AuthSignInRoute '/auth/sign-up': typeof AuthSignUpRoute '/_app/_explore/explore': typeof AppExploreExploreRoute - '/_app/_explore/feed': typeof AppExploreFeedRoute '/_app/bangumi/manage': typeof AppBangumiManageRoute '/_app/credential3rd/create': typeof AppCredential3rdCreateRoute '/_app/credential3rd/manage': typeof AppCredential3rdManageRoute @@ -638,7 +619,6 @@ export interface FileRouteTypes { | '/auth/sign-in' | '/auth/sign-up' | '/explore' - | '/feed' | '/bangumi/manage' | '/credential3rd/create' | '/credential3rd/manage' @@ -668,7 +648,6 @@ export interface FileRouteTypes { | '/auth/sign-in' | '/auth/sign-up' | '/explore' - | '/feed' | '/bangumi/manage' | '/credential3rd/create' | '/credential3rd/manage' @@ -698,7 +677,6 @@ export interface FileRouteTypes { | '/auth/sign-in' | '/auth/sign-up' | '/_app/_explore/explore' - | '/_app/_explore/feed' | '/_app/bangumi/manage' | '/_app/credential3rd/create' | '/_app/credential3rd/manage' @@ -767,8 +745,7 @@ export const routeTree = rootRoute "/_app/settings", "/_app/subscriptions", "/_app/tasks", - "/_app/_explore/explore", - "/_app/_explore/feed" + "/_app/_explore/explore" ] }, "/404": { @@ -836,10 +813,6 @@ export const routeTree = rootRoute "filePath": "_app/_explore/explore.tsx", "parent": "/_app" }, - "/_app/_explore/feed": { - "filePath": "_app/_explore/feed.tsx", - "parent": "/_app" - }, "/_app/bangumi/manage": { "filePath": "_app/bangumi/manage.tsx", "parent": "/_app/bangumi" diff --git a/apps/webui/src/presentation/routes/_app/_explore/feed.tsx b/apps/webui/src/presentation/routes/_app/_explore/feed.tsx deleted file mode 100644 index bef8d8d..0000000 --- a/apps/webui/src/presentation/routes/_app/_explore/feed.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { RouteStateDataOption } from '@/infra/routes/traits'; -import { createFileRoute } from '@tanstack/react-router'; - -export const Route = createFileRoute('/_app/_explore/feed')({ - component: FeedRouteComponent, - staticData: { - breadcrumb: { - label: 'Feed', - }, - } satisfies RouteStateDataOption, -}); - -function FeedRouteComponent() { - return
Hello "/_app/feed"!
; -} diff --git a/apps/webui/src/presentation/routes/_app/credential3rd/manage.tsx b/apps/webui/src/presentation/routes/_app/credential3rd/manage.tsx index 970f712..ee8758e 100644 --- a/apps/webui/src/presentation/routes/_app/credential3rd/manage.tsx +++ b/apps/webui/src/presentation/routes/_app/credential3rd/manage.tsx @@ -269,7 +269,7 @@ function CredentialManageRouteComponent() { }, [handleDeleteRecord, navigate, showPasswords, togglePasswordVisibility]); const table = useReactTable({ - data: data?.credential3rd?.nodes ?? [], + data: useMemo(() => credentials?.nodes ?? [], [credentials]), columns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), @@ -335,7 +335,7 @@ function CredentialManageRouteComponent() { {showSkeleton && - Array.from(new Array(pagination.pageSize)).map((_, index) => ( + Array.from(new Array(10)).map((_, index) => ( {table.getVisibleLeafColumns().map((column) => ( diff --git a/apps/webui/src/presentation/routes/_app/subscriptions/detail.$id.tsx b/apps/webui/src/presentation/routes/_app/subscriptions/detail.$id.tsx index 3883dff..c8b148a 100644 --- a/apps/webui/src/presentation/routes/_app/subscriptions/detail.$id.tsx +++ b/apps/webui/src/presentation/routes/_app/subscriptions/detail.$id.tsx @@ -220,13 +220,14 @@ function SubscriptionDetailRouteComponent() { - {subscription.credential3rd && ( + {subscription.category === + SubscriptionCategoryEnum.MikanSeason && ( <>
- {subscription.credential3rd.id} + {subscription.credential3rd?.id ?? '-'}
@@ -237,7 +238,7 @@ function SubscriptionDetailRouteComponent() {
- {subscription.credential3rd.username} + {subscription.credential3rd?.username ?? '-'}
diff --git a/apps/webui/src/presentation/routes/_app/subscriptions/manage.tsx b/apps/webui/src/presentation/routes/_app/subscriptions/manage.tsx index ca17c89..fea1640 100644 --- a/apps/webui/src/presentation/routes/_app/subscriptions/manage.tsx +++ b/apps/webui/src/presentation/routes/_app/subscriptions/manage.tsx @@ -90,6 +90,7 @@ function SubscriptionManageRouteComponent() { }, } ); + const [updateSubscription] = useMutation(UPDATE_SUBSCRIPTIONS, { onCompleted: async () => { const refetchResult = await refetch(); @@ -260,7 +261,7 @@ function SubscriptionManageRouteComponent() { }, [handleUpdateRecord, handleDeleteRecord, navigate]); const table = useReactTable({ - data: data?.subscriptions?.nodes ?? [], + data: useMemo(() => subscriptions?.nodes ?? [], [subscriptions]), columns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), @@ -270,6 +271,8 @@ function SubscriptionManageRouteComponent() { pageCount: subscriptions?.paginationInfo?.pages, rowCount: subscriptions?.paginationInfo?.total, enableColumnPinning: true, + autoResetPageIndex: true, + manualPagination: true, state: { pagination, sorting, @@ -323,7 +326,7 @@ function SubscriptionManageRouteComponent() { {showSkeleton && - Array.from(new Array(pagination.pageSize)).map((_, index) => ( + Array.from(new Array(10)).map((_, index) => ( {table.getVisibleLeafColumns().map((column) => (