feat: init cron webui

This commit is contained in:
master 2025-07-05 02:08:55 +08:00
parent a1c2eeded1
commit 004fed9b2e
33 changed files with 3329 additions and 551 deletions

View File

@ -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":""}

View File

@ -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

View File

@ -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;"#,

View File

@ -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";

View File

@ -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)?;

View File

@ -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]

View File

@ -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,86 +152,95 @@ 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?;
if let Some(shutdown_signal) = shutdown_signal { async move {
monitor if let Some(shutdown_signal) = shutdown_signal {
.run_with_signal(async move { monitor
shutdown_signal().await; .run_with_signal(async move {
tracing::info!("apalis shutting down..."); shutdown_signal().await;
Ok(()) tracing::info!("apalis shutting down...");
}) Ok(())
.await?; })
} else { .await?;
monitor.run().await?; } else {
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();
async move {
if let Err(e) =
Self::listen_cron_due(listener, ctx, &cron_worker_id, retry_duration)
.await
{ {
tracing::error!("Error listening to cron due: {e}"); let ctx = self.ctx.clone();
} async move {
} if let Err(e) =
}); Self::listen_cron_due(listener, ctx, &cron_worker_id, retry_duration)
.await
tokio::task::spawn({ {
let ctx = self.ctx.clone(); tracing::error!("Error listening to cron due: {e}");
async move { }
let mut interval = tokio::time::interval(cron_interval_duration);
loop {
interval.tick().await;
if let Err(e) = cron::Model::check_and_cleanup_expired_cron_locks(
ctx.as_ref(),
retry_duration,
)
.await
{
tracing::error!(
"Error checking and cleaning up expired cron locks: {e}"
);
} }
if let Err(e) = },
cron::Model::check_and_trigger_due_crons(ctx.as_ref()).await {
{ let ctx = self.ctx.clone();
tracing::error!("Error checking and triggering due crons: {e}"); let mut interval = tokio::time::interval(cron_interval_duration);
async move {
loop {
interval.tick().await;
if let Err(e) = cron::Model::check_and_cleanup_expired_cron_locks(
ctx.as_ref(),
retry_duration,
)
.await
{
tracing::error!(
"Error checking and cleaning up expired cron locks: {e}"
);
}
if let Err(e) =
cron::Model::check_and_trigger_due_crons(ctx.as_ref()).await
{
tracing::error!("Error checking and triggering due crons: {e}");
}
}
} }
} }
} );
}); Ok::<_, RecorderError>(())
}
} => {}
};
Ok::<_, RecorderError>(())
}
)?;
Ok(()) Ok(())
} }
@ -299,14 +308,17 @@ 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);
ctx.as_ref(), continue;
notification, } else if notification.channel() == CRON_DUE_EVENT
worker_id, && let Err(e) = cron::Model::handle_cron_notification(
retry_duration, ctx.as_ref(),
) notification,
.await worker_id,
retry_duration,
)
.await
{ {
tracing::error!("Error handling cron notification: {e}"); tracing::error!("Error handling cron notification: {e}");
} }
@ -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(())
} }

View File

@ -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>,

View File

@ -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)

View File

@ -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',
},
},
], ],
}, },
{ {

View 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>
);
}
);

View 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)
}
`;

View File

@ -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

View File

@ -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

View File

@ -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: {

View File

@ -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,
} }

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 <Button onClick={() => navigate({ to: '/credential3rd/create' })}>
</p> <Plus className="mr-2 h-4 w-4" />
</div> Add Credential
<Button onClick={() => navigate({ to: '/credential3rd/create' })}> </Button>
<Plus className="mr-2 h-4 w-4" /> }
Add Credential />
</Button>
</div>
<div className="flex items-center py-2"> <div className="flex items-center py-2">
<DataTableViewOptions table={table} /> <DataTableViewOptions table={table} />
</div> </div>

View File

@ -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(

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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>
);
}
}

View File

@ -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>
}

View File

@ -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>
}

View 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>
);
}

View File

@ -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',
})
);

View File

@ -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} <Button variant="outline" size="sm" onClick={() => refetch()}>
className="h-8 w-8 p-0" <RefreshCw className="h-4 w-4" />
> Refresh
<ArrowLeft className="h-4 w-4" />
</Button> </Button>
<div> }
<h1 className="font-bold text-2xl">Task Detail</h1> />
<p className="mt-1 text-muted-foreground">View task #{task.id}</p>
</div>
</div>
<Button variant="outline" size="sm" onClick={() => refetch()}>
<RefreshCw className="h-4 w-4" />
Refresh
</Button>
</div>
<Card> <Card>
<CardHeader> <CardHeader>

View File

@ -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 &&