feat: init cron webui
This commit is contained in:
parent
a1c2eeded1
commit
004fed9b2e
@ -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":""}
|
||||||
|
@ -107,7 +107,7 @@ impl App {
|
|||||||
Ok::<(), RecorderError>(())
|
Ok::<(), RecorderError>(())
|
||||||
},
|
},
|
||||||
async {
|
async {
|
||||||
task.run(if graceful_shutdown {
|
task.run_with_signal(if graceful_shutdown {
|
||||||
Some(Self::shutdown_signal)
|
Some(Self::shutdown_signal)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -8,9 +8,10 @@ use crate::{
|
|||||||
Subscriptions, table_auto_z,
|
Subscriptions, table_auto_z,
|
||||||
},
|
},
|
||||||
models::cron::{
|
models::cron::{
|
||||||
CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME, CRON_DUE_EVENT, CronStatus, CronStatusEnum,
|
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,
|
CronStatus, CronStatusEnum, NOTIFY_DUE_CRON_WHEN_MUTATING_FUNCTION_NAME,
|
||||||
SETUP_CRON_EXTRA_FOREIGN_KEYS_FUNCTION_NAME, SETUP_CRON_EXTRA_FOREIGN_KEYS_TRIGGER_NAME,
|
NOTIFY_DUE_CRON_WHEN_MUTATING_TRIGGER_NAME, SETUP_CRON_EXTRA_FOREIGN_KEYS_FUNCTION_NAME,
|
||||||
|
SETUP_CRON_EXTRA_FOREIGN_KEYS_TRIGGER_NAME,
|
||||||
},
|
},
|
||||||
task::{
|
task::{
|
||||||
SETUP_APALIS_JOBS_EXTRA_FOREIGN_KEYS_FUNCTION_NAME, SUBSCRIBER_TASK_APALIS_NAME,
|
SETUP_APALIS_JOBS_EXTRA_FOREIGN_KEYS_FUNCTION_NAME, SUBSCRIBER_TASK_APALIS_NAME,
|
||||||
@ -48,14 +49,13 @@ impl MigrationTrait for Migration {
|
|||||||
.col(string_null(Cron::LockedBy))
|
.col(string_null(Cron::LockedBy))
|
||||||
.col(timestamp_with_time_zone_null(Cron::LockedAt))
|
.col(timestamp_with_time_zone_null(Cron::LockedAt))
|
||||||
.col(integer_null(Cron::TimeoutMs))
|
.col(integer_null(Cron::TimeoutMs))
|
||||||
.col(integer(Cron::Attempts))
|
.col(integer(Cron::Attempts).default(0))
|
||||||
.col(integer(Cron::MaxAttempts))
|
.col(integer(Cron::MaxAttempts).default(1))
|
||||||
.col(integer(Cron::Priority))
|
.col(integer(Cron::Priority).default(0))
|
||||||
.col(enumeration(
|
.col(
|
||||||
Cron::Status,
|
enumeration(Cron::Status, CronStatusEnum, CronStatus::iden_values())
|
||||||
CronStatusEnum,
|
.default(CronStatus::Pending),
|
||||||
CronStatus::iden_values(),
|
)
|
||||||
))
|
|
||||||
.col(json_binary_null(Cron::SubscriberTaskCron))
|
.col(json_binary_null(Cron::SubscriberTaskCron))
|
||||||
.col(json_binary_null(Cron::SystemTaskCron))
|
.col(json_binary_null(Cron::SystemTaskCron))
|
||||||
.foreign_key(
|
.foreign_key(
|
||||||
@ -207,9 +207,12 @@ impl MigrationTrait for Migration {
|
|||||||
ORDER BY {priority} ASC, {next_run} ASC
|
ORDER BY {priority} ASC, {next_run} ASC
|
||||||
FOR UPDATE SKIP LOCKED
|
FOR UPDATE SKIP LOCKED
|
||||||
LOOP
|
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);
|
PERFORM pg_notify('{CRON_DUE_EVENT}', row_to_json(cron_record)::text);
|
||||||
notification_count := notification_count + 1;
|
notification_count := notification_count + 1;
|
||||||
END LOOP;
|
END LOOP;
|
||||||
|
|
||||||
|
-- PERFORM pg_notify('{CRON_DUE_DEBUG_EVENT}', format('Notification count: %I; Now time: %s', notification_count, CURRENT_TIMESTAMP));
|
||||||
RETURN notification_count;
|
RETURN notification_count;
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;"#,
|
$$ LANGUAGE plpgsql;"#,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
pub const CRON_DUE_EVENT: &str = "cron_due";
|
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";
|
pub const CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME: &str = "check_and_trigger_due_crons";
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
mod core;
|
mod core;
|
||||||
|
|
||||||
pub use 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,
|
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,
|
SETUP_CRON_EXTRA_FOREIGN_KEYS_FUNCTION_NAME, SETUP_CRON_EXTRA_FOREIGN_KEYS_TRIGGER_NAME,
|
||||||
};
|
};
|
||||||
@ -59,8 +59,8 @@ pub struct Model {
|
|||||||
pub last_error: Option<String>,
|
pub last_error: Option<String>,
|
||||||
pub locked_by: Option<String>,
|
pub locked_by: Option<String>,
|
||||||
pub locked_at: Option<DateTimeUtc>,
|
pub locked_at: Option<DateTimeUtc>,
|
||||||
#[sea_orm(default_expr = "5000")]
|
// default_expr = "5000"
|
||||||
pub timeout_ms: i32,
|
pub timeout_ms: Option<i32>,
|
||||||
#[sea_orm(default_expr = "0")]
|
#[sea_orm(default_expr = "0")]
|
||||||
pub attempts: i32,
|
pub attempts: i32,
|
||||||
#[sea_orm(default_expr = "1")]
|
#[sea_orm(default_expr = "1")]
|
||||||
@ -223,7 +223,10 @@ impl Model {
|
|||||||
&& cron.attempts < cron.max_attempts
|
&& cron.attempts < cron.max_attempts
|
||||||
&& cron.status == CronStatus::Pending
|
&& cron.status == CronStatus::Pending
|
||||||
&& (cron.locked_at.is_none_or(|locked_at| {
|
&& (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())
|
&& cron.next_run.is_some_and(|next_run| next_run <= Utc::now())
|
||||||
{
|
{
|
||||||
@ -376,7 +379,15 @@ impl Model {
|
|||||||
locked_cron
|
locked_cron
|
||||||
.mark_cron_failed(
|
.mark_cron_failed(
|
||||||
ctx,
|
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,
|
retry_duration,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@ -389,7 +400,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn calculate_next_run(cron_expr: &str) -> RecorderResult<DateTime<Utc>> {
|
pub fn calculate_next_run(cron_expr: &str) -> RecorderResult<DateTime<Utc>> {
|
||||||
let cron_expr = Cron::new(cron_expr).parse()?;
|
let cron_expr = Cron::new(cron_expr).with_seconds_optional().parse()?;
|
||||||
|
|
||||||
let next = cron_expr.find_next_occurrence(&Utc::now(), false)?;
|
let next = cron_expr.find_next_occurrence(&Utc::now(), false)?;
|
||||||
|
|
||||||
|
@ -60,6 +60,8 @@ pub enum Relation {
|
|||||||
Feed,
|
Feed,
|
||||||
#[sea_orm(has_many = "super::subscriber_tasks::Entity")]
|
#[sea_orm(has_many = "super::subscriber_tasks::Entity")]
|
||||||
SubscriberTask,
|
SubscriberTask,
|
||||||
|
#[sea_orm(has_many = "super::cron::Entity")]
|
||||||
|
Cron,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Related<super::subscribers::Entity> for Entity {
|
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)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
|
||||||
pub enum RelatedEntity {
|
pub enum RelatedEntity {
|
||||||
#[sea_orm(entity = "super::subscribers::Entity")]
|
#[sea_orm(entity = "super::subscribers::Entity")]
|
||||||
@ -144,6 +152,8 @@ pub enum RelatedEntity {
|
|||||||
Feed,
|
Feed,
|
||||||
#[sea_orm(entity = "super::subscriber_tasks::Entity")]
|
#[sea_orm(entity = "super::subscriber_tasks::Entity")]
|
||||||
SubscriberTask,
|
SubscriberTask,
|
||||||
|
#[sea_orm(entity = "super::cron::Entity")]
|
||||||
|
Cron,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
@ -13,7 +13,7 @@ use uuid::Uuid;
|
|||||||
use crate::{
|
use crate::{
|
||||||
app::AppContextTrait,
|
app::AppContextTrait,
|
||||||
errors::{RecorderError, RecorderResult},
|
errors::{RecorderError, RecorderResult},
|
||||||
models::cron::{self, CRON_DUE_EVENT},
|
models::cron::{self, CRON_DUE_DEBUG_EVENT, CRON_DUE_EVENT},
|
||||||
task::{
|
task::{
|
||||||
AsyncTaskTrait, SUBSCRIBER_TASK_APALIS_NAME, SYSTEM_TASK_APALIS_NAME, SubscriberTask,
|
AsyncTaskTrait, SUBSCRIBER_TASK_APALIS_NAME, SYSTEM_TASK_APALIS_NAME, SubscriberTask,
|
||||||
TaskConfig,
|
TaskConfig,
|
||||||
@ -152,14 +152,20 @@ impl TaskService {
|
|||||||
Ok(m)
|
Ok(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run<F, Fut>(&self, shutdown_signal: Option<F>) -> RecorderResult<()>
|
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
|
where
|
||||||
F: Fn() -> Fut + Send + 'static,
|
F: FnOnce() -> Fut + Send + 'static,
|
||||||
Fut: Future<Output = ()> + Send,
|
Fut: Future<Output = ()> + Send,
|
||||||
{
|
{
|
||||||
tokio::try_join!(
|
tokio::select! {
|
||||||
async {
|
_ = {
|
||||||
let monitor = self.setup_apalis_monitor().await?;
|
let monitor = self.setup_apalis_monitor().await?;
|
||||||
|
async move {
|
||||||
if let Some(shutdown_signal) = shutdown_signal {
|
if let Some(shutdown_signal) = shutdown_signal {
|
||||||
monitor
|
monitor
|
||||||
.run_with_signal(async move {
|
.run_with_signal(async move {
|
||||||
@ -172,27 +178,28 @@ impl TaskService {
|
|||||||
monitor.run().await?;
|
monitor.run().await?;
|
||||||
}
|
}
|
||||||
Ok::<_, RecorderError>(())
|
Ok::<_, RecorderError>(())
|
||||||
},
|
}
|
||||||
async {
|
} => {}
|
||||||
|
_ = {
|
||||||
let listener = self.setup_apalis_listener().await?;
|
let listener = self.setup_apalis_listener().await?;
|
||||||
tokio::task::spawn(async move {
|
async move {
|
||||||
if let Err(e) = listener.listen().await {
|
if let Err(e) = listener.listen().await {
|
||||||
tracing::error!("Error listening to apalis: {e}");
|
tracing::error!("Error listening to apalis: {e}");
|
||||||
}
|
}
|
||||||
});
|
|
||||||
Ok::<_, RecorderError>(())
|
Ok::<_, RecorderError>(())
|
||||||
},
|
}
|
||||||
async {
|
} => {},
|
||||||
|
_ = {
|
||||||
let mut listener = self.setup_cron_due_listening().await?;
|
let mut listener = self.setup_cron_due_listening().await?;
|
||||||
let cron_worker_id = self.cron_worker_id.clone();
|
let cron_worker_id = self.cron_worker_id.clone();
|
||||||
let retry_duration = chrono::Duration::milliseconds(
|
let retry_duration =
|
||||||
self.config.cron_retry_duration.as_millis() as i64,
|
chrono::Duration::milliseconds(self.config.cron_retry_duration.as_millis() as i64);
|
||||||
);
|
|
||||||
let cron_interval_duration = self.config.cron_interval_duration;
|
let cron_interval_duration = self.config.cron_interval_duration;
|
||||||
listener.listen(CRON_DUE_EVENT).await?;
|
async move {
|
||||||
tracing::debug!("Listening for cron due event...");
|
listener.listen_all([CRON_DUE_EVENT as &str, CRON_DUE_DEBUG_EVENT as &str]).await?;
|
||||||
|
|
||||||
tokio::task::spawn({
|
tokio::join!(
|
||||||
|
{
|
||||||
let ctx = self.ctx.clone();
|
let ctx = self.ctx.clone();
|
||||||
async move {
|
async move {
|
||||||
if let Err(e) =
|
if let Err(e) =
|
||||||
@ -202,12 +209,11 @@ impl TaskService {
|
|||||||
tracing::error!("Error listening to cron due: {e}");
|
tracing::error!("Error listening to cron due: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
{
|
||||||
tokio::task::spawn({
|
|
||||||
let ctx = self.ctx.clone();
|
let ctx = self.ctx.clone();
|
||||||
async move {
|
|
||||||
let mut interval = tokio::time::interval(cron_interval_duration);
|
let mut interval = tokio::time::interval(cron_interval_duration);
|
||||||
|
async move {
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
if let Err(e) = cron::Model::check_and_cleanup_expired_cron_locks(
|
if let Err(e) = cron::Model::check_and_cleanup_expired_cron_locks(
|
||||||
@ -220,6 +226,7 @@ impl TaskService {
|
|||||||
"Error checking and cleaning up expired cron locks: {e}"
|
"Error checking and cleaning up expired cron locks: {e}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) =
|
if let Err(e) =
|
||||||
cron::Model::check_and_trigger_due_crons(ctx.as_ref()).await
|
cron::Model::check_and_trigger_due_crons(ctx.as_ref()).await
|
||||||
{
|
{
|
||||||
@ -227,11 +234,13 @@ impl TaskService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
Ok::<_, RecorderError>(())
|
Ok::<_, RecorderError>(())
|
||||||
}
|
}
|
||||||
)?;
|
} => {}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,8 +308,11 @@ impl TaskService {
|
|||||||
) -> RecorderResult<()> {
|
) -> RecorderResult<()> {
|
||||||
loop {
|
loop {
|
||||||
let notification = listener.recv().await?;
|
let notification = listener.recv().await?;
|
||||||
tracing::debug!("Received cron due event: {:?}", notification);
|
if notification.channel() == CRON_DUE_DEBUG_EVENT {
|
||||||
if let Err(e) = cron::Model::handle_cron_notification(
|
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(),
|
ctx.as_ref(),
|
||||||
notification,
|
notification,
|
||||||
worker_id,
|
worker_id,
|
||||||
@ -319,6 +331,7 @@ impl TaskService {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
use rstest::{fixture, rstest};
|
use rstest::{fixture, rstest};
|
||||||
use sea_orm::ActiveValue;
|
use sea_orm::ActiveValue;
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
@ -340,12 +353,14 @@ mod tests {
|
|||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
// #[tracing_test::traced_test]
|
#[tracing_test::traced_test]
|
||||||
async fn test_cron_due_listening(before_each: ()) -> RecorderResult<()> {
|
async fn test_check_and_trigger_due_crons_with_certain_interval(
|
||||||
|
before_each: (),
|
||||||
|
) -> RecorderResult<()> {
|
||||||
let preset = TestingPreset::default_with_config(
|
let preset = TestingPreset::default_with_config(
|
||||||
TestingAppContextConfig::builder()
|
TestingAppContextConfig::builder()
|
||||||
.task_config(TaskConfig {
|
.task_config(TaskConfig {
|
||||||
cron_interval_duration: Duration::from_secs(1),
|
cron_interval_duration: Duration::from_millis(1500),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.build(),
|
.build(),
|
||||||
@ -364,15 +379,53 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = task_service
|
task_service.add_system_task_cron(echo_cron).await?;
|
||||||
.run(Some(async move || {
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
|
||||||
}))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// assert!(logs_contain(&format!(
|
task_service
|
||||||
// "EchoTask {task_id} start running at"
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ impl TestingAppContext {
|
|||||||
.build(),
|
.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);
|
app_ctx.set_task(task_service);
|
||||||
|
|
||||||
@ -134,7 +134,7 @@ impl AppContextTrait for TestingAppContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(TypedBuilder)]
|
#[derive(TypedBuilder, Debug)]
|
||||||
#[builder(field_defaults(default, setter(strip_option)))]
|
#[builder(field_defaults(default, setter(strip_option)))]
|
||||||
pub struct TestingAppContextConfig {
|
pub struct TestingAppContextConfig {
|
||||||
pub mikan_base_url: Option<String>,
|
pub mikan_base_url: Option<String>,
|
||||||
|
@ -7,11 +7,10 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub async fn build_testing_task_service(
|
pub async fn build_testing_task_service(
|
||||||
|
config: Option<TaskConfig>,
|
||||||
ctx: Arc<dyn AppContextTrait>,
|
ctx: Arc<dyn AppContextTrait>,
|
||||||
) -> RecorderResult<TaskService> {
|
) -> RecorderResult<TaskService> {
|
||||||
let config = TaskConfig {
|
let config = config.unwrap_or_default();
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let task_service = TaskService::from_config_and_ctx(config, ctx).await?;
|
let task_service = TaskService::from_config_and_ctx(config, ctx).await?;
|
||||||
|
|
||||||
Ok(task_service)
|
Ok(task_service)
|
||||||
|
@ -65,11 +65,17 @@ export const AppNavMainData: NavMainGroup[] = [
|
|||||||
icon: ListTodo,
|
icon: ListTodo,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
title: 'Manage',
|
title: 'Tasks',
|
||||||
link: {
|
link: {
|
||||||
to: '/tasks/manage',
|
to: '/tasks/manage',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Crons',
|
||||||
|
link: {
|
||||||
|
to: '/tasks/cron/manage',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
58
apps/webui/src/domains/recorder/schema/cron.ts
Normal file
58
apps/webui/src/domains/recorder/schema/cron.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
`;
|
@ -117,6 +117,25 @@ query GetSubscriptionDetail ($id: Int!) {
|
|||||||
id
|
id
|
||||||
username
|
username
|
||||||
}
|
}
|
||||||
|
cron {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
cronExpr
|
||||||
|
nextRun
|
||||||
|
lastRun
|
||||||
|
lastError
|
||||||
|
status
|
||||||
|
lockedAt
|
||||||
|
lockedBy
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
timeoutMs
|
||||||
|
maxAttempts
|
||||||
|
priority
|
||||||
|
attempts
|
||||||
|
subscriberTaskCron
|
||||||
|
}
|
||||||
|
}
|
||||||
bangumi {
|
bangumi {
|
||||||
nodes {
|
nodes {
|
||||||
createdAt
|
createdAt
|
||||||
|
@ -20,13 +20,15 @@ type Documents = {
|
|||||||
"\n mutation DeleteCredential3rd($filter: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filter)\n }\n": typeof types.DeleteCredential3rdDocument,
|
"\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 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,
|
"\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 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 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 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 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 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 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,
|
"\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 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 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,
|
"\n mutation DeleteTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filter)\n }\n": typeof types.DeleteTasksDocument,
|
||||||
@ -39,13 +41,15 @@ const documents: Documents = {
|
|||||||
"\n mutation DeleteCredential3rd($filter: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filter)\n }\n": types.DeleteCredential3rdDocument,
|
"\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 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,
|
"\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 InsertFeed($data: FeedsInsertInput!) {\n feedsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n feedType\n token\n }\n }\n": types.InsertFeedDocument,
|
"\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 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 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 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 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,
|
"\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 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 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,
|
"\n mutation DeleteTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filter)\n }\n": types.DeleteTasksDocument,
|
||||||
@ -90,6 +94,14 @@ 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.
|
* 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"];
|
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.
|
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
@ -117,7 +129,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.
|
* 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.
|
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
import { guardRouteIndexAsNotFound } from '@/components/layout/app-not-found';
|
import { guardRouteIndexAsNotFound } from '@/components/layout/app-not-found';
|
||||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||||
import { Outlet } from '@tanstack/react-router';
|
import { Outlet, type RouteOptions } from '@tanstack/react-router';
|
||||||
|
|
||||||
export interface BuildVirtualBranchRouteOptions {
|
export interface BuildVirtualBranchRouteOptions {
|
||||||
title: string;
|
title: string;
|
||||||
@ -8,7 +8,11 @@ export interface BuildVirtualBranchRouteOptions {
|
|||||||
|
|
||||||
export function buildVirtualBranchRouteOptions(
|
export function buildVirtualBranchRouteOptions(
|
||||||
options: BuildVirtualBranchRouteOptions
|
options: BuildVirtualBranchRouteOptions
|
||||||
) {
|
): {
|
||||||
|
beforeLoad: RouteOptions['beforeLoad'];
|
||||||
|
staticData: RouteStateDataOption;
|
||||||
|
component: RouteOptions['component'];
|
||||||
|
} {
|
||||||
return {
|
return {
|
||||||
beforeLoad: guardRouteIndexAsNotFound,
|
beforeLoad: guardRouteIndexAsNotFound,
|
||||||
staticData: {
|
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 AppCredential3rdCreateRouteImport } from './routes/_app/credential3rd/create'
|
||||||
import { Route as AppBangumiManageRouteImport } from './routes/_app/bangumi/manage'
|
import { Route as AppBangumiManageRouteImport } from './routes/_app/bangumi/manage'
|
||||||
import { Route as AppExploreExploreRouteImport } from './routes/_app/_explore/explore'
|
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 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 AppSubscriptionsEditIdRouteImport } from './routes/_app/subscriptions/edit.$id'
|
||||||
import { Route as AppSubscriptionsDetailIdRouteImport } from './routes/_app/subscriptions/detail.$id'
|
import { Route as AppSubscriptionsDetailIdRouteImport } from './routes/_app/subscriptions/detail.$id'
|
||||||
import { Route as AppCredential3rdEditIdRouteImport } from './routes/_app/credential3rd/edit.$id'
|
import { Route as AppCredential3rdEditIdRouteImport } from './routes/_app/credential3rd/edit.$id'
|
||||||
import { Route as AppCredential3rdDetailIdRouteImport } from './routes/_app/credential3rd/detail.$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({
|
const AboutRoute = AboutRouteImport.update({
|
||||||
id: '/about',
|
id: '/about',
|
||||||
@ -148,11 +152,21 @@ const AppExploreExploreRoute = AppExploreExploreRouteImport.update({
|
|||||||
path: '/explore',
|
path: '/explore',
|
||||||
getParentRoute: () => AppRouteRoute,
|
getParentRoute: () => AppRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AppTasksCronRouteRoute = AppTasksCronRouteRouteImport.update({
|
||||||
|
id: '/cron',
|
||||||
|
path: '/cron',
|
||||||
|
getParentRoute: () => AppTasksRouteRoute,
|
||||||
|
} as any)
|
||||||
const AppTasksDetailIdRoute = AppTasksDetailIdRouteImport.update({
|
const AppTasksDetailIdRoute = AppTasksDetailIdRouteImport.update({
|
||||||
id: '/detail/$id',
|
id: '/detail/$id',
|
||||||
path: '/detail/$id',
|
path: '/detail/$id',
|
||||||
getParentRoute: () => AppTasksRouteRoute,
|
getParentRoute: () => AppTasksRouteRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AppTasksCronManageRoute = AppTasksCronManageRouteImport.update({
|
||||||
|
id: '/manage',
|
||||||
|
path: '/manage',
|
||||||
|
getParentRoute: () => AppTasksCronRouteRoute,
|
||||||
|
} as any)
|
||||||
const AppSubscriptionsEditIdRoute = AppSubscriptionsEditIdRouteImport.update({
|
const AppSubscriptionsEditIdRoute = AppSubscriptionsEditIdRouteImport.update({
|
||||||
id: '/edit/$id',
|
id: '/edit/$id',
|
||||||
path: '/edit/$id',
|
path: '/edit/$id',
|
||||||
@ -175,6 +189,16 @@ const AppCredential3rdDetailIdRoute =
|
|||||||
path: '/detail/$id',
|
path: '/detail/$id',
|
||||||
getParentRoute: () => AppCredential3rdRouteRoute,
|
getParentRoute: () => AppCredential3rdRouteRoute,
|
||||||
} as any)
|
} 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 {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
@ -189,6 +213,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/tasks': typeof AppTasksRouteRouteWithChildren
|
'/tasks': typeof AppTasksRouteRouteWithChildren
|
||||||
'/auth/sign-in': typeof AuthSignInRoute
|
'/auth/sign-in': typeof AuthSignInRoute
|
||||||
'/auth/sign-up': typeof AuthSignUpRoute
|
'/auth/sign-up': typeof AuthSignUpRoute
|
||||||
|
'/tasks/cron': typeof AppTasksCronRouteRouteWithChildren
|
||||||
'/explore': typeof AppExploreExploreRoute
|
'/explore': typeof AppExploreExploreRoute
|
||||||
'/bangumi/manage': typeof AppBangumiManageRoute
|
'/bangumi/manage': typeof AppBangumiManageRoute
|
||||||
'/credential3rd/create': typeof AppCredential3rdCreateRoute
|
'/credential3rd/create': typeof AppCredential3rdCreateRoute
|
||||||
@ -203,7 +228,10 @@ export interface FileRoutesByFullPath {
|
|||||||
'/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
|
'/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
|
||||||
'/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
|
'/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
|
||||||
'/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
|
'/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
|
||||||
|
'/tasks/cron/manage': typeof AppTasksCronManageRoute
|
||||||
'/tasks/detail/$id': typeof AppTasksDetailIdRoute
|
'/tasks/detail/$id': typeof AppTasksDetailIdRoute
|
||||||
|
'/tasks/cron/detail/$id': typeof AppTasksCronDetailIdRoute
|
||||||
|
'/tasks/cron/edit/$id': typeof AppTasksCronEditIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
@ -218,6 +246,7 @@ export interface FileRoutesByTo {
|
|||||||
'/tasks': typeof AppTasksRouteRouteWithChildren
|
'/tasks': typeof AppTasksRouteRouteWithChildren
|
||||||
'/auth/sign-in': typeof AuthSignInRoute
|
'/auth/sign-in': typeof AuthSignInRoute
|
||||||
'/auth/sign-up': typeof AuthSignUpRoute
|
'/auth/sign-up': typeof AuthSignUpRoute
|
||||||
|
'/tasks/cron': typeof AppTasksCronRouteRouteWithChildren
|
||||||
'/explore': typeof AppExploreExploreRoute
|
'/explore': typeof AppExploreExploreRoute
|
||||||
'/bangumi/manage': typeof AppBangumiManageRoute
|
'/bangumi/manage': typeof AppBangumiManageRoute
|
||||||
'/credential3rd/create': typeof AppCredential3rdCreateRoute
|
'/credential3rd/create': typeof AppCredential3rdCreateRoute
|
||||||
@ -232,7 +261,10 @@ export interface FileRoutesByTo {
|
|||||||
'/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
|
'/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
|
||||||
'/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
|
'/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
|
||||||
'/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
|
'/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
|
||||||
|
'/tasks/cron/manage': typeof AppTasksCronManageRoute
|
||||||
'/tasks/detail/$id': typeof AppTasksDetailIdRoute
|
'/tasks/detail/$id': typeof AppTasksDetailIdRoute
|
||||||
|
'/tasks/cron/detail/$id': typeof AppTasksCronDetailIdRoute
|
||||||
|
'/tasks/cron/edit/$id': typeof AppTasksCronEditIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
@ -248,6 +280,7 @@ export interface FileRoutesById {
|
|||||||
'/_app/tasks': typeof AppTasksRouteRouteWithChildren
|
'/_app/tasks': typeof AppTasksRouteRouteWithChildren
|
||||||
'/auth/sign-in': typeof AuthSignInRoute
|
'/auth/sign-in': typeof AuthSignInRoute
|
||||||
'/auth/sign-up': typeof AuthSignUpRoute
|
'/auth/sign-up': typeof AuthSignUpRoute
|
||||||
|
'/_app/tasks/cron': typeof AppTasksCronRouteRouteWithChildren
|
||||||
'/_app/_explore/explore': typeof AppExploreExploreRoute
|
'/_app/_explore/explore': typeof AppExploreExploreRoute
|
||||||
'/_app/bangumi/manage': typeof AppBangumiManageRoute
|
'/_app/bangumi/manage': typeof AppBangumiManageRoute
|
||||||
'/_app/credential3rd/create': typeof AppCredential3rdCreateRoute
|
'/_app/credential3rd/create': typeof AppCredential3rdCreateRoute
|
||||||
@ -262,7 +295,10 @@ export interface FileRoutesById {
|
|||||||
'/_app/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
|
'/_app/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
|
||||||
'/_app/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
|
'/_app/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
|
||||||
'/_app/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
|
'/_app/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
|
||||||
|
'/_app/tasks/cron/manage': typeof AppTasksCronManageRoute
|
||||||
'/_app/tasks/detail/$id': typeof AppTasksDetailIdRoute
|
'/_app/tasks/detail/$id': typeof AppTasksDetailIdRoute
|
||||||
|
'/_app/tasks/cron/detail/$id': typeof AppTasksCronDetailIdRoute
|
||||||
|
'/_app/tasks/cron/edit/$id': typeof AppTasksCronEditIdRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
@ -279,6 +315,7 @@ export interface FileRouteTypes {
|
|||||||
| '/tasks'
|
| '/tasks'
|
||||||
| '/auth/sign-in'
|
| '/auth/sign-in'
|
||||||
| '/auth/sign-up'
|
| '/auth/sign-up'
|
||||||
|
| '/tasks/cron'
|
||||||
| '/explore'
|
| '/explore'
|
||||||
| '/bangumi/manage'
|
| '/bangumi/manage'
|
||||||
| '/credential3rd/create'
|
| '/credential3rd/create'
|
||||||
@ -293,7 +330,10 @@ export interface FileRouteTypes {
|
|||||||
| '/credential3rd/edit/$id'
|
| '/credential3rd/edit/$id'
|
||||||
| '/subscriptions/detail/$id'
|
| '/subscriptions/detail/$id'
|
||||||
| '/subscriptions/edit/$id'
|
| '/subscriptions/edit/$id'
|
||||||
|
| '/tasks/cron/manage'
|
||||||
| '/tasks/detail/$id'
|
| '/tasks/detail/$id'
|
||||||
|
| '/tasks/cron/detail/$id'
|
||||||
|
| '/tasks/cron/edit/$id'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
@ -308,6 +348,7 @@ export interface FileRouteTypes {
|
|||||||
| '/tasks'
|
| '/tasks'
|
||||||
| '/auth/sign-in'
|
| '/auth/sign-in'
|
||||||
| '/auth/sign-up'
|
| '/auth/sign-up'
|
||||||
|
| '/tasks/cron'
|
||||||
| '/explore'
|
| '/explore'
|
||||||
| '/bangumi/manage'
|
| '/bangumi/manage'
|
||||||
| '/credential3rd/create'
|
| '/credential3rd/create'
|
||||||
@ -322,7 +363,10 @@ export interface FileRouteTypes {
|
|||||||
| '/credential3rd/edit/$id'
|
| '/credential3rd/edit/$id'
|
||||||
| '/subscriptions/detail/$id'
|
| '/subscriptions/detail/$id'
|
||||||
| '/subscriptions/edit/$id'
|
| '/subscriptions/edit/$id'
|
||||||
|
| '/tasks/cron/manage'
|
||||||
| '/tasks/detail/$id'
|
| '/tasks/detail/$id'
|
||||||
|
| '/tasks/cron/detail/$id'
|
||||||
|
| '/tasks/cron/edit/$id'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
@ -337,6 +381,7 @@ export interface FileRouteTypes {
|
|||||||
| '/_app/tasks'
|
| '/_app/tasks'
|
||||||
| '/auth/sign-in'
|
| '/auth/sign-in'
|
||||||
| '/auth/sign-up'
|
| '/auth/sign-up'
|
||||||
|
| '/_app/tasks/cron'
|
||||||
| '/_app/_explore/explore'
|
| '/_app/_explore/explore'
|
||||||
| '/_app/bangumi/manage'
|
| '/_app/bangumi/manage'
|
||||||
| '/_app/credential3rd/create'
|
| '/_app/credential3rd/create'
|
||||||
@ -351,7 +396,10 @@ export interface FileRouteTypes {
|
|||||||
| '/_app/credential3rd/edit/$id'
|
| '/_app/credential3rd/edit/$id'
|
||||||
| '/_app/subscriptions/detail/$id'
|
| '/_app/subscriptions/detail/$id'
|
||||||
| '/_app/subscriptions/edit/$id'
|
| '/_app/subscriptions/edit/$id'
|
||||||
|
| '/_app/tasks/cron/manage'
|
||||||
| '/_app/tasks/detail/$id'
|
| '/_app/tasks/detail/$id'
|
||||||
|
| '/_app/tasks/cron/detail/$id'
|
||||||
|
| '/_app/tasks/cron/edit/$id'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
@ -520,6 +568,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AppExploreExploreRouteImport
|
preLoaderRoute: typeof AppExploreExploreRouteImport
|
||||||
parentRoute: typeof AppRouteRoute
|
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': {
|
'/_app/tasks/detail/$id': {
|
||||||
id: '/_app/tasks/detail/$id'
|
id: '/_app/tasks/detail/$id'
|
||||||
path: '/detail/$id'
|
path: '/detail/$id'
|
||||||
@ -527,6 +582,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AppTasksDetailIdRouteImport
|
preLoaderRoute: typeof AppTasksDetailIdRouteImport
|
||||||
parentRoute: typeof AppTasksRouteRoute
|
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': {
|
'/_app/subscriptions/edit/$id': {
|
||||||
id: '/_app/subscriptions/edit/$id'
|
id: '/_app/subscriptions/edit/$id'
|
||||||
path: '/edit/$id'
|
path: '/edit/$id'
|
||||||
@ -555,6 +617,20 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AppCredential3rdDetailIdRouteImport
|
preLoaderRoute: typeof AppCredential3rdDetailIdRouteImport
|
||||||
parentRoute: typeof AppCredential3rdRouteRoute
|
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,
|
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 {
|
interface AppTasksRouteRouteChildren {
|
||||||
|
AppTasksCronRouteRoute: typeof AppTasksCronRouteRouteWithChildren
|
||||||
AppTasksManageRoute: typeof AppTasksManageRoute
|
AppTasksManageRoute: typeof AppTasksManageRoute
|
||||||
AppTasksDetailIdRoute: typeof AppTasksDetailIdRoute
|
AppTasksDetailIdRoute: typeof AppTasksDetailIdRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppTasksRouteRouteChildren: AppTasksRouteRouteChildren = {
|
const AppTasksRouteRouteChildren: AppTasksRouteRouteChildren = {
|
||||||
|
AppTasksCronRouteRoute: AppTasksCronRouteRouteWithChildren,
|
||||||
AppTasksManageRoute: AppTasksManageRoute,
|
AppTasksManageRoute: AppTasksManageRoute,
|
||||||
AppTasksDetailIdRoute: AppTasksDetailIdRoute,
|
AppTasksDetailIdRoute: AppTasksDetailIdRoute,
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
|
import { ContainerHeader } from '@/components/ui/container-header';
|
||||||
import { FormFieldErrors } from '@/components/ui/form-field-errors';
|
import { FormFieldErrors } from '@/components/ui/form-field-errors';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
@ -123,14 +124,11 @@ function CredentialCreateRouteComponent() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-2xl py-6">
|
<div className="container mx-auto max-w-2xl py-6">
|
||||||
<div className="mb-6 flex items-center gap-4">
|
<ContainerHeader
|
||||||
<div>
|
title="Create third-party credential"
|
||||||
<h1 className="font-bold text-2xl">Create third-party credential</h1>
|
description="Add new third-party login credential"
|
||||||
<p className="mt-1 text-muted-foreground">
|
defaultBackTo="/credential3rd/manage"
|
||||||
Add new third-party login credential
|
/>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
|
import { ContainerHeader } from '@/components/ui/container-header';
|
||||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||||
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
||||||
import { Label } from '@/components/ui/label';
|
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 { GetCredential3rdDetailQuery } from '@/infra/graphql/gql/graphql';
|
||||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||||
import { useQuery } from '@apollo/client';
|
import { useQuery } from '@apollo/client';
|
||||||
import {
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||||
createFileRoute,
|
|
||||||
useCanGoBack,
|
|
||||||
useNavigate,
|
|
||||||
useRouter,
|
|
||||||
} from '@tanstack/react-router';
|
|
||||||
import { format } from 'date-fns/format';
|
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 { useState } from 'react';
|
||||||
import { Credential3rdCheckAvailableViewDialogContent } from './-check-available';
|
import { Credential3rdCheckAvailableViewDialogContent } from './-check-available';
|
||||||
|
|
||||||
@ -38,21 +34,9 @@ export const Route = createFileRoute('/_app/credential3rd/detail/$id')({
|
|||||||
function Credential3rdDetailRouteComponent() {
|
function Credential3rdDetailRouteComponent() {
|
||||||
const { id } = Route.useParams();
|
const { id } = Route.useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const router = useRouter();
|
|
||||||
const canGoBack = useCanGoBack();
|
|
||||||
|
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
const handleBack = () => {
|
|
||||||
if (canGoBack) {
|
|
||||||
router.history.back();
|
|
||||||
} else {
|
|
||||||
navigate({
|
|
||||||
to: '/credential3rd/manage',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { loading, error, data } = useQuery<GetCredential3rdDetailQuery>(
|
const { loading, error, data } = useQuery<GetCredential3rdDetailQuery>(
|
||||||
GET_CREDENTIAL_3RD_DETAIL,
|
GET_CREDENTIAL_3RD_DETAIL,
|
||||||
{
|
{
|
||||||
@ -91,31 +75,17 @@ function Credential3rdDetailRouteComponent() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-4xl py-6">
|
<div className="container mx-auto max-w-4xl py-6">
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<ContainerHeader
|
||||||
<div className="flex items-center gap-4">
|
title="Credential Detail"
|
||||||
<Button
|
description={`View credential #${credential.id}`}
|
||||||
variant="ghost"
|
defaultBackTo="/credential3rd/manage"
|
||||||
size="sm"
|
actions={
|
||||||
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">
|
|
||||||
<Button onClick={handleEnterEditMode}>
|
<Button onClick={handleEnterEditMode}>
|
||||||
<Edit className="mr-2 h-4 w-4" />
|
<Edit className="mr-2 h-4 w-4" />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
|
import { ContainerHeader } from '@/components/ui/container-header';
|
||||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
@ -39,13 +40,8 @@ import type {
|
|||||||
} from '@/infra/graphql/gql/graphql';
|
} from '@/infra/graphql/gql/graphql';
|
||||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||||
import { useMutation, useQuery } from '@apollo/client';
|
import { useMutation, useQuery } from '@apollo/client';
|
||||||
import {
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
createFileRoute,
|
import { Eye, EyeOff, Save } from 'lucide-react';
|
||||||
useCanGoBack,
|
|
||||||
useNavigate,
|
|
||||||
useRouter,
|
|
||||||
} from '@tanstack/react-router';
|
|
||||||
import { ArrowLeft, Eye, EyeOff, Save, X } from 'lucide-react';
|
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
@ -63,23 +59,10 @@ function FormView({
|
|||||||
credential: Credential3rdDetailDto;
|
credential: Credential3rdDetailDto;
|
||||||
onCompleted: VoidFunction;
|
onCompleted: VoidFunction;
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate();
|
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const togglePasswordVisibility = () => {
|
const togglePasswordVisibility = () => {
|
||||||
setShowPassword((prev) => !prev);
|
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<
|
const [updateCredential, { loading: updating }] = useMutation<
|
||||||
UpdateCredential3rdMutation,
|
UpdateCredential3rdMutation,
|
||||||
@ -121,35 +104,17 @@ function FormView({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-4xl py-6">
|
<div className="container mx-auto max-w-4xl py-6">
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<ContainerHeader
|
||||||
<div className="flex items-center gap-4">
|
title="Credential Edit"
|
||||||
<Button
|
description={`Edit credential #${credential.id}`}
|
||||||
variant="ghost"
|
defaultBackTo={`/credential3rd/detail/${credential.id}`}
|
||||||
size="sm"
|
actions={
|
||||||
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>
|
|
||||||
<Button onClick={() => form.handleSubmit()} disabled={updating}>
|
<Button onClick={() => form.handleSubmit()} disabled={updating}>
|
||||||
<Save className="mr-2 h-4 w-4" />
|
<Save className="mr-2 h-4 w-4" />
|
||||||
{updating ? 'Saving...' : 'Save'}
|
{updating ? 'Saving...' : 'Save'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { ContainerHeader } from '@/components/ui/container-header';
|
||||||
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
||||||
import { DataTableViewOptions } from '@/components/ui/data-table-view-options';
|
import { DataTableViewOptions } from '@/components/ui/data-table-view-options';
|
||||||
import { DialogTrigger } from '@/components/ui/dialog';
|
import { DialogTrigger } from '@/components/ui/dialog';
|
||||||
@ -297,18 +298,16 @@ function CredentialManageRouteComponent() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto space-y-4 rounded-md">
|
<div className="container mx-auto space-y-4 rounded-md">
|
||||||
<div className="flex items-center justify-between pt-4">
|
<ContainerHeader
|
||||||
<div>
|
title="Credential 3rd Management"
|
||||||
<h1 className="font-bold text-2xl">Credential 3rd Management</h1>
|
description="Manage your third-party platform login credentials"
|
||||||
<p className="text-muted-foreground">
|
actions={
|
||||||
Manage your third-party platform login credentials
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button onClick={() => navigate({ to: '/credential3rd/create' })}>
|
<Button onClick={() => navigate({ to: '/credential3rd/create' })}>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Add Credential
|
Add Credential
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
<div className="flex items-center py-2">
|
<div className="flex items-center py-2">
|
||||||
<DataTableViewOptions table={table} />
|
<DataTableViewOptions table={table} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -122,6 +122,7 @@ export const SubscriptionSyncView = memo(
|
|||||||
export interface SubscriptionSyncDialogContentProps {
|
export interface SubscriptionSyncDialogContentProps {
|
||||||
id: number;
|
id: number;
|
||||||
onCancel?: VoidFunction;
|
onCancel?: VoidFunction;
|
||||||
|
isCron?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SubscriptionSyncDialogContent = memo(
|
export const SubscriptionSyncDialogContent = memo(
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
|
import { ContainerHeader } from '@/components/ui/container-header';
|
||||||
import { FormFieldErrors } from '@/components/ui/form-field-errors';
|
import { FormFieldErrors } from '@/components/ui/form-field-errors';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
@ -96,14 +97,11 @@ function SubscriptionCreateRouteComponent() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-2xl py-6">
|
<div className="container mx-auto max-w-2xl py-6">
|
||||||
<div className="mb-6 flex items-center gap-4">
|
<ContainerHeader
|
||||||
<div>
|
title="Create Bangumi Subscription"
|
||||||
<h1 className="font-bold text-2xl">Create Bangumi Subscription</h1>
|
description="Add a new bangumi subscription source"
|
||||||
<p className="mt-1 text-muted-foreground">
|
defaultBackTo="/subscriptions/manage"
|
||||||
Add a new bangumi subscription source
|
/>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
|
import { ContainerHeader } from '@/components/ui/container-header';
|
||||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||||
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
||||||
import { Img } from '@/components/ui/img';
|
import { Img } from '@/components/ui/img';
|
||||||
@ -33,15 +34,9 @@ import {
|
|||||||
SubscriptionCategoryEnum,
|
SubscriptionCategoryEnum,
|
||||||
} from '@/infra/graphql/gql/graphql';
|
} from '@/infra/graphql/gql/graphql';
|
||||||
import { useMutation, useQuery } from '@apollo/client';
|
import { useMutation, useQuery } from '@apollo/client';
|
||||||
import {
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||||
createFileRoute,
|
|
||||||
useCanGoBack,
|
|
||||||
useNavigate,
|
|
||||||
useRouter,
|
|
||||||
} from '@tanstack/react-router';
|
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
|
||||||
Edit,
|
Edit,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
ListIcon,
|
ListIcon,
|
||||||
@ -61,20 +56,8 @@ export const Route = createFileRoute('/_app/subscriptions/detail/$id')({
|
|||||||
function SubscriptionDetailRouteComponent() {
|
function SubscriptionDetailRouteComponent() {
|
||||||
const { id } = Route.useParams();
|
const { id } = Route.useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const router = useRouter();
|
|
||||||
const canGoBack = useCanGoBack();
|
|
||||||
const subscriptionService = useInject(SubscriptionService);
|
const subscriptionService = useInject(SubscriptionService);
|
||||||
|
|
||||||
const handleBack = () => {
|
|
||||||
if (canGoBack) {
|
|
||||||
router.history.back();
|
|
||||||
} else {
|
|
||||||
navigate({
|
|
||||||
to: '/subscriptions/manage',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReload = async () => {
|
const handleReload = async () => {
|
||||||
const result = await refetch();
|
const result = await refetch();
|
||||||
const error = getApolloQueryError(result);
|
const error = getApolloQueryError(result);
|
||||||
@ -177,31 +160,16 @@ function SubscriptionDetailRouteComponent() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-4xl py-6">
|
<div className="container mx-auto max-w-4xl py-6">
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<ContainerHeader
|
||||||
<div className="flex items-center gap-4">
|
title="Subscription Detail"
|
||||||
<Button
|
description={`View subscription #${subscription.id}`}
|
||||||
variant="ghost"
|
actions={
|
||||||
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">
|
|
||||||
<Button onClick={handleEnterEditMode}>
|
<Button onClick={handleEnterEditMode}>
|
||||||
<Edit className="mr-2 h-4 w-4" />
|
<Edit className="mr-2 h-4 w-4" />
|
||||||
Edit
|
Edit
|
||||||
</Button>{' '}
|
</Button>
|
||||||
</div>
|
}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@ -421,6 +389,79 @@ function SubscriptionDetailRouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
</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" />
|
||||||
|
Crons
|
||||||
|
</Button>
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm">
|
||||||
|
<RefreshCcwIcon className="h-4 w-4" />
|
||||||
|
Setup Cron
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<SubscriptionSyncDialogContent
|
||||||
|
id={subscription.id}
|
||||||
|
onCancel={handleReload}
|
||||||
|
isCron={true}
|
||||||
|
/>
|
||||||
|
</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 />
|
<Separator />
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
|
import { ContainerHeader } from '@/components/ui/container-header';
|
||||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||||
import { FormFieldErrors } from '@/components/ui/form-field-errors';
|
import { FormFieldErrors } from '@/components/ui/form-field-errors';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@ -44,8 +45,8 @@ import {
|
|||||||
} from '@/infra/graphql/gql/graphql';
|
} from '@/infra/graphql/gql/graphql';
|
||||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||||
import { useMutation, useQuery } from '@apollo/client';
|
import { useMutation, useQuery } from '@apollo/client';
|
||||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
import { ArrowLeft, Save, X } from 'lucide-react';
|
import { Save } from 'lucide-react';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Credential3rdSelectContent } from './-credential3rd-select';
|
import { Credential3rdSelectContent } from './-credential3rd-select';
|
||||||
@ -68,16 +69,8 @@ function FormView({
|
|||||||
subscription: SubscriptionDetailDto;
|
subscription: SubscriptionDetailDto;
|
||||||
onCompleted: VoidFunction;
|
onCompleted: VoidFunction;
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate();
|
|
||||||
const subscriptionService = useInject(SubscriptionService);
|
const subscriptionService = useInject(SubscriptionService);
|
||||||
|
|
||||||
const handleBack = () => {
|
|
||||||
navigate({
|
|
||||||
to: '/subscriptions/detail/$id',
|
|
||||||
params: { id: subscription.id.toString() },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const [updateSubscription, { loading: updating }] = useMutation<
|
const [updateSubscription, { loading: updating }] = useMutation<
|
||||||
UpdateSubscriptionsMutation,
|
UpdateSubscriptionsMutation,
|
||||||
UpdateSubscriptionsMutationVariables
|
UpdateSubscriptionsMutationVariables
|
||||||
@ -149,35 +142,17 @@ function FormView({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-4xl py-6">
|
<div className="container mx-auto max-w-4xl py-6">
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<ContainerHeader
|
||||||
<div className="flex items-center gap-4">
|
title="Subscription Edit"
|
||||||
<Button
|
description={`Edit subscription #${subscription.id}`}
|
||||||
variant="ghost"
|
defaultBackTo={`/subscriptions/detail/${subscription.id}`}
|
||||||
size="sm"
|
actions={
|
||||||
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>
|
|
||||||
<Button onClick={() => form.handleSubmit()} disabled={updating}>
|
<Button onClick={() => form.handleSubmit()} disabled={updating}>
|
||||||
<Save className="mr-2 h-4 w-4" />
|
<Save className="mr-2 h-4 w-4" />
|
||||||
{updating ? 'Saving...' : 'Save'}
|
{updating ? 'Saving...' : 'Save'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { ContainerHeader } from '@/components/ui/container-header';
|
||||||
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
||||||
import { DataTableViewOptions } from '@/components/ui/data-table-view-options';
|
import { DataTableViewOptions } from '@/components/ui/data-table-view-options';
|
||||||
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
||||||
@ -277,16 +278,16 @@ function SubscriptionManageRouteComponent() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto space-y-4 rounded-md">
|
<div className="container mx-auto space-y-4 rounded-md">
|
||||||
<div className="flex items-center justify-between pt-4">
|
<ContainerHeader
|
||||||
<div>
|
title="Subscription Management"
|
||||||
<h1 className="font-bold text-2xl">Subscription Management</h1>
|
description="Manage your subscription"
|
||||||
<p className="text-muted-foreground">Manage your subscription</p>
|
actions={
|
||||||
</div>
|
|
||||||
<Button onClick={() => navigate({ to: '/subscriptions/create' })}>
|
<Button onClick={() => navigate({ to: '/subscriptions/create' })}>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
Add Subscription
|
Add Subscription
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
<div className="flex items-center py-2">
|
<div className="flex items-center py-2">
|
||||||
<DataTableViewOptions table={table} />
|
<DataTableViewOptions table={table} />
|
||||||
</div>
|
</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',
|
||||||
|
})
|
||||||
|
);
|
@ -8,6 +8,7 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
|
import { ContainerHeader } from '@/components/ui/container-header';
|
||||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { QueryErrorView } from '@/components/ui/query-error-view';
|
import { QueryErrorView } from '@/components/ui/query-error-view';
|
||||||
@ -24,14 +25,9 @@ import {
|
|||||||
} from '@/infra/graphql/gql/graphql';
|
} from '@/infra/graphql/gql/graphql';
|
||||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||||
import { useMutation, useQuery } from '@apollo/client';
|
import { useMutation, useQuery } from '@apollo/client';
|
||||||
import {
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
createFileRoute,
|
|
||||||
useCanGoBack,
|
|
||||||
useNavigate,
|
|
||||||
useRouter,
|
|
||||||
} from '@tanstack/react-router';
|
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { ArrowLeft, RefreshCw } from 'lucide-react';
|
import { RefreshCw } from 'lucide-react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { prettyTaskType } from './-pretty-task-type';
|
import { prettyTaskType } from './-pretty-task-type';
|
||||||
@ -46,19 +42,6 @@ export const Route = createFileRoute('/_app/tasks/detail/$id')({
|
|||||||
|
|
||||||
function TaskDetailRouteComponent() {
|
function TaskDetailRouteComponent() {
|
||||||
const { id } = Route.useParams();
|
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<
|
const { data, loading, error, refetch } = useQuery<
|
||||||
GetTasksQuery,
|
GetTasksQuery,
|
||||||
@ -129,27 +112,17 @@ function TaskDetailRouteComponent() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-4xl py-6">
|
<div className="container mx-auto max-w-4xl py-6">
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<ContainerHeader
|
||||||
<div className="flex items-center gap-4">
|
title="Task Detail"
|
||||||
<Button
|
description={`View task #${task.id}`}
|
||||||
variant="ghost"
|
defaultBackTo="/tasks/manage"
|
||||||
size="sm"
|
actions={
|
||||||
onClick={handleBack}
|
|
||||||
className="h-8 w-8 p-0"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-4 w-4" />
|
|
||||||
</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()}>
|
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
@ -15,6 +15,7 @@ import {
|
|||||||
type DeleteTasksMutation,
|
type DeleteTasksMutation,
|
||||||
type DeleteTasksMutationVariables,
|
type DeleteTasksMutationVariables,
|
||||||
type GetTasksQuery,
|
type GetTasksQuery,
|
||||||
|
type GetTasksQueryVariables,
|
||||||
type RetryTasksMutation,
|
type RetryTasksMutation,
|
||||||
type RetryTasksMutationVariables,
|
type RetryTasksMutationVariables,
|
||||||
SubscriberTaskStatusEnum,
|
SubscriberTaskStatusEnum,
|
||||||
@ -35,6 +36,7 @@ import {
|
|||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { RefreshCw } from 'lucide-react';
|
import { RefreshCw } from 'lucide-react';
|
||||||
|
|
||||||
|
import { ContainerHeader } from '@/components/ui/container-header';
|
||||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||||
import {
|
import {
|
||||||
apolloErrorToMessage,
|
apolloErrorToMessage,
|
||||||
@ -55,18 +57,17 @@ export const Route = createFileRoute('/_app/tasks/manage')({
|
|||||||
function TaskManageRouteComponent() {
|
function TaskManageRouteComponent() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||||
lockAt: false,
|
|
||||||
lockBy: false,
|
|
||||||
attempts: false,
|
|
||||||
});
|
|
||||||
const [sorting, setSorting] = useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { loading, error, data, refetch } = useQuery<GetTasksQuery>(GET_TASKS, {
|
const { loading, error, data, refetch } = useQuery<
|
||||||
|
GetTasksQuery,
|
||||||
|
GetTasksQueryVariables
|
||||||
|
>(GET_TASKS, {
|
||||||
variables: {
|
variables: {
|
||||||
pagination: {
|
pagination: {
|
||||||
page: {
|
page: {
|
||||||
@ -172,16 +173,16 @@ function TaskManageRouteComponent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto space-y-4 px-4">
|
<div className="container mx-auto max-w-4xl space-y-4 px-4">
|
||||||
<div className="flex items-center justify-between pt-4">
|
<ContainerHeader
|
||||||
<div>
|
title="Tasks Management"
|
||||||
<h1 className="font-bold text-2xl">Tasks Management</h1>
|
description="Manage your tasks"
|
||||||
<p className="text-muted-foreground">Manage your tasks</p>
|
actions={
|
||||||
</div>
|
|
||||||
<Button onClick={() => refetch()} variant="outline" size="sm">
|
<Button onClick={() => refetch()} variant="outline" size="sm">
|
||||||
<RefreshCw className="h-4 w-4" />
|
<RefreshCw className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{showSkeleton &&
|
{showSkeleton &&
|
||||||
|
Loading…
Reference in New Issue
Block a user