Compare commits
3 Commits
147df00155
...
3aad31a36b
Author | SHA1 | Date | |
---|---|---|---|
3aad31a36b | |||
004fed9b2e | |||
a1c2eeded1 |
33
Cargo.lock
generated
33
Cargo.lock
generated
@ -4846,15 +4846,6 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nanoid"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8"
|
||||
dependencies = [
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.14"
|
||||
@ -6766,6 +6757,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"chrono-tz 0.10.3",
|
||||
"clap",
|
||||
"cocoon",
|
||||
"color-eyre",
|
||||
@ -6797,7 +6789,6 @@ dependencies = [
|
||||
"mime_guess",
|
||||
"mockito",
|
||||
"moka",
|
||||
"nanoid",
|
||||
"nom 8.0.0",
|
||||
"num-traits",
|
||||
"num_cpus",
|
||||
@ -6836,6 +6827,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"tracing-test",
|
||||
"tracing-tree",
|
||||
"ts-rs",
|
||||
"typed-builder 0.21.0",
|
||||
@ -9244,6 +9236,27 @@ dependencies = [
|
||||
"tracing-serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-test"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68"
|
||||
dependencies = [
|
||||
"tracing-core",
|
||||
"tracing-subscriber",
|
||||
"tracing-test-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-test-macro"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-tree"
|
||||
version = "0.4.0"
|
||||
|
@ -64,7 +64,7 @@ convert_case = "0.8"
|
||||
color-eyre = "0.6.5"
|
||||
inquire = "0.7.5"
|
||||
image = "0.25.6"
|
||||
uuid = { version = "1.6.0", features = ["v4"] }
|
||||
uuid = { version = "1.6.0", features = ["v7"] }
|
||||
maplit = "1.0.2"
|
||||
once_cell = "1.20.2"
|
||||
rand = "0.9.1"
|
||||
@ -83,5 +83,6 @@ typed-builder = "0.21.0"
|
||||
nanoid = "0.4.0"
|
||||
webp = "0.3.0"
|
||||
|
||||
|
||||
[patch.crates-io]
|
||||
seaography = { git = "https://github.com/dumtruck/seaography.git", rev = "9f7fc7c" }
|
||||
|
@ -1 +1 @@
|
||||
{"filesOrder":["konobangu","konobangu-prod","mikan-doppel"],"selectedList":["mikan-doppel","konobangu-prod"],"disabledDefalutRules":true,"defalutRules":""}
|
||||
{"filesOrder":["konobangu","konobangu-prod","mikan-doppel"],"selectedList":["mikan-doppel","konobangu"],"disabledDefalutRules":true,"defalutRules":""}
|
||||
|
@ -97,7 +97,6 @@ tracing-appender = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
ipnetwork = { workspace = true }
|
||||
typed-builder = { workspace = true }
|
||||
nanoid = { workspace = true }
|
||||
webp = { workspace = true }
|
||||
|
||||
sea-orm = { version = "1.1", features = [
|
||||
@ -170,11 +169,13 @@ croner = "2.2.0"
|
||||
ts-rs = "11.0.1"
|
||||
secrecy = { version = "0.10.3", features = ["serde"] }
|
||||
paste = "1.0.15"
|
||||
chrono-tz = "0.10.3"
|
||||
|
||||
[dev-dependencies]
|
||||
inquire = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
serial_test = "3"
|
||||
insta = { version = "1", features = ["redactions", "toml", "filters"] }
|
||||
rstest = "0.25"
|
||||
ctor = "0.4.0"
|
||||
tracing-test = "0.2.5"
|
||||
rstest = "0.25"
|
||||
|
@ -107,7 +107,7 @@ impl App {
|
||||
Ok::<(), RecorderError>(())
|
||||
},
|
||||
async {
|
||||
task.run(if graceful_shutdown {
|
||||
task.run_with_signal(if graceful_shutdown {
|
||||
Some(Self::shutdown_signal)
|
||||
} else {
|
||||
None
|
||||
|
@ -18,6 +18,8 @@ use crate::{
|
||||
#[derive(Snafu, Debug)]
|
||||
#[snafu(visibility(pub(crate)))]
|
||||
pub enum RecorderError {
|
||||
#[snafu(transparent)]
|
||||
ChronoTzParseError { source: chrono_tz::ParseError },
|
||||
#[snafu(transparent)]
|
||||
SeaographyError { source: seaography::SeaographyError },
|
||||
#[snafu(transparent)]
|
||||
|
@ -20,6 +20,7 @@ fn skip_columns_for_entity_input(context: &mut BuilderContext) {
|
||||
cron::Column::SubscriberTaskCron
|
||||
| cron::Column::SystemTaskCron
|
||||
| cron::Column::CronExpr
|
||||
| cron::Column::CronTimezone
|
||||
| cron::Column::Enabled
|
||||
| cron::Column::TimeoutMs
|
||||
| cron::Column::MaxAttempts
|
||||
@ -30,7 +31,8 @@ fn skip_columns_for_entity_input(context: &mut BuilderContext) {
|
||||
context.entity_input.insert_skips.push(entity_column_key);
|
||||
}
|
||||
for column in cron::Column::iter() {
|
||||
if matches!(column, |cron::Column::CronExpr| cron::Column::Enabled
|
||||
if matches!(column, |cron::Column::CronExpr| cron::Column::CronTimezone
|
||||
| cron::Column::Enabled
|
||||
| cron::Column::TimeoutMs
|
||||
| cron::Column::Priority
|
||||
| cron::Column::MaxAttempts)
|
||||
|
@ -3,6 +3,7 @@ use std::sync::Arc;
|
||||
use async_graphql::dynamic::ResolverContext;
|
||||
use sea_orm::Value as SeaValue;
|
||||
use seaography::{Builder as SeaographyBuilder, BuilderContext, SeaResult};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
graphql::{
|
||||
@ -35,7 +36,9 @@ pub fn register_feeds_to_schema_context(context: &mut BuilderContext) {
|
||||
if field_name == entity_create_one_mutation_field_name.as_str()
|
||||
|| field_name == entity_create_batch_mutation_field_name.as_str()
|
||||
{
|
||||
Ok(Some(SeaValue::String(Some(Box::new(nanoid::nanoid!())))))
|
||||
Ok(Some(SeaValue::String(Some(Box::new(
|
||||
Uuid::now_v7().to_string(),
|
||||
)))))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
|
@ -178,6 +178,7 @@ pub enum Cron {
|
||||
SubscriberId,
|
||||
SubscriptionId,
|
||||
CronExpr,
|
||||
CronTimezone,
|
||||
NextRun,
|
||||
LastRun,
|
||||
LastError,
|
||||
|
@ -8,9 +8,10 @@ use crate::{
|
||||
Subscriptions, table_auto_z,
|
||||
},
|
||||
models::cron::{
|
||||
CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME, CRON_DUE_EVENT, CronStatus, CronStatusEnum,
|
||||
NOTIFY_DUE_CRON_WHEN_MUTATING_FUNCTION_NAME, NOTIFY_DUE_CRON_WHEN_MUTATING_TRIGGER_NAME,
|
||||
SETUP_CRON_EXTRA_FOREIGN_KEYS_FUNCTION_NAME, SETUP_CRON_EXTRA_FOREIGN_KEYS_TRIGGER_NAME,
|
||||
CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME, CRON_DUE_DEBUG_EVENT, CRON_DUE_EVENT,
|
||||
CronStatus, CronStatusEnum, NOTIFY_DUE_CRON_WHEN_MUTATING_FUNCTION_NAME,
|
||||
NOTIFY_DUE_CRON_WHEN_MUTATING_TRIGGER_NAME, SETUP_CRON_EXTRA_FOREIGN_KEYS_FUNCTION_NAME,
|
||||
SETUP_CRON_EXTRA_FOREIGN_KEYS_TRIGGER_NAME,
|
||||
},
|
||||
task::{
|
||||
SETUP_APALIS_JOBS_EXTRA_FOREIGN_KEYS_FUNCTION_NAME, SUBSCRIBER_TASK_APALIS_NAME,
|
||||
@ -39,6 +40,7 @@ impl MigrationTrait for Migration {
|
||||
table_auto_z(Cron::Table)
|
||||
.col(pk_auto(Cron::Id))
|
||||
.col(string(Cron::CronExpr))
|
||||
.col(string(Cron::CronTimezone))
|
||||
.col(integer_null(Cron::SubscriberId))
|
||||
.col(integer_null(Cron::SubscriptionId))
|
||||
.col(timestamp_with_time_zone_null(Cron::NextRun))
|
||||
@ -48,14 +50,13 @@ impl MigrationTrait for Migration {
|
||||
.col(string_null(Cron::LockedBy))
|
||||
.col(timestamp_with_time_zone_null(Cron::LockedAt))
|
||||
.col(integer_null(Cron::TimeoutMs))
|
||||
.col(integer(Cron::Attempts))
|
||||
.col(integer(Cron::MaxAttempts))
|
||||
.col(integer(Cron::Priority))
|
||||
.col(enumeration(
|
||||
Cron::Status,
|
||||
CronStatusEnum,
|
||||
CronStatus::iden_values(),
|
||||
))
|
||||
.col(integer(Cron::Attempts).default(0))
|
||||
.col(integer(Cron::MaxAttempts).default(1))
|
||||
.col(integer(Cron::Priority).default(0))
|
||||
.col(
|
||||
enumeration(Cron::Status, CronStatusEnum, CronStatus::iden_values())
|
||||
.default(CronStatus::Pending),
|
||||
)
|
||||
.col(json_binary_null(Cron::SubscriberTaskCron))
|
||||
.col(json_binary_null(Cron::SystemTaskCron))
|
||||
.foreign_key(
|
||||
@ -139,7 +140,7 @@ impl MigrationTrait for Migration {
|
||||
IF NEW.{next_run} IS NOT NULL
|
||||
AND NEW.{next_run} <= CURRENT_TIMESTAMP
|
||||
AND NEW.{enabled} = true
|
||||
AND NEW.{status} = '{pending}'
|
||||
AND NEW.{status} = '{pending}'::{status_type}
|
||||
AND NEW.{attempts} < NEW.{max_attempts}
|
||||
-- Check if not locked or lock timeout
|
||||
AND (
|
||||
@ -171,6 +172,7 @@ impl MigrationTrait for Migration {
|
||||
pending = &CronStatus::Pending.to_value(),
|
||||
attempts = &Cron::Attempts.to_string(),
|
||||
max_attempts = &Cron::MaxAttempts.to_string(),
|
||||
status_type = &CronStatus::name().to_string(),
|
||||
))
|
||||
.await?;
|
||||
|
||||
@ -194,7 +196,7 @@ impl MigrationTrait for Migration {
|
||||
WHERE {next_run} IS NOT NULL
|
||||
AND {next_run} <= CURRENT_TIMESTAMP
|
||||
AND {enabled} = true
|
||||
AND {status} = '{pending}'
|
||||
AND {status} = '{pending}'::{status_type}
|
||||
AND {attempts} < {max_attempts}
|
||||
AND (
|
||||
{locked_at} IS NULL
|
||||
@ -206,9 +208,12 @@ impl MigrationTrait for Migration {
|
||||
ORDER BY {priority} ASC, {next_run} ASC
|
||||
FOR UPDATE SKIP LOCKED
|
||||
LOOP
|
||||
-- PERFORM pg_notify('{CRON_DUE_DEBUG_EVENT}',format('Found due cron: value=%s; Now time: %s', row_to_json(cron_record)::text, CURRENT_TIMESTAMP));
|
||||
PERFORM pg_notify('{CRON_DUE_EVENT}', row_to_json(cron_record)::text);
|
||||
notification_count := notification_count + 1;
|
||||
END LOOP;
|
||||
|
||||
-- PERFORM pg_notify('{CRON_DUE_DEBUG_EVENT}', format('Notification count: %I; Now time: %s', notification_count, CURRENT_TIMESTAMP));
|
||||
RETURN notification_count;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;"#,
|
||||
@ -222,6 +227,7 @@ impl MigrationTrait for Migration {
|
||||
priority = &Cron::Priority.to_string(),
|
||||
attempts = &Cron::Attempts.to_string(),
|
||||
max_attempts = &Cron::MaxAttempts.to_string(),
|
||||
status_type = &CronStatus::name().to_string(),
|
||||
))
|
||||
.await?;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
pub const CRON_DUE_EVENT: &str = "cron_due";
|
||||
pub const CRON_DUE_DEBUG_EVENT: &str = "cron_due_debug";
|
||||
|
||||
pub const CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME: &str = "check_and_trigger_due_crons";
|
||||
|
||||
|
@ -1,13 +1,14 @@
|
||||
mod core;
|
||||
|
||||
pub use core::{
|
||||
CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME, CRON_DUE_EVENT,
|
||||
CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME, CRON_DUE_DEBUG_EVENT, CRON_DUE_EVENT,
|
||||
NOTIFY_DUE_CRON_WHEN_MUTATING_FUNCTION_NAME, NOTIFY_DUE_CRON_WHEN_MUTATING_TRIGGER_NAME,
|
||||
SETUP_CRON_EXTRA_FOREIGN_KEYS_FUNCTION_NAME, SETUP_CRON_EXTRA_FOREIGN_KEYS_TRIGGER_NAME,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono_tz::Tz;
|
||||
use croner::Cron;
|
||||
use sea_orm::{
|
||||
ActiveValue::{self, Set},
|
||||
@ -54,13 +55,14 @@ pub struct Model {
|
||||
pub subscriber_id: Option<i32>,
|
||||
pub subscription_id: Option<i32>,
|
||||
pub cron_expr: String,
|
||||
pub cron_timezone: String,
|
||||
pub next_run: Option<DateTimeUtc>,
|
||||
pub last_run: Option<DateTimeUtc>,
|
||||
pub last_error: Option<String>,
|
||||
pub locked_by: Option<String>,
|
||||
pub locked_at: Option<DateTimeUtc>,
|
||||
#[sea_orm(default_expr = "5000")]
|
||||
pub timeout_ms: i32,
|
||||
// default_expr = "5000"
|
||||
pub timeout_ms: Option<i32>,
|
||||
#[sea_orm(default_expr = "0")]
|
||||
pub attempts: i32,
|
||||
#[sea_orm(default_expr = "1")]
|
||||
@ -140,16 +142,37 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
where
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
if let ActiveValue::Set(ref cron_expr) = self.cron_expr
|
||||
&& matches!(
|
||||
self.next_run,
|
||||
ActiveValue::NotSet | ActiveValue::Unchanged(_)
|
||||
)
|
||||
{
|
||||
let next_run =
|
||||
Model::calculate_next_run(cron_expr).map_err(|e| DbErr::Custom(e.to_string()))?;
|
||||
self.next_run = Set(Some(next_run));
|
||||
}
|
||||
match (
|
||||
&self.cron_expr as &ActiveValue<String>,
|
||||
&self.cron_timezone as &ActiveValue<String>,
|
||||
) {
|
||||
(ActiveValue::Set(cron_expr), ActiveValue::Set(timezone)) => {
|
||||
if matches!(
|
||||
&self.next_run,
|
||||
ActiveValue::NotSet | ActiveValue::Unchanged(_)
|
||||
) {
|
||||
let next_run = Model::calculate_next_run(cron_expr, timezone)
|
||||
.map_err(|e| DbErr::Custom(e.to_string()))?;
|
||||
self.next_run = Set(Some(next_run));
|
||||
}
|
||||
}
|
||||
(
|
||||
ActiveValue::Unchanged(_) | ActiveValue::NotSet,
|
||||
ActiveValue::Unchanged(_) | ActiveValue::NotSet,
|
||||
) => {}
|
||||
(_, _) => {
|
||||
if matches!(
|
||||
self.next_run,
|
||||
ActiveValue::NotSet | ActiveValue::Unchanged(_)
|
||||
) {
|
||||
return Err(DbErr::Custom(
|
||||
"Cron expr and timezone must be insert or update at same time when next \
|
||||
run is not set"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
if let ActiveValue::Set(Some(subscriber_id)) = self.subscriber_id
|
||||
&& let ActiveValue::Set(Some(ref subscriber_task)) = self.subscriber_task_cron
|
||||
&& subscriber_task.get_subscriber_id() != subscriber_id
|
||||
@ -223,7 +246,10 @@ impl Model {
|
||||
&& cron.attempts < cron.max_attempts
|
||||
&& cron.status == CronStatus::Pending
|
||||
&& (cron.locked_at.is_none_or(|locked_at| {
|
||||
locked_at + chrono::Duration::milliseconds(cron.timeout_ms as i64) <= Utc::now()
|
||||
cron.timeout_ms.is_some_and(|cron_timeout_ms| {
|
||||
locked_at + chrono::Duration::milliseconds(cron_timeout_ms as i64)
|
||||
<= Utc::now()
|
||||
})
|
||||
}))
|
||||
&& cron.next_run.is_some_and(|next_run| next_run <= Utc::now())
|
||||
{
|
||||
@ -269,7 +295,7 @@ impl Model {
|
||||
async fn mark_cron_completed(&self, ctx: &dyn AppContextTrait) -> RecorderResult<()> {
|
||||
let db = ctx.db();
|
||||
|
||||
let next_run = Self::calculate_next_run(&self.cron_expr)?;
|
||||
let next_run = Self::calculate_next_run(&self.cron_expr, &self.cron_timezone)?;
|
||||
|
||||
ActiveModel {
|
||||
id: Set(self.id),
|
||||
@ -307,7 +333,10 @@ impl Model {
|
||||
let next_run = if should_retry {
|
||||
Some(Utc::now() + retry_duration)
|
||||
} else {
|
||||
Some(Self::calculate_next_run(&self.cron_expr)?)
|
||||
Some(Self::calculate_next_run(
|
||||
&self.cron_expr,
|
||||
&self.cron_timezone,
|
||||
)?)
|
||||
};
|
||||
|
||||
ActiveModel {
|
||||
@ -376,7 +405,15 @@ impl Model {
|
||||
locked_cron
|
||||
.mark_cron_failed(
|
||||
ctx,
|
||||
format!("Cron timeout of {}ms", locked_cron.timeout_ms).as_str(),
|
||||
format!(
|
||||
"Cron timeout of {}ms",
|
||||
locked_cron
|
||||
.timeout_ms
|
||||
.as_ref()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| "Infinite".to_string())
|
||||
)
|
||||
.as_str(),
|
||||
retry_duration,
|
||||
)
|
||||
.await?;
|
||||
@ -388,11 +425,17 @@ impl Model {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn calculate_next_run(cron_expr: &str) -> RecorderResult<DateTime<Utc>> {
|
||||
let cron_expr = Cron::new(cron_expr).parse()?;
|
||||
pub fn calculate_next_run(cron_expr: &str, timezone: &str) -> RecorderResult<DateTime<Utc>> {
|
||||
let user_tz = timezone.parse::<Tz>()?;
|
||||
|
||||
let next = cron_expr.find_next_occurrence(&Utc::now(), false)?;
|
||||
let user_tz_now = Utc::now().with_timezone(&user_tz);
|
||||
|
||||
Ok(next)
|
||||
let cron_expr = Cron::new(cron_expr).with_seconds_optional().parse()?;
|
||||
|
||||
let next = cron_expr.find_next_occurrence(&user_tz_now, false)?;
|
||||
|
||||
let next_utc = next.with_timezone(&Utc);
|
||||
|
||||
Ok(next_utc)
|
||||
}
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ impl ActiveModelBehavior for ActiveModel {
|
||||
C: ConnectionTrait,
|
||||
{
|
||||
if insert && let ActiveValue::NotSet = self.token {
|
||||
let token = nanoid::nanoid!(10);
|
||||
let token = Uuid::now_v7().to_string();
|
||||
self.token = ActiveValue::Set(token);
|
||||
}
|
||||
Ok(self)
|
||||
|
@ -60,6 +60,8 @@ pub enum Relation {
|
||||
Feed,
|
||||
#[sea_orm(has_many = "super::subscriber_tasks::Entity")]
|
||||
SubscriberTask,
|
||||
#[sea_orm(has_many = "super::cron::Entity")]
|
||||
Cron,
|
||||
}
|
||||
|
||||
impl Related<super::subscribers::Entity> for Entity {
|
||||
@ -126,6 +128,12 @@ impl Related<super::subscriber_tasks::Entity> for Entity {
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::cron::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Cron.def()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
|
||||
pub enum RelatedEntity {
|
||||
#[sea_orm(entity = "super::subscribers::Entity")]
|
||||
@ -144,6 +152,8 @@ pub enum RelatedEntity {
|
||||
Feed,
|
||||
#[sea_orm(entity = "super::subscriber_tasks::Entity")]
|
||||
SubscriberTask,
|
||||
#[sea_orm(entity = "super::cron::Entity")]
|
||||
Cron,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
@ -278,7 +278,7 @@ impl StorageService {
|
||||
|
||||
if let Some(mut ranges) = ranges {
|
||||
if ranges.len() > 1 {
|
||||
let boundary = Uuid::new_v4().to_string();
|
||||
let boundary = Uuid::now_v7().to_string();
|
||||
let reader = self.reader(storage_path.as_ref()).await?;
|
||||
let stream: impl Stream<Item = Result<Bytes, RecorderError>> = {
|
||||
let boundary = boundary.clone();
|
||||
|
@ -14,6 +14,8 @@ pub struct TaskConfig {
|
||||
pub system_task_reenqueue_orphaned_after: Duration,
|
||||
#[serde(default = "default_cron_retry_duration")]
|
||||
pub cron_retry_duration: Duration,
|
||||
#[serde(default = "default_cron_interval_duration")]
|
||||
pub cron_interval_duration: Duration,
|
||||
}
|
||||
|
||||
impl Default for TaskConfig {
|
||||
@ -25,6 +27,7 @@ impl Default for TaskConfig {
|
||||
default_subscriber_task_reenqueue_orphaned_after(),
|
||||
system_task_reenqueue_orphaned_after: default_system_task_reenqueue_orphaned_after(),
|
||||
cron_retry_duration: default_cron_retry_duration(),
|
||||
cron_interval_duration: default_cron_interval_duration(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -45,6 +48,10 @@ pub fn default_system_task_workers() -> u32 {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_cron_interval_duration() -> Duration {
|
||||
Duration::from_secs(60)
|
||||
}
|
||||
|
||||
pub fn default_subscriber_task_reenqueue_orphaned_after() -> Duration {
|
||||
Duration::from_secs(3600)
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ pub use core::{
|
||||
|
||||
pub use config::TaskConfig;
|
||||
pub use registry::{
|
||||
OptimizeImageTask, SubscriberTask, SubscriberTaskInput, SubscriberTaskType,
|
||||
EchoTask, OptimizeImageTask, SubscriberTask, SubscriberTaskInput, SubscriberTaskType,
|
||||
SubscriberTaskTypeEnum, SubscriberTaskTypeVariant, SubscriberTaskTypeVariantIter,
|
||||
SyncOneSubscriptionFeedsFullTask, SyncOneSubscriptionFeedsIncrementalTask,
|
||||
SyncOneSubscriptionSourcesTask, SystemTask, SystemTaskInput, SystemTaskType,
|
||||
|
@ -9,6 +9,6 @@ pub use subscriber::{
|
||||
};
|
||||
pub(crate) use system::register_system_task_type;
|
||||
pub use system::{
|
||||
OptimizeImageTask, SystemTask, SystemTaskInput, SystemTaskType, SystemTaskTypeEnum,
|
||||
EchoTask, OptimizeImageTask, SystemTask, SystemTaskInput, SystemTaskType, SystemTaskTypeEnum,
|
||||
SystemTaskTypeVariant, SystemTaskTypeVariantIter,
|
||||
};
|
||||
|
29
apps/recorder/src/task/registry/system/misc.rs
Normal file
29
apps/recorder/src/task/registry/system/misc.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::Utc;
|
||||
|
||||
use crate::{
|
||||
app::AppContextTrait,
|
||||
errors::RecorderResult,
|
||||
task::{AsyncTaskTrait, register_system_task_type},
|
||||
};
|
||||
|
||||
register_system_task_type! {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct EchoTask {
|
||||
pub task_id: String,
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncTaskTrait for EchoTask {
|
||||
async fn run_async(self, _ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> {
|
||||
tracing::info!(
|
||||
"EchoTask {} start running at {}",
|
||||
self.task_id,
|
||||
Utc::now().to_rfc3339()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
mod base;
|
||||
mod media;
|
||||
mod misc;
|
||||
|
||||
pub(crate) use base::register_system_task_type;
|
||||
pub use media::OptimizeImageTask;
|
||||
pub use misc::EchoTask;
|
||||
use sea_orm::{DeriveActiveEnum, DeriveDisplay, EnumIter, FromJsonQueryResult};
|
||||
|
||||
macro_rules! register_system_task_types {
|
||||
@ -131,30 +133,6 @@ macro_rules! register_system_task_types {
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(any(test, feature = "test-utils")))]
|
||||
register_system_task_types! {
|
||||
task_type_enum: {
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
Copy,
|
||||
DeriveActiveEnum,
|
||||
DeriveDisplay,
|
||||
EnumIter,
|
||||
)]
|
||||
pub enum SystemTaskType {
|
||||
OptimizeImage => "optimize_image"
|
||||
}
|
||||
},
|
||||
task_enum: {
|
||||
#[derive(Clone, Debug, FromJsonQueryResult)]
|
||||
pub enum SystemTask {
|
||||
OptimizeImage(OptimizeImageTask)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-utils"))]
|
||||
register_system_task_types! {
|
||||
task_type_enum: {
|
||||
#[derive(
|
||||
@ -174,7 +152,7 @@ register_system_task_types! {
|
||||
#[derive(Clone, Debug, FromJsonQueryResult)]
|
||||
pub enum SystemTask {
|
||||
OptimizeImage(OptimizeImageTask),
|
||||
Test(crate::test_utils::task::TestSystemTask),
|
||||
Echo(EchoTask),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,13 +6,14 @@ use apalis_sql::{
|
||||
context::SqlContext,
|
||||
postgres::{PgListen as ApalisPgListen, PostgresStorage as ApalisPostgresStorage},
|
||||
};
|
||||
use sea_orm::sqlx::postgres::PgListener;
|
||||
use sea_orm::{ActiveModelTrait, sqlx::postgres::PgListener};
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
app::AppContextTrait,
|
||||
errors::{RecorderError, RecorderResult},
|
||||
models::cron::{self, CRON_DUE_EVENT},
|
||||
models::cron::{self, CRON_DUE_DEBUG_EVENT, CRON_DUE_EVENT},
|
||||
task::{
|
||||
AsyncTaskTrait, SUBSCRIBER_TASK_APALIS_NAME, SYSTEM_TASK_APALIS_NAME, SubscriberTask,
|
||||
TaskConfig,
|
||||
@ -53,7 +54,7 @@ impl TaskService {
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
cron_worker_id: nanoid::nanoid!(),
|
||||
cron_worker_id: Uuid::now_v7().to_string(),
|
||||
ctx,
|
||||
subscriber_task_storage: Arc::new(RwLock::new(subscriber_task_storage)),
|
||||
system_task_storage: Arc::new(RwLock::new(system_task_storage)),
|
||||
@ -136,83 +137,110 @@ impl TaskService {
|
||||
Ok(task_id)
|
||||
}
|
||||
|
||||
pub async fn run<F, Fut>(&self, shutdown_signal: Option<F>) -> RecorderResult<()>
|
||||
pub async fn add_subscriber_task_cron(
|
||||
&self,
|
||||
cm: cron::ActiveModel,
|
||||
) -> RecorderResult<cron::Model> {
|
||||
let db = self.ctx.db();
|
||||
let m = cm.insert(db).await?;
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
pub async fn add_system_task_cron(&self, cm: cron::ActiveModel) -> RecorderResult<cron::Model> {
|
||||
let db = self.ctx.db();
|
||||
let m = cm.insert(db).await?;
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
pub async fn run(&self) -> RecorderResult<()> {
|
||||
self.run_with_signal(None::<fn() -> std::future::Ready<()>>)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn run_with_signal<F, Fut>(&self, shutdown_signal: Option<F>) -> RecorderResult<()>
|
||||
where
|
||||
F: Fn() -> Fut + Send + 'static,
|
||||
F: FnOnce() -> Fut + Send + 'static,
|
||||
Fut: Future<Output = ()> + Send,
|
||||
{
|
||||
tokio::try_join!(
|
||||
async {
|
||||
tokio::select! {
|
||||
_ = {
|
||||
let monitor = self.setup_apalis_monitor().await?;
|
||||
if let Some(shutdown_signal) = shutdown_signal {
|
||||
monitor
|
||||
.run_with_signal(async move {
|
||||
shutdown_signal().await;
|
||||
tracing::info!("apalis shutting down...");
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
} else {
|
||||
monitor.run().await?;
|
||||
async move {
|
||||
if let Some(shutdown_signal) = shutdown_signal {
|
||||
monitor
|
||||
.run_with_signal(async move {
|
||||
shutdown_signal().await;
|
||||
tracing::info!("apalis shutting down...");
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
} else {
|
||||
monitor.run().await?;
|
||||
}
|
||||
Ok::<_, RecorderError>(())
|
||||
}
|
||||
Ok::<_, RecorderError>(())
|
||||
},
|
||||
async {
|
||||
} => {}
|
||||
_ = {
|
||||
let listener = self.setup_apalis_listener().await?;
|
||||
tokio::task::spawn(async move {
|
||||
async move {
|
||||
if let Err(e) = listener.listen().await {
|
||||
tracing::error!("Error listening to apalis: {e}");
|
||||
}
|
||||
});
|
||||
Ok::<_, RecorderError>(())
|
||||
},
|
||||
async {
|
||||
let listener = self.setup_cron_due_listening().await?;
|
||||
let ctx = self.ctx.clone();
|
||||
Ok::<_, RecorderError>(())
|
||||
}
|
||||
} => {},
|
||||
_ = {
|
||||
let mut listener = self.setup_cron_due_listening().await?;
|
||||
let cron_worker_id = self.cron_worker_id.clone();
|
||||
let retry_duration = chrono::Duration::milliseconds(
|
||||
self.config.cron_retry_duration.as_millis() as i64,
|
||||
);
|
||||
let retry_duration =
|
||||
chrono::Duration::milliseconds(self.config.cron_retry_duration.as_millis() as i64);
|
||||
let cron_interval_duration = self.config.cron_interval_duration;
|
||||
async move {
|
||||
listener.listen_all([CRON_DUE_EVENT as &str, CRON_DUE_DEBUG_EVENT as &str]).await?;
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
if let Err(e) =
|
||||
Self::listen_cron_due(listener, ctx, &cron_worker_id, retry_duration).await
|
||||
{
|
||||
tracing::error!("Error listening to cron due: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
Ok::<_, RecorderError>(())
|
||||
},
|
||||
async {
|
||||
let ctx = self.ctx.clone();
|
||||
let retry_duration = chrono::Duration::milliseconds(
|
||||
self.config.cron_retry_duration.as_millis() as i64,
|
||||
);
|
||||
tokio::task::spawn(async move {
|
||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(60));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if let Err(e) = cron::Model::check_and_cleanup_expired_cron_locks(
|
||||
ctx.as_ref(),
|
||||
retry_duration,
|
||||
)
|
||||
.await
|
||||
tokio::join!(
|
||||
{
|
||||
tracing::error!(
|
||||
"Error checking and cleaning up expired cron locks: {e}"
|
||||
);
|
||||
}
|
||||
if let Err(e) = cron::Model::check_and_trigger_due_crons(ctx.as_ref()).await
|
||||
let ctx = self.ctx.clone();
|
||||
async move {
|
||||
if let Err(e) =
|
||||
Self::listen_cron_due(listener, ctx, &cron_worker_id, retry_duration)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error listening to cron due: {e}");
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
tracing::error!("Error checking and triggering due crons: {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
let ctx = self.ctx.clone();
|
||||
let mut interval = tokio::time::interval(cron_interval_duration);
|
||||
async move {
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if let Err(e) = cron::Model::check_and_cleanup_expired_cron_locks(
|
||||
ctx.as_ref(),
|
||||
retry_duration,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Error checking and cleaning up expired cron locks: {e}"
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(e) =
|
||||
cron::Model::check_and_trigger_due_crons(ctx.as_ref()).await
|
||||
{
|
||||
tracing::error!("Error checking and triggering due crons: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
Ok::<_, RecorderError>(())
|
||||
}
|
||||
} => {}
|
||||
};
|
||||
|
||||
Ok::<_, RecorderError>(())
|
||||
}
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -267,6 +295,7 @@ impl TaskService {
|
||||
async fn setup_cron_due_listening(&self) -> RecorderResult<PgListener> {
|
||||
let pool = self.ctx.db().get_postgres_connection_pool().clone();
|
||||
let listener = PgListener::connect_with(&pool).await?;
|
||||
tracing::debug!("Cron due listener connected to postgres");
|
||||
|
||||
Ok(listener)
|
||||
}
|
||||
@ -277,17 +306,19 @@ impl TaskService {
|
||||
worker_id: &str,
|
||||
retry_duration: chrono::Duration,
|
||||
) -> RecorderResult<()> {
|
||||
listener.listen(CRON_DUE_EVENT).await?;
|
||||
|
||||
loop {
|
||||
let notification = listener.recv().await?;
|
||||
if let Err(e) = cron::Model::handle_cron_notification(
|
||||
ctx.as_ref(),
|
||||
notification,
|
||||
worker_id,
|
||||
retry_duration,
|
||||
)
|
||||
.await
|
||||
if notification.channel() == CRON_DUE_DEBUG_EVENT {
|
||||
tracing::debug!("Received cron due debug event: {:?}", notification);
|
||||
continue;
|
||||
} else if notification.channel() == CRON_DUE_EVENT
|
||||
&& let Err(e) = cron::Model::handle_cron_notification(
|
||||
ctx.as_ref(),
|
||||
notification,
|
||||
worker_id,
|
||||
retry_duration,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error handling cron notification: {e}");
|
||||
}
|
||||
@ -298,13 +329,21 @@ impl TaskService {
|
||||
#[cfg(test)]
|
||||
#[allow(unused_variables)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::Utc;
|
||||
use rstest::{fixture, rstest};
|
||||
use sea_orm::ActiveValue;
|
||||
use tracing::Level;
|
||||
|
||||
use super::*;
|
||||
use crate::test_utils::{
|
||||
// app::TestingPreset,
|
||||
tracing::try_init_testing_tracing,
|
||||
use crate::{
|
||||
models::cron,
|
||||
task::EchoTask,
|
||||
test_utils::{
|
||||
app::{TestingAppContextConfig, TestingPreset},
|
||||
tracing::try_init_testing_tracing,
|
||||
},
|
||||
};
|
||||
|
||||
#[fixture]
|
||||
@ -314,7 +353,80 @@ mod tests {
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
async fn test_cron_due_listening(before_each: ()) -> RecorderResult<()> {
|
||||
todo!()
|
||||
#[tracing_test::traced_test]
|
||||
async fn test_check_and_trigger_due_crons_with_certain_interval(
|
||||
before_each: (),
|
||||
) -> RecorderResult<()> {
|
||||
let preset = TestingPreset::default_with_config(
|
||||
TestingAppContextConfig::builder()
|
||||
.task_config(TaskConfig {
|
||||
cron_interval_duration: Duration::from_millis(1500),
|
||||
..Default::default()
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
.await?;
|
||||
let app_ctx = preset.app_ctx;
|
||||
let task_service = app_ctx.task();
|
||||
|
||||
let task_id = Uuid::now_v7().to_string();
|
||||
|
||||
let echo_cron = cron::ActiveModel {
|
||||
cron_expr: ActiveValue::Set("*/1 * * * * *".to_string()),
|
||||
system_task_cron: ActiveValue::Set(Some(
|
||||
EchoTask::builder().task_id(task_id.clone()).build().into(),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
task_service.add_system_task_cron(echo_cron).await?;
|
||||
|
||||
task_service
|
||||
.run_with_signal(Some(async move || {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
}))
|
||||
.await?;
|
||||
|
||||
assert!(logs_contain(&format!(
|
||||
"EchoTask {task_id} start running at"
|
||||
)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
#[tracing_test::traced_test]
|
||||
async fn test_trigger_due_cron_when_mutating(before_each: ()) -> RecorderResult<()> {
|
||||
let preset = TestingPreset::default().await?;
|
||||
let app_ctx = preset.app_ctx;
|
||||
let task_service = app_ctx.task();
|
||||
|
||||
let task_id = Uuid::now_v7().to_string();
|
||||
|
||||
let echo_cron = cron::ActiveModel {
|
||||
cron_expr: ActiveValue::Set("* * * */1 * *".to_string()),
|
||||
next_run: ActiveValue::Set(Some(Utc::now() + chrono::Duration::seconds(-10))),
|
||||
system_task_cron: ActiveValue::Set(Some(
|
||||
EchoTask::builder().task_id(task_id.clone()).build().into(),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let task_runner = task_service.run_with_signal(Some(async move || {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||
}));
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
|
||||
task_service.add_system_task_cron(echo_cron).await?;
|
||||
|
||||
task_runner.await?;
|
||||
|
||||
assert!(logs_contain(&format!(
|
||||
"EchoTask {task_id} start running at"
|
||||
)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ use typed_builder::TypedBuilder;
|
||||
use crate::{
|
||||
app::AppContextTrait,
|
||||
errors::RecorderResult,
|
||||
task::TaskConfig,
|
||||
test_utils::{
|
||||
crypto::build_testing_crypto_service,
|
||||
database::{TestingDatabaseServiceConfig, build_testing_database_service},
|
||||
@ -43,10 +44,11 @@ impl TestingAppContext {
|
||||
self.task.get_or_init(|| task);
|
||||
}
|
||||
|
||||
pub async fn from_preset(preset: TestingAppContextPreset) -> RecorderResult<Arc<Self>> {
|
||||
let mikan_client = build_testing_mikan_client(preset.mikan_base_url).await?;
|
||||
pub async fn from_config(config: TestingAppContextConfig) -> RecorderResult<Arc<Self>> {
|
||||
let mikan_base_url = config.mikan_base_url.expect("mikan_base_url is required");
|
||||
let mikan_client = build_testing_mikan_client(mikan_base_url).await?;
|
||||
let db_service =
|
||||
build_testing_database_service(preset.database_config.unwrap_or_default()).await?;
|
||||
build_testing_database_service(config.database_config.unwrap_or_default()).await?;
|
||||
let crypto_service = build_testing_crypto_service().await?;
|
||||
let storage_service = build_testing_storage_service().await?;
|
||||
let media_service = build_testing_media_service().await?;
|
||||
@ -60,7 +62,7 @@ impl TestingAppContext {
|
||||
.build(),
|
||||
);
|
||||
|
||||
let task_service = build_testing_task_service(app_ctx.clone()).await?;
|
||||
let task_service = build_testing_task_service(config.task_config, app_ctx.clone()).await?;
|
||||
|
||||
app_ctx.set_task(task_service);
|
||||
|
||||
@ -132,9 +134,12 @@ impl AppContextTrait for TestingAppContext {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestingAppContextPreset {
|
||||
pub mikan_base_url: String,
|
||||
#[derive(TypedBuilder, Debug)]
|
||||
#[builder(field_defaults(default, setter(strip_option)))]
|
||||
pub struct TestingAppContextConfig {
|
||||
pub mikan_base_url: Option<String>,
|
||||
pub database_config: Option<TestingDatabaseServiceConfig>,
|
||||
pub task_config: Option<TaskConfig>,
|
||||
}
|
||||
|
||||
#[derive(TypedBuilder)]
|
||||
@ -144,15 +149,15 @@ pub struct TestingPreset {
|
||||
}
|
||||
|
||||
impl TestingPreset {
|
||||
pub async fn default() -> RecorderResult<Self> {
|
||||
pub async fn default_with_config(config: TestingAppContextConfig) -> RecorderResult<Self> {
|
||||
let mikan_server = MikanMockServer::new().await?;
|
||||
let database_config = TestingDatabaseServiceConfig::default();
|
||||
|
||||
let app_ctx = TestingAppContext::from_preset(TestingAppContextPreset {
|
||||
mikan_base_url: mikan_server.base_url().to_string(),
|
||||
database_config: Some(database_config),
|
||||
})
|
||||
.await?;
|
||||
let mixed_config = TestingAppContextConfig {
|
||||
mikan_base_url: Some(mikan_server.base_url().to_string()),
|
||||
..config
|
||||
};
|
||||
|
||||
let app_ctx = TestingAppContext::from_config(mixed_config).await?;
|
||||
|
||||
let preset = Self::builder()
|
||||
.mikan_server(mikan_server)
|
||||
@ -160,4 +165,13 @@ impl TestingPreset {
|
||||
.build();
|
||||
Ok(preset)
|
||||
}
|
||||
|
||||
pub async fn default() -> RecorderResult<Self> {
|
||||
Self::default_with_config(TestingAppContextConfig {
|
||||
mikan_base_url: None,
|
||||
database_config: None,
|
||||
task_config: None,
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ pub async fn build_testing_database_service(
|
||||
uri: connection_string,
|
||||
enable_logging: true,
|
||||
min_connections: 1,
|
||||
max_connections: 1,
|
||||
max_connections: 5,
|
||||
connect_timeout: 5000,
|
||||
idle_timeout: 10000,
|
||||
acquire_timeout: None,
|
||||
|
@ -1,42 +1,16 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::Utc;
|
||||
|
||||
use crate::{
|
||||
app::AppContextTrait,
|
||||
errors::RecorderResult,
|
||||
task::{AsyncTaskTrait, TaskConfig, TaskService, register_system_task_type},
|
||||
task::{TaskConfig, TaskService},
|
||||
};
|
||||
|
||||
register_system_task_type! {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TestSystemTask {
|
||||
pub task_id: String,
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncTaskTrait for TestSystemTask {
|
||||
async fn run_async(self, ctx: Arc<dyn AppContextTrait>) -> RecorderResult<()> {
|
||||
let storage = ctx.storage();
|
||||
|
||||
storage
|
||||
.write(
|
||||
storage.build_test_path(self.task_id),
|
||||
serde_json::json!({ "exec_time": Utc::now().timestamp_millis() })
|
||||
.to_string()
|
||||
.into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_testing_task_service(
|
||||
config: Option<TaskConfig>,
|
||||
ctx: Arc<dyn AppContextTrait>,
|
||||
) -> RecorderResult<TaskService> {
|
||||
let config = TaskConfig::default();
|
||||
let config = config.unwrap_or_default();
|
||||
let task_service = TaskService::from_config_and_ctx(config, ctx).await?;
|
||||
|
||||
Ok(task_service)
|
||||
|
@ -110,7 +110,7 @@ fn make_request_id(maybe_request_id: Option<HeaderValue>) -> String {
|
||||
});
|
||||
id.filter(|s| !s.is_empty())
|
||||
})
|
||||
.unwrap_or_else(|| Uuid::new_v4().to_string())
|
||||
.unwrap_or_else(|| Uuid::now_v7().to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -11,13 +11,14 @@
|
||||
"codegen-watch": "graphql-codegen --config graphql-codegen.ts --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"recorder": "workspace:*",
|
||||
"@abraham/reflection": "^0.13.0",
|
||||
"@apollo/client": "^3.13.8",
|
||||
"@codemirror/language": "6.11.1",
|
||||
"@corvu/drawer": "^0.2.4",
|
||||
"@corvu/otp-field": "^0.1.4",
|
||||
"@corvu/resizable": "^0.2.5",
|
||||
"@datasert/cronjs-matcher": "^1.4.0",
|
||||
"@datasert/cronjs-parser": "^1.4.0",
|
||||
"@graphiql/toolkit": "^0.11.3",
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@outposts/injection-js": "^2.5.1",
|
||||
@ -72,9 +73,11 @@
|
||||
"react-dom": "^19.1.0",
|
||||
"react-resizable-panels": "^3.0.2",
|
||||
"recharts": "^2.15.3",
|
||||
"recorder": "workspace:*",
|
||||
"rxjs": "^7.8.2",
|
||||
"sonner": "^2.0.5",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
"tailwindcss": "^4.1.10",
|
||||
"tw-animate-css": "^1.3.4",
|
||||
"type-fest": "^4.41.0",
|
||||
|
@ -144,4 +144,6 @@
|
||||
[role="button"]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@plugin "tailwind-scrollbar";
|
||||
|
@ -65,11 +65,17 @@ export const AppNavMainData: NavMainGroup[] = [
|
||||
icon: ListTodo,
|
||||
children: [
|
||||
{
|
||||
title: 'Manage',
|
||||
title: 'Tasks',
|
||||
link: {
|
||||
to: '/tasks/manage',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Crons',
|
||||
link: {
|
||||
to: '/tasks/cron/manage',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
291
apps/webui/src/components/domains/cron/README.md
Normal file
291
apps/webui/src/components/domains/cron/README.md
Normal file
@ -0,0 +1,291 @@
|
||||
# Cron Components
|
||||
|
||||
A comprehensive set of React components for creating, editing, and displaying cron expressions with TypeScript support and shadcn/ui integration.
|
||||
|
||||
## Features
|
||||
|
||||
- 🎯 **Multiple Input Modes**: Text input, visual builder, or both
|
||||
- 🔍 **Real-time Validation**: Powered by `@datasert/cronjs-parser`
|
||||
- ⏰ **Next Run Preview**: Shows upcoming execution times with `@datasert/cronjs-matcher`
|
||||
- 🌍 **Timezone Support**: Display times in different timezones
|
||||
- 📱 **Responsive Design**: Works seamlessly on desktop and mobile
|
||||
- 🎨 **shadcn/ui Integration**: Consistent with your existing design system
|
||||
- 🔧 **TypeScript Support**: Full type definitions included
|
||||
- 🚀 **Customizable**: Extensive props for customization
|
||||
|
||||
## Components
|
||||
|
||||
### `<Cron />` - Main Component
|
||||
|
||||
The primary component that combines all functionality.
|
||||
|
||||
```tsx
|
||||
import { Cron } from '@/components/cron';
|
||||
|
||||
function MyScheduler() {
|
||||
const [cronExpression, setCronExpression] = useState('0 0 9 * * 1-5');
|
||||
|
||||
return (
|
||||
<Cron
|
||||
value={cronExpression}
|
||||
onChange={setCronExpression}
|
||||
mode="both" // 'input' | 'builder' | 'both'
|
||||
showPreview={true}
|
||||
showDescription={true}
|
||||
timezone="UTC"
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `value` | `string` | `''` | Current cron expression |
|
||||
| `onChange` | `(value: string) => void` | - | Called when expression changes |
|
||||
| `onValidate` | `(isValid: boolean) => void` | - | Called when validation state changes |
|
||||
| `mode` | `'input' \| 'builder' \| 'both'` | `'both'` | Display mode |
|
||||
| `disabled` | `boolean` | `false` | Disable all inputs |
|
||||
| `placeholder` | `string` | `'0 0 * * * *'` | Input placeholder text |
|
||||
| `showPreview` | `boolean` | `true` | Show next run times preview |
|
||||
| `showDescription` | `boolean` | `true` | Show human-readable description |
|
||||
| `timezone` | `string` | `'UTC'` | Timezone for preview times |
|
||||
| `error` | `string` | - | External error message |
|
||||
| `className` | `ClassValue` | - | Additional CSS classes |
|
||||
|
||||
### `<CronInput />` - Text Input Component
|
||||
|
||||
Simple text input with validation and help text.
|
||||
|
||||
```tsx
|
||||
import { CronInput } from '@/components/cron';
|
||||
|
||||
function QuickEntry() {
|
||||
const [expression, setExpression] = useState('');
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
|
||||
return (
|
||||
<CronInput
|
||||
value={expression}
|
||||
onChange={setExpression}
|
||||
onValidate={setIsValid}
|
||||
placeholder="Enter cron expression..."
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `value` | `string` | - | Current expression value |
|
||||
| `onChange` | `(value: string) => void` | - | Called when input changes |
|
||||
| `onValidate` | `(isValid: boolean) => void` | - | Called when validation changes |
|
||||
| `placeholder` | `string` | `'0 0 * * * *'` | Placeholder text |
|
||||
| `disabled` | `boolean` | `false` | Disable input |
|
||||
| `readOnly` | `boolean` | `false` | Make input read-only |
|
||||
| `error` | `string` | - | Error message to display |
|
||||
| `className` | `ClassValue` | - | Additional CSS classes |
|
||||
|
||||
### `<CronBuilder />` - Visual Builder Component
|
||||
|
||||
Visual interface for building cron expressions with presets and field editors.
|
||||
|
||||
```tsx
|
||||
import { CronBuilder } from '@/components/cron';
|
||||
|
||||
function VisualScheduler() {
|
||||
const [expression, setExpression] = useState('0 0 * * * *');
|
||||
|
||||
return (
|
||||
<CronBuilder
|
||||
value={expression}
|
||||
onChange={setExpression}
|
||||
showPreview={true}
|
||||
defaultTab="daily"
|
||||
allowedPeriods={['hourly', 'daily', 'weekly']}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `value` | `string` | `'0 0 * * * *'` | Current expression |
|
||||
| `onChange` | `(value: string) => void` | - | Called when expression changes |
|
||||
| `disabled` | `boolean` | `false` | Disable all controls |
|
||||
| `showPreview` | `boolean` | `true` | Show preview section |
|
||||
| `defaultTab` | `CronPeriod` | `'hourly'` | Default active tab |
|
||||
| `allowedPeriods` | `CronPeriod[]` | All periods | Which tabs to show |
|
||||
| `presets` | `CronPreset[]` | Built-in presets | Custom preset list |
|
||||
| `className` | `ClassValue` | - | Additional CSS classes |
|
||||
|
||||
### `<CronDisplay />` - Display Component
|
||||
|
||||
Read-only component for displaying cron expression information.
|
||||
|
||||
```tsx
|
||||
import { CronDisplay } from '@/components/cron';
|
||||
|
||||
function ScheduleInfo({ schedule }) {
|
||||
return (
|
||||
<CronDisplay
|
||||
expression={schedule.cronExpression}
|
||||
showNextRuns={true}
|
||||
showDescription={true}
|
||||
nextRunsCount={5}
|
||||
timezone={schedule.timezone}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `expression` | `string` | - | Cron expression to display |
|
||||
| `showNextRuns` | `boolean` | `true` | Show upcoming run times |
|
||||
| `showDescription` | `boolean` | `true` | Show human-readable description |
|
||||
| `nextRunsCount` | `number` | `5` | Number of future runs to show |
|
||||
| `timezone` | `string` | `'UTC'` | Timezone for times |
|
||||
| `className` | `ClassValue` | - | Additional CSS classes |
|
||||
|
||||
## Cron Expression Format
|
||||
|
||||
The components support 6-field cron expressions with seconds:
|
||||
|
||||
```
|
||||
┌─────────────── second (0-59)
|
||||
│ ┌───────────── minute (0-59)
|
||||
│ │ ┌─────────── hour (0-23)
|
||||
│ │ │ ┌───────── day of month (1-31)
|
||||
│ │ │ │ ┌─────── month (1-12)
|
||||
│ │ │ │ │ ┌───── day of week (0-6, Sunday=0)
|
||||
│ │ │ │ │ │
|
||||
* * * * * *
|
||||
```
|
||||
|
||||
### Special Characters
|
||||
|
||||
| Character | Description | Example |
|
||||
|-----------|-------------|---------|
|
||||
| `*` | Any value | `*` = every value |
|
||||
| `,` | List separator | `1,3,5` = values 1, 3, and 5 |
|
||||
| `-` | Range | `1-5` = values 1 through 5 |
|
||||
| `/` | Step values | `*/5` = every 5th value |
|
||||
| `?` | No specific value | Used when day/weekday conflict |
|
||||
| `L` | Last | Last day of month/week |
|
||||
| `W` | Weekday | Nearest weekday |
|
||||
|
||||
### Common Examples
|
||||
|
||||
| Expression | Description |
|
||||
|------------|-------------|
|
||||
| `0 * * * * *` | Every minute |
|
||||
| `0 */5 * * * *` | Every 5 minutes |
|
||||
| `0 0 * * * *` | Every hour |
|
||||
| `0 0 9 * * *` | Daily at 9 AM |
|
||||
| `0 30 9 * * 1-5` | Weekdays at 9:30 AM |
|
||||
| `0 0 0 * * 0` | Every Sunday at midnight |
|
||||
| `0 0 0 1 * *` | First day of every month |
|
||||
| `0 0 0 1 1 *` | Every January 1st |
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `@datasert/cronjs-parser` - For parsing and validating cron expressions
|
||||
- `@datasert/cronjs-matcher` - For calculating next run times
|
||||
- `@radix-ui/react-*` - UI primitives (via shadcn/ui)
|
||||
- `lucide-react` - Icons
|
||||
|
||||
## Installation
|
||||
|
||||
1. Copy the component files to your project
|
||||
2. Ensure you have the required dependencies:
|
||||
|
||||
```bash
|
||||
npm install @datasert/cronjs-parser @datasert/cronjs-matcher
|
||||
```
|
||||
|
||||
3. Import and use the components:
|
||||
|
||||
```tsx
|
||||
import { Cron } from '@/components/cron';
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
### Custom Presets
|
||||
|
||||
```tsx
|
||||
const customPresets = [
|
||||
{
|
||||
label: 'Business Hours',
|
||||
value: '0 0 9-17 * * 1-5',
|
||||
description: 'Every hour during business hours',
|
||||
category: 'custom'
|
||||
},
|
||||
// ... more presets
|
||||
];
|
||||
|
||||
<CronBuilder presets={customPresets} />
|
||||
```
|
||||
|
||||
### Restricted Periods
|
||||
|
||||
```tsx
|
||||
<CronBuilder
|
||||
allowedPeriods={['daily', 'weekly']}
|
||||
defaultTab="daily"
|
||||
/>
|
||||
```
|
||||
|
||||
### Custom Validation
|
||||
|
||||
```tsx
|
||||
function MyComponent() {
|
||||
const [expression, setExpression] = useState('');
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
|
||||
const handleValidation = (valid: boolean) => {
|
||||
setIsValid(valid);
|
||||
// Custom validation logic
|
||||
};
|
||||
|
||||
return (
|
||||
<Cron
|
||||
value={expression}
|
||||
onChange={setExpression}
|
||||
onValidate={handleValidation}
|
||||
error={!isValid ? 'Invalid expression' : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Support
|
||||
|
||||
All components include comprehensive TypeScript definitions:
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
CronProps,
|
||||
CronExpression,
|
||||
CronValidationResult,
|
||||
CronPeriod
|
||||
} from '@/components/cron';
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
See `CronExample` component for comprehensive usage examples and interactive demos.
|
||||
|
||||
## Browser Support
|
||||
|
||||
- Modern browsers with ES2015+ support
|
||||
- React 16.8+ (hooks support required)
|
||||
- TypeScript 4.0+ recommended
|
643
apps/webui/src/components/domains/cron/cron-builder.tsx
Normal file
643
apps/webui/src/components/domains/cron/cron-builder.tsx
Normal file
@ -0,0 +1,643 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||
import { cn } from '@/presentation/utils';
|
||||
import { getFutureMatches } from '@datasert/cronjs-matcher';
|
||||
import { Calendar, Clock, Info, Settings, Zap } from 'lucide-react';
|
||||
import {
|
||||
type CSSProperties,
|
||||
type FC,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
type CronBuilderProps,
|
||||
CronField,
|
||||
type CronFieldConfig,
|
||||
CronPeriod,
|
||||
type CronPreset,
|
||||
} from './types';
|
||||
|
||||
const CRON_PRESETS: CronPreset[] = [
|
||||
{
|
||||
label: 'Every minute',
|
||||
value: '0 * * * * *',
|
||||
description: 'Runs every minute',
|
||||
category: 'common',
|
||||
},
|
||||
{
|
||||
label: 'Every 5 minutes',
|
||||
value: '0 */5 * * * *',
|
||||
description: 'Runs every 5 minutes',
|
||||
category: 'common',
|
||||
},
|
||||
{
|
||||
label: 'Every 15 minutes',
|
||||
value: '0 */15 * * * *',
|
||||
description: 'Runs every 15 minutes',
|
||||
category: 'common',
|
||||
},
|
||||
{
|
||||
label: 'Every 30 minutes',
|
||||
value: '0 */30 * * * *',
|
||||
description: 'Runs every 30 minutes',
|
||||
category: 'common',
|
||||
},
|
||||
{
|
||||
label: 'Every hour',
|
||||
value: '0 0 * * * *',
|
||||
description: 'Runs at the top of every hour',
|
||||
category: 'common',
|
||||
},
|
||||
{
|
||||
label: 'Every 6 hours',
|
||||
value: '0 0 */6 * * *',
|
||||
description: 'Runs every 6 hours',
|
||||
category: 'common',
|
||||
},
|
||||
{
|
||||
label: 'Daily at midnight',
|
||||
value: '0 0 0 * * *',
|
||||
description: 'Runs once daily at 00:00',
|
||||
category: 'daily',
|
||||
},
|
||||
{
|
||||
label: 'Daily at 9 AM',
|
||||
value: '0 0 9 * * *',
|
||||
description: 'Runs daily at 9:00 AM',
|
||||
category: 'daily',
|
||||
},
|
||||
{
|
||||
label: 'Weekdays at 9 AM',
|
||||
value: '0 0 9 * * 1-5',
|
||||
description: 'Runs Monday to Friday at 9:00 AM',
|
||||
category: 'weekly',
|
||||
},
|
||||
{
|
||||
label: 'Every Sunday',
|
||||
value: '0 0 0 * * 0',
|
||||
description: 'Runs every Sunday at midnight',
|
||||
category: 'weekly',
|
||||
},
|
||||
{
|
||||
label: 'First day of month',
|
||||
value: '0 0 0 1 * *',
|
||||
description: 'Runs on the 1st day of every month',
|
||||
category: 'monthly',
|
||||
},
|
||||
{
|
||||
label: 'Every year',
|
||||
value: '0 0 0 1 1 *',
|
||||
description: 'Runs on January 1st every year',
|
||||
category: 'yearly',
|
||||
},
|
||||
];
|
||||
|
||||
const FIELD_CONFIGS: Record<CronField, CronFieldConfig> = {
|
||||
seconds: {
|
||||
min: 0,
|
||||
max: 59,
|
||||
step: 1,
|
||||
allowSpecial: ['*', '?'],
|
||||
},
|
||||
minutes: {
|
||||
min: 0,
|
||||
max: 59,
|
||||
step: 1,
|
||||
allowSpecial: ['*', '?'],
|
||||
},
|
||||
hours: {
|
||||
min: 0,
|
||||
max: 23,
|
||||
step: 1,
|
||||
allowSpecial: ['*', '?'],
|
||||
},
|
||||
dayOfMonth: {
|
||||
min: 1,
|
||||
max: 31,
|
||||
step: 1,
|
||||
allowSpecial: ['*', '?', 'L', 'W'],
|
||||
options: [
|
||||
{ label: 'Any day', value: '*' },
|
||||
{ label: 'No specific day', value: '?' },
|
||||
{ label: 'Last day', value: 'L' },
|
||||
{ label: 'Weekday', value: 'W' },
|
||||
],
|
||||
},
|
||||
month: {
|
||||
min: 1,
|
||||
max: 12,
|
||||
step: 1,
|
||||
allowSpecial: ['*'],
|
||||
options: [
|
||||
{ label: 'January', value: 1 },
|
||||
{ label: 'February', value: 2 },
|
||||
{ label: 'March', value: 3 },
|
||||
{ label: 'April', value: 4 },
|
||||
{ label: 'May', value: 5 },
|
||||
{ label: 'June', value: 6 },
|
||||
{ label: 'July', value: 7 },
|
||||
{ label: 'August', value: 8 },
|
||||
{ label: 'September', value: 9 },
|
||||
{ label: 'October', value: 10 },
|
||||
{ label: 'November', value: 11 },
|
||||
{ label: 'December', value: 12 },
|
||||
],
|
||||
},
|
||||
dayOfWeek: {
|
||||
min: 0,
|
||||
max: 6,
|
||||
step: 1,
|
||||
allowSpecial: ['*', '?'],
|
||||
options: [
|
||||
{ label: 'Sunday', value: 0 },
|
||||
{ label: 'Monday', value: 1 },
|
||||
{ label: 'Tuesday', value: 2 },
|
||||
{ label: 'Wednesday', value: 3 },
|
||||
{ label: 'Thursday', value: 4 },
|
||||
{ label: 'Friday', value: 5 },
|
||||
{ label: 'Saturday', value: 6 },
|
||||
],
|
||||
},
|
||||
year: {
|
||||
min: 0,
|
||||
max: 9999,
|
||||
step: 1,
|
||||
allowSpecial: ['*', '?'],
|
||||
},
|
||||
};
|
||||
|
||||
const PERIOD_CONFIGS = {
|
||||
minute: {
|
||||
label: CronPeriod.Minute,
|
||||
description: 'Run every minute',
|
||||
template: '0 * * * * *',
|
||||
fields: [CronField.Minutes],
|
||||
},
|
||||
hourly: {
|
||||
label: CronPeriod.Hourly,
|
||||
description: 'Run every hour',
|
||||
template: '0 0 * * * *',
|
||||
fields: [CronField.Minutes, CronField.Hours],
|
||||
},
|
||||
daily: {
|
||||
label: CronPeriod.Daily,
|
||||
description: 'Run every day',
|
||||
template: '0 0 0 * * *',
|
||||
fields: [CronField.Seconds, CronField.Minutes, CronField.Hours],
|
||||
},
|
||||
weekly: {
|
||||
label: CronPeriod.Weekly,
|
||||
description: 'Run every week',
|
||||
template: '0 0 0 * * 0',
|
||||
fields: [
|
||||
CronField.Seconds,
|
||||
CronField.Minutes,
|
||||
CronField.Hours,
|
||||
CronField.DayOfWeek,
|
||||
],
|
||||
},
|
||||
monthly: {
|
||||
label: CronPeriod.Monthly,
|
||||
description: 'Run every month',
|
||||
template: '0 0 0 1 * *',
|
||||
fields: [
|
||||
CronField.Seconds,
|
||||
CronField.Minutes,
|
||||
CronField.Hours,
|
||||
CronField.DayOfMonth,
|
||||
],
|
||||
},
|
||||
yearly: {
|
||||
label: CronPeriod.Yearly,
|
||||
description: 'Run every year',
|
||||
template: '0 0 0 1 1 *',
|
||||
fields: [
|
||||
CronField.Seconds,
|
||||
CronField.Minutes,
|
||||
CronField.Hours,
|
||||
CronField.DayOfMonth,
|
||||
CronField.Month,
|
||||
],
|
||||
},
|
||||
custom: {
|
||||
label: CronPeriod.Custom,
|
||||
description: 'Custom expression',
|
||||
template: '0 0 * * * *',
|
||||
fields: [
|
||||
CronField.Seconds,
|
||||
CronField.Minutes,
|
||||
CronField.Hours,
|
||||
CronField.DayOfMonth,
|
||||
CronField.Month,
|
||||
CronField.DayOfWeek,
|
||||
],
|
||||
},
|
||||
} as const;
|
||||
|
||||
const CronBuilder: FC<CronBuilderProps> = ({
|
||||
timezone = 'UTC',
|
||||
value = '0 0 * * * *',
|
||||
onChange,
|
||||
className,
|
||||
disabled = false,
|
||||
showPreview = true,
|
||||
showPresets = true,
|
||||
displayPeriods = [
|
||||
CronPeriod.Custom,
|
||||
CronPeriod.Minute,
|
||||
CronPeriod.Hourly,
|
||||
CronPeriod.Daily,
|
||||
CronPeriod.Weekly,
|
||||
CronPeriod.Monthly,
|
||||
CronPeriod.Yearly,
|
||||
],
|
||||
defaultTab = CronPeriod.Custom,
|
||||
presets = CRON_PRESETS,
|
||||
showGeneratedExpression = true,
|
||||
withCard = true,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<CronPeriod>(defaultTab);
|
||||
const [cronFields, setCronFields] = useState(() =>
|
||||
parseCronExpression(value)
|
||||
);
|
||||
|
||||
const currentExpression = useMemo(() => {
|
||||
return `${cronFields.seconds} ${cronFields.minutes} ${cronFields.hours} ${cronFields.dayOfMonth} ${cronFields.month} ${cronFields.dayOfWeek}`;
|
||||
}, [cronFields]);
|
||||
|
||||
const nextRuns = useMemo(() => {
|
||||
if (!showPreview) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const matches = getFutureMatches(`${currentExpression} *`, {
|
||||
matchCount: 3,
|
||||
timezone,
|
||||
formatInTimezone: true,
|
||||
hasSeconds: true,
|
||||
});
|
||||
return matches.map((match) => new Date(match));
|
||||
} catch (error) {
|
||||
console.error('Failed to get future matched runs', error);
|
||||
return [];
|
||||
}
|
||||
}, [currentExpression, showPreview, timezone]);
|
||||
|
||||
useEffect(() => {
|
||||
setCronFields(parseCronExpression(value));
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
onChange?.(currentExpression);
|
||||
}, [currentExpression, onChange]);
|
||||
|
||||
const handlePresetSelect = useCallback((preset: CronPreset) => {
|
||||
setCronFields(parseCronExpression(preset.value));
|
||||
}, []);
|
||||
|
||||
const handleFieldChange = useCallback(
|
||||
(field: CronField, newValue: string) => {
|
||||
setCronFields((prev) => ({ ...prev, [field]: newValue }));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handlePeriodChange = useCallback((period: CronPeriod) => {
|
||||
setActiveTab(period);
|
||||
if (period !== 'custom') {
|
||||
const config = PERIOD_CONFIGS[period];
|
||||
setCronFields(parseCronExpression(config.template));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const filteredPresets = useMemo(() => {
|
||||
return presets.filter((preset) => {
|
||||
if (activeTab === 'custom') {
|
||||
return true;
|
||||
}
|
||||
return preset.category === activeTab;
|
||||
});
|
||||
}, [presets, activeTab]);
|
||||
|
||||
return (
|
||||
<div className={cn(withCard && 'space-y-6', className)}>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(value) => handlePeriodChange(value as CronPeriod)}
|
||||
>
|
||||
<div className="overflow-x-auto">
|
||||
<TabsList
|
||||
className="grid w-(--all-grids-width) grid-cols-7 whitespace-nowrap lg:w-full"
|
||||
style={
|
||||
{
|
||||
'--my-grid-cols': `grid-template-columns: repeat(${displayPeriods.length}, minmax(0, 1fr))`,
|
||||
'--all-grids-width':
|
||||
displayPeriods.length > 4
|
||||
? `${displayPeriods.length * 25 - 20}%`
|
||||
: '100%',
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
{displayPeriods.map((period) => (
|
||||
<TabsTrigger
|
||||
key={period}
|
||||
value={period}
|
||||
disabled={disabled}
|
||||
className="text-xs capitalize"
|
||||
>
|
||||
{PERIOD_CONFIGS[period].label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</div>
|
||||
{displayPeriods.map((period) => (
|
||||
<TabsContent
|
||||
key={period}
|
||||
value={period}
|
||||
className={cn(withCard ? 'space-y-4' : 'px-0')}
|
||||
>
|
||||
<Card className={cn(!withCard && 'border-none shadow-none')}>
|
||||
<CardHeader className={cn('pb-1', !withCard && 'px-0')}>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Settings className="h-4 w-4" />
|
||||
<span className="capitalize">
|
||||
{PERIOD_CONFIGS[period].label} Configuration
|
||||
</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{PERIOD_CONFIGS[period].description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className={cn('space-y-4', !withCard && 'px-0')}>
|
||||
<CronFieldEditor
|
||||
period={period}
|
||||
fields={cronFields}
|
||||
onChange={handleFieldChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{showPresets && filteredPresets.length > 0 && (
|
||||
<Card className={cn(!withCard && 'border-none shadow-none')}>
|
||||
<CardHeader className={cn(!withCard && 'px-0')}>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Zap className="h-4 w-4" />
|
||||
Quick Presets
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Common cron expressions for quick setup
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className={cn(!withCard && 'px-0')}>
|
||||
<div className="grid gap-3 sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{filteredPresets.map((preset, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="outline"
|
||||
className="h-auto justify-start p-4 text-left"
|
||||
onClick={() => handlePresetSelect(preset)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className="w-full space-y-2">
|
||||
<div className="font-medium text-sm">
|
||||
{preset.label}
|
||||
</div>
|
||||
<div className="whitespace-normal break-words text-muted-foreground text-xs leading-relaxed">
|
||||
{preset.description}
|
||||
</div>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="mt-1 break-all font-mono text-xs"
|
||||
>
|
||||
{preset.value}
|
||||
</Badge>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
{/* Current Expression & Preview */}
|
||||
{showGeneratedExpression && (
|
||||
<Card className={cn(!withCard && 'border-none shadow-none')}>
|
||||
<CardHeader className={cn(!withCard && 'px-0')}>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Clock className="h-4 w-4" />
|
||||
Generated Expression
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className={cn('space-y-4', !withCard && 'px-0')}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="px-3 py-1 font-mono text-sm">
|
||||
{currentExpression}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{showPreview && nextRuns.length > 0 && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<h4 className="flex items-center gap-2 font-medium text-sm">
|
||||
<Calendar className="h-4 w-4" />
|
||||
Next Runs({timezone})
|
||||
</h4>
|
||||
<div className="space-y-1">
|
||||
{nextRuns.map((date, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between rounded bg-muted/50 px-3 py-2 text-sm"
|
||||
>
|
||||
<span className="font-medium text-muted-foreground">
|
||||
#{index + 1}
|
||||
</span>
|
||||
|
||||
<span className="font-mono">
|
||||
{date.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface CronFieldEditorProps {
|
||||
period: CronPeriod;
|
||||
fields: Record<CronField, string>;
|
||||
onChange: (field: CronField, value: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const CronFieldEditor: FC<CronFieldEditorProps> = ({
|
||||
period,
|
||||
fields,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const relevantFields = [...PERIOD_CONFIGS[period].fields] as CronField[];
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{relevantFields.map((field) => {
|
||||
const config = FIELD_CONFIGS[field];
|
||||
const currentValue = fields[field];
|
||||
|
||||
return (
|
||||
<div key={field} className="space-y-2">
|
||||
<Label className="font-medium text-sm capitalize">
|
||||
{field.replace(/([A-Z])/g, ' $1').toLowerCase()}
|
||||
</Label>
|
||||
|
||||
{field === 'month' || field === 'dayOfWeek' ? (
|
||||
<Select
|
||||
value={currentValue}
|
||||
onValueChange={(value) => onChange(field, value)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="*">Any</SelectItem>
|
||||
{config.options?.map((option, index) => (
|
||||
<SelectItem key={index} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
// biome-ignore lint/nursery/noNestedTernary: <explanation>
|
||||
) : field === 'dayOfMonth' ? (
|
||||
<div className="space-y-2">
|
||||
<Select
|
||||
value={currentValue}
|
||||
onValueChange={(value) => onChange(field, value)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{config.options?.map((option, index) => (
|
||||
<SelectItem key={index} value={option.value.toString()}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
{Array.from({ length: 31 }, (_, i) => i + 1).map((day) => (
|
||||
<SelectItem key={day} value={day.toString()}>
|
||||
{day}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={currentValue === '*' ? '*' : 'specific'}
|
||||
onValueChange={(value) => {
|
||||
if (value === '*') {
|
||||
onChange(field, '*');
|
||||
} else if (value === 'specific' && currentValue === '*') {
|
||||
onChange(field, '0');
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<ToggleGroupItem value="*" className="min-w-fit text-xs">
|
||||
Any
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="specific"
|
||||
className="min-w-fit text-xs"
|
||||
>
|
||||
Specific
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
|
||||
{currentValue !== '*' && (
|
||||
<Input
|
||||
type="text"
|
||||
value={currentValue}
|
||||
onChange={(e) => onChange(field, e.target.value)}
|
||||
placeholder={`0-${config.max}`}
|
||||
disabled={disabled}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="text-muted-foreground text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<Info className="h-3 w-3" />
|
||||
<span>
|
||||
Range: {config.min}-{config.max}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
Supports: *, numbers, ranges (1-5), lists (1,3,5), steps
|
||||
(*/5)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function parseCronExpression(expression: string): Record<CronField, string> {
|
||||
const parts = expression.split(' ');
|
||||
|
||||
// Ensure we have 6 parts, pad with defaults if needed
|
||||
while (parts.length < 6) {
|
||||
parts.push('*');
|
||||
}
|
||||
|
||||
return {
|
||||
seconds: parts[0] || '0',
|
||||
minutes: parts[1] || '*',
|
||||
hours: parts[2] || '*',
|
||||
dayOfMonth: parts[3] || '*',
|
||||
month: parts[4] || '*',
|
||||
dayOfWeek: parts[5] || '*',
|
||||
year: parts[6] || '*',
|
||||
};
|
||||
}
|
||||
|
||||
export { CronBuilder };
|
277
apps/webui/src/components/domains/cron/cron-display.tsx
Normal file
277
apps/webui/src/components/domains/cron/cron-display.tsx
Normal file
@ -0,0 +1,277 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { cn } from '@/presentation/utils';
|
||||
import { getFutureMatches, isTimeMatches } from '@datasert/cronjs-matcher';
|
||||
import { parse } from '@datasert/cronjs-parser';
|
||||
import { AlertCircle, CalendarDays, CheckCircle, Clock } from 'lucide-react';
|
||||
import { type FC, useMemo } from 'react';
|
||||
import type {
|
||||
CronDisplayProps,
|
||||
CronNextRun,
|
||||
CronValidationResult,
|
||||
} from './types.js';
|
||||
|
||||
const CronDisplay: FC<CronDisplayProps> = ({
|
||||
expression,
|
||||
className,
|
||||
showNextRuns = true,
|
||||
nextRunsCount = 5,
|
||||
timezone = 'UTC',
|
||||
showDescription = true,
|
||||
withCard = true,
|
||||
}) => {
|
||||
const validationResult = useMemo((): CronValidationResult => {
|
||||
if (!expression) {
|
||||
return { isValid: false, error: 'No expression provided' };
|
||||
}
|
||||
|
||||
try {
|
||||
const _parsed = parse(`${expression} *`, { hasSeconds: true });
|
||||
return {
|
||||
isValid: true,
|
||||
description: generateDescription(expression),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: error instanceof Error ? error.message : 'Invalid expression',
|
||||
};
|
||||
}
|
||||
}, [expression]);
|
||||
|
||||
const nextRuns = useMemo((): CronNextRun[] => {
|
||||
if (!expression || !validationResult.isValid || !showNextRuns) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const matches = getFutureMatches(`${expression} *`, {
|
||||
matchCount: nextRunsCount,
|
||||
timezone,
|
||||
formatInTimezone: true,
|
||||
hasSeconds: true,
|
||||
});
|
||||
|
||||
return matches.map((match) => {
|
||||
const date = new Date(match);
|
||||
return {
|
||||
date,
|
||||
timestamp: date.getTime(),
|
||||
formatted: date.toLocaleString(),
|
||||
relative: getRelativeTime(date),
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to get future matches:', error);
|
||||
return [];
|
||||
}
|
||||
}, [
|
||||
expression,
|
||||
validationResult.isValid,
|
||||
showNextRuns,
|
||||
nextRunsCount,
|
||||
timezone,
|
||||
]);
|
||||
|
||||
const isCurrentTimeMatch = useMemo(() => {
|
||||
if (!expression || !validationResult.isValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return isTimeMatches(
|
||||
`${expression} *`,
|
||||
new Date().toISOString(),
|
||||
timezone
|
||||
);
|
||||
} catch (_error: unknown) {
|
||||
return false;
|
||||
}
|
||||
}, [expression, validationResult.isValid, timezone]);
|
||||
|
||||
if (!expression) {
|
||||
return (
|
||||
<Card className={cn(className, !withCard && 'border-none shadow-none')}>
|
||||
<CardContent className={cn('p-4', !withCard && 'px-0')}>
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<span className="text-sm">No cron expression set</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className={cn(className, !withCard && 'border-none shadow-none')}>
|
||||
<CardHeader className={cn(!withCard && 'px-0')}>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Clock className="h-4 w-4" />
|
||||
Cron Expression
|
||||
{isCurrentTimeMatch && (
|
||||
<Badge variant="default" className="text-xs">
|
||||
<CheckCircle className="mr-1 h-3 w-3" />
|
||||
Active Now
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
<Badge
|
||||
variant={validationResult.isValid ? 'secondary' : 'destructive'}
|
||||
className="font-mono text-xs"
|
||||
>
|
||||
{expression}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{validationResult.isValid &&
|
||||
showDescription &&
|
||||
validationResult.description && (
|
||||
<CardDescription className="text-sm">
|
||||
{validationResult.description}
|
||||
</CardDescription>
|
||||
)}
|
||||
|
||||
{!validationResult.isValid && validationResult.error && (
|
||||
<CardDescription className="flex items-center gap-2 text-destructive text-sm">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
{validationResult.error}
|
||||
</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
|
||||
{validationResult.isValid && showNextRuns && nextRuns.length > 0 && (
|
||||
<CardContent className={cn('pt-0', !withCard && 'px-0')}>
|
||||
<div className="space-y-3">
|
||||
<h4 className="flex items-center gap-2 font-medium text-sm">
|
||||
<CalendarDays className="h-4 w-4" />
|
||||
Next Runs
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{timezone}
|
||||
</Badge>
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{nextRuns.map((run, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between rounded border bg-muted/50 p-2"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-6 font-medium text-muted-foreground text-xs">
|
||||
#{index + 1}
|
||||
</span>
|
||||
<span className="font-mono text-sm">{run.formatted}</span>
|
||||
</div>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{run.relative}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
function generateDescription(expression: string): string {
|
||||
// Enhanced description generator based on common patterns
|
||||
const parts = expression.split(' ');
|
||||
if (parts.length !== 6) {
|
||||
return expression;
|
||||
}
|
||||
|
||||
const [sec, min, hour, day, month, weekday] = parts;
|
||||
|
||||
// Common patterns
|
||||
const patterns: Record<string, string> = {
|
||||
'* * * * * *': 'Every second',
|
||||
'0 * * * * *': 'Every minute',
|
||||
'0 0 * * * *': 'Every hour',
|
||||
'0 0 0 * * *': 'Daily at midnight',
|
||||
'0 0 0 * * 0': 'Every Sunday at midnight',
|
||||
'0 0 0 * * 1': 'Every Monday at midnight',
|
||||
'0 0 0 * * 2': 'Every Tuesday at midnight',
|
||||
'0 0 0 * * 3': 'Every Wednesday at midnight',
|
||||
'0 0 0 * * 4': 'Every Thursday at midnight',
|
||||
'0 0 0 * * 5': 'Every Friday at midnight',
|
||||
'0 0 0 * * 6': 'Every Saturday at midnight',
|
||||
'0 0 0 1 * *': 'Monthly on the 1st at midnight',
|
||||
'0 0 0 1 1 *': 'Yearly on January 1st at midnight',
|
||||
'0 30 9 * * 1-5': 'Weekdays at 9:30 AM',
|
||||
'0 0 */6 * * *': 'Every 6 hours',
|
||||
'0 */30 * * * *': 'Every 30 minutes',
|
||||
'0 */15 * * * *': 'Every 15 minutes',
|
||||
'0 */5 * * * *': 'Every 5 minutes',
|
||||
};
|
||||
|
||||
if (patterns[expression]) {
|
||||
return patterns[expression];
|
||||
}
|
||||
|
||||
// Generate dynamic description
|
||||
let description = 'At ';
|
||||
|
||||
if (sec !== '*' && sec !== '0') {
|
||||
description += `second ${sec}, `;
|
||||
}
|
||||
if (min !== '*') {
|
||||
description += `minute ${min}, `;
|
||||
}
|
||||
if (hour !== '*') {
|
||||
description += `hour ${hour}, `;
|
||||
}
|
||||
|
||||
if (day !== '*' && weekday !== '*') {
|
||||
description += `on day ${day} and weekday ${weekday} `;
|
||||
} else if (day !== '*') {
|
||||
description += `on day ${day} `;
|
||||
} else if (weekday !== '*') {
|
||||
description += `on weekday ${weekday} `;
|
||||
}
|
||||
|
||||
if (month !== '*') {
|
||||
description += `in month ${month}`;
|
||||
}
|
||||
|
||||
// biome-ignore lint/performance/useTopLevelRegex: <explanation>
|
||||
return description.replace(/,\s*$/, '').replace(/At\s*$/, 'Every occurrence');
|
||||
}
|
||||
|
||||
function getRelativeTime(date: Date): string {
|
||||
const now = new Date();
|
||||
const diffMs = date.getTime() - now.getTime();
|
||||
|
||||
if (diffMs < 0) {
|
||||
return 'Past';
|
||||
}
|
||||
|
||||
const diffSec = Math.floor(diffMs / 1000);
|
||||
const diffMin = Math.floor(diffSec / 60);
|
||||
const diffHour = Math.floor(diffMin / 60);
|
||||
const diffDay = Math.floor(diffHour / 24);
|
||||
|
||||
if (diffSec < 60) {
|
||||
return `in ${diffSec}s`;
|
||||
}
|
||||
if (diffMin < 60) {
|
||||
return `in ${diffMin}m`;
|
||||
}
|
||||
if (diffHour < 24) {
|
||||
return `in ${diffHour}h`;
|
||||
}
|
||||
if (diffDay < 7) {
|
||||
return `in ${diffDay}d`;
|
||||
}
|
||||
|
||||
return `in ${Math.floor(diffDay / 7)}w`;
|
||||
}
|
||||
|
||||
export { CronDisplay };
|
413
apps/webui/src/components/domains/cron/cron-example.tsx
Normal file
413
apps/webui/src/components/domains/cron/cron-example.tsx
Normal file
@ -0,0 +1,413 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Code2, Play, Settings, Type } from 'lucide-react';
|
||||
import { type FC, useCallback, useState } from 'react';
|
||||
import { CronBuilder } from './cron-builder.js';
|
||||
import { CronDisplay } from './cron-display.js';
|
||||
import { CronInput } from './cron-input.js';
|
||||
import { Cron } from './cron.js';
|
||||
|
||||
const CronExample: FC = () => {
|
||||
const [inputValue, setInputValue] = useState('0 30 9 * * 1-5');
|
||||
const [builderValue, setBuilderValue] = useState('0 0 */6 * * *');
|
||||
const [fullValue, setFullValue] = useState('0 */15 * * * *');
|
||||
const [displayValue] = useState('0 0 0 * * 0');
|
||||
|
||||
const examples = [
|
||||
{
|
||||
label: 'Every minute',
|
||||
expression: '0 * * * * *',
|
||||
description: 'Runs at the start of every minute',
|
||||
},
|
||||
{
|
||||
label: 'Every 5 minutes',
|
||||
expression: '0 */5 * * * *',
|
||||
description: 'Runs every 5 minutes',
|
||||
},
|
||||
{
|
||||
label: 'Every hour',
|
||||
expression: '0 0 * * * *',
|
||||
description: 'Runs at the start of every hour',
|
||||
},
|
||||
{
|
||||
label: 'Daily at 9 AM',
|
||||
expression: '0 0 9 * * *',
|
||||
description: 'Runs every day at 9:00 AM',
|
||||
},
|
||||
{
|
||||
label: 'Weekdays at 9:30 AM',
|
||||
expression: '0 30 9 * * 1-5',
|
||||
description: 'Runs Monday through Friday at 9:30 AM',
|
||||
},
|
||||
{
|
||||
label: 'Every Sunday',
|
||||
expression: '0 0 0 * * 0',
|
||||
description: 'Runs every Sunday at midnight',
|
||||
},
|
||||
{
|
||||
label: 'First day of month',
|
||||
expression: '0 0 0 1 * *',
|
||||
description: 'Runs on the 1st day of every month',
|
||||
},
|
||||
{
|
||||
label: 'Every quarter',
|
||||
expression: '0 0 0 1 */3 *',
|
||||
description: 'Runs on the 1st day of every quarter',
|
||||
},
|
||||
];
|
||||
|
||||
const handleCopyExample = useCallback(async (expression: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(expression);
|
||||
} catch (error) {
|
||||
console.warn('Failed to copy to clipboard:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Header */}
|
||||
<div className="space-y-2">
|
||||
<h1 className="font-bold text-3xl">Cron Expression Components</h1>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
A comprehensive set of components for creating and managing cron
|
||||
expressions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Examples */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Code2 className="h-5 w-5" />
|
||||
Common Examples
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Click any example to copy the expression to your clipboard
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{examples.map((example, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="outline"
|
||||
className="h-auto flex-col items-start p-4 text-left"
|
||||
onClick={() => handleCopyExample(example.expression)}
|
||||
>
|
||||
<div className="w-full space-y-2">
|
||||
<div className="font-medium text-sm">{example.label}</div>
|
||||
<Badge variant="secondary" className="font-mono text-xs">
|
||||
{example.expression}
|
||||
</Badge>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{example.description}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Component Examples */}
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-2">
|
||||
<h2 className="font-semibold text-2xl">Component Examples</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Interactive examples showing different ways to use the cron
|
||||
components.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="full" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="full">Complete</TabsTrigger>
|
||||
<TabsTrigger value="input">Input Only</TabsTrigger>
|
||||
<TabsTrigger value="builder">Builder Only</TabsTrigger>
|
||||
<TabsTrigger value="display">Display Only</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="full" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Settings className="h-5 w-5" />
|
||||
Complete Cron Component
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Full-featured component with both input and visual builder
|
||||
modes, validation, preview, and help documentation.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<Cron
|
||||
value={fullValue}
|
||||
onChange={setFullValue}
|
||||
mode="both"
|
||||
showPreview={true}
|
||||
showDescription={true}
|
||||
timezone="UTC"
|
||||
/>
|
||||
|
||||
<div className="rounded bg-muted p-4">
|
||||
<h4 className="mb-2 font-medium text-sm">Current Value:</h4>
|
||||
<Badge variant="outline" className="font-mono">
|
||||
{fullValue || 'No expression set'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="input" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Type className="h-5 w-5" />
|
||||
Text Input Component
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Simple text input with validation, help text, and real-time
|
||||
feedback.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<CronInput
|
||||
value={inputValue}
|
||||
onChange={setInputValue}
|
||||
placeholder="Enter cron expression..."
|
||||
/>
|
||||
|
||||
<div className="rounded bg-muted p-4">
|
||||
<h4 className="mb-2 font-medium text-sm">Current Value:</h4>
|
||||
<Badge variant="outline" className="font-mono">
|
||||
{inputValue || 'No expression set'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Input-Only Mode</CardTitle>
|
||||
<CardDescription>
|
||||
Using the main Cron component in input-only mode with preview.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Cron
|
||||
value={inputValue}
|
||||
onChange={setInputValue}
|
||||
mode="input"
|
||||
showPreview={true}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="builder" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Settings className="h-5 w-5" />
|
||||
Visual Builder Component
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Visual interface for building cron expressions with presets
|
||||
and field editors.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<CronBuilder
|
||||
value={builderValue}
|
||||
onChange={setBuilderValue}
|
||||
showPreview={true}
|
||||
/>
|
||||
|
||||
<div className="rounded bg-muted p-4">
|
||||
<h4 className="mb-2 font-medium text-sm">Current Value:</h4>
|
||||
<Badge variant="outline" className="font-mono">
|
||||
{builderValue || 'No expression set'}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Builder-Only Mode</CardTitle>
|
||||
<CardDescription>
|
||||
Using the main Cron component in builder-only mode.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Cron
|
||||
value={builderValue}
|
||||
onChange={setBuilderValue}
|
||||
mode="builder"
|
||||
showPreview={false}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="display" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Play className="h-5 w-5" />
|
||||
Display Component
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Read-only component that shows cron expression details,
|
||||
description, and next run times.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<CronDisplay
|
||||
expression={displayValue}
|
||||
showNextRuns={true}
|
||||
showDescription={true}
|
||||
nextRunsCount={5}
|
||||
timezone="UTC"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Multiple Timezone Display</CardTitle>
|
||||
<CardDescription>
|
||||
Same expression displayed in different timezones.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<div>
|
||||
<h4 className="mb-2 font-medium text-sm">UTC</h4>
|
||||
<CronDisplay
|
||||
expression="0 0 12 * * *"
|
||||
showNextRuns={true}
|
||||
nextRunsCount={3}
|
||||
timezone="UTC"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 font-medium text-sm">
|
||||
America/New_York
|
||||
</h4>
|
||||
<CronDisplay
|
||||
expression="0 0 12 * * *"
|
||||
showNextRuns={true}
|
||||
nextRunsCount={3}
|
||||
timezone="America/New_York"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* Usage Examples */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Usage Examples</CardTitle>
|
||||
<CardDescription>
|
||||
Code examples showing how to integrate these components into your
|
||||
application.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h4 className="mb-2 font-medium text-sm">Basic Usage</h4>
|
||||
<div className="rounded bg-muted p-4 font-mono text-sm">
|
||||
<pre>{`import { Cron } from '@/components/cron';
|
||||
|
||||
function MyComponent() {
|
||||
const [cronExpression, setCronExpression] = useState('0 0 * * * *');
|
||||
|
||||
return (
|
||||
<Cron
|
||||
value={cronExpression}
|
||||
onChange={setCronExpression}
|
||||
mode="both"
|
||||
showPreview={true}
|
||||
/>
|
||||
);
|
||||
}`}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="mb-2 font-medium text-sm">
|
||||
Input Only with Validation
|
||||
</h4>
|
||||
<div className="rounded bg-muted p-4 font-mono text-sm">
|
||||
<pre>{`import { CronInput } from '@/components/cron';
|
||||
|
||||
function ScheduleForm() {
|
||||
const [expression, setExpression] = useState('');
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
|
||||
return (
|
||||
<CronInput
|
||||
value={expression}
|
||||
onChange={setExpression}
|
||||
onValidate={setIsValid}
|
||||
placeholder="0 0 * * * *"
|
||||
/>
|
||||
);
|
||||
}`}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="mb-2 font-medium text-sm">
|
||||
Display Schedule Information
|
||||
</h4>
|
||||
<div className="rounded bg-muted p-4 font-mono text-sm">
|
||||
<pre>{`import { CronDisplay } from '@/components/cron';
|
||||
|
||||
function SchedulePreview({ schedule }) {
|
||||
return (
|
||||
<CronDisplay
|
||||
expression={schedule.cronExpression}
|
||||
showNextRuns={true}
|
||||
showDescription={true}
|
||||
timezone={schedule.timezone}
|
||||
/>
|
||||
);
|
||||
}`}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { CronExample };
|
186
apps/webui/src/components/domains/cron/cron-input.tsx
Normal file
186
apps/webui/src/components/domains/cron/cron-input.tsx
Normal file
@ -0,0 +1,186 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { cn } from '@/presentation/utils';
|
||||
import { parse } from '@datasert/cronjs-parser';
|
||||
import { AlertCircle, CheckCircle, Info } from 'lucide-react';
|
||||
import {
|
||||
type ChangeEvent,
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import type { CronInputProps, CronValidationResult } from './types.js';
|
||||
|
||||
const CronInput = forwardRef<HTMLInputElement, CronInputProps>(
|
||||
(
|
||||
{
|
||||
value,
|
||||
onChange,
|
||||
onValidate,
|
||||
placeholder = '0 0 * * * *',
|
||||
className,
|
||||
disabled,
|
||||
readOnly,
|
||||
error,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [internalValue, setInternalValue] = useState(value || '');
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const validationResult = useMemo((): CronValidationResult => {
|
||||
if (!internalValue.trim()) {
|
||||
return { isValid: false, error: 'Expression is required' };
|
||||
}
|
||||
|
||||
try {
|
||||
parse(`${internalValue} *`, { hasSeconds: true });
|
||||
return { isValid: true };
|
||||
} catch (parseError) {
|
||||
return {
|
||||
isValid: false,
|
||||
error:
|
||||
parseError instanceof Error
|
||||
? parseError.message
|
||||
: 'Invalid cron expression',
|
||||
};
|
||||
}
|
||||
}, [internalValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalValue(value || '');
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
onValidate?.(validationResult.isValid);
|
||||
}, [validationResult.isValid, onValidate]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value;
|
||||
setInternalValue(newValue);
|
||||
onChange?.(newValue);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
setIsFocused(true);
|
||||
}, []);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
setIsFocused(false);
|
||||
}, []);
|
||||
|
||||
const hasError =
|
||||
error || (!validationResult.isValid && internalValue.trim());
|
||||
const showSuccess =
|
||||
validationResult.isValid && internalValue.trim() && !isFocused;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="relative">
|
||||
<Input
|
||||
ref={ref}
|
||||
type="text"
|
||||
value={internalValue}
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
placeholder={placeholder}
|
||||
className={cn(
|
||||
'pr-10 font-mono text-sm',
|
||||
hasError && 'border-destructive focus-visible:ring-destructive',
|
||||
showSuccess && 'border-success focus-visible:ring-success',
|
||||
className
|
||||
)}
|
||||
disabled={disabled}
|
||||
readOnly={readOnly}
|
||||
aria-invalid={hasError ? 'true' : 'false'}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
{/* Status icon */}
|
||||
<div className="-translate-y-1/2 absolute top-1/2 right-3">
|
||||
{hasError && <AlertCircle className="h-4 w-4 text-destructive" />}
|
||||
{showSuccess && <CheckCircle className="h-4 w-4 text-success" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
{hasError && (
|
||||
<div className="flex items-center gap-2 text-destructive text-sm">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<span>{error || validationResult.error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Help text when focused */}
|
||||
{isFocused && !hasError && (
|
||||
<div className="space-y-2 text-muted-foreground text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Info className="h-4 w-4" />
|
||||
<span>Format: second minute hour day month weekday</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="flex items-center gap-1">
|
||||
<Badge variant="outline" className="font-mono text-xs">
|
||||
*
|
||||
</Badge>
|
||||
<span>any value</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Badge variant="outline" className="font-mono text-xs">
|
||||
,
|
||||
</Badge>
|
||||
<span>list separator</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Badge variant="outline" className="font-mono text-xs">
|
||||
-
|
||||
</Badge>
|
||||
<span>range</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Badge variant="outline" className="font-mono text-xs">
|
||||
/
|
||||
</Badge>
|
||||
<span>step value</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 space-y-1">
|
||||
<div className="font-medium text-xs">Examples:</div>
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge variant="secondary" className="font-mono text-xs">
|
||||
0 * * * * *
|
||||
</Badge>
|
||||
<span>Every minute</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge variant="secondary" className="font-mono text-xs">
|
||||
0 0 * * * *
|
||||
</Badge>
|
||||
<span>Every hour</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge variant="secondary" className="font-mono text-xs">
|
||||
0 30 9 * * 1-5
|
||||
</Badge>
|
||||
<span>Weekdays at 9:30 AM</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CronInput.displayName = 'CronInput';
|
||||
|
||||
export { CronInput };
|
500
apps/webui/src/components/domains/cron/cron.tsx
Normal file
500
apps/webui/src/components/domains/cron/cron.tsx
Normal file
@ -0,0 +1,500 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { cn } from '@/presentation/utils';
|
||||
import { parse } from '@datasert/cronjs-parser';
|
||||
import {
|
||||
AlertCircle,
|
||||
Bolt,
|
||||
Check,
|
||||
Code2,
|
||||
Copy,
|
||||
Settings,
|
||||
Type,
|
||||
} from 'lucide-react';
|
||||
import { type FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { CronBuilder } from './cron-builder';
|
||||
import { CronDisplay } from './cron-display';
|
||||
import { CronInput } from './cron-input';
|
||||
import {
|
||||
CronMode,
|
||||
type CronPrimitiveMode,
|
||||
type CronProps,
|
||||
type CronValidationResult,
|
||||
} from './types';
|
||||
|
||||
const PLACEHOLDER = '0 0 * * * *';
|
||||
|
||||
const Cron: FC<CronProps> = ({
|
||||
value = '',
|
||||
onChange,
|
||||
activeMode = 'input',
|
||||
onActiveModeChange,
|
||||
onValidate,
|
||||
className,
|
||||
mode = 'both',
|
||||
disabled = false,
|
||||
placeholder = PLACEHOLDER,
|
||||
showPreview = true,
|
||||
showDescription = true,
|
||||
timezone = 'UTC',
|
||||
error,
|
||||
children,
|
||||
showHelp = true,
|
||||
displayPeriods,
|
||||
defaultTab,
|
||||
presets,
|
||||
showPresets,
|
||||
withCard = true,
|
||||
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
|
||||
}) => {
|
||||
const [internalValue, setInternalValue] = useState(value || '');
|
||||
const [internalActiveMode, setInternalActiveMode] =
|
||||
useState<CronPrimitiveMode>(
|
||||
mode === CronMode.Both ? activeMode : (mode as CronPrimitiveMode)
|
||||
);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const validationResult = useMemo((): CronValidationResult => {
|
||||
if (!internalValue.trim()) {
|
||||
return { isValid: false, error: 'Expression is required', isEmpty: true };
|
||||
}
|
||||
|
||||
try {
|
||||
parse(`${internalValue} *`, { hasSeconds: true });
|
||||
return { isValid: true };
|
||||
} catch (parseError) {
|
||||
return {
|
||||
isValid: false,
|
||||
error:
|
||||
parseError instanceof Error
|
||||
? parseError.message
|
||||
: 'Invalid cron expression',
|
||||
};
|
||||
}
|
||||
}, [internalValue]);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalValue(value || '');
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
onValidate?.(validationResult.isValid);
|
||||
}, [validationResult.isValid, onValidate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === 'both') {
|
||||
setInternalActiveMode(activeMode);
|
||||
}
|
||||
}, [activeMode, mode]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(newValue: string) => {
|
||||
setInternalValue(newValue);
|
||||
onChange?.(newValue);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const handleActiveModeChange = useCallback(
|
||||
(mode: CronPrimitiveMode) => {
|
||||
setInternalActiveMode(mode);
|
||||
onActiveModeChange?.(mode);
|
||||
},
|
||||
[onActiveModeChange]
|
||||
);
|
||||
|
||||
const handleCopy = useCallback(async () => {
|
||||
if (!internalValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(internalValue);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (error) {
|
||||
console.warn('Failed to copy to clipboard:', error);
|
||||
}
|
||||
}, [internalValue]);
|
||||
|
||||
const hasError =
|
||||
!!error || !!(!validationResult.isValid && internalValue.trim());
|
||||
|
||||
if (mode === 'input') {
|
||||
return (
|
||||
<div className={cn(withCard && 'space-y-4', className)}>
|
||||
<CronInput
|
||||
value={internalValue}
|
||||
onChange={handleChange}
|
||||
onValidate={onValidate}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
error={error}
|
||||
/>
|
||||
|
||||
{showPreview &&
|
||||
(validationResult.isValid || validationResult.isEmpty) && (
|
||||
<CronDisplay
|
||||
expression={
|
||||
validationResult.isEmpty ? placeholder : internalValue
|
||||
}
|
||||
showNextRuns={true}
|
||||
showDescription={showDescription}
|
||||
timezone={timezone}
|
||||
nextRunsCount={3}
|
||||
withCard={withCard}
|
||||
/>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (mode === 'builder') {
|
||||
return (
|
||||
<div className={cn(withCard && 'space-y-4', className)}>
|
||||
<CronBuilder
|
||||
value={internalValue}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
showPreview={showPreview}
|
||||
displayPeriods={displayPeriods}
|
||||
defaultTab={defaultTab}
|
||||
presets={presets}
|
||||
showPresets={showPresets}
|
||||
showGeneratedExpression={true}
|
||||
timezone={timezone}
|
||||
withCard={withCard}
|
||||
/>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(withCard && 'space-y-6', className)}>
|
||||
<Card className={cn(!withCard && 'border-none pt-0 shadow-none')}>
|
||||
<CardHeader className={cn(!withCard && 'px-0')}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Bolt className="h-4 w-4" />
|
||||
Cron Expression Builder
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm">
|
||||
Create and validate cron expressions using visual builder or
|
||||
text input
|
||||
</CardDescription>
|
||||
</div>
|
||||
{internalValue && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
validationResult.isValid ? 'secondary' : 'destructive'
|
||||
}
|
||||
className="font-mono text-sm"
|
||||
>
|
||||
{internalValue}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCopy}
|
||||
disabled={!internalValue || hasError}
|
||||
className="h-8 px-2"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasError && (
|
||||
<div className="mt-3 flex items-center gap-2 text-destructive text-sm">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<span>{error || validationResult.error}</span>
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className={cn(!withCard && 'px-0')}>
|
||||
<Tabs
|
||||
value={internalActiveMode}
|
||||
onValueChange={(value) =>
|
||||
handleActiveModeChange(value as 'input' | 'builder')
|
||||
}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="input" className="flex items-center gap-2">
|
||||
<Type className="h-4 w-4" />
|
||||
Text Input
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="builder" className="flex items-center gap-2">
|
||||
<Settings className="h-4 w-4" />
|
||||
Visual Builder
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="input" className="mt-6 space-y-4">
|
||||
<CronInput
|
||||
value={internalValue}
|
||||
onChange={handleChange}
|
||||
onValidate={onValidate}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
error={error}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="builder" className="mt-6">
|
||||
<CronBuilder
|
||||
value={internalValue}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
showPreview={false}
|
||||
displayPeriods={displayPeriods}
|
||||
defaultTab={defaultTab}
|
||||
presets={presets}
|
||||
showPresets={showPresets}
|
||||
showGeneratedExpression={false}
|
||||
timezone={timezone}
|
||||
withCard={withCard}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Preview Section */}
|
||||
{showPreview &&
|
||||
(validationResult.isValid || validationResult.isEmpty) && (
|
||||
<>
|
||||
{!withCard && <Separator />}
|
||||
<CronDisplay
|
||||
expression={
|
||||
validationResult.isEmpty ? placeholder : internalValue
|
||||
}
|
||||
showNextRuns={true}
|
||||
showDescription={showDescription}
|
||||
timezone={timezone}
|
||||
nextRunsCount={3}
|
||||
withCard={withCard}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Help Section */}
|
||||
{showHelp && (
|
||||
<>
|
||||
{!withCard && <Separator />}
|
||||
<Card className={cn(!withCard && 'border-none shadow-none')}>
|
||||
<CardHeader className={cn(!withCard && 'px-0')}>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Code2 className="h-4 w-4" />
|
||||
Cron Expression Format
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className={cn(!withCard && 'px-0')}>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-6 gap-2 text-center text-sm">
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium font-mono text-muted-foreground">
|
||||
Second
|
||||
</div>
|
||||
<div className="text-xs">0-59</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium font-mono text-muted-foreground">
|
||||
Minute
|
||||
</div>
|
||||
<div className="text-xs">0-59</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium font-mono text-muted-foreground">
|
||||
Hour
|
||||
</div>
|
||||
<div className="text-xs">0-23</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium font-mono text-muted-foreground">
|
||||
Day
|
||||
</div>
|
||||
<div className="text-xs">1-31</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium font-mono text-muted-foreground">
|
||||
Month
|
||||
</div>
|
||||
<div className="text-xs">1-12</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium font-mono text-muted-foreground">
|
||||
Weekday
|
||||
</div>
|
||||
<div className="text-xs">0-6</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="font-mono">
|
||||
*
|
||||
</Badge>
|
||||
<span className="text-sm">Any value</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
Matches all possible values
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="font-mono">
|
||||
5
|
||||
</Badge>
|
||||
<span className="text-sm">Specific value</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
Matches exactly this value
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="font-mono">
|
||||
1-5
|
||||
</Badge>
|
||||
<span className="text-sm">Range</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
Matches values 1 through 5
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="font-mono">
|
||||
1,3,5
|
||||
</Badge>
|
||||
<span className="text-sm">List</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
Matches values 1, 3, and 5
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="font-mono">
|
||||
*/5
|
||||
</Badge>
|
||||
<span className="text-sm">Step</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
Every 5th value
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="font-mono">
|
||||
0-10/2
|
||||
</Badge>
|
||||
<span className="text-sm">Range + Step</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
Even values 0-10
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="font-mono">
|
||||
?
|
||||
</Badge>
|
||||
<span className="text-sm">No specific</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
Used when day/weekday conflicts
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="font-mono">
|
||||
L
|
||||
</Badge>
|
||||
<span className="text-sm">Last</span>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
Last day of month/week
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium text-sm">Common Examples:</h4>
|
||||
<div className="grid gap-2 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge variant="secondary" className="font-mono text-xs">
|
||||
0 0 * * * *
|
||||
</Badge>
|
||||
<span className="text-muted-foreground">Every hour</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge variant="secondary" className="font-mono text-xs">
|
||||
0 */15 * * * *
|
||||
</Badge>
|
||||
<span className="text-muted-foreground">
|
||||
Every 15 minutes
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge variant="secondary" className="font-mono text-xs">
|
||||
0 0 0 * * *
|
||||
</Badge>
|
||||
<span className="text-muted-foreground">
|
||||
Daily at midnight
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge variant="secondary" className="font-mono text-xs">
|
||||
0 30 9 * * 1-5
|
||||
</Badge>
|
||||
<span className="text-muted-foreground">
|
||||
Weekdays at 9:30 AM
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Cron };
|
20
apps/webui/src/components/domains/cron/index.ts
Normal file
20
apps/webui/src/components/domains/cron/index.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export { Cron } from './cron';
|
||||
export { CronInput } from './cron-input';
|
||||
export { CronBuilder } from './cron-builder';
|
||||
export { CronDisplay } from './cron-display';
|
||||
export { CronExample } from './cron-example';
|
||||
|
||||
export {
|
||||
type CronProps,
|
||||
type CronInputProps,
|
||||
type CronBuilderProps,
|
||||
type CronDisplayProps,
|
||||
type CronExpression,
|
||||
CronPeriod,
|
||||
type CronPreset,
|
||||
type CronValidationResult,
|
||||
type CronNextRun,
|
||||
type CronFieldConfig,
|
||||
CronField,
|
||||
type PeriodConfig,
|
||||
} from './types';
|
162
apps/webui/src/components/domains/cron/types.ts
Normal file
162
apps/webui/src/components/domains/cron/types.ts
Normal file
@ -0,0 +1,162 @@
|
||||
import type { ClassValue } from 'clsx';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface CronExpression {
|
||||
seconds?: string;
|
||||
minutes?: string;
|
||||
hours?: string;
|
||||
dayOfMonth?: string;
|
||||
month?: string;
|
||||
dayOfWeek?: string;
|
||||
year?: string;
|
||||
}
|
||||
|
||||
export interface CronDisplayProps {
|
||||
expression: string;
|
||||
className?: ClassValue;
|
||||
showNextRuns?: boolean;
|
||||
nextRunsCount?: number;
|
||||
timezone?: string;
|
||||
showDescription?: boolean;
|
||||
withCard?: boolean;
|
||||
}
|
||||
|
||||
export interface CronInputProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
onValidate?: (isValid: boolean) => void;
|
||||
placeholder?: string;
|
||||
className?: ClassValue;
|
||||
disabled?: boolean;
|
||||
readOnly?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface CronBuilderProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
className?: ClassValue;
|
||||
disabled?: boolean;
|
||||
showPreview?: boolean;
|
||||
defaultTab?: CronPeriod;
|
||||
displayPeriods?: CronPeriod[];
|
||||
presets?: CronPreset[];
|
||||
showPresets?: boolean;
|
||||
showGeneratedExpression?: boolean;
|
||||
timezone?: string;
|
||||
withCard?: boolean;
|
||||
}
|
||||
|
||||
export const CronPrimitiveMode = {
|
||||
Input: 'input',
|
||||
Builder: 'builder',
|
||||
} as const;
|
||||
|
||||
export type CronPrimitiveMode =
|
||||
(typeof CronPrimitiveMode)[keyof typeof CronPrimitiveMode];
|
||||
|
||||
export const CronMode = {
|
||||
Input: 'input',
|
||||
Builder: 'builder',
|
||||
Both: 'both',
|
||||
} as const;
|
||||
|
||||
export type CronMode = (typeof CronMode)[keyof typeof CronMode];
|
||||
|
||||
export interface CronProps {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
activeMode?: CronPrimitiveMode;
|
||||
onActiveModeChange?: (mode: CronPrimitiveMode) => void;
|
||||
onValidate?: (isValid: boolean) => void;
|
||||
className?: ClassValue;
|
||||
mode?: CronMode;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
showPreview?: boolean;
|
||||
showDescription?: boolean;
|
||||
timezone?: string;
|
||||
error?: string;
|
||||
children?: ReactNode;
|
||||
defaultTab?: CronPeriod;
|
||||
displayPeriods?: CronPeriod[];
|
||||
presets?: CronPreset[];
|
||||
showHelp?: boolean;
|
||||
showPresets?: boolean;
|
||||
withCard?: boolean;
|
||||
}
|
||||
|
||||
export const CronPeriod = {
|
||||
Minute: 'minute',
|
||||
Hourly: 'hourly',
|
||||
Daily: 'daily',
|
||||
Weekly: 'weekly',
|
||||
Monthly: 'monthly',
|
||||
Yearly: 'yearly',
|
||||
Custom: 'custom',
|
||||
} as const;
|
||||
|
||||
export type CronPeriod = (typeof CronPeriod)[keyof typeof CronPeriod];
|
||||
|
||||
export interface CronFieldProps {
|
||||
period: CronPeriod;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
className?: ClassValue;
|
||||
}
|
||||
|
||||
export interface CronPreset {
|
||||
label: string;
|
||||
value: string;
|
||||
description: string;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
export interface CronValidationResult {
|
||||
isValid: boolean;
|
||||
error?: string;
|
||||
description?: string;
|
||||
isEmpty?: boolean;
|
||||
}
|
||||
|
||||
export interface CronNextRun {
|
||||
date: Date;
|
||||
timestamp: number;
|
||||
formatted: string;
|
||||
relative: string;
|
||||
}
|
||||
|
||||
export interface PeriodConfig {
|
||||
label: string;
|
||||
description: string;
|
||||
defaultValue: string;
|
||||
fields: {
|
||||
seconds?: boolean;
|
||||
minutes?: boolean;
|
||||
hours?: boolean;
|
||||
dayOfMonth?: boolean;
|
||||
month?: boolean;
|
||||
dayOfWeek?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const CronField = {
|
||||
Seconds: 'seconds',
|
||||
Minutes: 'minutes',
|
||||
Hours: 'hours',
|
||||
DayOfMonth: 'dayOfMonth',
|
||||
Month: 'month',
|
||||
DayOfWeek: 'dayOfWeek',
|
||||
Year: 'year',
|
||||
} as const;
|
||||
|
||||
export type CronField = (typeof CronField)[keyof typeof CronField];
|
||||
|
||||
export interface CronFieldConfig {
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
options?: Array<{ label: string; value: number | string }>;
|
||||
allowSpecial?: string[];
|
||||
}
|
52
apps/webui/src/components/ui/container-header.tsx
Normal file
52
apps/webui/src/components/ui/container-header.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { useCanGoBack, useNavigate, useRouter } from "@tanstack/react-router";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { type ReactNode, memo } from "react";
|
||||
import { Button } from "./button";
|
||||
|
||||
export interface ContainerHeaderProps {
|
||||
title: string;
|
||||
description: string;
|
||||
defaultBackTo?: string;
|
||||
actions?: ReactNode;
|
||||
}
|
||||
|
||||
export const ContainerHeader = memo(
|
||||
({ title, description, defaultBackTo, actions }: ContainerHeaderProps) => {
|
||||
const navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
const canGoBack = useCanGoBack();
|
||||
|
||||
const finalCanGoBack = canGoBack || !!defaultBackTo;
|
||||
|
||||
const handleBack = () => {
|
||||
if (canGoBack) {
|
||||
router.history.back();
|
||||
} else {
|
||||
navigate({ to: defaultBackTo });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
{finalCanGoBack && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">{title}</h1>
|
||||
<p className="mt-1 text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">{actions}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
@ -1,5 +1,5 @@
|
||||
import { Card, CardContent, CardHeader } from './ui/card';
|
||||
import { Skeleton } from './ui/skeleton';
|
||||
import { Card, CardContent, CardHeader } from "./card";
|
||||
import { Skeleton } from "./skeleton";
|
||||
|
||||
export function DetailCardSkeleton() {
|
||||
return (
|
120
apps/webui/src/domains/recorder/schema/cron.ts
Normal file
120
apps/webui/src/domains/recorder/schema/cron.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import type { CronPreset } from '@/components/domains/cron';
|
||||
import type { GetCronsQuery } from '@/infra/graphql/gql/graphql';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_CRONS = gql`
|
||||
query GetCrons($filter: CronFilterInput!, $orderBy: CronOrderInput!, $pagination: PaginationInput!) {
|
||||
cron(pagination: $pagination, filter: $filter, orderBy: $orderBy) {
|
||||
nodes {
|
||||
id
|
||||
cronExpr
|
||||
nextRun
|
||||
lastRun
|
||||
lastError
|
||||
status
|
||||
lockedAt
|
||||
lockedBy
|
||||
createdAt
|
||||
updatedAt
|
||||
timeoutMs
|
||||
maxAttempts
|
||||
priority
|
||||
attempts
|
||||
subscriberTaskCron
|
||||
subscriberTask {
|
||||
nodes {
|
||||
id,
|
||||
job,
|
||||
taskType,
|
||||
status,
|
||||
attempts,
|
||||
maxAttempts,
|
||||
runAt,
|
||||
lastError,
|
||||
lockAt,
|
||||
lockBy,
|
||||
doneAt,
|
||||
priority,
|
||||
subscription {
|
||||
displayName
|
||||
sourceUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
paginationInfo {
|
||||
total
|
||||
pages
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export type CronDto = GetCronsQuery['cron']['nodes'][number];
|
||||
|
||||
export const DELETE_CRONS = gql`
|
||||
mutation DeleteCrons($filter: CronFilterInput!) {
|
||||
cronDelete(filter: $filter)
|
||||
}
|
||||
`;
|
||||
|
||||
export const INSERT_CRON = gql`
|
||||
mutation InsertCron($data: CronInsertInput!) {
|
||||
cronCreateOne(data: $data) {
|
||||
id
|
||||
cronExpr
|
||||
nextRun
|
||||
lastRun
|
||||
lastError
|
||||
status
|
||||
lockedAt
|
||||
lockedBy
|
||||
createdAt
|
||||
updatedAt
|
||||
timeoutMs
|
||||
maxAttempts
|
||||
priority
|
||||
attempts
|
||||
subscriberTaskCron
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SUBSCRIPTION_TASK_CRON_PRESETS: CronPreset[] = [
|
||||
{
|
||||
label: 'Daily at midnight',
|
||||
value: '0 0 0 * * *',
|
||||
description: 'Runs once daily at 00:00',
|
||||
category: 'daily',
|
||||
},
|
||||
{
|
||||
label: 'Daily at 9 AM',
|
||||
value: '0 0 9 * * *',
|
||||
description: 'Runs daily at 9:00 AM',
|
||||
category: 'daily',
|
||||
},
|
||||
{
|
||||
label: 'Weekdays at 9 AM',
|
||||
value: '0 0 9 * * 1-5',
|
||||
description: 'Runs Monday to Friday at 9:00 AM',
|
||||
category: 'weekly',
|
||||
},
|
||||
{
|
||||
label: 'Every Sunday',
|
||||
value: '0 0 0 * * 0',
|
||||
description: 'Runs every Sunday at midnight',
|
||||
category: 'weekly',
|
||||
},
|
||||
{
|
||||
label: 'First day of month',
|
||||
value: '0 0 0 1 * *',
|
||||
description: 'Runs on the 1st day of every month',
|
||||
category: 'monthly',
|
||||
},
|
||||
{
|
||||
label: 'Every year',
|
||||
value: '0 0 0 1 1 *',
|
||||
description: 'Runs on January 1st every year',
|
||||
category: 'yearly',
|
||||
},
|
||||
];
|
@ -117,6 +117,25 @@ query GetSubscriptionDetail ($id: Int!) {
|
||||
id
|
||||
username
|
||||
}
|
||||
cron {
|
||||
nodes {
|
||||
id
|
||||
cronExpr
|
||||
nextRun
|
||||
lastRun
|
||||
lastError
|
||||
status
|
||||
lockedAt
|
||||
lockedBy
|
||||
createdAt
|
||||
updatedAt
|
||||
timeoutMs
|
||||
maxAttempts
|
||||
priority
|
||||
attempts
|
||||
subscriberTaskCron
|
||||
}
|
||||
}
|
||||
bangumi {
|
||||
nodes {
|
||||
createdAt
|
||||
|
@ -20,13 +20,16 @@ type Documents = {
|
||||
"\n mutation DeleteCredential3rd($filter: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filter)\n }\n": typeof types.DeleteCredential3rdDocument,
|
||||
"\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filter: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n": typeof types.GetCredential3rdDetailDocument,
|
||||
"\n mutation CheckCredential3rdAvailable($filter: Credential3rdFilterInput!) {\n credential3rdCheckAvailable(filter: $filter) {\n available\n }\n }\n": typeof types.CheckCredential3rdAvailableDocument,
|
||||
"\nquery GetCrons($filter: CronFilterInput!, $orderBy: CronOrderInput!, $pagination: PaginationInput!) {\n cron(pagination: $pagination, filter: $filter, orderBy: $orderBy) {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n subscriberTask {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n }\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetCronsDocument,
|
||||
"\n mutation DeleteCrons($filter: CronFilterInput!) {\n cronDelete(filter: $filter)\n }\n": typeof types.DeleteCronsDocument,
|
||||
"\n mutation InsertCron($data: CronInsertInput!) {\n cronCreateOne(data: $data) {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n": typeof types.InsertCronDocument,
|
||||
"\n mutation InsertFeed($data: FeedsInsertInput!) {\n feedsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n feedType\n token\n }\n }\n": typeof types.InsertFeedDocument,
|
||||
"\n mutation DeleteFeed($filter: FeedsFilterInput!) {\n feedsDelete(filter: $filter)\n }\n": typeof types.DeleteFeedDocument,
|
||||
"\n query GetSubscriptions($filter: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetSubscriptionsDocument,
|
||||
"\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": typeof types.InsertSubscriptionDocument,
|
||||
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filter: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filter\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": typeof types.UpdateSubscriptionsDocument,
|
||||
"\n mutation DeleteSubscriptions($filter: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filter)\n }\n": typeof types.DeleteSubscriptionsDocument,
|
||||
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument,
|
||||
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument,
|
||||
"\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetTasksDocument,
|
||||
"\n mutation InsertSubscriberTask($data: SubscriberTasksInsertInput!) {\n subscriberTasksCreateOne(data: $data) {\n id\n }\n }\n": typeof types.InsertSubscriberTaskDocument,
|
||||
"\n mutation DeleteTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filter)\n }\n": typeof types.DeleteTasksDocument,
|
||||
@ -39,13 +42,16 @@ const documents: Documents = {
|
||||
"\n mutation DeleteCredential3rd($filter: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filter)\n }\n": types.DeleteCredential3rdDocument,
|
||||
"\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filter: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n": types.GetCredential3rdDetailDocument,
|
||||
"\n mutation CheckCredential3rdAvailable($filter: Credential3rdFilterInput!) {\n credential3rdCheckAvailable(filter: $filter) {\n available\n }\n }\n": types.CheckCredential3rdAvailableDocument,
|
||||
"\nquery GetCrons($filter: CronFilterInput!, $orderBy: CronOrderInput!, $pagination: PaginationInput!) {\n cron(pagination: $pagination, filter: $filter, orderBy: $orderBy) {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n subscriberTask {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n }\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetCronsDocument,
|
||||
"\n mutation DeleteCrons($filter: CronFilterInput!) {\n cronDelete(filter: $filter)\n }\n": types.DeleteCronsDocument,
|
||||
"\n mutation InsertCron($data: CronInsertInput!) {\n cronCreateOne(data: $data) {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n": types.InsertCronDocument,
|
||||
"\n mutation InsertFeed($data: FeedsInsertInput!) {\n feedsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n feedType\n token\n }\n }\n": types.InsertFeedDocument,
|
||||
"\n mutation DeleteFeed($filter: FeedsFilterInput!) {\n feedsDelete(filter: $filter)\n }\n": types.DeleteFeedDocument,
|
||||
"\n query GetSubscriptions($filter: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetSubscriptionsDocument,
|
||||
"\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": types.InsertSubscriptionDocument,
|
||||
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filter: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filter\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": types.UpdateSubscriptionsDocument,
|
||||
"\n mutation DeleteSubscriptions($filter: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filter)\n }\n": types.DeleteSubscriptionsDocument,
|
||||
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument,
|
||||
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument,
|
||||
"\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetTasksDocument,
|
||||
"\n mutation InsertSubscriberTask($data: SubscriberTasksInsertInput!) {\n subscriberTasksCreateOne(data: $data) {\n id\n }\n }\n": types.InsertSubscriberTaskDocument,
|
||||
"\n mutation DeleteTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filter)\n }\n": types.DeleteTasksDocument,
|
||||
@ -90,6 +96,18 @@ export function gql(source: "\n query GetCredential3rdDetail($id: Int!) {\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 mutation CheckCredential3rdAvailable($filter: Credential3rdFilterInput!) {\n credential3rdCheckAvailable(filter: $filter) {\n available\n }\n }\n"): (typeof documents)["\n mutation CheckCredential3rdAvailable($filter: Credential3rdFilterInput!) {\n credential3rdCheckAvailable(filter: $filter) {\n available\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: "\nquery GetCrons($filter: CronFilterInput!, $orderBy: CronOrderInput!, $pagination: PaginationInput!) {\n cron(pagination: $pagination, filter: $filter, orderBy: $orderBy) {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n subscriberTask {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n }\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"): (typeof documents)["\nquery GetCrons($filter: CronFilterInput!, $orderBy: CronOrderInput!, $pagination: PaginationInput!) {\n cron(pagination: $pagination, filter: $filter, orderBy: $orderBy) {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n subscriberTask {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n }\n }\n }\n paginationInfo {\n total\n pages\n }\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 mutation DeleteCrons($filter: CronFilterInput!) {\n cronDelete(filter: $filter)\n }\n"): (typeof documents)["\n mutation DeleteCrons($filter: CronFilterInput!) {\n cronDelete(filter: $filter)\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 mutation InsertCron($data: CronInsertInput!) {\n cronCreateOne(data: $data) {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n"): (typeof documents)["\n mutation InsertCron($data: CronInsertInput!) {\n cronCreateOne(data: $data) {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n"];
|
||||
/**
|
||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@ -117,7 +135,7 @@ export function gql(source: "\n mutation DeleteSubscriptions($filter: Subscri
|
||||
/**
|
||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"];
|
||||
export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"];
|
||||
/**
|
||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
@ -21,7 +21,7 @@ export type Scalars = {
|
||||
JsonbFilterInput: { input: any; output: any; }
|
||||
/** type SubscriberTaskType = { "taskType": "sync_one_subscription_feeds_incremental" } & SyncOneSubscriptionFeedsIncrementalTask | { "taskType": "sync_one_subscription_feeds_full" } & SyncOneSubscriptionFeedsFullTask | { "taskType": "sync_one_subscription_sources" } & SyncOneSubscriptionSourcesTask; */
|
||||
SubscriberTaskType: { input: SubscriberTaskInput; output: SubscriberTaskType; }
|
||||
/** type SystemTaskType = { "taskType": "optimize_image" } & OptimizeImageTask; */
|
||||
/** type SystemTaskType = { "taskType": "optimize_image" } & OptimizeImageTask | { "taskType": "test" } & EchoTask; */
|
||||
SystemTaskType: { input: any; output: any; }
|
||||
};
|
||||
|
||||
@ -344,6 +344,7 @@ export type Cron = {
|
||||
attempts: Scalars['Int']['output'];
|
||||
createdAt: Scalars['String']['output'];
|
||||
cronExpr: Scalars['String']['output'];
|
||||
cronTimezone: Scalars['String']['output'];
|
||||
enabled: Scalars['Boolean']['output'];
|
||||
id: Scalars['Int']['output'];
|
||||
lastError?: Maybe<Scalars['String']['output']>;
|
||||
@ -362,7 +363,7 @@ export type Cron = {
|
||||
subscriptionId?: Maybe<Scalars['Int']['output']>;
|
||||
systemTask: SystemTasksConnection;
|
||||
systemTaskCron?: Maybe<Scalars['SystemTaskType']['output']>;
|
||||
timeoutMs: Scalars['Int']['output'];
|
||||
timeoutMs?: Maybe<Scalars['Int']['output']>;
|
||||
updatedAt: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
@ -385,6 +386,7 @@ export type CronBasic = {
|
||||
attempts: Scalars['Int']['output'];
|
||||
createdAt: Scalars['String']['output'];
|
||||
cronExpr: Scalars['String']['output'];
|
||||
cronTimezone: Scalars['String']['output'];
|
||||
enabled: Scalars['Boolean']['output'];
|
||||
id: Scalars['Int']['output'];
|
||||
lastError?: Maybe<Scalars['String']['output']>;
|
||||
@ -399,7 +401,7 @@ export type CronBasic = {
|
||||
subscriberTaskCron?: Maybe<Scalars['SubscriberTaskType']['output']>;
|
||||
subscriptionId?: Maybe<Scalars['Int']['output']>;
|
||||
systemTaskCron?: Maybe<Scalars['SystemTaskType']['output']>;
|
||||
timeoutMs: Scalars['Int']['output'];
|
||||
timeoutMs?: Maybe<Scalars['Int']['output']>;
|
||||
updatedAt: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
@ -422,6 +424,7 @@ export type CronFilterInput = {
|
||||
attempts?: InputMaybe<IntegerFilterInput>;
|
||||
createdAt?: InputMaybe<TextFilterInput>;
|
||||
cronExpr?: InputMaybe<StringFilterInput>;
|
||||
cronTimezone?: InputMaybe<StringFilterInput>;
|
||||
enabled?: InputMaybe<BooleanFilterInput>;
|
||||
id?: InputMaybe<IntegerFilterInput>;
|
||||
lastError?: InputMaybe<StringFilterInput>;
|
||||
@ -443,6 +446,7 @@ export type CronFilterInput = {
|
||||
|
||||
export type CronInsertInput = {
|
||||
cronExpr: Scalars['String']['input'];
|
||||
cronTimezone: Scalars['String']['input'];
|
||||
enabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
maxAttempts?: InputMaybe<Scalars['Int']['input']>;
|
||||
subscriberTaskCron?: InputMaybe<Scalars['SubscriberTaskType']['input']>;
|
||||
@ -454,6 +458,7 @@ export type CronOrderInput = {
|
||||
attempts?: InputMaybe<OrderByEnum>;
|
||||
createdAt?: InputMaybe<OrderByEnum>;
|
||||
cronExpr?: InputMaybe<OrderByEnum>;
|
||||
cronTimezone?: InputMaybe<OrderByEnum>;
|
||||
enabled?: InputMaybe<OrderByEnum>;
|
||||
id?: InputMaybe<OrderByEnum>;
|
||||
lastError?: InputMaybe<OrderByEnum>;
|
||||
@ -495,6 +500,7 @@ export type CronStatusEnumFilterInput = {
|
||||
|
||||
export type CronUpdateInput = {
|
||||
cronExpr?: InputMaybe<Scalars['String']['input']>;
|
||||
cronTimezone?: InputMaybe<Scalars['String']['input']>;
|
||||
enabled?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
maxAttempts?: InputMaybe<Scalars['Int']['input']>;
|
||||
priority?: InputMaybe<Scalars['Int']['input']>;
|
||||
@ -1986,6 +1992,7 @@ export type Subscriptions = {
|
||||
createdAt: Scalars['String']['output'];
|
||||
credential3rd?: Maybe<Credential3rd>;
|
||||
credentialId?: Maybe<Scalars['Int']['output']>;
|
||||
cron: CronConnection;
|
||||
displayName: Scalars['String']['output'];
|
||||
enabled: Scalars['Boolean']['output'];
|
||||
episode: EpisodesConnection;
|
||||
@ -2008,6 +2015,13 @@ export type SubscriptionsBangumiArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type SubscriptionsCronArgs = {
|
||||
filter?: InputMaybe<CronFilterInput>;
|
||||
orderBy?: InputMaybe<CronOrderInput>;
|
||||
pagination?: InputMaybe<PaginationInput>;
|
||||
};
|
||||
|
||||
|
||||
export type SubscriptionsEpisodeArgs = {
|
||||
filter?: InputMaybe<EpisodesFilterInput>;
|
||||
orderBy?: InputMaybe<EpisodesOrderInput>;
|
||||
@ -2129,7 +2143,8 @@ export const SystemTaskStatusEnum = {
|
||||
|
||||
export type SystemTaskStatusEnum = typeof SystemTaskStatusEnum[keyof typeof SystemTaskStatusEnum];
|
||||
export const SystemTaskTypeEnum = {
|
||||
OptimizeImage: 'optimize_image'
|
||||
OptimizeImage: 'optimize_image',
|
||||
Test: 'test'
|
||||
} as const;
|
||||
|
||||
export type SystemTaskTypeEnum = typeof SystemTaskTypeEnum[keyof typeof SystemTaskTypeEnum];
|
||||
@ -2286,6 +2301,29 @@ export type CheckCredential3rdAvailableMutationVariables = Exact<{
|
||||
|
||||
export type CheckCredential3rdAvailableMutation = { __typename?: 'Mutation', credential3rdCheckAvailable: { __typename?: 'Credential3rdCheckAvailableInfo', available: boolean } };
|
||||
|
||||
export type GetCronsQueryVariables = Exact<{
|
||||
filter: CronFilterInput;
|
||||
orderBy: CronOrderInput;
|
||||
pagination: PaginationInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type GetCronsQuery = { __typename?: 'Query', cron: { __typename?: 'CronConnection', nodes: Array<{ __typename?: 'Cron', id: number, cronExpr: string, nextRun?: string | null, lastRun?: string | null, lastError?: string | null, status: CronStatusEnum, lockedAt?: string | null, lockedBy?: string | null, createdAt: string, updatedAt: string, timeoutMs?: number | null, maxAttempts: number, priority: number, attempts: number, subscriberTaskCron?: SubscriberTaskType | null, subscriberTask: { __typename?: 'SubscriberTasksConnection', nodes: Array<{ __typename?: 'SubscriberTasks', id: string, job: SubscriberTaskType, taskType: SubscriberTaskTypeEnum, status: SubscriberTaskStatusEnum, attempts: number, maxAttempts: number, runAt: string, lastError?: string | null, lockAt?: string | null, lockBy?: string | null, doneAt?: string | null, priority: number, subscription?: { __typename?: 'Subscriptions', displayName: string, sourceUrl: string } | null }> } }>, paginationInfo?: { __typename?: 'PaginationInfo', total: number, pages: number } | null } };
|
||||
|
||||
export type DeleteCronsMutationVariables = Exact<{
|
||||
filter: CronFilterInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteCronsMutation = { __typename?: 'Mutation', cronDelete: number };
|
||||
|
||||
export type InsertCronMutationVariables = Exact<{
|
||||
data: CronInsertInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type InsertCronMutation = { __typename?: 'Mutation', cronCreateOne: { __typename?: 'CronBasic', id: number, cronExpr: string, nextRun?: string | null, lastRun?: string | null, lastError?: string | null, status: CronStatusEnum, lockedAt?: string | null, lockedBy?: string | null, createdAt: string, updatedAt: string, timeoutMs?: number | null, maxAttempts: number, priority: number, attempts: number, subscriberTaskCron?: SubscriberTaskType | null } };
|
||||
|
||||
export type InsertFeedMutationVariables = Exact<{
|
||||
data: FeedsInsertInput;
|
||||
}>;
|
||||
@ -2336,7 +2374,7 @@ export type GetSubscriptionDetailQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type GetSubscriptionDetailQuery = { __typename?: 'Query', subscriptions: { __typename?: 'SubscriptionsConnection', nodes: Array<{ __typename?: 'Subscriptions', id: number, subscriberId: number, displayName: string, createdAt: string, updatedAt: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean, feed: { __typename?: 'FeedsConnection', nodes: Array<{ __typename?: 'Feeds', id: number, createdAt: string, updatedAt: string, token: string, feedType: FeedTypeEnum, feedSource: FeedSourceEnum }> }, subscriberTask: { __typename?: 'SubscriberTasksConnection', nodes: Array<{ __typename?: 'SubscriberTasks', id: string, taskType: SubscriberTaskTypeEnum, status: SubscriberTaskStatusEnum }> }, credential3rd?: { __typename?: 'Credential3rd', id: number, username?: string | null } | null, bangumi: { __typename?: 'BangumiConnection', nodes: Array<{ __typename?: 'Bangumi', createdAt: string, updatedAt: string, id: number, mikanBangumiId?: string | null, displayName: string, season: number, seasonRaw?: string | null, fansub?: string | null, mikanFansubId?: string | null, rssLink?: string | null, posterLink?: string | null, homepage?: string | null }> } }> } };
|
||||
export type GetSubscriptionDetailQuery = { __typename?: 'Query', subscriptions: { __typename?: 'SubscriptionsConnection', nodes: Array<{ __typename?: 'Subscriptions', id: number, subscriberId: number, displayName: string, createdAt: string, updatedAt: string, category: SubscriptionCategoryEnum, sourceUrl: string, enabled: boolean, feed: { __typename?: 'FeedsConnection', nodes: Array<{ __typename?: 'Feeds', id: number, createdAt: string, updatedAt: string, token: string, feedType: FeedTypeEnum, feedSource: FeedSourceEnum }> }, subscriberTask: { __typename?: 'SubscriberTasksConnection', nodes: Array<{ __typename?: 'SubscriberTasks', id: string, taskType: SubscriberTaskTypeEnum, status: SubscriberTaskStatusEnum }> }, credential3rd?: { __typename?: 'Credential3rd', id: number, username?: string | null } | null, cron: { __typename?: 'CronConnection', nodes: Array<{ __typename?: 'Cron', id: number, cronExpr: string, nextRun?: string | null, lastRun?: string | null, lastError?: string | null, status: CronStatusEnum, lockedAt?: string | null, lockedBy?: string | null, createdAt: string, updatedAt: string, timeoutMs?: number | null, maxAttempts: number, priority: number, attempts: number, subscriberTaskCron?: SubscriberTaskType | null }> }, bangumi: { __typename?: 'BangumiConnection', nodes: Array<{ __typename?: 'Bangumi', createdAt: string, updatedAt: string, id: number, mikanBangumiId?: string | null, displayName: string, season: number, seasonRaw?: string | null, fansub?: string | null, mikanFansubId?: string | null, rssLink?: string | null, posterLink?: string | null, homepage?: string | null }> } }> } };
|
||||
|
||||
export type GetTasksQueryVariables = Exact<{
|
||||
filter: SubscriberTasksFilterInput;
|
||||
@ -2375,13 +2413,16 @@ export const UpdateCredential3rdDocument = {"kind":"Document","definitions":[{"k
|
||||
export const DeleteCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rdDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}]}]}}]} as unknown as DocumentNode<DeleteCredential3rdMutation, DeleteCredential3rdMutationVariables>;
|
||||
export const GetCredential3rdDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCredential3rdDetail"},"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":"credential3rd"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"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":"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<GetCredential3rdDetailQuery, GetCredential3rdDetailQueryVariables>;
|
||||
export const CheckCredential3rdAvailableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CheckCredential3rdAvailable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rdCheckAvailable"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"available"}}]}}]}}]} as unknown as DocumentNode<CheckCredential3rdAvailableMutation, CheckCredential3rdAvailableMutationVariables>;
|
||||
export const GetCronsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCrons"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CronFilterInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CronOrderInput"}}}},{"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":"cron"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"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":"cronExpr"}},{"kind":"Field","name":{"kind":"Name","value":"nextRun"}},{"kind":"Field","name":{"kind":"Name","value":"lastRun"}},{"kind":"Field","name":{"kind":"Name","value":"lastError"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"lockedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lockedBy"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"timeoutMs"}},{"kind":"Field","name":{"kind":"Name","value":"maxAttempts"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}},{"kind":"Field","name":{"kind":"Name","value":"attempts"}},{"kind":"Field","name":{"kind":"Name","value":"subscriberTaskCron"}},{"kind":"Field","name":{"kind":"Name","value":"subscriberTask"},"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":"job"}},{"kind":"Field","name":{"kind":"Name","value":"taskType"}},{"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":"subscription"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}}]}}]}}]}}]}},{"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<GetCronsQuery, GetCronsQueryVariables>;
|
||||
export const DeleteCronsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteCrons"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CronFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cronDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}]}]}}]} as unknown as DocumentNode<DeleteCronsMutation, DeleteCronsMutationVariables>;
|
||||
export const InsertCronDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertCron"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CronInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cronCreateOne"},"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":"cronExpr"}},{"kind":"Field","name":{"kind":"Name","value":"nextRun"}},{"kind":"Field","name":{"kind":"Name","value":"lastRun"}},{"kind":"Field","name":{"kind":"Name","value":"lastError"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"lockedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lockedBy"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"timeoutMs"}},{"kind":"Field","name":{"kind":"Name","value":"maxAttempts"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}},{"kind":"Field","name":{"kind":"Name","value":"attempts"}},{"kind":"Field","name":{"kind":"Name","value":"subscriberTaskCron"}}]}}]}}]} as unknown as DocumentNode<InsertCronMutation, InsertCronMutationVariables>;
|
||||
export const InsertFeedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertFeed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FeedsInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feedsCreateOne"},"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":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"feedType"}},{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]}}]} as unknown as DocumentNode<InsertFeedMutation, InsertFeedMutationVariables>;
|
||||
export const DeleteFeedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteFeed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FeedsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"feedsDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}]}]}}]} as unknown as DocumentNode<DeleteFeedMutation, DeleteFeedMutationVariables>;
|
||||
export const GetSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsOrderInput"}}}},{"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":"subscriptions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"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":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"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":"credentialId"}}]}},{"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<GetSubscriptionsQuery, GetSubscriptionsQueryVariables>;
|
||||
export const InsertSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsCreateOne"},"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":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"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":"credentialId"}}]}}]}}]} as unknown as DocumentNode<InsertSubscriptionMutation, InsertSubscriptionMutationVariables>;
|
||||
export const UpdateSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsUpdateInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsUpdate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"category"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}}]}}]}}]} as unknown as DocumentNode<UpdateSubscriptionsMutation, UpdateSubscriptionsMutationVariables>;
|
||||
export const DeleteSubscriptionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteSubscriptions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriptionsFilterInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionsDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}]}]}}]} as unknown as DocumentNode<DeleteSubscriptionsMutation, DeleteSubscriptionsMutationVariables>;
|
||||
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":"filter"},"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":"subscriberId"}},{"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":"feed"},"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":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"feedType"}},{"kind":"Field","name":{"kind":"Name","value":"feedSource"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"subscriberTask"},"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":"taskType"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}},{"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":"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":"homepage"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetSubscriptionDetailQuery, GetSubscriptionDetailQueryVariables>;
|
||||
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":"filter"},"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":"subscriberId"}},{"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":"feed"},"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":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"feedType"}},{"kind":"Field","name":{"kind":"Name","value":"feedSource"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"subscriberTask"},"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":"taskType"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}},{"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":"cron"},"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":"cronExpr"}},{"kind":"Field","name":{"kind":"Name","value":"nextRun"}},{"kind":"Field","name":{"kind":"Name","value":"lastRun"}},{"kind":"Field","name":{"kind":"Name","value":"lastError"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"lockedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lockedBy"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"timeoutMs"}},{"kind":"Field","name":{"kind":"Name","value":"maxAttempts"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}},{"kind":"Field","name":{"kind":"Name","value":"attempts"}},{"kind":"Field","name":{"kind":"Name","value":"subscriberTaskCron"}}]}}]}},{"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":"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":"homepage"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetSubscriptionDetailQuery, GetSubscriptionDetailQueryVariables>;
|
||||
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":"filter"}},"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":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"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":"job"}},{"kind":"Field","name":{"kind":"Name","value":"taskType"}},{"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":"subscription"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"displayName"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}}]}}]}},{"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<GetTasksQuery, GetTasksQueryVariables>;
|
||||
export const InsertSubscriberTaskDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InsertSubscriberTask"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriberTasksInsertInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriberTasksCreateOne"},"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"}}]}}]}}]} as unknown as DocumentNode<InsertSubscriberTaskMutation, InsertSubscriberTaskMutationVariables>;
|
||||
export const DeleteTasksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteTasks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriberTasksFilterInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriberTasksDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}]}]}}]} as unknown as DocumentNode<DeleteTasksMutation, DeleteTasksMutationVariables>;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { guardRouteIndexAsNotFound } from '@/components/layout/app-not-found';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { Outlet } from '@tanstack/react-router';
|
||||
import { Outlet, type RouteOptions } from '@tanstack/react-router';
|
||||
|
||||
export interface BuildVirtualBranchRouteOptions {
|
||||
title: string;
|
||||
@ -8,7 +8,11 @@ export interface BuildVirtualBranchRouteOptions {
|
||||
|
||||
export function buildVirtualBranchRouteOptions(
|
||||
options: BuildVirtualBranchRouteOptions
|
||||
) {
|
||||
): {
|
||||
beforeLoad: RouteOptions['beforeLoad'];
|
||||
staticData: RouteStateDataOption;
|
||||
component: RouteOptions['component'];
|
||||
} {
|
||||
return {
|
||||
beforeLoad: guardRouteIndexAsNotFound,
|
||||
staticData: {
|
||||
|
@ -31,11 +31,15 @@ import { Route as AppCredential3rdManageRouteImport } from './routes/_app/creden
|
||||
import { Route as AppCredential3rdCreateRouteImport } from './routes/_app/credential3rd/create'
|
||||
import { Route as AppBangumiManageRouteImport } from './routes/_app/bangumi/manage'
|
||||
import { Route as AppExploreExploreRouteImport } from './routes/_app/_explore/explore'
|
||||
import { Route as AppTasksCronRouteRouteImport } from './routes/_app/tasks/cron/route'
|
||||
import { Route as AppTasksDetailIdRouteImport } from './routes/_app/tasks/detail.$id'
|
||||
import { Route as AppTasksCronManageRouteImport } from './routes/_app/tasks/cron/manage'
|
||||
import { Route as AppSubscriptionsEditIdRouteImport } from './routes/_app/subscriptions/edit.$id'
|
||||
import { Route as AppSubscriptionsDetailIdRouteImport } from './routes/_app/subscriptions/detail.$id'
|
||||
import { Route as AppCredential3rdEditIdRouteImport } from './routes/_app/credential3rd/edit.$id'
|
||||
import { Route as AppCredential3rdDetailIdRouteImport } from './routes/_app/credential3rd/detail.$id'
|
||||
import { Route as AppTasksCronEditIdRouteImport } from './routes/_app/tasks/cron/edit.$id'
|
||||
import { Route as AppTasksCronDetailIdRouteImport } from './routes/_app/tasks/cron/detail.$id'
|
||||
|
||||
const AboutRoute = AboutRouteImport.update({
|
||||
id: '/about',
|
||||
@ -148,11 +152,21 @@ const AppExploreExploreRoute = AppExploreExploreRouteImport.update({
|
||||
path: '/explore',
|
||||
getParentRoute: () => AppRouteRoute,
|
||||
} as any)
|
||||
const AppTasksCronRouteRoute = AppTasksCronRouteRouteImport.update({
|
||||
id: '/cron',
|
||||
path: '/cron',
|
||||
getParentRoute: () => AppTasksRouteRoute,
|
||||
} as any)
|
||||
const AppTasksDetailIdRoute = AppTasksDetailIdRouteImport.update({
|
||||
id: '/detail/$id',
|
||||
path: '/detail/$id',
|
||||
getParentRoute: () => AppTasksRouteRoute,
|
||||
} as any)
|
||||
const AppTasksCronManageRoute = AppTasksCronManageRouteImport.update({
|
||||
id: '/manage',
|
||||
path: '/manage',
|
||||
getParentRoute: () => AppTasksCronRouteRoute,
|
||||
} as any)
|
||||
const AppSubscriptionsEditIdRoute = AppSubscriptionsEditIdRouteImport.update({
|
||||
id: '/edit/$id',
|
||||
path: '/edit/$id',
|
||||
@ -175,6 +189,16 @@ const AppCredential3rdDetailIdRoute =
|
||||
path: '/detail/$id',
|
||||
getParentRoute: () => AppCredential3rdRouteRoute,
|
||||
} as any)
|
||||
const AppTasksCronEditIdRoute = AppTasksCronEditIdRouteImport.update({
|
||||
id: '/edit/$id',
|
||||
path: '/edit/$id',
|
||||
getParentRoute: () => AppTasksCronRouteRoute,
|
||||
} as any)
|
||||
const AppTasksCronDetailIdRoute = AppTasksCronDetailIdRouteImport.update({
|
||||
id: '/detail/$id',
|
||||
path: '/detail/$id',
|
||||
getParentRoute: () => AppTasksCronRouteRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
@ -189,6 +213,7 @@ export interface FileRoutesByFullPath {
|
||||
'/tasks': typeof AppTasksRouteRouteWithChildren
|
||||
'/auth/sign-in': typeof AuthSignInRoute
|
||||
'/auth/sign-up': typeof AuthSignUpRoute
|
||||
'/tasks/cron': typeof AppTasksCronRouteRouteWithChildren
|
||||
'/explore': typeof AppExploreExploreRoute
|
||||
'/bangumi/manage': typeof AppBangumiManageRoute
|
||||
'/credential3rd/create': typeof AppCredential3rdCreateRoute
|
||||
@ -203,7 +228,10 @@ export interface FileRoutesByFullPath {
|
||||
'/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
|
||||
'/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
|
||||
'/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
|
||||
'/tasks/cron/manage': typeof AppTasksCronManageRoute
|
||||
'/tasks/detail/$id': typeof AppTasksDetailIdRoute
|
||||
'/tasks/cron/detail/$id': typeof AppTasksCronDetailIdRoute
|
||||
'/tasks/cron/edit/$id': typeof AppTasksCronEditIdRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
@ -218,6 +246,7 @@ export interface FileRoutesByTo {
|
||||
'/tasks': typeof AppTasksRouteRouteWithChildren
|
||||
'/auth/sign-in': typeof AuthSignInRoute
|
||||
'/auth/sign-up': typeof AuthSignUpRoute
|
||||
'/tasks/cron': typeof AppTasksCronRouteRouteWithChildren
|
||||
'/explore': typeof AppExploreExploreRoute
|
||||
'/bangumi/manage': typeof AppBangumiManageRoute
|
||||
'/credential3rd/create': typeof AppCredential3rdCreateRoute
|
||||
@ -232,7 +261,10 @@ export interface FileRoutesByTo {
|
||||
'/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
|
||||
'/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
|
||||
'/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
|
||||
'/tasks/cron/manage': typeof AppTasksCronManageRoute
|
||||
'/tasks/detail/$id': typeof AppTasksDetailIdRoute
|
||||
'/tasks/cron/detail/$id': typeof AppTasksCronDetailIdRoute
|
||||
'/tasks/cron/edit/$id': typeof AppTasksCronEditIdRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
@ -248,6 +280,7 @@ export interface FileRoutesById {
|
||||
'/_app/tasks': typeof AppTasksRouteRouteWithChildren
|
||||
'/auth/sign-in': typeof AuthSignInRoute
|
||||
'/auth/sign-up': typeof AuthSignUpRoute
|
||||
'/_app/tasks/cron': typeof AppTasksCronRouteRouteWithChildren
|
||||
'/_app/_explore/explore': typeof AppExploreExploreRoute
|
||||
'/_app/bangumi/manage': typeof AppBangumiManageRoute
|
||||
'/_app/credential3rd/create': typeof AppCredential3rdCreateRoute
|
||||
@ -262,7 +295,10 @@ export interface FileRoutesById {
|
||||
'/_app/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
|
||||
'/_app/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
|
||||
'/_app/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
|
||||
'/_app/tasks/cron/manage': typeof AppTasksCronManageRoute
|
||||
'/_app/tasks/detail/$id': typeof AppTasksDetailIdRoute
|
||||
'/_app/tasks/cron/detail/$id': typeof AppTasksCronDetailIdRoute
|
||||
'/_app/tasks/cron/edit/$id': typeof AppTasksCronEditIdRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
@ -279,6 +315,7 @@ export interface FileRouteTypes {
|
||||
| '/tasks'
|
||||
| '/auth/sign-in'
|
||||
| '/auth/sign-up'
|
||||
| '/tasks/cron'
|
||||
| '/explore'
|
||||
| '/bangumi/manage'
|
||||
| '/credential3rd/create'
|
||||
@ -293,7 +330,10 @@ export interface FileRouteTypes {
|
||||
| '/credential3rd/edit/$id'
|
||||
| '/subscriptions/detail/$id'
|
||||
| '/subscriptions/edit/$id'
|
||||
| '/tasks/cron/manage'
|
||||
| '/tasks/detail/$id'
|
||||
| '/tasks/cron/detail/$id'
|
||||
| '/tasks/cron/edit/$id'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
@ -308,6 +348,7 @@ export interface FileRouteTypes {
|
||||
| '/tasks'
|
||||
| '/auth/sign-in'
|
||||
| '/auth/sign-up'
|
||||
| '/tasks/cron'
|
||||
| '/explore'
|
||||
| '/bangumi/manage'
|
||||
| '/credential3rd/create'
|
||||
@ -322,7 +363,10 @@ export interface FileRouteTypes {
|
||||
| '/credential3rd/edit/$id'
|
||||
| '/subscriptions/detail/$id'
|
||||
| '/subscriptions/edit/$id'
|
||||
| '/tasks/cron/manage'
|
||||
| '/tasks/detail/$id'
|
||||
| '/tasks/cron/detail/$id'
|
||||
| '/tasks/cron/edit/$id'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
@ -337,6 +381,7 @@ export interface FileRouteTypes {
|
||||
| '/_app/tasks'
|
||||
| '/auth/sign-in'
|
||||
| '/auth/sign-up'
|
||||
| '/_app/tasks/cron'
|
||||
| '/_app/_explore/explore'
|
||||
| '/_app/bangumi/manage'
|
||||
| '/_app/credential3rd/create'
|
||||
@ -351,7 +396,10 @@ export interface FileRouteTypes {
|
||||
| '/_app/credential3rd/edit/$id'
|
||||
| '/_app/subscriptions/detail/$id'
|
||||
| '/_app/subscriptions/edit/$id'
|
||||
| '/_app/tasks/cron/manage'
|
||||
| '/_app/tasks/detail/$id'
|
||||
| '/_app/tasks/cron/detail/$id'
|
||||
| '/_app/tasks/cron/edit/$id'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
@ -520,6 +568,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AppExploreExploreRouteImport
|
||||
parentRoute: typeof AppRouteRoute
|
||||
}
|
||||
'/_app/tasks/cron': {
|
||||
id: '/_app/tasks/cron'
|
||||
path: '/cron'
|
||||
fullPath: '/tasks/cron'
|
||||
preLoaderRoute: typeof AppTasksCronRouteRouteImport
|
||||
parentRoute: typeof AppTasksRouteRoute
|
||||
}
|
||||
'/_app/tasks/detail/$id': {
|
||||
id: '/_app/tasks/detail/$id'
|
||||
path: '/detail/$id'
|
||||
@ -527,6 +582,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AppTasksDetailIdRouteImport
|
||||
parentRoute: typeof AppTasksRouteRoute
|
||||
}
|
||||
'/_app/tasks/cron/manage': {
|
||||
id: '/_app/tasks/cron/manage'
|
||||
path: '/manage'
|
||||
fullPath: '/tasks/cron/manage'
|
||||
preLoaderRoute: typeof AppTasksCronManageRouteImport
|
||||
parentRoute: typeof AppTasksCronRouteRoute
|
||||
}
|
||||
'/_app/subscriptions/edit/$id': {
|
||||
id: '/_app/subscriptions/edit/$id'
|
||||
path: '/edit/$id'
|
||||
@ -555,6 +617,20 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AppCredential3rdDetailIdRouteImport
|
||||
parentRoute: typeof AppCredential3rdRouteRoute
|
||||
}
|
||||
'/_app/tasks/cron/edit/$id': {
|
||||
id: '/_app/tasks/cron/edit/$id'
|
||||
path: '/edit/$id'
|
||||
fullPath: '/tasks/cron/edit/$id'
|
||||
preLoaderRoute: typeof AppTasksCronEditIdRouteImport
|
||||
parentRoute: typeof AppTasksCronRouteRoute
|
||||
}
|
||||
'/_app/tasks/cron/detail/$id': {
|
||||
id: '/_app/tasks/cron/detail/$id'
|
||||
path: '/detail/$id'
|
||||
fullPath: '/tasks/cron/detail/$id'
|
||||
preLoaderRoute: typeof AppTasksCronDetailIdRouteImport
|
||||
parentRoute: typeof AppTasksCronRouteRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -630,12 +706,29 @@ const AppSubscriptionsRouteRouteWithChildren =
|
||||
AppSubscriptionsRouteRouteChildren,
|
||||
)
|
||||
|
||||
interface AppTasksCronRouteRouteChildren {
|
||||
AppTasksCronManageRoute: typeof AppTasksCronManageRoute
|
||||
AppTasksCronDetailIdRoute: typeof AppTasksCronDetailIdRoute
|
||||
AppTasksCronEditIdRoute: typeof AppTasksCronEditIdRoute
|
||||
}
|
||||
|
||||
const AppTasksCronRouteRouteChildren: AppTasksCronRouteRouteChildren = {
|
||||
AppTasksCronManageRoute: AppTasksCronManageRoute,
|
||||
AppTasksCronDetailIdRoute: AppTasksCronDetailIdRoute,
|
||||
AppTasksCronEditIdRoute: AppTasksCronEditIdRoute,
|
||||
}
|
||||
|
||||
const AppTasksCronRouteRouteWithChildren =
|
||||
AppTasksCronRouteRoute._addFileChildren(AppTasksCronRouteRouteChildren)
|
||||
|
||||
interface AppTasksRouteRouteChildren {
|
||||
AppTasksCronRouteRoute: typeof AppTasksCronRouteRouteWithChildren
|
||||
AppTasksManageRoute: typeof AppTasksManageRoute
|
||||
AppTasksDetailIdRoute: typeof AppTasksDetailIdRoute
|
||||
}
|
||||
|
||||
const AppTasksRouteRouteChildren: AppTasksRouteRouteChildren = {
|
||||
AppTasksCronRouteRoute: AppTasksCronRouteRouteWithChildren,
|
||||
AppTasksManageRoute: AppTasksManageRoute,
|
||||
AppTasksDetailIdRoute: AppTasksDetailIdRoute,
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { FormFieldErrors } from '@/components/ui/form-field-errors';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@ -123,14 +124,11 @@ function CredentialCreateRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-2xl py-6">
|
||||
<div className="mb-6 flex items-center gap-4">
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Create third-party credential</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Add new third-party login credential
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ContainerHeader
|
||||
title="Create third-party credential"
|
||||
description="Add new third-party login credential"
|
||||
defaultBackTo="/credential3rd/manage"
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { DetailCardSkeleton } from '@/components/detail-card-skeleton';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@ -8,6 +7,8 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DetailCardSkeleton } from '@/components/ui/detail-card-skeleton';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@ -17,14 +18,9 @@ import { GET_CREDENTIAL_3RD_DETAIL } from '@/domains/recorder/schema/credential3
|
||||
import type { GetCredential3rdDetailQuery } from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import {
|
||||
createFileRoute,
|
||||
useCanGoBack,
|
||||
useNavigate,
|
||||
useRouter,
|
||||
} from '@tanstack/react-router';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { format } from 'date-fns/format';
|
||||
import { ArrowLeft, CheckIcon, Edit, Eye, EyeOff } from 'lucide-react';
|
||||
import { CheckIcon, Edit, Eye, EyeOff } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { Credential3rdCheckAvailableViewDialogContent } from './-check-available';
|
||||
|
||||
@ -38,21 +34,9 @@ export const Route = createFileRoute('/_app/credential3rd/detail/$id')({
|
||||
function Credential3rdDetailRouteComponent() {
|
||||
const { id } = Route.useParams();
|
||||
const navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
const canGoBack = useCanGoBack();
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleBack = () => {
|
||||
if (canGoBack) {
|
||||
router.history.back();
|
||||
} else {
|
||||
navigate({
|
||||
to: '/credential3rd/manage',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { loading, error, data } = useQuery<GetCredential3rdDetailQuery>(
|
||||
GET_CREDENTIAL_3RD_DETAIL,
|
||||
{
|
||||
@ -91,31 +75,17 @@ function Credential3rdDetailRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl py-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Credential detail</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
View credential #{credential.id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<ContainerHeader
|
||||
title="Credential Detail"
|
||||
description={`View credential #${credential.id}`}
|
||||
defaultBackTo="/credential3rd/manage"
|
||||
actions={
|
||||
<Button onClick={handleEnterEditMode}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { DetailCardSkeleton } from '@/components/detail-card-skeleton';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@ -8,6 +7,8 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DetailCardSkeleton } from '@/components/ui/detail-card-skeleton';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@ -39,13 +40,8 @@ import type {
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import {
|
||||
createFileRoute,
|
||||
useCanGoBack,
|
||||
useNavigate,
|
||||
useRouter,
|
||||
} from '@tanstack/react-router';
|
||||
import { ArrowLeft, Eye, EyeOff, Save, X } from 'lucide-react';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { Eye, EyeOff, Save } from 'lucide-react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@ -63,23 +59,10 @@ function FormView({
|
||||
credential: Credential3rdDetailDto;
|
||||
onCompleted: VoidFunction;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const togglePasswordVisibility = () => {
|
||||
setShowPassword((prev) => !prev);
|
||||
};
|
||||
const router = useRouter();
|
||||
const canGoBack = useCanGoBack();
|
||||
|
||||
const handleBack = () => {
|
||||
if (canGoBack) {
|
||||
router.history.back();
|
||||
} else {
|
||||
navigate({
|
||||
to: '/credential3rd/manage',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const [updateCredential, { loading: updating }] = useMutation<
|
||||
UpdateCredential3rdMutation,
|
||||
@ -121,35 +104,17 @@ function FormView({
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl py-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Credential edit</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Edit credential #{credential.id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleBack} disabled={updating}>
|
||||
<X className="mr-2 h-4 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
<ContainerHeader
|
||||
title="Credential Edit"
|
||||
description={`Edit credential #${credential.id}`}
|
||||
defaultBackTo={`/credential3rd/detail/${credential.id}`}
|
||||
actions={
|
||||
<Button onClick={() => form.handleSubmit()} disabled={updating}>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{updating ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
||||
import { DataTableViewOptions } from '@/components/ui/data-table-view-options';
|
||||
import { DialogTrigger } from '@/components/ui/dialog';
|
||||
@ -297,18 +298,16 @@ function CredentialManageRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto space-y-4 rounded-md">
|
||||
<div className="flex items-center justify-between pt-4">
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Credential 3rd Management</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Manage your third-party platform login credentials
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => navigate({ to: '/credential3rd/create' })}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Credential
|
||||
</Button>
|
||||
</div>
|
||||
<ContainerHeader
|
||||
title="Credential 3rd Management"
|
||||
description="Manage your third-party platform login credentials"
|
||||
actions={
|
||||
<Button onClick={() => navigate({ to: '/credential3rd/create' })}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Credential
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center py-2">
|
||||
<DataTableViewOptions table={table} />
|
||||
</div>
|
||||
|
@ -0,0 +1,152 @@
|
||||
import { Cron } from '@/components/domains/cron';
|
||||
import { CronMode } from '@/components/domains/cron/types';
|
||||
import {
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { INSERT_CRON } from '@/domains/recorder/schema/cron';
|
||||
import type {
|
||||
InsertCronMutation,
|
||||
InsertCronMutationVariables,
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const SUBSCRIPTION_TASK_CRON_PRESETS = [
|
||||
{
|
||||
label: 'Every hour',
|
||||
value: '0 0 * * * *',
|
||||
description: 'Runs at the top of every hour',
|
||||
category: 'common',
|
||||
},
|
||||
{
|
||||
label: 'Daily at midnight',
|
||||
value: '0 0 0 * * *',
|
||||
description: 'Runs once daily at 00:00',
|
||||
category: 'daily',
|
||||
},
|
||||
{
|
||||
label: 'Daily at 9 AM',
|
||||
value: '0 0 9 * * *',
|
||||
description: 'Runs daily at 9:00 AM',
|
||||
category: 'daily',
|
||||
},
|
||||
{
|
||||
label: 'Every Sunday',
|
||||
value: '0 0 0 * * 0',
|
||||
description: 'Runs every Sunday at midnight',
|
||||
category: 'weekly',
|
||||
},
|
||||
{
|
||||
label: 'First day of month',
|
||||
value: '0 0 0 1 * *',
|
||||
description: 'Runs on the 1st day of every month',
|
||||
category: 'monthly',
|
||||
},
|
||||
{
|
||||
label: 'Every year',
|
||||
value: '0 0 0 1 1 *',
|
||||
description: 'Runs on January 1st every year',
|
||||
category: 'yearly',
|
||||
},
|
||||
];
|
||||
|
||||
export type SubscriptionCronCreationViewCompletePayload = {
|
||||
id: number;
|
||||
};
|
||||
|
||||
export interface SubscriptionCronCreationViewProps {
|
||||
subscriptionId: number;
|
||||
onComplete: (payload: SubscriptionCronCreationViewCompletePayload) => void;
|
||||
}
|
||||
|
||||
export const SubscriptionCronCreationView = memo(
|
||||
({ subscriptionId, onComplete }: SubscriptionCronCreationViewProps) => {
|
||||
const [insertCron, { loading: loadingInsert }] = useMutation<
|
||||
InsertCronMutation,
|
||||
InsertCronMutationVariables
|
||||
>(INSERT_CRON, {
|
||||
onCompleted: (data) => {
|
||||
toast.success('Cron created');
|
||||
onComplete(data.cronCreateOne);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error('Failed to sync subscription', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const loading = loadingInsert;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Cron
|
||||
mode={CronMode.Both}
|
||||
withCard={true}
|
||||
showPresets={false}
|
||||
presets={SUBSCRIPTION_TASK_CRON_PRESETS}
|
||||
timezone={'Asia/Shanghai'}
|
||||
/>
|
||||
|
||||
{loading && (
|
||||
<div className="absolute inset-0 flex flex-row items-center justify-center gap-2">
|
||||
<Spinner variant="circle-filled" size="16" />
|
||||
<span>Creating cron...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export interface SubscriptionCronCreationDialogContentProps {
|
||||
subscriptionId: number;
|
||||
onCancel?: VoidFunction;
|
||||
}
|
||||
|
||||
export const SubscriptionCronCreationDialogContent = memo(
|
||||
({
|
||||
subscriptionId,
|
||||
onCancel,
|
||||
}: SubscriptionCronCreationDialogContentProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleCreationComplete = useCallback(
|
||||
(payload: SubscriptionCronCreationViewCompletePayload) => {
|
||||
navigate({
|
||||
to: '/tasks/cron/detail/$id',
|
||||
params: {
|
||||
id: `${payload.id}`,
|
||||
},
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
return (
|
||||
<DialogContent
|
||||
onAbort={onCancel}
|
||||
className="flex max-h-[80vh] flex-col overflow-y-auto xl:max-w-2xl"
|
||||
>
|
||||
<DialogHeader className="sticky">
|
||||
<DialogTitle>Create Cron</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a cron to execute the subscription.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
<SubscriptionCronCreationView
|
||||
subscriptionId={subscriptionId}
|
||||
onComplete={handleCreationComplete}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
);
|
@ -18,17 +18,17 @@ import { RefreshCcwIcon } from 'lucide-react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export type SubscriptionSyncViewCompletePayload = {
|
||||
export type SubscriptionTaskCreationViewCompletePayload = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export interface SubscriptionSyncViewProps {
|
||||
id: number;
|
||||
onComplete: (payload: SubscriptionSyncViewCompletePayload) => void;
|
||||
export interface SubscriptionTaskCreationViewProps {
|
||||
subscriptionId: number;
|
||||
onComplete: (payload: SubscriptionTaskCreationViewCompletePayload) => void;
|
||||
}
|
||||
|
||||
export const SubscriptionSyncView = memo(
|
||||
({ id, onComplete }: SubscriptionSyncViewProps) => {
|
||||
export const SubscriptionTaskCreationView = memo(
|
||||
({ subscriptionId, onComplete }: SubscriptionTaskCreationViewProps) => {
|
||||
const [insertSubscriberTask, { loading: loadingInsert }] = useMutation<
|
||||
InsertSubscriberTaskMutation,
|
||||
InsertSubscriberTaskMutationVariables
|
||||
@ -56,7 +56,7 @@ export const SubscriptionSyncView = memo(
|
||||
variables: {
|
||||
data: {
|
||||
job: {
|
||||
subscriptionId: id,
|
||||
subscriptionId: subscriptionId,
|
||||
taskType: SubscriberTaskTypeEnum.SyncOneSubscriptionSources,
|
||||
},
|
||||
},
|
||||
@ -75,7 +75,7 @@ export const SubscriptionSyncView = memo(
|
||||
variables: {
|
||||
data: {
|
||||
job: {
|
||||
subscriptionId: id,
|
||||
subscriptionId: subscriptionId,
|
||||
taskType:
|
||||
SubscriberTaskTypeEnum.SyncOneSubscriptionFeedsIncremental,
|
||||
},
|
||||
@ -95,7 +95,7 @@ export const SubscriptionSyncView = memo(
|
||||
variables: {
|
||||
data: {
|
||||
job: {
|
||||
subscriptionId: id,
|
||||
subscriptionId: subscriptionId,
|
||||
taskType:
|
||||
SubscriberTaskTypeEnum.SyncOneSubscriptionFeedsFull,
|
||||
},
|
||||
@ -111,7 +111,7 @@ export const SubscriptionSyncView = memo(
|
||||
{loading && (
|
||||
<div className="absolute inset-0 flex flex-row items-center justify-center gap-2">
|
||||
<Spinner variant="circle-filled" size="16" />
|
||||
<span>Syncing...</span>
|
||||
<span>Running...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -119,17 +119,20 @@ export const SubscriptionSyncView = memo(
|
||||
}
|
||||
);
|
||||
|
||||
export interface SubscriptionSyncDialogContentProps {
|
||||
id: number;
|
||||
export interface SubscriptionTaskCreationDialogContentProps {
|
||||
subscriptionId: number;
|
||||
onCancel?: VoidFunction;
|
||||
}
|
||||
|
||||
export const SubscriptionSyncDialogContent = memo(
|
||||
({ id, onCancel }: SubscriptionSyncDialogContentProps) => {
|
||||
export const SubscriptionTaskCreationDialogContent = memo(
|
||||
({
|
||||
subscriptionId,
|
||||
onCancel,
|
||||
}: SubscriptionTaskCreationDialogContentProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSyncComplete = useCallback(
|
||||
(payload: SubscriptionSyncViewCompletePayload) => {
|
||||
const handleCreationComplete = useCallback(
|
||||
(payload: SubscriptionTaskCreationViewCompletePayload) => {
|
||||
navigate({
|
||||
to: '/tasks/detail/$id',
|
||||
params: {
|
||||
@ -143,12 +146,15 @@ export const SubscriptionSyncDialogContent = memo(
|
||||
return (
|
||||
<DialogContent onAbort={onCancel}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Sync Subscription</DialogTitle>
|
||||
<DialogTitle>Run Task</DialogTitle>
|
||||
<DialogDescription>
|
||||
Sync the subscription with sources and feeds.
|
||||
Run the task for the subscription.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<SubscriptionSyncView id={id} onComplete={handleSyncComplete} />
|
||||
<SubscriptionTaskCreationView
|
||||
subscriptionId={subscriptionId}
|
||||
onComplete={handleCreationComplete}
|
||||
/>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
@ -6,6 +6,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { FormFieldErrors } from '@/components/ui/form-field-errors';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@ -96,14 +97,11 @@ function SubscriptionCreateRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-2xl py-6">
|
||||
<div className="mb-6 flex items-center gap-4">
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Create Bangumi Subscription</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Add a new bangumi subscription source
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ContainerHeader
|
||||
title="Create Bangumi Subscription"
|
||||
description="Add a new bangumi subscription source"
|
||||
defaultBackTo="/subscriptions/manage"
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { DetailCardSkeleton } from '@/components/detail-card-skeleton';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@ -8,6 +7,8 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DetailCardSkeleton } from '@/components/ui/detail-card-skeleton';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Img } from '@/components/ui/img';
|
||||
@ -33,15 +34,9 @@ import {
|
||||
SubscriptionCategoryEnum,
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import {
|
||||
createFileRoute,
|
||||
useCanGoBack,
|
||||
useNavigate,
|
||||
useRouter,
|
||||
} from '@tanstack/react-router';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Edit,
|
||||
ExternalLink,
|
||||
ListIcon,
|
||||
@ -52,7 +47,8 @@ import {
|
||||
import { useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { prettyTaskType } from '../tasks/-pretty-task-type';
|
||||
import { SubscriptionSyncDialogContent } from './-sync';
|
||||
import { SubscriptionCronCreationDialogContent } from './-cron-creation';
|
||||
import { SubscriptionTaskCreationDialogContent } from './-task-creation';
|
||||
|
||||
export const Route = createFileRoute('/_app/subscriptions/detail/$id')({
|
||||
component: SubscriptionDetailRouteComponent,
|
||||
@ -61,20 +57,8 @@ export const Route = createFileRoute('/_app/subscriptions/detail/$id')({
|
||||
function SubscriptionDetailRouteComponent() {
|
||||
const { id } = Route.useParams();
|
||||
const navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
const canGoBack = useCanGoBack();
|
||||
const subscriptionService = useInject(SubscriptionService);
|
||||
|
||||
const handleBack = () => {
|
||||
if (canGoBack) {
|
||||
router.history.back();
|
||||
} else {
|
||||
navigate({
|
||||
to: '/subscriptions/manage',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleReload = async () => {
|
||||
const result = await refetch();
|
||||
const error = getApolloQueryError(result);
|
||||
@ -177,31 +161,16 @@ function SubscriptionDetailRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl py-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Subscription detail</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
View subscription #{subscription.id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<ContainerHeader
|
||||
title="Subscription Detail"
|
||||
description={`View subscription #${subscription.id}`}
|
||||
actions={
|
||||
<Button onClick={handleEnterEditMode}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</Button>{' '}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@ -421,6 +390,78 @@ function SubscriptionDetailRouteComponent() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="font-medium text-sm">Associated Crons</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: '/tasks/cron/manage',
|
||||
})
|
||||
}
|
||||
>
|
||||
<ListIcon className="h-4 w-4" />
|
||||
More
|
||||
</Button>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Cron
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<SubscriptionCronCreationDialogContent
|
||||
subscriptionId={subscription.id}
|
||||
onCancel={handleReload}
|
||||
/>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{subscription.cron?.nodes &&
|
||||
subscription.cron.nodes.length > 0 ? (
|
||||
subscription.cron.nodes.map((task) => (
|
||||
<Card
|
||||
key={task.id}
|
||||
className="group relative cursor-pointer p-4 transition-colors hover:bg-accent/50"
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: '/tasks/cron/detail/$id',
|
||||
params: {
|
||||
id: task.id.toString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="font-medium text-sm capitalize">
|
||||
<span>{task.cronExpr}</span>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<code className="break-all rounded bg-muted px-2 py-1 font-mono text-xs">
|
||||
{task.id}
|
||||
</code>
|
||||
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{task.status}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className="col-span-full py-8 text-center text-muted-foreground">
|
||||
No associated crons now
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
@ -436,17 +477,17 @@ function SubscriptionDetailRouteComponent() {
|
||||
}
|
||||
>
|
||||
<ListIcon className="h-4 w-4" />
|
||||
Tasks
|
||||
More
|
||||
</Button>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<RefreshCcwIcon className="h-4 w-4" />
|
||||
Sync
|
||||
Run Task
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<SubscriptionSyncDialogContent
|
||||
id={subscription.id}
|
||||
<SubscriptionTaskCreationDialogContent
|
||||
subscriptionId={subscription.id}
|
||||
onCancel={handleReload}
|
||||
/>
|
||||
</Dialog>
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { DetailCardSkeleton } from '@/components/detail-card-skeleton';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@ -8,6 +7,8 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DetailCardSkeleton } from '@/components/ui/detail-card-skeleton';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { FormFieldErrors } from '@/components/ui/form-field-errors';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@ -44,8 +45,8 @@ import {
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { ArrowLeft, Save, X } from 'lucide-react';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { Save } from 'lucide-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Credential3rdSelectContent } from './-credential3rd-select';
|
||||
@ -68,16 +69,8 @@ function FormView({
|
||||
subscription: SubscriptionDetailDto;
|
||||
onCompleted: VoidFunction;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const subscriptionService = useInject(SubscriptionService);
|
||||
|
||||
const handleBack = () => {
|
||||
navigate({
|
||||
to: '/subscriptions/detail/$id',
|
||||
params: { id: subscription.id.toString() },
|
||||
});
|
||||
};
|
||||
|
||||
const [updateSubscription, { loading: updating }] = useMutation<
|
||||
UpdateSubscriptionsMutation,
|
||||
UpdateSubscriptionsMutationVariables
|
||||
@ -149,35 +142,17 @@ function FormView({
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl py-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Subscription edit</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Edit subscription #{subscription.id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleBack} disabled={updating}>
|
||||
<X className="mr-2 h-4 w-4" />
|
||||
Cancel
|
||||
</Button>
|
||||
<ContainerHeader
|
||||
title="Subscription Edit"
|
||||
description={`Edit subscription #${subscription.id}`}
|
||||
defaultBackTo={`/subscriptions/detail/${subscription.id}`}
|
||||
actions={
|
||||
<Button onClick={() => form.handleSubmit()} disabled={updating}>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{updating ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
||||
import { DataTableViewOptions } from '@/components/ui/data-table-view-options';
|
||||
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
||||
@ -46,7 +47,7 @@ import { format } from 'date-fns';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { SubscriptionSyncDialogContent } from './-sync';
|
||||
import { SubscriptionTaskCreationDialogContent } from './-task-creation';
|
||||
|
||||
export const Route = createFileRoute('/_app/subscriptions/manage')({
|
||||
component: SubscriptionManageRouteComponent,
|
||||
@ -237,7 +238,9 @@ function SubscriptionManageRouteComponent() {
|
||||
Sync
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<SubscriptionSyncDialogContent id={row.original.id} />
|
||||
<SubscriptionTaskCreationDialogContent
|
||||
subscriptionId={row.original.id}
|
||||
/>
|
||||
</Dialog>
|
||||
</DropdownMenuActions>
|
||||
),
|
||||
@ -277,16 +280,16 @@ function SubscriptionManageRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto space-y-4 rounded-md">
|
||||
<div className="flex items-center justify-between pt-4">
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Subscription Management</h1>
|
||||
<p className="text-muted-foreground">Manage your subscription</p>
|
||||
</div>
|
||||
<Button onClick={() => navigate({ to: '/subscriptions/create' })}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Subscription
|
||||
</Button>
|
||||
</div>
|
||||
<ContainerHeader
|
||||
title="Subscription Management"
|
||||
description="Manage your subscription"
|
||||
actions={
|
||||
<Button onClick={() => navigate({ to: '/subscriptions/create' })}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Subscription
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center py-2">
|
||||
<DataTableViewOptions table={table} />
|
||||
</div>
|
||||
|
@ -0,0 +1,42 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { CronStatusEnum } from '@/infra/graphql/gql/graphql';
|
||||
import { AlertCircle, CheckCircle, Clock, Loader2 } from 'lucide-react';
|
||||
|
||||
export function getStatusBadge(status: CronStatusEnum) {
|
||||
switch (status) {
|
||||
case CronStatusEnum.Completed:
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-green-100 text-green-800">
|
||||
<CheckCircle className="mr-1 h-3 w-3 capitalize" />
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
case CronStatusEnum.Running:
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-blue-100 text-blue-800">
|
||||
<Loader2 className="mr-1 h-3 w-3 animate-spin capitalize" />
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
case CronStatusEnum.Failed:
|
||||
return (
|
||||
<Badge variant="destructive">
|
||||
<AlertCircle className="mr-1 h-3 w-3 capitalize" />
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
case CronStatusEnum.Pending:
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-yellow-100 text-yellow-800">
|
||||
<Clock className="mr-1 h-3 w-3 capitalize" />
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Badge variant="outline" className="capitalize">
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_app/tasks/cron/detail/$id')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/_app/tasks/cron/detail/$id"!</div>
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_app/tasks/cron/edit/$id')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/_app/tasks/cron/edit/$id"!</div>
|
||||
}
|
306
apps/webui/src/presentation/routes/_app/tasks/cron/manage.tsx
Normal file
306
apps/webui/src/presentation/routes/_app/tasks/cron/manage.tsx
Normal file
@ -0,0 +1,306 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||
import { DropdownMenuActions } from '@/components/ui/dropdown-menu-actions';
|
||||
import { QueryErrorView } from '@/components/ui/query-error-view';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import {
|
||||
type CronDto,
|
||||
DELETE_CRONS,
|
||||
GET_CRONS,
|
||||
} from '@/domains/recorder/schema/cron';
|
||||
import {
|
||||
apolloErrorToMessage,
|
||||
getApolloQueryError,
|
||||
} from '@/infra/errors/apollo';
|
||||
import {
|
||||
CronStatusEnum,
|
||||
type DeleteCronsMutation,
|
||||
type DeleteCronsMutationVariables,
|
||||
type GetCronsQuery,
|
||||
type GetCronsQueryVariables,
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import {
|
||||
type ColumnDef,
|
||||
type PaginationState,
|
||||
type SortingState,
|
||||
type VisibilityState,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import { format } from 'date-fns';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { getStatusBadge } from './-status-badge';
|
||||
|
||||
export const Route = createFileRoute('/_app/tasks/cron/manage')({
|
||||
component: TaskCronManageRouteComponent,
|
||||
staticData: {
|
||||
breadcrumb: { label: 'Manage' },
|
||||
} satisfies RouteStateDataOption,
|
||||
});
|
||||
|
||||
function TaskCronManageRouteComponent() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { loading, error, data, refetch } = useQuery<
|
||||
GetCronsQuery,
|
||||
GetCronsQueryVariables
|
||||
>(GET_CRONS, {
|
||||
variables: {
|
||||
pagination: {
|
||||
page: {
|
||||
page: pagination.pageIndex,
|
||||
limit: pagination.pageSize,
|
||||
},
|
||||
},
|
||||
filter: {},
|
||||
orderBy: {
|
||||
nextRun: 'DESC',
|
||||
},
|
||||
},
|
||||
pollInterval: 5000, // Auto-refresh every 5 seconds
|
||||
});
|
||||
|
||||
const { showSkeleton } = useDebouncedSkeleton({ loading });
|
||||
|
||||
const crons = data?.cron;
|
||||
|
||||
const [deleteCron] = useMutation<
|
||||
DeleteCronsMutation,
|
||||
DeleteCronsMutationVariables
|
||||
>(DELETE_CRONS, {
|
||||
onCompleted: async () => {
|
||||
const refetchResult = await refetch();
|
||||
const error = getApolloQueryError(refetchResult);
|
||||
if (error) {
|
||||
toast.error('Failed to delete tasks', {
|
||||
description: apolloErrorToMessage(error),
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast.success('Tasks deleted');
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error('Failed to delete tasks', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const cs: ColumnDef<CronDto>[] = [
|
||||
{
|
||||
header: 'ID',
|
||||
accessorKey: 'id',
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div
|
||||
className="max-w-[200px] truncate font-mono text-sm"
|
||||
title={row.original.id.toString()}
|
||||
>
|
||||
{row.original.id}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return cs;
|
||||
}, []);
|
||||
|
||||
const table = useReactTable({
|
||||
data: useMemo(() => (crons?.nodes ?? []) as CronDto[], [crons]),
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onSortingChange: setSorting,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
pageCount: crons?.paginationInfo?.pages,
|
||||
rowCount: crons?.paginationInfo?.total,
|
||||
enableColumnPinning: true,
|
||||
autoResetPageIndex: true,
|
||||
manualPagination: true,
|
||||
state: {
|
||||
pagination,
|
||||
sorting,
|
||||
columnVisibility,
|
||||
},
|
||||
initialState: {
|
||||
columnPinning: {
|
||||
right: ['actions'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <QueryErrorView message={error.message} onRetry={refetch} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl space-y-4 px-4">
|
||||
<ContainerHeader
|
||||
title="Crons Management"
|
||||
description="Manage your crons"
|
||||
actions={
|
||||
<Button onClick={() => refetch()} variant="outline" size="sm">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="space-y-3">
|
||||
{showSkeleton &&
|
||||
Array.from(new Array(10)).map((_, index) => (
|
||||
<Skeleton key={index} className="h-32 w-full" />
|
||||
))}
|
||||
|
||||
{!showSkeleton && table.getRowModel().rows?.length > 0 ? (
|
||||
table.getRowModel().rows.map((row, index) => {
|
||||
const cron = row.original;
|
||||
return (
|
||||
<div
|
||||
className="space-y-3 rounded-lg border bg-card p-4"
|
||||
key={`${cron.id}-${index}`}
|
||||
>
|
||||
{/* Header with status and priority */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="font-mono text-muted-foreground text-xs">
|
||||
# {cron.id}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Badge variant="outline" className="capitalize">
|
||||
{cron.cronExpr}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
{getStatusBadge(cron.status)}
|
||||
<Badge variant="outline">Priority: {cron.priority}</Badge>
|
||||
<div className="mr-0 ml-auto">
|
||||
<DropdownMenuActions
|
||||
id={cron.id}
|
||||
showDetail
|
||||
onDetail={() => {
|
||||
navigate({
|
||||
to: '/tasks/cron/detail/$id',
|
||||
params: { id: cron.id.toString() },
|
||||
});
|
||||
}}
|
||||
showDelete
|
||||
onDelete={() =>
|
||||
deleteCron({
|
||||
variables: {
|
||||
filter: {
|
||||
id: {
|
||||
eq: cron.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{cron.status === CronStatusEnum.Failed && (
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
// TODO: Retry cron
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuActions>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Time info */}
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Next run: </span>
|
||||
<span>
|
||||
{cron.nextRun
|
||||
? format(new Date(cron.nextRun), 'MM/dd HH:mm')
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-muted-foreground">Last run: </span>
|
||||
<span>
|
||||
{cron.lastRun
|
||||
? format(new Date(cron.lastRun), 'MM/dd HH:mm')
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Attempts */}
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">Attempts: </span>
|
||||
<span>
|
||||
{cron.attempts} / {cron.maxAttempts}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Lock at */}
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">Lock at: </span>
|
||||
<span>
|
||||
{cron.lockedAt
|
||||
? format(new Date(cron.lockedAt), 'MM/dd HH:mm')
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subscriber task cron */}
|
||||
{cron.subscriberTaskCron && (
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">Task:</span>
|
||||
<br />
|
||||
<span
|
||||
className="whitespace-pre-wrap"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
cron.subscriberTaskCron,
|
||||
null,
|
||||
2
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error if exists */}
|
||||
{cron.status === CronStatusEnum.Failed && cron.lastError && (
|
||||
<div className="rounded bg-destructive/10 p-2 text-destructive text-sm">
|
||||
{cron.lastError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<DetailEmptyView message="No tasks found" fullWidth />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DataTablePagination table={table} showSelectedRowCount={false} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import { buildVirtualBranchRouteOptions } from '@/infra/routes/utils';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/_app/tasks/cron')(
|
||||
buildVirtualBranchRouteOptions({
|
||||
title: 'Cron',
|
||||
})
|
||||
);
|
@ -1,4 +1,3 @@
|
||||
import { DetailCardSkeleton } from '@/components/detail-card-skeleton';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@ -8,6 +7,8 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DetailCardSkeleton } from '@/components/ui/detail-card-skeleton';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { QueryErrorView } from '@/components/ui/query-error-view';
|
||||
@ -24,14 +25,9 @@ import {
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import {
|
||||
createFileRoute,
|
||||
useCanGoBack,
|
||||
useNavigate,
|
||||
useRouter,
|
||||
} from '@tanstack/react-router';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { format } from 'date-fns';
|
||||
import { ArrowLeft, RefreshCw } from 'lucide-react';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { prettyTaskType } from './-pretty-task-type';
|
||||
@ -46,19 +42,6 @@ export const Route = createFileRoute('/_app/tasks/detail/$id')({
|
||||
|
||||
function TaskDetailRouteComponent() {
|
||||
const { id } = Route.useParams();
|
||||
const navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
const canGoBack = useCanGoBack();
|
||||
|
||||
const handleBack = () => {
|
||||
if (canGoBack) {
|
||||
router.history.back();
|
||||
} else {
|
||||
navigate({
|
||||
to: '/tasks/manage',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { data, loading, error, refetch } = useQuery<
|
||||
GetTasksQuery,
|
||||
@ -129,27 +112,17 @@ function TaskDetailRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl py-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<ContainerHeader
|
||||
title="Task Detail"
|
||||
description={`View task #${task.id}`}
|
||||
defaultBackTo="/tasks/manage"
|
||||
actions={
|
||||
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Task Detail</h1>
|
||||
<p className="mt-1 text-muted-foreground">View task #{task.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
type DeleteTasksMutation,
|
||||
type DeleteTasksMutationVariables,
|
||||
type GetTasksQuery,
|
||||
type GetTasksQueryVariables,
|
||||
type RetryTasksMutation,
|
||||
type RetryTasksMutationVariables,
|
||||
SubscriberTaskStatusEnum,
|
||||
@ -35,6 +36,7 @@ import {
|
||||
import { format } from 'date-fns';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
apolloErrorToMessage,
|
||||
@ -55,18 +57,17 @@ export const Route = createFileRoute('/_app/tasks/manage')({
|
||||
function TaskManageRouteComponent() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
|
||||
lockAt: false,
|
||||
lockBy: false,
|
||||
attempts: false,
|
||||
});
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { loading, error, data, refetch } = useQuery<GetTasksQuery>(GET_TASKS, {
|
||||
const { loading, error, data, refetch } = useQuery<
|
||||
GetTasksQuery,
|
||||
GetTasksQueryVariables
|
||||
>(GET_TASKS, {
|
||||
variables: {
|
||||
pagination: {
|
||||
page: {
|
||||
@ -172,16 +173,16 @@ function TaskManageRouteComponent() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto space-y-4 px-4">
|
||||
<div className="flex items-center justify-between pt-4">
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Tasks Management</h1>
|
||||
<p className="text-muted-foreground">Manage your tasks</p>
|
||||
</div>
|
||||
<Button onClick={() => refetch()} variant="outline" size="sm">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="container mx-auto max-w-4xl space-y-4 px-4">
|
||||
<ContainerHeader
|
||||
title="Tasks Management"
|
||||
description="Manage your tasks"
|
||||
actions={
|
||||
<Button onClick={() => refetch()} variant="outline" size="sm">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="space-y-3">
|
||||
{showSkeleton &&
|
||||
|
@ -44,6 +44,14 @@
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": ["**/tsconfig.json", "**/tsconfig.*.json"],
|
||||
"json": {
|
||||
"parser": {
|
||||
"allowComments": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"include": ["apps/webui/src/infra/graphql/gql/**/*"],
|
||||
"linter": {
|
||||
|
62
pnpm-lock.yaml
generated
62
pnpm-lock.yaml
generated
@ -89,6 +89,12 @@ importers:
|
||||
'@corvu/resizable':
|
||||
specifier: ^0.2.5
|
||||
version: 0.2.5(solid-js@1.9.7)
|
||||
'@datasert/cronjs-matcher':
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
'@datasert/cronjs-parser':
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
'@graphiql/toolkit':
|
||||
specifier: ^0.11.3
|
||||
version: 0.11.3(@types/node@24.0.10)(graphql-ws@6.0.4(graphql@16.11.0)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5)))(graphql@16.11.0)
|
||||
@ -263,6 +269,9 @@ importers:
|
||||
tailwind-merge:
|
||||
specifier: ^3.3.1
|
||||
version: 3.3.1
|
||||
tailwind-scrollbar:
|
||||
specifier: ^4.0.2
|
||||
version: 4.0.2(react@19.1.0)(tailwindcss@4.1.10)
|
||||
tailwindcss:
|
||||
specifier: ^4.1.10
|
||||
version: 4.1.10
|
||||
@ -712,6 +721,12 @@ packages:
|
||||
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@datasert/cronjs-matcher@1.4.0':
|
||||
resolution: {integrity: sha512-5wAAKYfClZQDWjOeGReEnGLlBKds5K0CitnTv17sH32X4PSuck1dysX71zzCgrm0JCSpobDNg4b292ewhoy6ww==}
|
||||
|
||||
'@datasert/cronjs-parser@1.4.0':
|
||||
resolution: {integrity: sha512-zHGlrWanS4Zjgf0aMi/sp/HTSa2xWDEtXW9xshhlGf/jPx3zTIqfX14PZnoFF7XVOwzC49Zy0SFWG90rlRY36Q==}
|
||||
|
||||
'@date-fns/tz@1.2.0':
|
||||
resolution: {integrity: sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg==}
|
||||
|
||||
@ -3167,6 +3182,9 @@ packages:
|
||||
'@types/node@24.0.10':
|
||||
resolution: {integrity: sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==}
|
||||
|
||||
'@types/prismjs@1.26.5':
|
||||
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
|
||||
|
||||
'@types/react-dom@19.1.6':
|
||||
resolution: {integrity: sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==}
|
||||
peerDependencies:
|
||||
@ -5008,6 +5026,10 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
luxon@3.6.1:
|
||||
resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
magic-string@0.30.17:
|
||||
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
|
||||
|
||||
@ -5467,6 +5489,11 @@ packages:
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
prism-react-renderer@2.4.1:
|
||||
resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==}
|
||||
peerDependencies:
|
||||
react: '>=16.0.0'
|
||||
|
||||
prismjs@1.29.0:
|
||||
resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==}
|
||||
engines: {node: '>=6'}
|
||||
@ -6162,6 +6189,12 @@ packages:
|
||||
tailwind-merge@3.3.1:
|
||||
resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
|
||||
|
||||
tailwind-scrollbar@4.0.2:
|
||||
resolution: {integrity: sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA==}
|
||||
engines: {node: '>=12.13.0'}
|
||||
peerDependencies:
|
||||
tailwindcss: 4.x
|
||||
|
||||
tailwindcss@4.1.10:
|
||||
resolution: {integrity: sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==}
|
||||
|
||||
@ -7216,6 +7249,13 @@ snapshots:
|
||||
'@csstools/css-tokenizer@3.0.4':
|
||||
optional: true
|
||||
|
||||
'@datasert/cronjs-matcher@1.4.0':
|
||||
dependencies:
|
||||
'@datasert/cronjs-parser': 1.4.0
|
||||
luxon: 3.6.1
|
||||
|
||||
'@datasert/cronjs-parser@1.4.0': {}
|
||||
|
||||
'@date-fns/tz@1.2.0': {}
|
||||
|
||||
'@emnapi/runtime@1.4.3':
|
||||
@ -9812,6 +9852,8 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 7.8.0
|
||||
|
||||
'@types/prismjs@1.26.5': {}
|
||||
|
||||
'@types/react-dom@19.1.6(@types/react@19.0.1)':
|
||||
dependencies:
|
||||
'@types/react': 19.0.1
|
||||
@ -10219,7 +10261,7 @@ snapshots:
|
||||
|
||||
browserslist@4.25.0:
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001721
|
||||
caniuse-lite: 1.0.30001726
|
||||
electron-to-chromium: 1.5.165
|
||||
node-releases: 2.0.19
|
||||
update-browserslist-db: 1.1.3(browserslist@4.25.0)
|
||||
@ -10282,8 +10324,7 @@ snapshots:
|
||||
|
||||
caniuse-lite@1.0.30001721: {}
|
||||
|
||||
caniuse-lite@1.0.30001726:
|
||||
optional: true
|
||||
caniuse-lite@1.0.30001726: {}
|
||||
|
||||
capital-case@1.0.4:
|
||||
dependencies:
|
||||
@ -11892,6 +11933,8 @@ snapshots:
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
|
||||
luxon@3.6.1: {}
|
||||
|
||||
magic-string@0.30.17:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
@ -12317,6 +12360,12 @@ snapshots:
|
||||
|
||||
prettier@3.5.3: {}
|
||||
|
||||
prism-react-renderer@2.4.1(react@19.1.0):
|
||||
dependencies:
|
||||
'@types/prismjs': 1.26.5
|
||||
clsx: 2.1.1
|
||||
react: 19.1.0
|
||||
|
||||
prismjs@1.29.0: {}
|
||||
|
||||
prismjs@1.30.0: {}
|
||||
@ -13154,6 +13203,13 @@ snapshots:
|
||||
|
||||
tailwind-merge@3.3.1: {}
|
||||
|
||||
tailwind-scrollbar@4.0.2(react@19.1.0)(tailwindcss@4.1.10):
|
||||
dependencies:
|
||||
prism-react-renderer: 2.4.1(react@19.1.0)
|
||||
tailwindcss: 4.1.10
|
||||
transitivePeerDependencies:
|
||||
- react
|
||||
|
||||
tailwindcss@4.1.10: {}
|
||||
|
||||
tapable@2.2.2: {}
|
||||
|
@ -19,8 +19,9 @@
|
||||
"target": "ES2020",
|
||||
"strictNullChecks": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
// controlled by biome
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"useDefineForClassFields": true
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,6 @@
|
||||
{
|
||||
"path": "./apps/webui"
|
||||
},
|
||||
|
||||
{
|
||||
"path": "./packages/email"
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user