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>(())
|
||||
},
|
||||
async {
|
||||
task.run(if graceful_shutdown {
|
||||
task.run_with_signal(if graceful_shutdown {
|
||||
Some(Self::shutdown_signal)
|
||||
} else {
|
||||
None
|
||||
|
@ -8,9 +8,10 @@ use crate::{
|
||||
Subscriptions, table_auto_z,
|
||||
},
|
||||
models::cron::{
|
||||
CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME, CRON_DUE_EVENT, CronStatus, CronStatusEnum,
|
||||
NOTIFY_DUE_CRON_WHEN_MUTATING_FUNCTION_NAME, NOTIFY_DUE_CRON_WHEN_MUTATING_TRIGGER_NAME,
|
||||
SETUP_CRON_EXTRA_FOREIGN_KEYS_FUNCTION_NAME, SETUP_CRON_EXTRA_FOREIGN_KEYS_TRIGGER_NAME,
|
||||
CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME, CRON_DUE_DEBUG_EVENT, CRON_DUE_EVENT,
|
||||
CronStatus, CronStatusEnum, NOTIFY_DUE_CRON_WHEN_MUTATING_FUNCTION_NAME,
|
||||
NOTIFY_DUE_CRON_WHEN_MUTATING_TRIGGER_NAME, SETUP_CRON_EXTRA_FOREIGN_KEYS_FUNCTION_NAME,
|
||||
SETUP_CRON_EXTRA_FOREIGN_KEYS_TRIGGER_NAME,
|
||||
},
|
||||
task::{
|
||||
SETUP_APALIS_JOBS_EXTRA_FOREIGN_KEYS_FUNCTION_NAME, SUBSCRIBER_TASK_APALIS_NAME,
|
||||
@ -48,14 +49,13 @@ impl MigrationTrait for Migration {
|
||||
.col(string_null(Cron::LockedBy))
|
||||
.col(timestamp_with_time_zone_null(Cron::LockedAt))
|
||||
.col(integer_null(Cron::TimeoutMs))
|
||||
.col(integer(Cron::Attempts))
|
||||
.col(integer(Cron::MaxAttempts))
|
||||
.col(integer(Cron::Priority))
|
||||
.col(enumeration(
|
||||
Cron::Status,
|
||||
CronStatusEnum,
|
||||
CronStatus::iden_values(),
|
||||
))
|
||||
.col(integer(Cron::Attempts).default(0))
|
||||
.col(integer(Cron::MaxAttempts).default(1))
|
||||
.col(integer(Cron::Priority).default(0))
|
||||
.col(
|
||||
enumeration(Cron::Status, CronStatusEnum, CronStatus::iden_values())
|
||||
.default(CronStatus::Pending),
|
||||
)
|
||||
.col(json_binary_null(Cron::SubscriberTaskCron))
|
||||
.col(json_binary_null(Cron::SystemTaskCron))
|
||||
.foreign_key(
|
||||
@ -207,9 +207,12 @@ impl MigrationTrait for Migration {
|
||||
ORDER BY {priority} ASC, {next_run} ASC
|
||||
FOR UPDATE SKIP LOCKED
|
||||
LOOP
|
||||
-- PERFORM pg_notify('{CRON_DUE_DEBUG_EVENT}',format('Found due cron: value=%s; Now time: %s', row_to_json(cron_record)::text, CURRENT_TIMESTAMP));
|
||||
PERFORM pg_notify('{CRON_DUE_EVENT}', row_to_json(cron_record)::text);
|
||||
notification_count := notification_count + 1;
|
||||
END LOOP;
|
||||
|
||||
-- PERFORM pg_notify('{CRON_DUE_DEBUG_EVENT}', format('Notification count: %I; Now time: %s', notification_count, CURRENT_TIMESTAMP));
|
||||
RETURN notification_count;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;"#,
|
||||
|
@ -1,4 +1,5 @@
|
||||
pub const CRON_DUE_EVENT: &str = "cron_due";
|
||||
pub const CRON_DUE_DEBUG_EVENT: &str = "cron_due_debug";
|
||||
|
||||
pub const CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME: &str = "check_and_trigger_due_crons";
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
mod core;
|
||||
|
||||
pub use core::{
|
||||
CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME, CRON_DUE_EVENT,
|
||||
CHECK_AND_TRIGGER_DUE_CRONS_FUNCTION_NAME, CRON_DUE_DEBUG_EVENT, CRON_DUE_EVENT,
|
||||
NOTIFY_DUE_CRON_WHEN_MUTATING_FUNCTION_NAME, NOTIFY_DUE_CRON_WHEN_MUTATING_TRIGGER_NAME,
|
||||
SETUP_CRON_EXTRA_FOREIGN_KEYS_FUNCTION_NAME, SETUP_CRON_EXTRA_FOREIGN_KEYS_TRIGGER_NAME,
|
||||
};
|
||||
@ -59,8 +59,8 @@ pub struct Model {
|
||||
pub last_error: Option<String>,
|
||||
pub locked_by: Option<String>,
|
||||
pub locked_at: Option<DateTimeUtc>,
|
||||
#[sea_orm(default_expr = "5000")]
|
||||
pub timeout_ms: i32,
|
||||
// default_expr = "5000"
|
||||
pub timeout_ms: Option<i32>,
|
||||
#[sea_orm(default_expr = "0")]
|
||||
pub attempts: i32,
|
||||
#[sea_orm(default_expr = "1")]
|
||||
@ -223,7 +223,10 @@ impl Model {
|
||||
&& cron.attempts < cron.max_attempts
|
||||
&& cron.status == CronStatus::Pending
|
||||
&& (cron.locked_at.is_none_or(|locked_at| {
|
||||
locked_at + chrono::Duration::milliseconds(cron.timeout_ms as i64) <= Utc::now()
|
||||
cron.timeout_ms.is_some_and(|cron_timeout_ms| {
|
||||
locked_at + chrono::Duration::milliseconds(cron_timeout_ms as i64)
|
||||
<= Utc::now()
|
||||
})
|
||||
}))
|
||||
&& cron.next_run.is_some_and(|next_run| next_run <= Utc::now())
|
||||
{
|
||||
@ -376,7 +379,15 @@ impl Model {
|
||||
locked_cron
|
||||
.mark_cron_failed(
|
||||
ctx,
|
||||
format!("Cron timeout of {}ms", locked_cron.timeout_ms).as_str(),
|
||||
format!(
|
||||
"Cron timeout of {}ms",
|
||||
locked_cron
|
||||
.timeout_ms
|
||||
.as_ref()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| "Infinite".to_string())
|
||||
)
|
||||
.as_str(),
|
||||
retry_duration,
|
||||
)
|
||||
.await?;
|
||||
@ -389,7 +400,7 @@ impl Model {
|
||||
}
|
||||
|
||||
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)?;
|
||||
|
||||
|
@ -60,6 +60,8 @@ pub enum Relation {
|
||||
Feed,
|
||||
#[sea_orm(has_many = "super::subscriber_tasks::Entity")]
|
||||
SubscriberTask,
|
||||
#[sea_orm(has_many = "super::cron::Entity")]
|
||||
Cron,
|
||||
}
|
||||
|
||||
impl Related<super::subscribers::Entity> for Entity {
|
||||
@ -126,6 +128,12 @@ impl Related<super::subscriber_tasks::Entity> for Entity {
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::cron::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Cron.def()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelatedEntity)]
|
||||
pub enum RelatedEntity {
|
||||
#[sea_orm(entity = "super::subscribers::Entity")]
|
||||
@ -144,6 +152,8 @@ pub enum RelatedEntity {
|
||||
Feed,
|
||||
#[sea_orm(entity = "super::subscriber_tasks::Entity")]
|
||||
SubscriberTask,
|
||||
#[sea_orm(entity = "super::cron::Entity")]
|
||||
Cron,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
@ -13,7 +13,7 @@ use uuid::Uuid;
|
||||
use crate::{
|
||||
app::AppContextTrait,
|
||||
errors::{RecorderError, RecorderResult},
|
||||
models::cron::{self, CRON_DUE_EVENT},
|
||||
models::cron::{self, CRON_DUE_DEBUG_EVENT, CRON_DUE_EVENT},
|
||||
task::{
|
||||
AsyncTaskTrait, SUBSCRIBER_TASK_APALIS_NAME, SYSTEM_TASK_APALIS_NAME, SubscriberTask,
|
||||
TaskConfig,
|
||||
@ -152,86 +152,95 @@ impl TaskService {
|
||||
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
|
||||
F: Fn() -> Fut + Send + 'static,
|
||||
F: FnOnce() -> Fut + Send + 'static,
|
||||
Fut: Future<Output = ()> + Send,
|
||||
{
|
||||
tokio::try_join!(
|
||||
async {
|
||||
tokio::select! {
|
||||
_ = {
|
||||
let monitor = self.setup_apalis_monitor().await?;
|
||||
if let Some(shutdown_signal) = shutdown_signal {
|
||||
monitor
|
||||
.run_with_signal(async move {
|
||||
shutdown_signal().await;
|
||||
tracing::info!("apalis shutting down...");
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
} else {
|
||||
monitor.run().await?;
|
||||
async move {
|
||||
if let Some(shutdown_signal) = shutdown_signal {
|
||||
monitor
|
||||
.run_with_signal(async move {
|
||||
shutdown_signal().await;
|
||||
tracing::info!("apalis shutting down...");
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
} else {
|
||||
monitor.run().await?;
|
||||
}
|
||||
Ok::<_, RecorderError>(())
|
||||
}
|
||||
Ok::<_, RecorderError>(())
|
||||
},
|
||||
async {
|
||||
} => {}
|
||||
_ = {
|
||||
let listener = self.setup_apalis_listener().await?;
|
||||
tokio::task::spawn(async move {
|
||||
async move {
|
||||
if let Err(e) = listener.listen().await {
|
||||
tracing::error!("Error listening to apalis: {e}");
|
||||
}
|
||||
});
|
||||
Ok::<_, RecorderError>(())
|
||||
},
|
||||
async {
|
||||
Ok::<_, RecorderError>(())
|
||||
}
|
||||
} => {},
|
||||
_ = {
|
||||
let mut listener = self.setup_cron_due_listening().await?;
|
||||
let cron_worker_id = self.cron_worker_id.clone();
|
||||
let retry_duration = chrono::Duration::milliseconds(
|
||||
self.config.cron_retry_duration.as_millis() as i64,
|
||||
);
|
||||
let retry_duration =
|
||||
chrono::Duration::milliseconds(self.config.cron_retry_duration.as_millis() as i64);
|
||||
let cron_interval_duration = self.config.cron_interval_duration;
|
||||
listener.listen(CRON_DUE_EVENT).await?;
|
||||
tracing::debug!("Listening for cron due event...");
|
||||
async move {
|
||||
listener.listen_all([CRON_DUE_EVENT as &str, CRON_DUE_DEBUG_EVENT as &str]).await?;
|
||||
|
||||
tokio::task::spawn({
|
||||
let ctx = self.ctx.clone();
|
||||
async move {
|
||||
if let Err(e) =
|
||||
Self::listen_cron_due(listener, ctx, &cron_worker_id, retry_duration)
|
||||
.await
|
||||
tokio::join!(
|
||||
{
|
||||
tracing::error!("Error listening to cron due: {e}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tokio::task::spawn({
|
||||
let ctx = self.ctx.clone();
|
||||
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}"
|
||||
);
|
||||
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}");
|
||||
}
|
||||
}
|
||||
if let Err(e) =
|
||||
cron::Model::check_and_trigger_due_crons(ctx.as_ref()).await
|
||||
{
|
||||
tracing::error!("Error checking and triggering due crons: {e}");
|
||||
},
|
||||
{
|
||||
let ctx = self.ctx.clone();
|
||||
let mut interval = tokio::time::interval(cron_interval_duration);
|
||||
async move {
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if let Err(e) = cron::Model::check_and_cleanup_expired_cron_locks(
|
||||
ctx.as_ref(),
|
||||
retry_duration,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Error checking and cleaning up expired cron locks: {e}"
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(e) =
|
||||
cron::Model::check_and_trigger_due_crons(ctx.as_ref()).await
|
||||
{
|
||||
tracing::error!("Error checking and triggering due crons: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
Ok::<_, RecorderError>(())
|
||||
}
|
||||
} => {}
|
||||
};
|
||||
|
||||
Ok::<_, RecorderError>(())
|
||||
}
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -299,14 +308,17 @@ impl TaskService {
|
||||
) -> RecorderResult<()> {
|
||||
loop {
|
||||
let notification = listener.recv().await?;
|
||||
tracing::debug!("Received cron due event: {:?}", notification);
|
||||
if let Err(e) = cron::Model::handle_cron_notification(
|
||||
ctx.as_ref(),
|
||||
notification,
|
||||
worker_id,
|
||||
retry_duration,
|
||||
)
|
||||
.await
|
||||
if notification.channel() == CRON_DUE_DEBUG_EVENT {
|
||||
tracing::debug!("Received cron due debug event: {:?}", notification);
|
||||
continue;
|
||||
} else if notification.channel() == CRON_DUE_EVENT
|
||||
&& let Err(e) = cron::Model::handle_cron_notification(
|
||||
ctx.as_ref(),
|
||||
notification,
|
||||
worker_id,
|
||||
retry_duration,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("Error handling cron notification: {e}");
|
||||
}
|
||||
@ -319,6 +331,7 @@ impl TaskService {
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::Utc;
|
||||
use rstest::{fixture, rstest};
|
||||
use sea_orm::ActiveValue;
|
||||
use tracing::Level;
|
||||
@ -340,12 +353,14 @@ mod tests {
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
// #[tracing_test::traced_test]
|
||||
async fn test_cron_due_listening(before_each: ()) -> RecorderResult<()> {
|
||||
#[tracing_test::traced_test]
|
||||
async fn test_check_and_trigger_due_crons_with_certain_interval(
|
||||
before_each: (),
|
||||
) -> RecorderResult<()> {
|
||||
let preset = TestingPreset::default_with_config(
|
||||
TestingAppContextConfig::builder()
|
||||
.task_config(TaskConfig {
|
||||
cron_interval_duration: Duration::from_secs(1),
|
||||
cron_interval_duration: Duration::from_millis(1500),
|
||||
..Default::default()
|
||||
})
|
||||
.build(),
|
||||
@ -364,15 +379,53 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _ = task_service
|
||||
.run(Some(async move || {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
}))
|
||||
.await;
|
||||
task_service.add_system_task_cron(echo_cron).await?;
|
||||
|
||||
// assert!(logs_contain(&format!(
|
||||
// "EchoTask {task_id} start running at"
|
||||
// )));
|
||||
task_service
|
||||
.run_with_signal(Some(async move || {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
}))
|
||||
.await?;
|
||||
|
||||
assert!(logs_contain(&format!(
|
||||
"EchoTask {task_id} start running at"
|
||||
)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[tokio::test]
|
||||
#[tracing_test::traced_test]
|
||||
async fn test_trigger_due_cron_when_mutating(before_each: ()) -> RecorderResult<()> {
|
||||
let preset = TestingPreset::default().await?;
|
||||
let app_ctx = preset.app_ctx;
|
||||
let task_service = app_ctx.task();
|
||||
|
||||
let task_id = Uuid::now_v7().to_string();
|
||||
|
||||
let echo_cron = cron::ActiveModel {
|
||||
cron_expr: ActiveValue::Set("* * * */1 * *".to_string()),
|
||||
next_run: ActiveValue::Set(Some(Utc::now() + chrono::Duration::seconds(-10))),
|
||||
system_task_cron: ActiveValue::Set(Some(
|
||||
EchoTask::builder().task_id(task_id.clone()).build().into(),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let task_runner = task_service.run_with_signal(Some(async move || {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||
}));
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
|
||||
task_service.add_system_task_cron(echo_cron).await?;
|
||||
|
||||
task_runner.await?;
|
||||
|
||||
assert!(logs_contain(&format!(
|
||||
"EchoTask {task_id} start running at"
|
||||
)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ impl TestingAppContext {
|
||||
.build(),
|
||||
);
|
||||
|
||||
let task_service = build_testing_task_service(app_ctx.clone()).await?;
|
||||
let task_service = build_testing_task_service(config.task_config, app_ctx.clone()).await?;
|
||||
|
||||
app_ctx.set_task(task_service);
|
||||
|
||||
@ -134,7 +134,7 @@ impl AppContextTrait for TestingAppContext {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(TypedBuilder)]
|
||||
#[derive(TypedBuilder, Debug)]
|
||||
#[builder(field_defaults(default, setter(strip_option)))]
|
||||
pub struct TestingAppContextConfig {
|
||||
pub mikan_base_url: Option<String>,
|
||||
|
@ -7,11 +7,10 @@ use crate::{
|
||||
};
|
||||
|
||||
pub async fn build_testing_task_service(
|
||||
config: Option<TaskConfig>,
|
||||
ctx: Arc<dyn AppContextTrait>,
|
||||
) -> RecorderResult<TaskService> {
|
||||
let config = TaskConfig {
|
||||
..Default::default()
|
||||
};
|
||||
let config = config.unwrap_or_default();
|
||||
let task_service = TaskService::from_config_and_ctx(config, ctx).await?;
|
||||
|
||||
Ok(task_service)
|
||||
|
@ -65,11 +65,17 @@ export const AppNavMainData: NavMainGroup[] = [
|
||||
icon: ListTodo,
|
||||
children: [
|
||||
{
|
||||
title: 'Manage',
|
||||
title: 'Tasks',
|
||||
link: {
|
||||
to: '/tasks/manage',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Crons',
|
||||
link: {
|
||||
to: '/tasks/cron/manage',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
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
|
||||
username
|
||||
}
|
||||
cron {
|
||||
nodes {
|
||||
id
|
||||
cronExpr
|
||||
nextRun
|
||||
lastRun
|
||||
lastError
|
||||
status
|
||||
lockedAt
|
||||
lockedBy
|
||||
createdAt
|
||||
updatedAt
|
||||
timeoutMs
|
||||
maxAttempts
|
||||
priority
|
||||
attempts
|
||||
subscriberTaskCron
|
||||
}
|
||||
}
|
||||
bangumi {
|
||||
nodes {
|
||||
createdAt
|
||||
|
@ -20,13 +20,15 @@ type Documents = {
|
||||
"\n mutation DeleteCredential3rd($filter: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filter)\n }\n": typeof types.DeleteCredential3rdDocument,
|
||||
"\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filter: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n": typeof types.GetCredential3rdDetailDocument,
|
||||
"\n mutation CheckCredential3rdAvailable($filter: Credential3rdFilterInput!) {\n credential3rdCheckAvailable(filter: $filter) {\n available\n }\n }\n": typeof types.CheckCredential3rdAvailableDocument,
|
||||
"\nquery GetCrons($filter: CronFilterInput!, $orderBy: CronOrderInput!, $pagination: PaginationInput!) {\n cron(pagination: $pagination, filter: $filter, orderBy: $orderBy) {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n subscriberTask {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n }\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetCronsDocument,
|
||||
"\n mutation DeleteCrons($filter: CronFilterInput!) {\n cronDelete(filter: $filter)\n }\n": typeof types.DeleteCronsDocument,
|
||||
"\n mutation InsertFeed($data: FeedsInsertInput!) {\n feedsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n feedType\n token\n }\n }\n": typeof types.InsertFeedDocument,
|
||||
"\n mutation DeleteFeed($filter: FeedsFilterInput!) {\n feedsDelete(filter: $filter)\n }\n": typeof types.DeleteFeedDocument,
|
||||
"\n query GetSubscriptions($filter: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetSubscriptionsDocument,
|
||||
"\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": typeof types.InsertSubscriptionDocument,
|
||||
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filter: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filter\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": typeof types.UpdateSubscriptionsDocument,
|
||||
"\n mutation DeleteSubscriptions($filter: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filter)\n }\n": typeof types.DeleteSubscriptionsDocument,
|
||||
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument,
|
||||
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument,
|
||||
"\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetTasksDocument,
|
||||
"\n mutation InsertSubscriberTask($data: SubscriberTasksInsertInput!) {\n subscriberTasksCreateOne(data: $data) {\n id\n }\n }\n": typeof types.InsertSubscriberTaskDocument,
|
||||
"\n mutation DeleteTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filter)\n }\n": typeof types.DeleteTasksDocument,
|
||||
@ -39,13 +41,15 @@ const documents: Documents = {
|
||||
"\n mutation DeleteCredential3rd($filter: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filter)\n }\n": types.DeleteCredential3rdDocument,
|
||||
"\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filter: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n": types.GetCredential3rdDetailDocument,
|
||||
"\n mutation CheckCredential3rdAvailable($filter: Credential3rdFilterInput!) {\n credential3rdCheckAvailable(filter: $filter) {\n available\n }\n }\n": types.CheckCredential3rdAvailableDocument,
|
||||
"\nquery GetCrons($filter: CronFilterInput!, $orderBy: CronOrderInput!, $pagination: PaginationInput!) {\n cron(pagination: $pagination, filter: $filter, orderBy: $orderBy) {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n subscriberTask {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n }\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetCronsDocument,
|
||||
"\n mutation DeleteCrons($filter: CronFilterInput!) {\n cronDelete(filter: $filter)\n }\n": types.DeleteCronsDocument,
|
||||
"\n mutation InsertFeed($data: FeedsInsertInput!) {\n feedsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n feedType\n token\n }\n }\n": types.InsertFeedDocument,
|
||||
"\n mutation DeleteFeed($filter: FeedsFilterInput!) {\n feedsDelete(filter: $filter)\n }\n": types.DeleteFeedDocument,
|
||||
"\n query GetSubscriptions($filter: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetSubscriptionsDocument,
|
||||
"\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": types.InsertSubscriptionDocument,
|
||||
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filter: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filter\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": types.UpdateSubscriptionsDocument,
|
||||
"\n mutation DeleteSubscriptions($filter: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filter)\n }\n": types.DeleteSubscriptionsDocument,
|
||||
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument,
|
||||
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument,
|
||||
"\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetTasksDocument,
|
||||
"\n mutation InsertSubscriberTask($data: SubscriberTasksInsertInput!) {\n subscriberTasksCreateOne(data: $data) {\n id\n }\n }\n": types.InsertSubscriberTaskDocument,
|
||||
"\n mutation DeleteTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filter)\n }\n": types.DeleteTasksDocument,
|
||||
@ -90,6 +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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@ -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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
import { guardRouteIndexAsNotFound } from '@/components/layout/app-not-found';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { Outlet } from '@tanstack/react-router';
|
||||
import { Outlet, type RouteOptions } from '@tanstack/react-router';
|
||||
|
||||
export interface BuildVirtualBranchRouteOptions {
|
||||
title: string;
|
||||
@ -8,7 +8,11 @@ export interface BuildVirtualBranchRouteOptions {
|
||||
|
||||
export function buildVirtualBranchRouteOptions(
|
||||
options: BuildVirtualBranchRouteOptions
|
||||
) {
|
||||
): {
|
||||
beforeLoad: RouteOptions['beforeLoad'];
|
||||
staticData: RouteStateDataOption;
|
||||
component: RouteOptions['component'];
|
||||
} {
|
||||
return {
|
||||
beforeLoad: guardRouteIndexAsNotFound,
|
||||
staticData: {
|
||||
|
@ -31,11 +31,15 @@ import { Route as AppCredential3rdManageRouteImport } from './routes/_app/creden
|
||||
import { Route as AppCredential3rdCreateRouteImport } from './routes/_app/credential3rd/create'
|
||||
import { Route as AppBangumiManageRouteImport } from './routes/_app/bangumi/manage'
|
||||
import { Route as AppExploreExploreRouteImport } from './routes/_app/_explore/explore'
|
||||
import { Route as AppTasksCronRouteRouteImport } from './routes/_app/tasks/cron/route'
|
||||
import { Route as AppTasksDetailIdRouteImport } from './routes/_app/tasks/detail.$id'
|
||||
import { Route as AppTasksCronManageRouteImport } from './routes/_app/tasks/cron/manage'
|
||||
import { Route as AppSubscriptionsEditIdRouteImport } from './routes/_app/subscriptions/edit.$id'
|
||||
import { Route as AppSubscriptionsDetailIdRouteImport } from './routes/_app/subscriptions/detail.$id'
|
||||
import { Route as AppCredential3rdEditIdRouteImport } from './routes/_app/credential3rd/edit.$id'
|
||||
import { Route as AppCredential3rdDetailIdRouteImport } from './routes/_app/credential3rd/detail.$id'
|
||||
import { Route as AppTasksCronEditIdRouteImport } from './routes/_app/tasks/cron/edit.$id'
|
||||
import { Route as AppTasksCronDetailIdRouteImport } from './routes/_app/tasks/cron/detail.$id'
|
||||
|
||||
const AboutRoute = AboutRouteImport.update({
|
||||
id: '/about',
|
||||
@ -148,11 +152,21 @@ const AppExploreExploreRoute = AppExploreExploreRouteImport.update({
|
||||
path: '/explore',
|
||||
getParentRoute: () => AppRouteRoute,
|
||||
} as any)
|
||||
const AppTasksCronRouteRoute = AppTasksCronRouteRouteImport.update({
|
||||
id: '/cron',
|
||||
path: '/cron',
|
||||
getParentRoute: () => AppTasksRouteRoute,
|
||||
} as any)
|
||||
const AppTasksDetailIdRoute = AppTasksDetailIdRouteImport.update({
|
||||
id: '/detail/$id',
|
||||
path: '/detail/$id',
|
||||
getParentRoute: () => AppTasksRouteRoute,
|
||||
} as any)
|
||||
const AppTasksCronManageRoute = AppTasksCronManageRouteImport.update({
|
||||
id: '/manage',
|
||||
path: '/manage',
|
||||
getParentRoute: () => AppTasksCronRouteRoute,
|
||||
} as any)
|
||||
const AppSubscriptionsEditIdRoute = AppSubscriptionsEditIdRouteImport.update({
|
||||
id: '/edit/$id',
|
||||
path: '/edit/$id',
|
||||
@ -175,6 +189,16 @@ const AppCredential3rdDetailIdRoute =
|
||||
path: '/detail/$id',
|
||||
getParentRoute: () => AppCredential3rdRouteRoute,
|
||||
} as any)
|
||||
const AppTasksCronEditIdRoute = AppTasksCronEditIdRouteImport.update({
|
||||
id: '/edit/$id',
|
||||
path: '/edit/$id',
|
||||
getParentRoute: () => AppTasksCronRouteRoute,
|
||||
} as any)
|
||||
const AppTasksCronDetailIdRoute = AppTasksCronDetailIdRouteImport.update({
|
||||
id: '/detail/$id',
|
||||
path: '/detail/$id',
|
||||
getParentRoute: () => AppTasksCronRouteRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
@ -189,6 +213,7 @@ export interface FileRoutesByFullPath {
|
||||
'/tasks': typeof AppTasksRouteRouteWithChildren
|
||||
'/auth/sign-in': typeof AuthSignInRoute
|
||||
'/auth/sign-up': typeof AuthSignUpRoute
|
||||
'/tasks/cron': typeof AppTasksCronRouteRouteWithChildren
|
||||
'/explore': typeof AppExploreExploreRoute
|
||||
'/bangumi/manage': typeof AppBangumiManageRoute
|
||||
'/credential3rd/create': typeof AppCredential3rdCreateRoute
|
||||
@ -203,7 +228,10 @@ export interface FileRoutesByFullPath {
|
||||
'/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
|
||||
'/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
|
||||
'/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
|
||||
'/tasks/cron/manage': typeof AppTasksCronManageRoute
|
||||
'/tasks/detail/$id': typeof AppTasksDetailIdRoute
|
||||
'/tasks/cron/detail/$id': typeof AppTasksCronDetailIdRoute
|
||||
'/tasks/cron/edit/$id': typeof AppTasksCronEditIdRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
@ -218,6 +246,7 @@ export interface FileRoutesByTo {
|
||||
'/tasks': typeof AppTasksRouteRouteWithChildren
|
||||
'/auth/sign-in': typeof AuthSignInRoute
|
||||
'/auth/sign-up': typeof AuthSignUpRoute
|
||||
'/tasks/cron': typeof AppTasksCronRouteRouteWithChildren
|
||||
'/explore': typeof AppExploreExploreRoute
|
||||
'/bangumi/manage': typeof AppBangumiManageRoute
|
||||
'/credential3rd/create': typeof AppCredential3rdCreateRoute
|
||||
@ -232,7 +261,10 @@ export interface FileRoutesByTo {
|
||||
'/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
|
||||
'/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
|
||||
'/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
|
||||
'/tasks/cron/manage': typeof AppTasksCronManageRoute
|
||||
'/tasks/detail/$id': typeof AppTasksDetailIdRoute
|
||||
'/tasks/cron/detail/$id': typeof AppTasksCronDetailIdRoute
|
||||
'/tasks/cron/edit/$id': typeof AppTasksCronEditIdRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
@ -248,6 +280,7 @@ export interface FileRoutesById {
|
||||
'/_app/tasks': typeof AppTasksRouteRouteWithChildren
|
||||
'/auth/sign-in': typeof AuthSignInRoute
|
||||
'/auth/sign-up': typeof AuthSignUpRoute
|
||||
'/_app/tasks/cron': typeof AppTasksCronRouteRouteWithChildren
|
||||
'/_app/_explore/explore': typeof AppExploreExploreRoute
|
||||
'/_app/bangumi/manage': typeof AppBangumiManageRoute
|
||||
'/_app/credential3rd/create': typeof AppCredential3rdCreateRoute
|
||||
@ -262,7 +295,10 @@ export interface FileRoutesById {
|
||||
'/_app/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
|
||||
'/_app/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
|
||||
'/_app/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
|
||||
'/_app/tasks/cron/manage': typeof AppTasksCronManageRoute
|
||||
'/_app/tasks/detail/$id': typeof AppTasksDetailIdRoute
|
||||
'/_app/tasks/cron/detail/$id': typeof AppTasksCronDetailIdRoute
|
||||
'/_app/tasks/cron/edit/$id': typeof AppTasksCronEditIdRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
@ -279,6 +315,7 @@ export interface FileRouteTypes {
|
||||
| '/tasks'
|
||||
| '/auth/sign-in'
|
||||
| '/auth/sign-up'
|
||||
| '/tasks/cron'
|
||||
| '/explore'
|
||||
| '/bangumi/manage'
|
||||
| '/credential3rd/create'
|
||||
@ -293,7 +330,10 @@ export interface FileRouteTypes {
|
||||
| '/credential3rd/edit/$id'
|
||||
| '/subscriptions/detail/$id'
|
||||
| '/subscriptions/edit/$id'
|
||||
| '/tasks/cron/manage'
|
||||
| '/tasks/detail/$id'
|
||||
| '/tasks/cron/detail/$id'
|
||||
| '/tasks/cron/edit/$id'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
@ -308,6 +348,7 @@ export interface FileRouteTypes {
|
||||
| '/tasks'
|
||||
| '/auth/sign-in'
|
||||
| '/auth/sign-up'
|
||||
| '/tasks/cron'
|
||||
| '/explore'
|
||||
| '/bangumi/manage'
|
||||
| '/credential3rd/create'
|
||||
@ -322,7 +363,10 @@ export interface FileRouteTypes {
|
||||
| '/credential3rd/edit/$id'
|
||||
| '/subscriptions/detail/$id'
|
||||
| '/subscriptions/edit/$id'
|
||||
| '/tasks/cron/manage'
|
||||
| '/tasks/detail/$id'
|
||||
| '/tasks/cron/detail/$id'
|
||||
| '/tasks/cron/edit/$id'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
@ -337,6 +381,7 @@ export interface FileRouteTypes {
|
||||
| '/_app/tasks'
|
||||
| '/auth/sign-in'
|
||||
| '/auth/sign-up'
|
||||
| '/_app/tasks/cron'
|
||||
| '/_app/_explore/explore'
|
||||
| '/_app/bangumi/manage'
|
||||
| '/_app/credential3rd/create'
|
||||
@ -351,7 +396,10 @@ export interface FileRouteTypes {
|
||||
| '/_app/credential3rd/edit/$id'
|
||||
| '/_app/subscriptions/detail/$id'
|
||||
| '/_app/subscriptions/edit/$id'
|
||||
| '/_app/tasks/cron/manage'
|
||||
| '/_app/tasks/detail/$id'
|
||||
| '/_app/tasks/cron/detail/$id'
|
||||
| '/_app/tasks/cron/edit/$id'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
@ -520,6 +568,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AppExploreExploreRouteImport
|
||||
parentRoute: typeof AppRouteRoute
|
||||
}
|
||||
'/_app/tasks/cron': {
|
||||
id: '/_app/tasks/cron'
|
||||
path: '/cron'
|
||||
fullPath: '/tasks/cron'
|
||||
preLoaderRoute: typeof AppTasksCronRouteRouteImport
|
||||
parentRoute: typeof AppTasksRouteRoute
|
||||
}
|
||||
'/_app/tasks/detail/$id': {
|
||||
id: '/_app/tasks/detail/$id'
|
||||
path: '/detail/$id'
|
||||
@ -527,6 +582,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AppTasksDetailIdRouteImport
|
||||
parentRoute: typeof AppTasksRouteRoute
|
||||
}
|
||||
'/_app/tasks/cron/manage': {
|
||||
id: '/_app/tasks/cron/manage'
|
||||
path: '/manage'
|
||||
fullPath: '/tasks/cron/manage'
|
||||
preLoaderRoute: typeof AppTasksCronManageRouteImport
|
||||
parentRoute: typeof AppTasksCronRouteRoute
|
||||
}
|
||||
'/_app/subscriptions/edit/$id': {
|
||||
id: '/_app/subscriptions/edit/$id'
|
||||
path: '/edit/$id'
|
||||
@ -555,6 +617,20 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AppCredential3rdDetailIdRouteImport
|
||||
parentRoute: typeof AppCredential3rdRouteRoute
|
||||
}
|
||||
'/_app/tasks/cron/edit/$id': {
|
||||
id: '/_app/tasks/cron/edit/$id'
|
||||
path: '/edit/$id'
|
||||
fullPath: '/tasks/cron/edit/$id'
|
||||
preLoaderRoute: typeof AppTasksCronEditIdRouteImport
|
||||
parentRoute: typeof AppTasksCronRouteRoute
|
||||
}
|
||||
'/_app/tasks/cron/detail/$id': {
|
||||
id: '/_app/tasks/cron/detail/$id'
|
||||
path: '/detail/$id'
|
||||
fullPath: '/tasks/cron/detail/$id'
|
||||
preLoaderRoute: typeof AppTasksCronDetailIdRouteImport
|
||||
parentRoute: typeof AppTasksCronRouteRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -630,12 +706,29 @@ const AppSubscriptionsRouteRouteWithChildren =
|
||||
AppSubscriptionsRouteRouteChildren,
|
||||
)
|
||||
|
||||
interface AppTasksCronRouteRouteChildren {
|
||||
AppTasksCronManageRoute: typeof AppTasksCronManageRoute
|
||||
AppTasksCronDetailIdRoute: typeof AppTasksCronDetailIdRoute
|
||||
AppTasksCronEditIdRoute: typeof AppTasksCronEditIdRoute
|
||||
}
|
||||
|
||||
const AppTasksCronRouteRouteChildren: AppTasksCronRouteRouteChildren = {
|
||||
AppTasksCronManageRoute: AppTasksCronManageRoute,
|
||||
AppTasksCronDetailIdRoute: AppTasksCronDetailIdRoute,
|
||||
AppTasksCronEditIdRoute: AppTasksCronEditIdRoute,
|
||||
}
|
||||
|
||||
const AppTasksCronRouteRouteWithChildren =
|
||||
AppTasksCronRouteRoute._addFileChildren(AppTasksCronRouteRouteChildren)
|
||||
|
||||
interface AppTasksRouteRouteChildren {
|
||||
AppTasksCronRouteRoute: typeof AppTasksCronRouteRouteWithChildren
|
||||
AppTasksManageRoute: typeof AppTasksManageRoute
|
||||
AppTasksDetailIdRoute: typeof AppTasksDetailIdRoute
|
||||
}
|
||||
|
||||
const AppTasksRouteRouteChildren: AppTasksRouteRouteChildren = {
|
||||
AppTasksCronRouteRoute: AppTasksCronRouteRouteWithChildren,
|
||||
AppTasksManageRoute: AppTasksManageRoute,
|
||||
AppTasksDetailIdRoute: AppTasksDetailIdRoute,
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { FormFieldErrors } from '@/components/ui/form-field-errors';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@ -123,14 +124,11 @@ function CredentialCreateRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-2xl py-6">
|
||||
<div className="mb-6 flex items-center gap-4">
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Create third-party credential</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Add new third-party login credential
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ContainerHeader
|
||||
title="Create third-party credential"
|
||||
description="Add new third-party login credential"
|
||||
defaultBackTo="/credential3rd/manage"
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@ -17,14 +18,9 @@ import { GET_CREDENTIAL_3RD_DETAIL } from '@/domains/recorder/schema/credential3
|
||||
import type { GetCredential3rdDetailQuery } from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import {
|
||||
createFileRoute,
|
||||
useCanGoBack,
|
||||
useNavigate,
|
||||
useRouter,
|
||||
} from '@tanstack/react-router';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { format } from 'date-fns/format';
|
||||
import { ArrowLeft, CheckIcon, Edit, Eye, EyeOff } from 'lucide-react';
|
||||
import { CheckIcon, Edit, Eye, EyeOff } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { Credential3rdCheckAvailableViewDialogContent } from './-check-available';
|
||||
|
||||
@ -38,21 +34,9 @@ export const Route = createFileRoute('/_app/credential3rd/detail/$id')({
|
||||
function Credential3rdDetailRouteComponent() {
|
||||
const { id } = Route.useParams();
|
||||
const navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
const canGoBack = useCanGoBack();
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleBack = () => {
|
||||
if (canGoBack) {
|
||||
router.history.back();
|
||||
} else {
|
||||
navigate({
|
||||
to: '/credential3rd/manage',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { loading, error, data } = useQuery<GetCredential3rdDetailQuery>(
|
||||
GET_CREDENTIAL_3RD_DETAIL,
|
||||
{
|
||||
@ -91,31 +75,17 @@ function Credential3rdDetailRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl py-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Credential detail</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
View credential #{credential.id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<ContainerHeader
|
||||
title="Credential Detail"
|
||||
description={`View credential #${credential.id}`}
|
||||
defaultBackTo="/credential3rd/manage"
|
||||
actions={
|
||||
<Button onClick={handleEnterEditMode}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@ -39,13 +40,8 @@ import type {
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import {
|
||||
createFileRoute,
|
||||
useCanGoBack,
|
||||
useNavigate,
|
||||
useRouter,
|
||||
} from '@tanstack/react-router';
|
||||
import { ArrowLeft, Eye, EyeOff, Save, X } from 'lucide-react';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { Eye, EyeOff, Save } from 'lucide-react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@ -63,23 +59,10 @@ function FormView({
|
||||
credential: Credential3rdDetailDto;
|
||||
onCompleted: VoidFunction;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const togglePasswordVisibility = () => {
|
||||
setShowPassword((prev) => !prev);
|
||||
};
|
||||
const router = useRouter();
|
||||
const canGoBack = useCanGoBack();
|
||||
|
||||
const handleBack = () => {
|
||||
if (canGoBack) {
|
||||
router.history.back();
|
||||
} else {
|
||||
navigate({
|
||||
to: '/credential3rd/manage',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const [updateCredential, { loading: updating }] = useMutation<
|
||||
UpdateCredential3rdMutation,
|
||||
@ -121,35 +104,17 @@ function FormView({
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl py-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Credential edit</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Edit credential #{credential.id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleBack} disabled={updating}>
|
||||
<X className="mr-2 h-4 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
<ContainerHeader
|
||||
title="Credential Edit"
|
||||
description={`Edit credential #${credential.id}`}
|
||||
defaultBackTo={`/credential3rd/detail/${credential.id}`}
|
||||
actions={
|
||||
<Button onClick={() => form.handleSubmit()} disabled={updating}>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{updating ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
||||
import { DataTableViewOptions } from '@/components/ui/data-table-view-options';
|
||||
import { DialogTrigger } from '@/components/ui/dialog';
|
||||
@ -297,18 +298,16 @@ function CredentialManageRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto space-y-4 rounded-md">
|
||||
<div className="flex items-center justify-between pt-4">
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Credential 3rd Management</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Manage your third-party platform login credentials
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => navigate({ to: '/credential3rd/create' })}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Credential
|
||||
</Button>
|
||||
</div>
|
||||
<ContainerHeader
|
||||
title="Credential 3rd Management"
|
||||
description="Manage your third-party platform login credentials"
|
||||
actions={
|
||||
<Button onClick={() => navigate({ to: '/credential3rd/create' })}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Credential
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center py-2">
|
||||
<DataTableViewOptions table={table} />
|
||||
</div>
|
||||
|
@ -122,6 +122,7 @@ export const SubscriptionSyncView = memo(
|
||||
export interface SubscriptionSyncDialogContentProps {
|
||||
id: number;
|
||||
onCancel?: VoidFunction;
|
||||
isCron?: boolean;
|
||||
}
|
||||
|
||||
export const SubscriptionSyncDialogContent = memo(
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { FormFieldErrors } from '@/components/ui/form-field-errors';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@ -96,14 +97,11 @@ function SubscriptionCreateRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-2xl py-6">
|
||||
<div className="mb-6 flex items-center gap-4">
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Create Bangumi Subscription</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Add a new bangumi subscription source
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ContainerHeader
|
||||
title="Create Bangumi Subscription"
|
||||
description="Add a new bangumi subscription source"
|
||||
defaultBackTo="/subscriptions/manage"
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Img } from '@/components/ui/img';
|
||||
@ -33,15 +34,9 @@ import {
|
||||
SubscriptionCategoryEnum,
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import {
|
||||
createFileRoute,
|
||||
useCanGoBack,
|
||||
useNavigate,
|
||||
useRouter,
|
||||
} from '@tanstack/react-router';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Edit,
|
||||
ExternalLink,
|
||||
ListIcon,
|
||||
@ -61,20 +56,8 @@ export const Route = createFileRoute('/_app/subscriptions/detail/$id')({
|
||||
function SubscriptionDetailRouteComponent() {
|
||||
const { id } = Route.useParams();
|
||||
const navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
const canGoBack = useCanGoBack();
|
||||
const subscriptionService = useInject(SubscriptionService);
|
||||
|
||||
const handleBack = () => {
|
||||
if (canGoBack) {
|
||||
router.history.back();
|
||||
} else {
|
||||
navigate({
|
||||
to: '/subscriptions/manage',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleReload = async () => {
|
||||
const result = await refetch();
|
||||
const error = getApolloQueryError(result);
|
||||
@ -177,31 +160,16 @@ function SubscriptionDetailRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl py-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Subscription detail</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
View subscription #{subscription.id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<ContainerHeader
|
||||
title="Subscription Detail"
|
||||
description={`View subscription #${subscription.id}`}
|
||||
actions={
|
||||
<Button onClick={handleEnterEditMode}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</Button>{' '}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@ -421,6 +389,79 @@ function SubscriptionDetailRouteComponent() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="font-medium text-sm">Associated Crons</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: '/tasks/cron/manage',
|
||||
})
|
||||
}
|
||||
>
|
||||
<ListIcon className="h-4 w-4" />
|
||||
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 />
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { FormFieldErrors } from '@/components/ui/form-field-errors';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@ -44,8 +45,8 @@ import {
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { ArrowLeft, Save, X } from 'lucide-react';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { Save } from 'lucide-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Credential3rdSelectContent } from './-credential3rd-select';
|
||||
@ -68,16 +69,8 @@ function FormView({
|
||||
subscription: SubscriptionDetailDto;
|
||||
onCompleted: VoidFunction;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const subscriptionService = useInject(SubscriptionService);
|
||||
|
||||
const handleBack = () => {
|
||||
navigate({
|
||||
to: '/subscriptions/detail/$id',
|
||||
params: { id: subscription.id.toString() },
|
||||
});
|
||||
};
|
||||
|
||||
const [updateSubscription, { loading: updating }] = useMutation<
|
||||
UpdateSubscriptionsMutation,
|
||||
UpdateSubscriptionsMutationVariables
|
||||
@ -149,35 +142,17 @@ function FormView({
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl py-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Subscription edit</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Edit subscription #{subscription.id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleBack} disabled={updating}>
|
||||
<X className="mr-2 h-4 w-4" />
|
||||
Cancel
|
||||
</Button>
|
||||
<ContainerHeader
|
||||
title="Subscription Edit"
|
||||
description={`Edit subscription #${subscription.id}`}
|
||||
defaultBackTo={`/subscriptions/detail/${subscription.id}`}
|
||||
actions={
|
||||
<Button onClick={() => form.handleSubmit()} disabled={updating}>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{updating ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
||||
import { DataTableViewOptions } from '@/components/ui/data-table-view-options';
|
||||
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
||||
@ -277,16 +278,16 @@ function SubscriptionManageRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto space-y-4 rounded-md">
|
||||
<div className="flex items-center justify-between pt-4">
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Subscription Management</h1>
|
||||
<p className="text-muted-foreground">Manage your subscription</p>
|
||||
</div>
|
||||
<Button onClick={() => navigate({ to: '/subscriptions/create' })}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Subscription
|
||||
</Button>
|
||||
</div>
|
||||
<ContainerHeader
|
||||
title="Subscription Management"
|
||||
description="Manage your subscription"
|
||||
actions={
|
||||
<Button onClick={() => navigate({ to: '/subscriptions/create' })}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Subscription
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center py-2">
|
||||
<DataTableViewOptions table={table} />
|
||||
</div>
|
||||
|
@ -0,0 +1,42 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { CronStatusEnum } from '@/infra/graphql/gql/graphql';
|
||||
import { AlertCircle, CheckCircle, Clock, Loader2 } from 'lucide-react';
|
||||
|
||||
export function getStatusBadge(status: CronStatusEnum) {
|
||||
switch (status) {
|
||||
case CronStatusEnum.Completed:
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-green-100 text-green-800">
|
||||
<CheckCircle className="mr-1 h-3 w-3 capitalize" />
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
case CronStatusEnum.Running:
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-blue-100 text-blue-800">
|
||||
<Loader2 className="mr-1 h-3 w-3 animate-spin capitalize" />
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
case CronStatusEnum.Failed:
|
||||
return (
|
||||
<Badge variant="destructive">
|
||||
<AlertCircle className="mr-1 h-3 w-3 capitalize" />
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
case CronStatusEnum.Pending:
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-yellow-100 text-yellow-800">
|
||||
<Clock className="mr-1 h-3 w-3 capitalize" />
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Badge variant="outline" className="capitalize">
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_app/tasks/cron/detail/$id')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/_app/tasks/cron/detail/$id"!</div>
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_app/tasks/cron/edit/$id')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/_app/tasks/cron/edit/$id"!</div>
|
||||
}
|
306
apps/webui/src/presentation/routes/_app/tasks/cron/manage.tsx
Normal file
306
apps/webui/src/presentation/routes/_app/tasks/cron/manage.tsx
Normal file
@ -0,0 +1,306 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||
import { DropdownMenuActions } from '@/components/ui/dropdown-menu-actions';
|
||||
import { QueryErrorView } from '@/components/ui/query-error-view';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import {
|
||||
type CronDto,
|
||||
DELETE_CRONS,
|
||||
GET_CRONS,
|
||||
} from '@/domains/recorder/schema/cron';
|
||||
import {
|
||||
apolloErrorToMessage,
|
||||
getApolloQueryError,
|
||||
} from '@/infra/errors/apollo';
|
||||
import {
|
||||
CronStatusEnum,
|
||||
type DeleteCronsMutation,
|
||||
type DeleteCronsMutationVariables,
|
||||
type GetCronsQuery,
|
||||
type GetCronsQueryVariables,
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import {
|
||||
type ColumnDef,
|
||||
type PaginationState,
|
||||
type SortingState,
|
||||
type VisibilityState,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import { format } from 'date-fns';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { getStatusBadge } from './-status-badge';
|
||||
|
||||
export const Route = createFileRoute('/_app/tasks/cron/manage')({
|
||||
component: TaskCronManageRouteComponent,
|
||||
staticData: {
|
||||
breadcrumb: { label: 'Manage' },
|
||||
} satisfies RouteStateDataOption,
|
||||
});
|
||||
|
||||
function TaskCronManageRouteComponent() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { loading, error, data, refetch } = useQuery<
|
||||
GetCronsQuery,
|
||||
GetCronsQueryVariables
|
||||
>(GET_CRONS, {
|
||||
variables: {
|
||||
pagination: {
|
||||
page: {
|
||||
page: pagination.pageIndex,
|
||||
limit: pagination.pageSize,
|
||||
},
|
||||
},
|
||||
filter: {},
|
||||
orderBy: {
|
||||
nextRun: 'DESC',
|
||||
},
|
||||
},
|
||||
pollInterval: 5000, // Auto-refresh every 5 seconds
|
||||
});
|
||||
|
||||
const { showSkeleton } = useDebouncedSkeleton({ loading });
|
||||
|
||||
const crons = data?.cron;
|
||||
|
||||
const [deleteCron] = useMutation<
|
||||
DeleteCronsMutation,
|
||||
DeleteCronsMutationVariables
|
||||
>(DELETE_CRONS, {
|
||||
onCompleted: async () => {
|
||||
const refetchResult = await refetch();
|
||||
const error = getApolloQueryError(refetchResult);
|
||||
if (error) {
|
||||
toast.error('Failed to delete tasks', {
|
||||
description: apolloErrorToMessage(error),
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast.success('Tasks deleted');
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error('Failed to delete tasks', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const cs: ColumnDef<CronDto>[] = [
|
||||
{
|
||||
header: 'ID',
|
||||
accessorKey: 'id',
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div
|
||||
className="max-w-[200px] truncate font-mono text-sm"
|
||||
title={row.original.id.toString()}
|
||||
>
|
||||
{row.original.id}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return cs;
|
||||
}, []);
|
||||
|
||||
const table = useReactTable({
|
||||
data: useMemo(() => (crons?.nodes ?? []) as CronDto[], [crons]),
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onSortingChange: setSorting,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
pageCount: crons?.paginationInfo?.pages,
|
||||
rowCount: crons?.paginationInfo?.total,
|
||||
enableColumnPinning: true,
|
||||
autoResetPageIndex: true,
|
||||
manualPagination: true,
|
||||
state: {
|
||||
pagination,
|
||||
sorting,
|
||||
columnVisibility,
|
||||
},
|
||||
initialState: {
|
||||
columnPinning: {
|
||||
right: ['actions'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <QueryErrorView message={error.message} onRetry={refetch} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl space-y-4 px-4">
|
||||
<ContainerHeader
|
||||
title="Crons Management"
|
||||
description="Manage your crons"
|
||||
actions={
|
||||
<Button onClick={() => refetch()} variant="outline" size="sm">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="space-y-3">
|
||||
{showSkeleton &&
|
||||
Array.from(new Array(10)).map((_, index) => (
|
||||
<Skeleton key={index} className="h-32 w-full" />
|
||||
))}
|
||||
|
||||
{!showSkeleton && table.getRowModel().rows?.length > 0 ? (
|
||||
table.getRowModel().rows.map((row, index) => {
|
||||
const cron = row.original;
|
||||
return (
|
||||
<div
|
||||
className="space-y-3 rounded-lg border bg-card p-4"
|
||||
key={`${cron.id}-${index}`}
|
||||
>
|
||||
{/* Header with status and priority */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="font-mono text-muted-foreground text-xs">
|
||||
# {cron.id}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Badge variant="outline" className="capitalize">
|
||||
{cron.cronExpr}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
{getStatusBadge(cron.status)}
|
||||
<Badge variant="outline">Priority: {cron.priority}</Badge>
|
||||
<div className="mr-0 ml-auto">
|
||||
<DropdownMenuActions
|
||||
id={cron.id}
|
||||
showDetail
|
||||
onDetail={() => {
|
||||
navigate({
|
||||
to: '/tasks/cron/detail/$id',
|
||||
params: { id: cron.id.toString() },
|
||||
});
|
||||
}}
|
||||
showDelete
|
||||
onDelete={() =>
|
||||
deleteCron({
|
||||
variables: {
|
||||
filter: {
|
||||
id: {
|
||||
eq: cron.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{cron.status === CronStatusEnum.Failed && (
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
// TODO: Retry cron
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuActions>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Time info */}
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Next run: </span>
|
||||
<span>
|
||||
{cron.nextRun
|
||||
? format(new Date(cron.nextRun), 'MM/dd HH:mm')
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-muted-foreground">Last run: </span>
|
||||
<span>
|
||||
{cron.lastRun
|
||||
? format(new Date(cron.lastRun), 'MM/dd HH:mm')
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Attempts */}
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">Attempts: </span>
|
||||
<span>
|
||||
{cron.attempts} / {cron.maxAttempts}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Lock at */}
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">Lock at: </span>
|
||||
<span>
|
||||
{cron.lockedAt
|
||||
? format(new Date(cron.lockedAt), 'MM/dd HH:mm')
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subscriber task cron */}
|
||||
{cron.subscriberTaskCron && (
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">Task:</span>
|
||||
<br />
|
||||
<span
|
||||
className="whitespace-pre-wrap"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
cron.subscriberTaskCron,
|
||||
null,
|
||||
2
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error if exists */}
|
||||
{cron.status === CronStatusEnum.Failed && cron.lastError && (
|
||||
<div className="rounded bg-destructive/10 p-2 text-destructive text-sm">
|
||||
{cron.lastError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<DetailEmptyView message="No tasks found" fullWidth />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DataTablePagination table={table} showSelectedRowCount={false} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import { buildVirtualBranchRouteOptions } from '@/infra/routes/utils';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/_app/tasks/cron')(
|
||||
buildVirtualBranchRouteOptions({
|
||||
title: 'Cron',
|
||||
})
|
||||
);
|
@ -8,6 +8,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { QueryErrorView } from '@/components/ui/query-error-view';
|
||||
@ -24,14 +25,9 @@ import {
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import {
|
||||
createFileRoute,
|
||||
useCanGoBack,
|
||||
useNavigate,
|
||||
useRouter,
|
||||
} from '@tanstack/react-router';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { format } from 'date-fns';
|
||||
import { ArrowLeft, RefreshCw } from 'lucide-react';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { prettyTaskType } from './-pretty-task-type';
|
||||
@ -46,19 +42,6 @@ export const Route = createFileRoute('/_app/tasks/detail/$id')({
|
||||
|
||||
function TaskDetailRouteComponent() {
|
||||
const { id } = Route.useParams();
|
||||
const navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
const canGoBack = useCanGoBack();
|
||||
|
||||
const handleBack = () => {
|
||||
if (canGoBack) {
|
||||
router.history.back();
|
||||
} else {
|
||||
navigate({
|
||||
to: '/tasks/manage',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { data, loading, error, refetch } = useQuery<
|
||||
GetTasksQuery,
|
||||
@ -129,27 +112,17 @@ function TaskDetailRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl py-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<ContainerHeader
|
||||
title="Task Detail"
|
||||
description={`View task #${task.id}`}
|
||||
defaultBackTo="/tasks/manage"
|
||||
actions={
|
||||
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Task Detail</h1>
|
||||
<p className="mt-1 text-muted-foreground">View task #{task.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
type DeleteTasksMutation,
|
||||
type DeleteTasksMutationVariables,
|
||||
type GetTasksQuery,
|
||||
type GetTasksQueryVariables,
|
||||
type RetryTasksMutation,
|
||||
type RetryTasksMutationVariables,
|
||||
SubscriberTaskStatusEnum,
|
||||
@ -35,6 +36,7 @@ import {
|
||||
import { format } from 'date-fns';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
apolloErrorToMessage,
|
||||
@ -55,18 +57,17 @@ export const Route = createFileRoute('/_app/tasks/manage')({
|
||||
function TaskManageRouteComponent() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
|
||||
lockAt: false,
|
||||
lockBy: false,
|
||||
attempts: false,
|
||||
});
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { loading, error, data, refetch } = useQuery<GetTasksQuery>(GET_TASKS, {
|
||||
const { loading, error, data, refetch } = useQuery<
|
||||
GetTasksQuery,
|
||||
GetTasksQueryVariables
|
||||
>(GET_TASKS, {
|
||||
variables: {
|
||||
pagination: {
|
||||
page: {
|
||||
@ -172,16 +173,16 @@ function TaskManageRouteComponent() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto space-y-4 px-4">
|
||||
<div className="flex items-center justify-between pt-4">
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Tasks Management</h1>
|
||||
<p className="text-muted-foreground">Manage your tasks</p>
|
||||
</div>
|
||||
<Button onClick={() => refetch()} variant="outline" size="sm">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="container mx-auto max-w-4xl space-y-4 px-4">
|
||||
<ContainerHeader
|
||||
title="Tasks Management"
|
||||
description="Manage your tasks"
|
||||
actions={
|
||||
<Button onClick={() => refetch()} variant="outline" size="sm">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="space-y-3">
|
||||
{showSkeleton &&
|
||||
|
Loading…
Reference in New Issue
Block a user