feat: task ui basic done
This commit is contained in:
parent
c60f6f511e
commit
882b29d7a1
14
.vscode/settings.json
vendored
14
.vscode/settings.json
vendored
@ -41,11 +41,11 @@
|
|||||||
],
|
],
|
||||||
"rust-analyzer.cargo.features": "all",
|
"rust-analyzer.cargo.features": "all",
|
||||||
// https://github.com/rust-lang/rust/issues/141540
|
// https://github.com/rust-lang/rust/issues/141540
|
||||||
"rust-analyzer.cargo.targetDir": "target/rust-analyzer",
|
// "rust-analyzer.cargo.targetDir": "target/rust-analyzer",
|
||||||
"rust-analyzer.check.extraEnv": {
|
// "rust-analyzer.check.extraEnv": {
|
||||||
"CARGO_TARGET_DIR": "target/rust-analyzer"
|
// "CARGO_TARGET_DIR": "target/rust-analyzer"
|
||||||
},
|
// },
|
||||||
"rust-analyzer.cargo.extraEnv": {
|
// "rust-analyzer.cargo.extraEnv": {
|
||||||
"CARGO_TARGET_DIR": "target/analyzer"
|
// "CARGO_TARGET_DIR": "target/analyzer"
|
||||||
}
|
// }
|
||||||
}
|
}
|
112
.vscode/tasks.json
vendored
Normal file
112
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "dev-all",
|
||||||
|
"dependsOn": [
|
||||||
|
"dev-webui",
|
||||||
|
"dev-recorder",
|
||||||
|
"dev-proxy",
|
||||||
|
"dev-codegen-wait",
|
||||||
|
"dev-deps",
|
||||||
|
],
|
||||||
|
"dependsOrder": "parallel",
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": false,
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"group": "new-group",
|
||||||
|
"echo": true,
|
||||||
|
"reveal": "always",
|
||||||
|
"panel": "shared",
|
||||||
|
"clear": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "dev-webui",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "just",
|
||||||
|
"args": [
|
||||||
|
"dev-webui"
|
||||||
|
],
|
||||||
|
"isBackground": true,
|
||||||
|
"problemMatcher": [],
|
||||||
|
"presentation": {
|
||||||
|
"panel": "dedicated",
|
||||||
|
"reveal": "always",
|
||||||
|
"focus": true,
|
||||||
|
"showReuseMessage": true,
|
||||||
|
"clear": true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "dev-deps",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "just",
|
||||||
|
"args": [
|
||||||
|
"dev-deps"
|
||||||
|
],
|
||||||
|
"isBackground": true,
|
||||||
|
"problemMatcher": [],
|
||||||
|
"presentation": {
|
||||||
|
"panel": "dedicated",
|
||||||
|
"reveal": "never",
|
||||||
|
"focus": false,
|
||||||
|
"showReuseMessage": true,
|
||||||
|
"clear": true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "dev-codegen-wait",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "just",
|
||||||
|
"args": [
|
||||||
|
"dev-codegen-wait"
|
||||||
|
],
|
||||||
|
"isBackground": true,
|
||||||
|
"problemMatcher": [],
|
||||||
|
"presentation": {
|
||||||
|
"panel": "dedicated",
|
||||||
|
"reveal": "never",
|
||||||
|
"focus": false,
|
||||||
|
"showReuseMessage": true,
|
||||||
|
"clear": true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "dev-recorder",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "just",
|
||||||
|
"args": [
|
||||||
|
"dev-recorder"
|
||||||
|
],
|
||||||
|
"isBackground": true,
|
||||||
|
"problemMatcher": [],
|
||||||
|
"presentation": {
|
||||||
|
"panel": "dedicated",
|
||||||
|
"reveal": "never",
|
||||||
|
"focus": false,
|
||||||
|
"showReuseMessage": true,
|
||||||
|
"clear": true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "dev-proxy",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "just",
|
||||||
|
"args": [
|
||||||
|
"dev-proxy",
|
||||||
|
],
|
||||||
|
"isBackground": true,
|
||||||
|
"problemMatcher": [],
|
||||||
|
"presentation": {
|
||||||
|
"panel": "dedicated",
|
||||||
|
"reveal": "never",
|
||||||
|
"focus": false,
|
||||||
|
"showReuseMessage": true,
|
||||||
|
"clear": true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -14,6 +14,8 @@ resolver = "2"
|
|||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
debug = 0
|
debug = 0
|
||||||
|
# https://github.com/rust-lang/rust/issues/141540
|
||||||
|
incremental = false
|
||||||
# [simd not supported by cranelift](https://github.com/rust-lang/rustc_codegen_cranelift/issues/171)
|
# [simd not supported by cranelift](https://github.com/rust-lang/rustc_codegen_cranelift/issues/171)
|
||||||
# codegen-backend = "cranelift"
|
# codegen-backend = "cranelift"
|
||||||
|
|
||||||
|
@ -267,6 +267,7 @@ where
|
|||||||
Box::new(
|
Box::new(
|
||||||
move |context: &ResolverContext| -> SeaResult<Option<SeaValue>> {
|
move |context: &ResolverContext| -> SeaResult<Option<SeaValue>> {
|
||||||
let field_name = context.field().name();
|
let field_name = context.field().name();
|
||||||
|
tracing::warn!("field_name: {:?}", field_name);
|
||||||
if field_name == entity_create_one_mutation_field_name.as_str()
|
if field_name == entity_create_one_mutation_field_name.as_str()
|
||||||
|| field_name == entity_create_batch_mutation_field_name.as_str()
|
|| field_name == entity_create_batch_mutation_field_name.as_str()
|
||||||
{
|
{
|
||||||
@ -291,7 +292,7 @@ where
|
|||||||
{
|
{
|
||||||
let entity_key = get_entity_key::<T>(context);
|
let entity_key = get_entity_key::<T>(context);
|
||||||
let entity_column_key = get_entity_column_key::<T>(context, column);
|
let entity_column_key = get_entity_column_key::<T>(context, column);
|
||||||
let column_name = context.entity_object.column_name.as_ref()(&entity_key, &entity_column_key);
|
|
||||||
context.guards.entity_guards.insert(
|
context.guards.entity_guards.insert(
|
||||||
entity_key.clone(),
|
entity_key.clone(),
|
||||||
guard_entity_with_subscriber_id::<T>(context, column),
|
guard_entity_with_subscriber_id::<T>(context, column),
|
||||||
@ -311,13 +312,9 @@ where
|
|||||||
generate_subscriber_id_filter_condition::<T>(context, column),
|
generate_subscriber_id_filter_condition::<T>(context, column),
|
||||||
);
|
);
|
||||||
context.types.input_none_conversions.insert(
|
context.types.input_none_conversions.insert(
|
||||||
column_name.clone(),
|
entity_column_key.clone(),
|
||||||
generate_default_subscriber_id_input_conversion::<T>(context, column),
|
generate_default_subscriber_id_input_conversion::<T>(context, column),
|
||||||
);
|
);
|
||||||
context
|
|
||||||
.entity_input
|
|
||||||
.insert_skips
|
|
||||||
.push(entity_column_key.clone());
|
|
||||||
|
|
||||||
context.entity_input.update_skips.push(entity_column_key);
|
context.entity_input.update_skips.push(entity_column_key);
|
||||||
}
|
}
|
||||||
|
@ -121,6 +121,7 @@ pub fn build_schema(
|
|||||||
builder.register_enumeration::<downloaders::DownloaderCategory>();
|
builder.register_enumeration::<downloaders::DownloaderCategory>();
|
||||||
builder.register_enumeration::<downloads::DownloadMime>();
|
builder.register_enumeration::<downloads::DownloadMime>();
|
||||||
builder.register_enumeration::<credential_3rd::Credential3rdType>();
|
builder.register_enumeration::<credential_3rd::Credential3rdType>();
|
||||||
|
builder.register_enumeration::<subscriber_tasks::SubscriberTaskStatus>();
|
||||||
}
|
}
|
||||||
|
|
||||||
builder = register_subscriptions_to_schema_builder(builder);
|
builder = register_subscriptions_to_schema_builder(builder);
|
||||||
|
@ -1,7 +1,27 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
pub use crate::task::{SubscriberTask, SubscriberTaskType};
|
pub use crate::task::{
|
||||||
|
SubscriberTask, SubscriberTaskType, SubscriberTaskTypeEnum, SubscriberTaskTypeVariant,
|
||||||
|
SubscriberTaskTypeVariantIter,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, DeriveActiveEnum, EnumIter, DeriveDisplay)]
|
||||||
|
#[sea_orm(rs_type = "String", db_type = "Text")]
|
||||||
|
pub enum SubscriberTaskStatus {
|
||||||
|
#[sea_orm(string_value = "Pending")]
|
||||||
|
Pending,
|
||||||
|
#[sea_orm(string_value = "Scheduled")]
|
||||||
|
Scheduled,
|
||||||
|
#[sea_orm(string_value = "Running")]
|
||||||
|
Running,
|
||||||
|
#[sea_orm(string_value = "Done")]
|
||||||
|
Done,
|
||||||
|
#[sea_orm(string_value = "Failed")]
|
||||||
|
Failed,
|
||||||
|
#[sea_orm(string_value = "Killed")]
|
||||||
|
Killed,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "subscriber_tasks")]
|
#[sea_orm(table_name = "subscriber_tasks")]
|
||||||
@ -11,7 +31,7 @@ pub struct Model {
|
|||||||
pub subscriber_id: i32,
|
pub subscriber_id: i32,
|
||||||
pub job: SubscriberTask,
|
pub job: SubscriberTask,
|
||||||
pub task_type: SubscriberTaskType,
|
pub task_type: SubscriberTaskType,
|
||||||
pub status: String,
|
pub status: SubscriberTaskStatus,
|
||||||
pub attempts: i32,
|
pub attempts: i32,
|
||||||
pub max_attempts: i32,
|
pub max_attempts: i32,
|
||||||
pub run_at: DateTimeUtc,
|
pub run_at: DateTimeUtc,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { Row } from "@tanstack/react-table";
|
|
||||||
import { MoreHorizontal } from "lucide-react";
|
import { MoreHorizontal } from "lucide-react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@ -14,12 +13,11 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import type * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
import type * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
|
||||||
import { ComponentProps, PropsWithChildren, useMemo } from "react";
|
import { ComponentProps, PropsWithChildren } from "react";
|
||||||
|
|
||||||
interface DataTableRowActionsProps<DataView, Id>
|
interface DropdownMenuActionsProps<Id>
|
||||||
extends ComponentProps<typeof DropdownMenuPrimitive.Root> {
|
extends ComponentProps<typeof DropdownMenuPrimitive.Root> {
|
||||||
row: Row<DataView>;
|
id: Id;
|
||||||
getId: (row: Row<DataView>) => Id;
|
|
||||||
showDetail?: boolean;
|
showDetail?: boolean;
|
||||||
showEdit?: boolean;
|
showEdit?: boolean;
|
||||||
showDelete?: boolean;
|
showDelete?: boolean;
|
||||||
@ -28,9 +26,8 @@ interface DataTableRowActionsProps<DataView, Id>
|
|||||||
onEdit?: (id: Id) => void;
|
onEdit?: (id: Id) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataTableRowActions<DataView, Id>({
|
export function DropdownMenuActions<Id>({
|
||||||
row,
|
id,
|
||||||
getId,
|
|
||||||
showDetail,
|
showDetail,
|
||||||
showDelete,
|
showDelete,
|
||||||
showEdit,
|
showEdit,
|
||||||
@ -39,8 +36,7 @@ export function DataTableRowActions<DataView, Id>({
|
|||||||
onEdit,
|
onEdit,
|
||||||
children,
|
children,
|
||||||
...rest
|
...rest
|
||||||
}: PropsWithChildren<DataTableRowActionsProps<DataView, Id>>) {
|
}: PropsWithChildren<DropdownMenuActionsProps<Id>>) {
|
||||||
const id = useMemo(() => getId(row), [getId, row]);
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu {...rest}>
|
<DropdownMenu {...rest}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
@ -133,6 +133,7 @@ export type BangumiInsertInput = {
|
|||||||
savePath?: InputMaybe<Scalars['String']['input']>;
|
savePath?: InputMaybe<Scalars['String']['input']>;
|
||||||
season: Scalars['Int']['input'];
|
season: Scalars['Int']['input'];
|
||||||
seasonRaw?: InputMaybe<Scalars['String']['input']>;
|
seasonRaw?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
subscriberId: Scalars['Int']['input'];
|
||||||
updatedAt?: InputMaybe<Scalars['String']['input']>;
|
updatedAt?: InputMaybe<Scalars['String']['input']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -266,6 +267,7 @@ export type Credential3rdInsertInput = {
|
|||||||
credentialType: Credential3rdTypeEnum;
|
credentialType: Credential3rdTypeEnum;
|
||||||
id?: InputMaybe<Scalars['Int']['input']>;
|
id?: InputMaybe<Scalars['Int']['input']>;
|
||||||
password?: InputMaybe<Scalars['String']['input']>;
|
password?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
subscriberId: Scalars['Int']['input'];
|
||||||
updatedAt?: InputMaybe<Scalars['String']['input']>;
|
updatedAt?: InputMaybe<Scalars['String']['input']>;
|
||||||
userAgent?: InputMaybe<Scalars['String']['input']>;
|
userAgent?: InputMaybe<Scalars['String']['input']>;
|
||||||
username?: InputMaybe<Scalars['String']['input']>;
|
username?: InputMaybe<Scalars['String']['input']>;
|
||||||
@ -448,6 +450,7 @@ export type DownloadersInsertInput = {
|
|||||||
id?: InputMaybe<Scalars['Int']['input']>;
|
id?: InputMaybe<Scalars['Int']['input']>;
|
||||||
password: Scalars['String']['input'];
|
password: Scalars['String']['input'];
|
||||||
savePath: Scalars['String']['input'];
|
savePath: Scalars['String']['input'];
|
||||||
|
subscriberId: Scalars['Int']['input'];
|
||||||
updatedAt?: InputMaybe<Scalars['String']['input']>;
|
updatedAt?: InputMaybe<Scalars['String']['input']>;
|
||||||
username: Scalars['String']['input'];
|
username: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
@ -563,6 +566,7 @@ export type DownloadsInsertInput = {
|
|||||||
rawName: Scalars['String']['input'];
|
rawName: Scalars['String']['input'];
|
||||||
savePath?: InputMaybe<Scalars['String']['input']>;
|
savePath?: InputMaybe<Scalars['String']['input']>;
|
||||||
status: DownloadStatusEnum;
|
status: DownloadStatusEnum;
|
||||||
|
subscriberId: Scalars['Int']['input'];
|
||||||
updatedAt?: InputMaybe<Scalars['String']['input']>;
|
updatedAt?: InputMaybe<Scalars['String']['input']>;
|
||||||
url: Scalars['String']['input'];
|
url: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
@ -725,6 +729,7 @@ export type EpisodesInsertInput = {
|
|||||||
season: Scalars['Int']['input'];
|
season: Scalars['Int']['input'];
|
||||||
seasonRaw?: InputMaybe<Scalars['String']['input']>;
|
seasonRaw?: InputMaybe<Scalars['String']['input']>;
|
||||||
source?: InputMaybe<Scalars['String']['input']>;
|
source?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
subscriberId: Scalars['Int']['input'];
|
||||||
subtitle?: InputMaybe<Scalars['String']['input']>;
|
subtitle?: InputMaybe<Scalars['String']['input']>;
|
||||||
updatedAt?: InputMaybe<Scalars['String']['input']>;
|
updatedAt?: InputMaybe<Scalars['String']['input']>;
|
||||||
};
|
};
|
||||||
@ -1191,6 +1196,16 @@ export type SubscriberIdFilterInput = {
|
|||||||
eq?: InputMaybe<Scalars['Int']['input']>;
|
eq?: InputMaybe<Scalars['Int']['input']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SubscriberTaskStatusEnum = {
|
||||||
|
Done: 'Done',
|
||||||
|
Failed: 'Failed',
|
||||||
|
Killed: 'Killed',
|
||||||
|
Pending: 'Pending',
|
||||||
|
Running: 'Running',
|
||||||
|
Scheduled: 'Scheduled'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type SubscriberTaskStatusEnum = typeof SubscriberTaskStatusEnum[keyof typeof SubscriberTaskStatusEnum];
|
||||||
export const SubscriberTaskTypeEnum = {
|
export const SubscriberTaskTypeEnum = {
|
||||||
SyncOneSubscriptionFeedsFull: 'sync_one_subscription_feeds_full',
|
SyncOneSubscriptionFeedsFull: 'sync_one_subscription_feeds_full',
|
||||||
SyncOneSubscriptionFeedsIncremental: 'sync_one_subscription_feeds_incremental',
|
SyncOneSubscriptionFeedsIncremental: 'sync_one_subscription_feeds_incremental',
|
||||||
@ -1210,7 +1225,7 @@ export type SubscriberTasks = {
|
|||||||
maxAttempts: Scalars['Int']['output'];
|
maxAttempts: Scalars['Int']['output'];
|
||||||
priority: Scalars['Int']['output'];
|
priority: Scalars['Int']['output'];
|
||||||
runAt: Scalars['String']['output'];
|
runAt: Scalars['String']['output'];
|
||||||
status: Scalars['String']['output'];
|
status: SubscriberTaskStatusEnum;
|
||||||
subscriber?: Maybe<Subscribers>;
|
subscriber?: Maybe<Subscribers>;
|
||||||
subscriberId: Scalars['Int']['output'];
|
subscriberId: Scalars['Int']['output'];
|
||||||
taskType: SubscriberTaskTypeEnum;
|
taskType: SubscriberTaskTypeEnum;
|
||||||
@ -1228,7 +1243,7 @@ export type SubscriberTasksBasic = {
|
|||||||
maxAttempts: Scalars['Int']['output'];
|
maxAttempts: Scalars['Int']['output'];
|
||||||
priority: Scalars['Int']['output'];
|
priority: Scalars['Int']['output'];
|
||||||
runAt: Scalars['String']['output'];
|
runAt: Scalars['String']['output'];
|
||||||
status: Scalars['String']['output'];
|
status: SubscriberTaskStatusEnum;
|
||||||
subscriberId: Scalars['Int']['output'];
|
subscriberId: Scalars['Int']['output'];
|
||||||
taskType: SubscriberTaskTypeEnum;
|
taskType: SubscriberTaskTypeEnum;
|
||||||
};
|
};
|
||||||
@ -1276,7 +1291,8 @@ export type SubscriberTasksInsertInput = {
|
|||||||
maxAttempts: Scalars['Int']['input'];
|
maxAttempts: Scalars['Int']['input'];
|
||||||
priority: Scalars['Int']['input'];
|
priority: Scalars['Int']['input'];
|
||||||
runAt: Scalars['String']['input'];
|
runAt: Scalars['String']['input'];
|
||||||
status: Scalars['String']['input'];
|
status: SubscriberTaskStatusEnum;
|
||||||
|
subscriberId: Scalars['Int']['input'];
|
||||||
taskType: SubscriberTaskTypeEnum;
|
taskType: SubscriberTaskTypeEnum;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1307,7 +1323,7 @@ export type SubscriberTasksUpdateInput = {
|
|||||||
maxAttempts?: InputMaybe<Scalars['Int']['input']>;
|
maxAttempts?: InputMaybe<Scalars['Int']['input']>;
|
||||||
priority?: InputMaybe<Scalars['Int']['input']>;
|
priority?: InputMaybe<Scalars['Int']['input']>;
|
||||||
runAt?: InputMaybe<Scalars['String']['input']>;
|
runAt?: InputMaybe<Scalars['String']['input']>;
|
||||||
status?: InputMaybe<Scalars['String']['input']>;
|
status?: InputMaybe<SubscriberTaskStatusEnum>;
|
||||||
taskType?: InputMaybe<SubscriberTaskTypeEnum>;
|
taskType?: InputMaybe<SubscriberTaskTypeEnum>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1433,6 +1449,7 @@ export type SubscriptionBangumiFilterInput = {
|
|||||||
export type SubscriptionBangumiInsertInput = {
|
export type SubscriptionBangumiInsertInput = {
|
||||||
bangumiId: Scalars['Int']['input'];
|
bangumiId: Scalars['Int']['input'];
|
||||||
id?: InputMaybe<Scalars['Int']['input']>;
|
id?: InputMaybe<Scalars['Int']['input']>;
|
||||||
|
subscriberId: Scalars['Int']['input'];
|
||||||
subscriptionId: Scalars['Int']['input'];
|
subscriptionId: Scalars['Int']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1515,6 +1532,7 @@ export type SubscriptionEpisodeFilterInput = {
|
|||||||
export type SubscriptionEpisodeInsertInput = {
|
export type SubscriptionEpisodeInsertInput = {
|
||||||
episodeId: Scalars['Int']['input'];
|
episodeId: Scalars['Int']['input'];
|
||||||
id?: InputMaybe<Scalars['Int']['input']>;
|
id?: InputMaybe<Scalars['Int']['input']>;
|
||||||
|
subscriberId: Scalars['Int']['input'];
|
||||||
subscriptionId: Scalars['Int']['input'];
|
subscriptionId: Scalars['Int']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1627,6 +1645,7 @@ export type SubscriptionsInsertInput = {
|
|||||||
enabled: Scalars['Boolean']['input'];
|
enabled: Scalars['Boolean']['input'];
|
||||||
id?: InputMaybe<Scalars['Int']['input']>;
|
id?: InputMaybe<Scalars['Int']['input']>;
|
||||||
sourceUrl: Scalars['String']['input'];
|
sourceUrl: Scalars['String']['input'];
|
||||||
|
subscriberId: Scalars['Int']['input'];
|
||||||
updatedAt?: InputMaybe<Scalars['String']['input']>;
|
updatedAt?: InputMaybe<Scalars['String']['input']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1790,7 +1809,7 @@ export type GetTasksQueryVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type GetTasksQuery = { __typename?: 'Query', subscriberTasks: { __typename?: 'SubscriberTasksConnection', nodes: Array<{ __typename?: 'SubscriberTasks', id: string, job: any, taskType: SubscriberTaskTypeEnum, status: string, attempts: number, maxAttempts: number, runAt: string, lastError?: string | null, lockAt?: string | null, lockBy?: string | null, doneAt?: string | null, priority: number }>, paginationInfo?: { __typename?: 'PaginationInfo', total: number, pages: number } | null } };
|
export type GetTasksQuery = { __typename?: 'Query', subscriberTasks: { __typename?: 'SubscriberTasksConnection', nodes: Array<{ __typename?: 'SubscriberTasks', id: string, job: any, taskType: SubscriberTaskTypeEnum, status: SubscriberTaskStatusEnum, attempts: number, maxAttempts: number, runAt: string, lastError?: string | null, lockAt?: string | null, lockBy?: string | null, doneAt?: string | null, priority: number }>, paginationInfo?: { __typename?: 'PaginationInfo', total: number, pages: number } | null } };
|
||||||
|
|
||||||
|
|
||||||
export const GetCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdFilterInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdOrderInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"paginationInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"pages"}}]}}]}}]}}]} as unknown as DocumentNode<GetCredential3rdQuery, GetCredential3rdQueryVariables>;
|
export const GetCredential3rdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCredential3rd"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdFilterInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Credential3rdOrderInput"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"credential3rd"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"cookies"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"credentialType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"paginationInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"pages"}}]}}]}}]}}]} as unknown as DocumentNode<GetCredential3rdQuery, GetCredential3rdQueryVariables>;
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -39,7 +39,12 @@ 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 { createFileRoute, useNavigate } from '@tanstack/react-router';
|
import {
|
||||||
|
createFileRoute,
|
||||||
|
useCanGoBack,
|
||||||
|
useNavigate,
|
||||||
|
useRouter,
|
||||||
|
} from '@tanstack/react-router';
|
||||||
import { ArrowLeft, Eye, EyeOff, Save, X } from 'lucide-react';
|
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,11 +68,17 @@ function FormView({
|
|||||||
const togglePasswordVisibility = () => {
|
const togglePasswordVisibility = () => {
|
||||||
setShowPassword((prev) => !prev);
|
setShowPassword((prev) => !prev);
|
||||||
};
|
};
|
||||||
|
const router = useRouter();
|
||||||
|
const canGoBack = useCanGoBack();
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
navigate({
|
if (canGoBack) {
|
||||||
to: '/credential3rd/manage',
|
router.history.back();
|
||||||
});
|
} else {
|
||||||
|
navigate({
|
||||||
|
to: '/credential3rd/manage',
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const [updateCredential, { loading: updating }] = useMutation<
|
const [updateCredential, { loading: updating }] = useMutation<
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
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 { DataTablePagination } from '@/components/ui/data-table-pagination';
|
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
||||||
import { DataTableRowActions } from '@/components/ui/data-table-row-actions';
|
|
||||||
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';
|
||||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||||
|
import { DropdownMenuActions } from '@/components/ui/dropdown-menu-actions';
|
||||||
import { QueryErrorView } from '@/components/ui/query-error-view';
|
import { QueryErrorView } from '@/components/ui/query-error-view';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import {
|
import {
|
||||||
@ -231,9 +231,8 @@ function CredentialManageRouteComponent() {
|
|||||||
{
|
{
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<DataTableRowActions
|
<DropdownMenuActions
|
||||||
row={row}
|
id={row.original.id}
|
||||||
getId={(row) => row.original.id}
|
|
||||||
showEdit
|
showEdit
|
||||||
showDelete
|
showDelete
|
||||||
showDetail
|
showDetail
|
||||||
@ -261,7 +260,7 @@ function CredentialManageRouteComponent() {
|
|||||||
id={row.original.id}
|
id={row.original.id}
|
||||||
/>
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</DataTableRowActions>
|
</DropdownMenuActions>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
||||||
import { DataTableRowActions } from '@/components/ui/data-table-row-actions';
|
|
||||||
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';
|
||||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||||
|
import { DropdownMenuActions } from '@/components/ui/dropdown-menu-actions';
|
||||||
import { QueryErrorView } from '@/components/ui/query-error-view';
|
import { QueryErrorView } from '@/components/ui/query-error-view';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
@ -225,9 +225,8 @@ function SubscriptionManageRouteComponent() {
|
|||||||
{
|
{
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<DataTableRowActions
|
<DropdownMenuActions
|
||||||
row={row}
|
id={row.original.id}
|
||||||
getId={(row) => row.original.id}
|
|
||||||
showDetail
|
showDetail
|
||||||
showEdit
|
showEdit
|
||||||
showDelete
|
showDelete
|
||||||
@ -253,7 +252,7 @@ function SubscriptionManageRouteComponent() {
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<SubscriptionSyncDialogContent id={row.original.id} />
|
<SubscriptionSyncDialogContent id={row.original.id} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</DataTableRowActions>
|
</DropdownMenuActions>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { SubscriberTaskStatusEnum } from '@/infra/graphql/gql/graphql';
|
||||||
|
import { AlertCircle, CheckCircle, Clock, Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
|
export function getStatusBadge(status: SubscriberTaskStatusEnum) {
|
||||||
|
switch (status) {
|
||||||
|
case SubscriberTaskStatusEnum.Done:
|
||||||
|
return (
|
||||||
|
<Badge variant="secondary" className="bg-green-100 text-green-800">
|
||||||
|
<CheckCircle className="mr-1 h-3 w-3 capitalize" />
|
||||||
|
{status}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
case SubscriberTaskStatusEnum.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 SubscriberTaskStatusEnum.Killed:
|
||||||
|
case SubscriberTaskStatusEnum.Failed:
|
||||||
|
return (
|
||||||
|
<Badge variant="destructive">
|
||||||
|
<AlertCircle className="mr-1 h-3 w-3 capitalize" />
|
||||||
|
{status}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
case SubscriberTaskStatusEnum.Scheduled:
|
||||||
|
case SubscriberTaskStatusEnum.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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,34 @@
|
|||||||
|
import { DetailCardSkeleton } from '@/components/detail-card-skeleton';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { QueryErrorView } from '@/components/ui/query-error-view';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { GET_TASKS } from '@/domains/recorder/schema/tasks';
|
||||||
|
import {
|
||||||
|
type GetTasksQuery,
|
||||||
|
type GetTasksQueryVariables,
|
||||||
|
SubscriberTaskStatusEnum,
|
||||||
|
} from '@/infra/graphql/gql/graphql';
|
||||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
import { useQuery } from '@apollo/client';
|
||||||
|
import {
|
||||||
|
createFileRoute,
|
||||||
|
useCanGoBack,
|
||||||
|
useNavigate,
|
||||||
|
useRouter,
|
||||||
|
} from '@tanstack/react-router';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { ArrowLeft, RefreshCw } from 'lucide-react';
|
||||||
|
import { getStatusBadge } from './-status-badge';
|
||||||
|
|
||||||
export const Route = createFileRoute('/_app/tasks/detail/$id')({
|
export const Route = createFileRoute('/_app/tasks/detail/$id')({
|
||||||
component: TaskDetailRouteComponent,
|
component: TaskDetailRouteComponent,
|
||||||
@ -9,5 +38,203 @@ export const Route = createFileRoute('/_app/tasks/detail/$id')({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function TaskDetailRouteComponent() {
|
function TaskDetailRouteComponent() {
|
||||||
return <div>Hello "/_app/tasks/detail/$id"!</div>;
|
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,
|
||||||
|
GetTasksQueryVariables
|
||||||
|
>(GET_TASKS, {
|
||||||
|
variables: {
|
||||||
|
filters: {
|
||||||
|
id: {
|
||||||
|
eq: id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pagination: {
|
||||||
|
page: {
|
||||||
|
page: 0,
|
||||||
|
limit: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: {},
|
||||||
|
},
|
||||||
|
pollInterval: 5000, // Auto-refresh every 5 seconds for running tasks
|
||||||
|
});
|
||||||
|
|
||||||
|
const task = data?.subscriberTasks?.nodes?.[0];
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <DetailCardSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <QueryErrorView message={error.message} onRetry={refetch} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
return <DetailEmptyView message="Task not found" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
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">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>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle>Task Information</CardTitle>
|
||||||
|
<CardDescription className="mt-2">
|
||||||
|
View task execution details
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getStatusBadge(task.status)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Basic Information */}
|
||||||
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="font-medium text-sm">Task ID</Label>
|
||||||
|
<div className="rounded-md bg-muted p-3">
|
||||||
|
<code className="text-sm">{task.id}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="font-medium text-sm">Task Type</Label>
|
||||||
|
<div className="rounded-md bg-muted p-3">
|
||||||
|
<Badge variant="secondary">{task.taskType}</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="font-medium text-sm">Priority</Label>
|
||||||
|
<div className="rounded-md bg-muted p-3">
|
||||||
|
<span className="text-sm">{task.priority}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="font-medium text-sm">Attempts</Label>
|
||||||
|
<div className="rounded-md bg-muted p-3">
|
||||||
|
<span className="text-sm">
|
||||||
|
{task.attempts} / {task.maxAttempts}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="font-medium text-sm">
|
||||||
|
Scheduled Run Time
|
||||||
|
</Label>
|
||||||
|
<div className="rounded-md bg-muted p-3">
|
||||||
|
<span className="text-sm">
|
||||||
|
{format(new Date(task.runAt), 'yyyy-MM-dd HH:mm:ss')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="font-medium text-sm">Done Time</Label>
|
||||||
|
<div className="rounded-md bg-muted p-3">
|
||||||
|
<span className="text-sm">
|
||||||
|
{task.doneAt
|
||||||
|
? format(new Date(task.doneAt), 'yyyy-MM-dd HH:mm:ss')
|
||||||
|
: '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="font-medium text-sm">Lock Time</Label>
|
||||||
|
<div className="rounded-md bg-muted p-3">
|
||||||
|
<span className="text-sm">
|
||||||
|
{task.lockAt
|
||||||
|
? format(new Date(task.lockAt), 'yyyy-MM-dd HH:mm:ss')
|
||||||
|
: '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="font-medium text-sm">Lock By</Label>
|
||||||
|
<div className="rounded-md bg-muted p-3">
|
||||||
|
<code className="text-sm">{task.lockBy || '-'}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Job Details */}
|
||||||
|
{task.job && (
|
||||||
|
<>
|
||||||
|
<Separator />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="font-medium text-sm">Job Details</Label>
|
||||||
|
<div className="rounded-md bg-muted p-3">
|
||||||
|
<pre className="overflow-x-auto whitespace-pre-wrap text-sm">
|
||||||
|
<code>{JSON.stringify(task.job, null, 2)}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error Information */}
|
||||||
|
{(task.status === SubscriberTaskStatusEnum.Failed ||
|
||||||
|
task.status === SubscriberTaskStatusEnum.Killed) &&
|
||||||
|
task.lastError && (
|
||||||
|
<>
|
||||||
|
<Separator />
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="font-medium text-sm">Last Error</Label>
|
||||||
|
<div className="rounded-md bg-destructive/10 p-3">
|
||||||
|
<p className="text-destructive text-sm">
|
||||||
|
{task.lastError}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
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 { DataTablePagination } from '@/components/ui/data-table-pagination';
|
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
||||||
import { DataTableRowActions } from '@/components/ui/data-table-row-actions';
|
|
||||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||||
|
import { DropdownMenuActions } from '@/components/ui/dropdown-menu-actions';
|
||||||
import { QueryErrorView } from '@/components/ui/query-error-view';
|
import { QueryErrorView } from '@/components/ui/query-error-view';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { GET_TASKS, type TaskDto } from '@/domains/recorder/schema/tasks';
|
import { GET_TASKS, type TaskDto } from '@/domains/recorder/schema/tasks';
|
||||||
import type { GetTasksQuery } from '@/infra/graphql/gql/graphql';
|
import {
|
||||||
|
type GetTasksQuery,
|
||||||
|
SubscriberTaskStatusEnum,
|
||||||
|
} from '@/infra/graphql/gql/graphql';
|
||||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||||
import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton';
|
import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton';
|
||||||
import { useQuery } from '@apollo/client';
|
import { useQuery } from '@apollo/client';
|
||||||
@ -21,14 +24,10 @@ import {
|
|||||||
useReactTable,
|
useReactTable,
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import {
|
import { RefreshCw } from 'lucide-react';
|
||||||
AlertCircle,
|
|
||||||
CheckCircle,
|
|
||||||
Clock,
|
|
||||||
Loader2,
|
|
||||||
RefreshCw,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
import { getStatusBadge } from './-status-badge';
|
||||||
|
|
||||||
export const Route = createFileRoute('/_app/tasks/manage')({
|
export const Route = createFileRoute('/_app/tasks/manage')({
|
||||||
component: TaskManageRouteComponent,
|
component: TaskManageRouteComponent,
|
||||||
@ -87,111 +86,9 @@ function TaskManageRouteComponent() {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
header: 'Status',
|
|
||||||
accessorKey: 'status',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return getStatusBadge(row.original.status);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Priority',
|
|
||||||
accessorKey: 'priority',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
return getPriorityBadge(row.original.priority);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Attempts',
|
|
||||||
accessorKey: 'attempts',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const attempts = row.original.attempts;
|
|
||||||
const maxAttempts = row.original.maxAttempts;
|
|
||||||
return (
|
|
||||||
<div className="text-sm">
|
|
||||||
{attempts} / {maxAttempts}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Run At',
|
|
||||||
accessorKey: 'runAt',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const runAt = row.original.runAt;
|
|
||||||
return (
|
|
||||||
<div className="text-sm">
|
|
||||||
{format(new Date(runAt), 'yyyy-MM-dd HH:mm:ss')}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Done At',
|
|
||||||
accessorKey: 'doneAt',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const doneAt = row.original.doneAt;
|
|
||||||
return (
|
|
||||||
<div className="text-sm">
|
|
||||||
{doneAt ? format(new Date(doneAt), 'yyyy-MM-dd HH:mm:ss') : '-'}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Last Error',
|
|
||||||
accessorKey: 'lastError',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const lastError = row.original.lastError;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="max-w-xs truncate text-sm"
|
|
||||||
title={lastError || undefined}
|
|
||||||
>
|
|
||||||
{lastError || '-'}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Lock At',
|
|
||||||
accessorKey: 'lockAt',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const lockAt = row.original.lockAt;
|
|
||||||
return (
|
|
||||||
<div className="text-sm">
|
|
||||||
{lockAt ? format(new Date(lockAt), 'yyyy-MM-dd HH:mm:ss') : '-'}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Lock By',
|
|
||||||
accessorKey: 'lockBy',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const lockBy = row.original.lockBy;
|
|
||||||
return <div className="font-mono text-sm">{lockBy || '-'}</div>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'actions',
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<DataTableRowActions
|
|
||||||
row={row}
|
|
||||||
getId={(row) => row.original.id}
|
|
||||||
showDetail
|
|
||||||
onDetail={() => {
|
|
||||||
navigate({
|
|
||||||
to: '/tasks/detail/$id',
|
|
||||||
params: { id: row.original.id },
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
return cs;
|
return cs;
|
||||||
}, [navigate]);
|
}, []);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: useMemo(() => (tasks?.nodes ?? []) as TaskDto[], [tasks]),
|
data: useMemo(() => (tasks?.nodes ?? []) as TaskDto[], [tasks]),
|
||||||
@ -226,8 +123,8 @@ function TaskManageRouteComponent() {
|
|||||||
<div className="container mx-auto space-y-4 px-4">
|
<div className="container mx-auto space-y-4 px-4">
|
||||||
<div className="flex items-center justify-between pt-4">
|
<div className="flex items-center justify-between pt-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="font-bold text-2xl">Subscription Management</h1>
|
<h1 className="font-bold text-2xl">Tasks Management</h1>
|
||||||
<p className="text-muted-foreground">Manage your subscription</p>
|
<p className="text-muted-foreground">Manage your tasks</p>
|
||||||
</div>
|
</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" />
|
||||||
@ -241,12 +138,12 @@ function TaskManageRouteComponent() {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{!showSkeleton && table.getRowModel().rows?.length > 0 ? (
|
{!showSkeleton && table.getRowModel().rows?.length > 0 ? (
|
||||||
table.getRowModel().rows.map((row) => {
|
table.getRowModel().rows.map((row, index) => {
|
||||||
const task = row.original;
|
const task = row.original;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={task.id}
|
|
||||||
className="space-y-3 rounded-lg border bg-card p-4"
|
className="space-y-3 rounded-lg border bg-card p-4"
|
||||||
|
key={`${task.id}-${index}`}
|
||||||
>
|
>
|
||||||
{/* Header with status and priority */}
|
{/* Header with status and priority */}
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
@ -259,10 +156,10 @@ function TaskManageRouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-1 flex items-center gap-2">
|
<div className="mt-1 flex items-center gap-2">
|
||||||
{getStatusBadge(task.status)}
|
{getStatusBadge(task.status)}
|
||||||
|
<Badge variant="outline">Priority: {task.priority}</Badge>
|
||||||
<div className="mr-0 ml-auto">
|
<div className="mr-0 ml-auto">
|
||||||
<DataTableRowActions
|
<DropdownMenuActions
|
||||||
row={row}
|
id={task.id}
|
||||||
getId={(r) => r.original.id}
|
|
||||||
showDetail
|
showDetail
|
||||||
onDetail={() => {
|
onDetail={() => {
|
||||||
navigate({
|
navigate({
|
||||||
@ -274,19 +171,6 @@ function TaskManageRouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{task.job && (
|
|
||||||
<div className="text-sm">
|
|
||||||
<span className="text-muted-foreground">Job: </span>
|
|
||||||
<br />
|
|
||||||
<span
|
|
||||||
className="whitespace-pre-wrap"
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: JSON.stringify(task.job, null, 2),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Time info */}
|
{/* Time info */}
|
||||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
<div>
|
<div>
|
||||||
@ -311,19 +195,38 @@ function TaskManageRouteComponent() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Priority */}
|
{/* Lock at */}
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<span className="text-muted-foreground">Priority: </span>
|
<span className="text-muted-foreground">Lock at: </span>
|
||||||
<span>{task.priority}</span>
|
<span>
|
||||||
|
{task.lockAt
|
||||||
|
? format(new Date(task.lockAt), 'MM/dd HH:mm')
|
||||||
|
: '-'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error if exists */}
|
{task.job && (
|
||||||
{task.status === 'error' && task.lastError && (
|
<div className="text-sm">
|
||||||
<div className="rounded bg-destructive/10 p-2 text-destructive text-sm">
|
<span className="text-muted-foreground">Job: </span>
|
||||||
{task.lastError}
|
<br />
|
||||||
|
<span
|
||||||
|
className="whitespace-pre-wrap"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: JSON.stringify(task.job, null, 2),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Error if exists */}
|
||||||
|
{(task.status === SubscriberTaskStatusEnum.Failed ||
|
||||||
|
task.status === SubscriberTaskStatusEnum.Killed) &&
|
||||||
|
task.lastError && (
|
||||||
|
<div className="rounded bg-destructive/10 p-2 text-destructive text-sm">
|
||||||
|
{task.lastError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@ -336,42 +239,3 @@ function TaskManageRouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusBadge(status: string) {
|
|
||||||
switch (status.toLowerCase()) {
|
|
||||||
case 'completed':
|
|
||||||
case 'done':
|
|
||||||
return (
|
|
||||||
<Badge variant="secondary" className="bg-green-100 text-green-800">
|
|
||||||
<CheckCircle className="mr-1 h-3 w-3" />
|
|
||||||
Completed
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
case 'running':
|
|
||||||
case 'active':
|
|
||||||
return (
|
|
||||||
<Badge variant="secondary" className="bg-blue-100 text-blue-800">
|
|
||||||
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
|
|
||||||
Running
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
case 'failed':
|
|
||||||
case 'error':
|
|
||||||
return (
|
|
||||||
<Badge variant="destructive">
|
|
||||||
<AlertCircle className="mr-1 h-3 w-3" />
|
|
||||||
Failed
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
case 'pending':
|
|
||||||
case 'waiting':
|
|
||||||
return (
|
|
||||||
<Badge variant="secondary" className="bg-yellow-100 text-yellow-800">
|
|
||||||
<Clock className="mr-1 h-3 w-3" />
|
|
||||||
Pending
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return <Badge variant="outline">{status}</Badge>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
12
justfile
12
justfile
@ -2,7 +2,7 @@ set windows-shell := ["pwsh.exe", "-c"]
|
|||||||
set dotenv-load := true
|
set dotenv-load := true
|
||||||
|
|
||||||
prepare-dev:
|
prepare-dev:
|
||||||
cargo install sea-orm-cli cargo-llvm-cov cargo-nextest killport
|
cargo install sea-orm-cli cargo-llvm-cov cargo-nextest
|
||||||
# install watchexec
|
# install watchexec
|
||||||
|
|
||||||
prepare-dev-testcontainers:
|
prepare-dev-testcontainers:
|
||||||
@ -32,9 +32,6 @@ dev-deps-clean:
|
|||||||
dev-codegen:
|
dev-codegen:
|
||||||
pnpm run --filter=webui codegen
|
pnpm run --filter=webui codegen
|
||||||
|
|
||||||
dev-all:
|
|
||||||
zellij --layout dev.kdl
|
|
||||||
|
|
||||||
dev-codegen-wait:
|
dev-codegen-wait:
|
||||||
@until nc -z localhost 5001; do echo "Waiting for Recorder..."; sleep 1; done
|
@until nc -z localhost 5001; do echo "Waiting for Recorder..."; sleep 1; done
|
||||||
pnpm run --filter=webui codegen-watch
|
pnpm run --filter=webui codegen-watch
|
||||||
@ -42,3 +39,10 @@ dev-codegen-wait:
|
|||||||
dev-coverage:
|
dev-coverage:
|
||||||
cargo llvm-cov test --html
|
cargo llvm-cov test --html
|
||||||
|
|
||||||
|
[unix]
|
||||||
|
dev-all:
|
||||||
|
zellij --layout dev.kdl
|
||||||
|
|
||||||
|
[windows]
|
||||||
|
dev-all:
|
||||||
|
@echo "zellij is not supported on Windows, please use vscode tasks 'dev-all'"
|
||||||
|
12
package.json
12
package.json
@ -32,6 +32,16 @@
|
|||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"codemirror-graphql>@codemirror/language": "^6.11.1"
|
"codemirror-graphql>@codemirror/language": "^6.11.1"
|
||||||
}
|
},
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"@biomejs/biome",
|
||||||
|
"@parcel/watcher",
|
||||||
|
"@tailwindcss/oxide",
|
||||||
|
"bufferutil",
|
||||||
|
"core-js",
|
||||||
|
"esbuild",
|
||||||
|
"sharp",
|
||||||
|
"utf-8-validate"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user