feat: add tasks manage view
This commit is contained in:
@@ -67,7 +67,7 @@ export const AppNavMainData: NavMainGroup[] = [
|
||||
{
|
||||
title: 'Manage',
|
||||
link: {
|
||||
to: '/task/manage',
|
||||
to: '/tasks/manage',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -12,10 +12,12 @@ import {
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import type * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
|
||||
import { PropsWithChildren, useMemo } from "react";
|
||||
import { ComponentProps, PropsWithChildren, useMemo } from "react";
|
||||
|
||||
interface DataTableRowActionsProps<DataView, Id> {
|
||||
interface DataTableRowActionsProps<DataView, Id>
|
||||
extends ComponentProps<typeof DropdownMenuPrimitive.Root> {
|
||||
row: Row<DataView>;
|
||||
getId: (row: Row<DataView>) => Id;
|
||||
showDetail?: boolean;
|
||||
@@ -24,7 +26,6 @@ interface DataTableRowActionsProps<DataView, Id> {
|
||||
onDetail?: (id: Id) => void;
|
||||
onDelete?: (id: Id) => void;
|
||||
onEdit?: (id: Id) => void;
|
||||
modal?: boolean;
|
||||
}
|
||||
|
||||
export function DataTableRowActions<DataView, Id>({
|
||||
@@ -37,11 +38,11 @@ export function DataTableRowActions<DataView, Id>({
|
||||
onDelete,
|
||||
onEdit,
|
||||
children,
|
||||
modal,
|
||||
...rest
|
||||
}: PropsWithChildren<DataTableRowActionsProps<DataView, Id>>) {
|
||||
const id = useMemo(() => getId(row), [getId, row]);
|
||||
return (
|
||||
<DropdownMenu modal={modal}>
|
||||
<DropdownMenu {...rest}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -6,7 +6,9 @@ import {
|
||||
import { gql } from '@apollo/client';
|
||||
import { type } from 'arktype';
|
||||
import {
|
||||
MikanSubscriptionBangumiSourceUrlSchema,
|
||||
MikanSubscriptionSeasonSourceUrlSchema,
|
||||
MikanSubscriptionSubscriberSourceUrlSchema,
|
||||
extractMikanSubscriptionBangumiSourceUrl,
|
||||
extractMikanSubscriptionSubscriberSourceUrl,
|
||||
} from './mikan';
|
||||
@@ -144,14 +146,14 @@ export const SYNC_SUBSCRIPTION_SOURCES = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const SubscriptionTypedMikanSeasonSchema =
|
||||
export const SubscriptionFormTypedMikanSeasonSchema =
|
||||
MikanSubscriptionSeasonSourceUrlSchema.and(
|
||||
type({
|
||||
credentialId: 'number>0',
|
||||
})
|
||||
);
|
||||
|
||||
export const SubscriptionTypedMikanBangumiSchema = type({
|
||||
export const SubscriptionFormTypedMikanBangumiSchema = type({
|
||||
category: `'${SubscriptionCategoryEnum.MikanBangumi}'`,
|
||||
sourceUrl: type.string
|
||||
.atLeastLength(1)
|
||||
@@ -160,7 +162,7 @@ export const SubscriptionTypedMikanBangumiSchema = type({
|
||||
),
|
||||
});
|
||||
|
||||
export const SubscriptionTypedMikanSubscriberSchema = type({
|
||||
export const SubscriptionFormTypedMikanSubscriberSchema = type({
|
||||
category: `'${SubscriptionCategoryEnum.MikanSubscriber}'`,
|
||||
sourceUrl: type.string
|
||||
.atLeastLength(1)
|
||||
@@ -169,13 +171,36 @@ export const SubscriptionTypedMikanSubscriberSchema = type({
|
||||
),
|
||||
});
|
||||
|
||||
export const SubscriptionFormTypedSchema =
|
||||
SubscriptionFormTypedMikanSeasonSchema.or(
|
||||
SubscriptionFormTypedMikanBangumiSchema
|
||||
).or(SubscriptionFormTypedMikanSubscriberSchema);
|
||||
|
||||
export const SubscriptionFormSchema = type({
|
||||
enabled: 'boolean',
|
||||
displayName: 'string>0',
|
||||
}).and(SubscriptionFormTypedSchema);
|
||||
|
||||
export type SubscriptionForm = typeof SubscriptionFormSchema.infer;
|
||||
|
||||
export const SubscriptionTypedMikanSeasonSchema =
|
||||
MikanSubscriptionSeasonSourceUrlSchema.and(
|
||||
type({
|
||||
credentialId: 'number>0',
|
||||
})
|
||||
);
|
||||
|
||||
export const SubscriptionTypedMikanBangumiSchema =
|
||||
MikanSubscriptionBangumiSourceUrlSchema;
|
||||
|
||||
export const SubscriptionTypedMikanSubscriberSchema =
|
||||
MikanSubscriptionSubscriberSourceUrlSchema;
|
||||
|
||||
export const SubscriptionTypedSchema = SubscriptionTypedMikanSeasonSchema.or(
|
||||
SubscriptionTypedMikanBangumiSchema
|
||||
).or(SubscriptionTypedMikanSubscriberSchema);
|
||||
|
||||
export const SubscriptionInsertFormSchema = type({
|
||||
enabled: 'boolean',
|
||||
displayName: 'string>0',
|
||||
export const SubscriptionSchema = type({
|
||||
subscription_id: 'number>0',
|
||||
subscriber_id: 'number>0',
|
||||
}).and(SubscriptionTypedSchema);
|
||||
|
||||
export type SubscriptionInsertForm = typeof SubscriptionInsertFormSchema.infer;
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import {
|
||||
type GetTasksQuery,
|
||||
SubscriberTaskTypeEnum,
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import { gql } from '@apollo/client';
|
||||
import { type } from 'arktype';
|
||||
import { SubscriptionSchema } from './subscriptions';
|
||||
|
||||
export const GET_TASKS = gql`
|
||||
query GetTasks($filters: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {
|
||||
@@ -9,6 +15,8 @@ export const GET_TASKS = gql`
|
||||
) {
|
||||
nodes {
|
||||
id,
|
||||
job,
|
||||
taskType,
|
||||
status,
|
||||
attempts,
|
||||
maxAttempts,
|
||||
@@ -26,3 +34,28 @@ export const GET_TASKS = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const TaskTypedSyncOneSubscriptionFeedsIncrementalSchema = type({
|
||||
taskType: `'${SubscriberTaskTypeEnum.SyncOneSubscriptionFeedsIncremental}'`,
|
||||
}).and(SubscriptionSchema);
|
||||
|
||||
export const TaskTypedSyncOneSubscriptionFeedsFullSchema = type({
|
||||
taskType: `'${SubscriberTaskTypeEnum.SyncOneSubscriptionFeedsFull}'`,
|
||||
}).and(SubscriptionSchema);
|
||||
|
||||
export const TaskTypedSyncOneSubscriptionSourcesSchema = type({
|
||||
taskType: `'${SubscriberTaskTypeEnum.SyncOneSubscriptionSources}'`,
|
||||
}).and(SubscriptionSchema);
|
||||
|
||||
export const TaskTypedSchema = TaskTypedSyncOneSubscriptionFeedsFullSchema.or(
|
||||
TaskTypedSyncOneSubscriptionFeedsIncrementalSchema
|
||||
).or(TaskTypedSyncOneSubscriptionSourcesSchema);
|
||||
|
||||
export type TaskTypedDto = typeof TaskTypedSchema.infer;
|
||||
|
||||
export type TaskDto = Omit<
|
||||
GetTasksQuery['subscriberTasks']['nodes'][number],
|
||||
'job'
|
||||
> & {
|
||||
job: TaskTypedDto;
|
||||
};
|
||||
|
||||
@@ -14,16 +14,14 @@ import {
|
||||
extractMikanSubscriptionSeasonSourceUrl,
|
||||
extractMikanSubscriptionSubscriberSourceUrl,
|
||||
} from '../schema/mikan';
|
||||
import type { SubscriptionInsertForm } from '../schema/subscriptions';
|
||||
import type { SubscriptionForm } from '../schema/subscriptions';
|
||||
import { MikanService } from './mikan.service';
|
||||
|
||||
@Injectable()
|
||||
export class SubscriptionService {
|
||||
private mikan = inject(MikanService);
|
||||
|
||||
transformInsertFormToInput(
|
||||
form: SubscriptionInsertForm
|
||||
): SubscriptionsInsertInput {
|
||||
transformInsertFormToInput(form: SubscriptionForm): SubscriptionsInsertInput {
|
||||
if (form.category === SubscriptionCategoryEnum.MikanSeason) {
|
||||
return {
|
||||
...omit(form, ['seasonStr', 'year']),
|
||||
|
||||
@@ -18,7 +18,7 @@ export function buildOidcConfig(): OpenIdConfiguration {
|
||||
responseType: 'code',
|
||||
silentRenew: true,
|
||||
useRefreshToken: true,
|
||||
logLevel: LogLevel.None,
|
||||
logLevel: LogLevel.Warn,
|
||||
autoUserInfo: !resource,
|
||||
renewUserInfoAfterTokenRenew: !resource,
|
||||
customParamsAuthRequest: {
|
||||
|
||||
@@ -15,8 +15,20 @@ export class OidcAuthProvider extends AuthProvider {
|
||||
checkAuthResultEvent$ = inject(CHECK_AUTH_RESULT_EVENT);
|
||||
injector = injectInjector();
|
||||
|
||||
private setupSilentRenew() {
|
||||
const parent = document.defaultView?.parent;
|
||||
if (parent) {
|
||||
const event = new CustomEvent('oidc-silent-renew-message', {
|
||||
detail: document.defaultView?.location!,
|
||||
});
|
||||
parent.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
setup() {
|
||||
this.oidcSecurityService.checkAuth().subscribe();
|
||||
this.oidcSecurityService.checkAuth().subscribe(() => {
|
||||
this.setupSilentRenew();
|
||||
});
|
||||
}
|
||||
|
||||
get isAuthenticated$() {
|
||||
|
||||
@@ -28,7 +28,7 @@ type Documents = {
|
||||
"\n mutation SyncSubscriptionFeedsIncremental($id: Int!) {\n subscriptionSyncOneFeedsIncremental(filter: { id: $id }) {\n taskId\n }\n }\n": typeof types.SyncSubscriptionFeedsIncrementalDocument,
|
||||
"\n mutation SyncSubscriptionFeedsFull($id: Int!) {\n subscriptionSyncOneFeedsFull(filter: { id: $id }) {\n taskId\n }\n }\n": typeof types.SyncSubscriptionFeedsFullDocument,
|
||||
"\n mutation SyncSubscriptionSources($id: Int!) {\n subscriptionSyncOneSources(filter: { id: $id }) {\n taskId\n }\n }\n": typeof types.SyncSubscriptionSourcesDocument,
|
||||
"\n query GetTasks($filters: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetTasksDocument,
|
||||
"\n query GetTasks($filters: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filters: $filters\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 }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetTasksDocument,
|
||||
};
|
||||
const documents: Documents = {
|
||||
"\n query GetCredential3rd($filters: Credential3rdFilterInput!, $orderBy: Credential3rdOrderInput, $pagination: PaginationInput) {\n credential3rd(filters: $filters, orderBy: $orderBy, pagination: $pagination) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetCredential3rdDocument,
|
||||
@@ -45,7 +45,7 @@ const documents: Documents = {
|
||||
"\n mutation SyncSubscriptionFeedsIncremental($id: Int!) {\n subscriptionSyncOneFeedsIncremental(filter: { id: $id }) {\n taskId\n }\n }\n": types.SyncSubscriptionFeedsIncrementalDocument,
|
||||
"\n mutation SyncSubscriptionFeedsFull($id: Int!) {\n subscriptionSyncOneFeedsFull(filter: { id: $id }) {\n taskId\n }\n }\n": types.SyncSubscriptionFeedsFullDocument,
|
||||
"\n mutation SyncSubscriptionSources($id: Int!) {\n subscriptionSyncOneSources(filter: { id: $id }) {\n taskId\n }\n }\n": types.SyncSubscriptionSourcesDocument,
|
||||
"\n query GetTasks($filters: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetTasksDocument,
|
||||
"\n query GetTasks($filters: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filters: $filters\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 }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetTasksDocument,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -121,7 +121,7 @@ export function gql(source: "\n mutation SyncSubscriptionSources($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 query GetTasks($filters: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"): (typeof documents)["\n query GetTasks($filters: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filters: $filters\n orderBy: $orderBy\n ) {\n nodes {\n id,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"];
|
||||
export function gql(source: "\n query GetTasks($filters: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filters: $filters\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 }\n paginationInfo {\n total\n pages\n }\n }\n }\n"): (typeof documents)["\n query GetTasks($filters: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filters: $filters\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 }\n paginationInfo {\n total\n pages\n }\n }\n }\n"];
|
||||
|
||||
export function gql(source: string) {
|
||||
return (documents as any)[source] ?? {};
|
||||
|
||||
@@ -14,6 +14,8 @@ export type Scalars = {
|
||||
Boolean: { input: boolean; output: boolean; }
|
||||
Int: { input: number; output: number; }
|
||||
Float: { input: number; output: number; }
|
||||
/** The `JSON` scalar type represents raw JSON values */
|
||||
Json: { input: any; output: any; }
|
||||
JsonbFilterInput: { input: any; output: any; }
|
||||
};
|
||||
|
||||
@@ -23,6 +25,7 @@ export type Bangumi = {
|
||||
displayName: Scalars['String']['output'];
|
||||
episode: EpisodesConnection;
|
||||
fansub?: Maybe<Scalars['String']['output']>;
|
||||
filter?: Maybe<Scalars['Json']['output']>;
|
||||
homepage?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['Int']['output'];
|
||||
mikanBangumiId?: Maybe<Scalars['String']['output']>;
|
||||
@@ -66,6 +69,7 @@ export type BangumiBasic = {
|
||||
createdAt: Scalars['String']['output'];
|
||||
displayName: Scalars['String']['output'];
|
||||
fansub?: Maybe<Scalars['String']['output']>;
|
||||
filter?: Maybe<Scalars['Json']['output']>;
|
||||
homepage?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['Int']['output'];
|
||||
mikanBangumiId?: Maybe<Scalars['String']['output']>;
|
||||
@@ -118,6 +122,7 @@ export type BangumiInsertInput = {
|
||||
createdAt?: InputMaybe<Scalars['String']['input']>;
|
||||
displayName: Scalars['String']['input'];
|
||||
fansub?: InputMaybe<Scalars['String']['input']>;
|
||||
filter?: InputMaybe<Scalars['Json']['input']>;
|
||||
homepage?: InputMaybe<Scalars['String']['input']>;
|
||||
id?: InputMaybe<Scalars['Int']['input']>;
|
||||
mikanBangumiId?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -154,6 +159,7 @@ export type BangumiUpdateInput = {
|
||||
createdAt?: InputMaybe<Scalars['String']['input']>;
|
||||
displayName?: InputMaybe<Scalars['String']['input']>;
|
||||
fansub?: InputMaybe<Scalars['String']['input']>;
|
||||
filter?: InputMaybe<Scalars['Json']['input']>;
|
||||
homepage?: InputMaybe<Scalars['String']['input']>;
|
||||
id?: InputMaybe<Scalars['Int']['input']>;
|
||||
mikanBangumiId?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -1185,11 +1191,19 @@ export type SubscriberIdFilterInput = {
|
||||
eq?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
export const SubscriberTaskTypeEnum = {
|
||||
SyncOneSubscriptionFeedsFull: 'sync_one_subscription_feeds_full',
|
||||
SyncOneSubscriptionFeedsIncremental: 'sync_one_subscription_feeds_incremental',
|
||||
SyncOneSubscriptionSources: 'sync_one_subscription_sources'
|
||||
} as const;
|
||||
|
||||
export type SubscriberTaskTypeEnum = typeof SubscriberTaskTypeEnum[keyof typeof SubscriberTaskTypeEnum];
|
||||
export type SubscriberTasks = {
|
||||
__typename?: 'SubscriberTasks';
|
||||
attempts: Scalars['Int']['output'];
|
||||
doneAt?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['String']['output'];
|
||||
job: Scalars['Json']['output'];
|
||||
lastError?: Maybe<Scalars['String']['output']>;
|
||||
lockAt?: Maybe<Scalars['String']['output']>;
|
||||
lockBy?: Maybe<Scalars['String']['output']>;
|
||||
@@ -1199,6 +1213,7 @@ export type SubscriberTasks = {
|
||||
status: Scalars['String']['output'];
|
||||
subscriber?: Maybe<Subscribers>;
|
||||
subscriberId: Scalars['Int']['output'];
|
||||
taskType: SubscriberTaskTypeEnum;
|
||||
};
|
||||
|
||||
export type SubscriberTasksBasic = {
|
||||
@@ -1206,6 +1221,7 @@ export type SubscriberTasksBasic = {
|
||||
attempts: Scalars['Int']['output'];
|
||||
doneAt?: Maybe<Scalars['String']['output']>;
|
||||
id: Scalars['String']['output'];
|
||||
job: Scalars['Json']['output'];
|
||||
lastError?: Maybe<Scalars['String']['output']>;
|
||||
lockAt?: Maybe<Scalars['String']['output']>;
|
||||
lockBy?: Maybe<Scalars['String']['output']>;
|
||||
@@ -1214,6 +1230,7 @@ export type SubscriberTasksBasic = {
|
||||
runAt: Scalars['String']['output'];
|
||||
status: Scalars['String']['output'];
|
||||
subscriberId: Scalars['Int']['output'];
|
||||
taskType: SubscriberTaskTypeEnum;
|
||||
};
|
||||
|
||||
export type SubscriberTasksConnection = {
|
||||
@@ -1245,12 +1262,14 @@ export type SubscriberTasksFilterInput = {
|
||||
runAt?: InputMaybe<TextFilterInput>;
|
||||
status?: InputMaybe<StringFilterInput>;
|
||||
subscriberId?: InputMaybe<SubscriberIdFilterInput>;
|
||||
taskType?: InputMaybe<StringFilterInput>;
|
||||
};
|
||||
|
||||
export type SubscriberTasksInsertInput = {
|
||||
attempts: Scalars['Int']['input'];
|
||||
doneAt?: InputMaybe<Scalars['String']['input']>;
|
||||
id?: InputMaybe<Scalars['String']['input']>;
|
||||
job: Scalars['Json']['input'];
|
||||
lastError?: InputMaybe<Scalars['String']['input']>;
|
||||
lockAt?: InputMaybe<Scalars['String']['input']>;
|
||||
lockBy?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -1258,6 +1277,7 @@ export type SubscriberTasksInsertInput = {
|
||||
priority: Scalars['Int']['input'];
|
||||
runAt: Scalars['String']['input'];
|
||||
status: Scalars['String']['input'];
|
||||
taskType: SubscriberTaskTypeEnum;
|
||||
};
|
||||
|
||||
export type SubscriberTasksOrderInput = {
|
||||
@@ -1273,12 +1293,14 @@ export type SubscriberTasksOrderInput = {
|
||||
runAt?: InputMaybe<OrderByEnum>;
|
||||
status?: InputMaybe<OrderByEnum>;
|
||||
subscriberId?: InputMaybe<OrderByEnum>;
|
||||
taskType?: InputMaybe<OrderByEnum>;
|
||||
};
|
||||
|
||||
export type SubscriberTasksUpdateInput = {
|
||||
attempts?: InputMaybe<Scalars['Int']['input']>;
|
||||
doneAt?: InputMaybe<Scalars['String']['input']>;
|
||||
id?: InputMaybe<Scalars['String']['input']>;
|
||||
job?: InputMaybe<Scalars['Json']['input']>;
|
||||
lastError?: InputMaybe<Scalars['String']['input']>;
|
||||
lockAt?: InputMaybe<Scalars['String']['input']>;
|
||||
lockBy?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -1286,11 +1308,13 @@ export type SubscriberTasksUpdateInput = {
|
||||
priority?: InputMaybe<Scalars['Int']['input']>;
|
||||
runAt?: InputMaybe<Scalars['String']['input']>;
|
||||
status?: InputMaybe<Scalars['String']['input']>;
|
||||
taskType?: InputMaybe<SubscriberTaskTypeEnum>;
|
||||
};
|
||||
|
||||
export type Subscribers = {
|
||||
__typename?: 'Subscribers';
|
||||
bangumi: BangumiConnection;
|
||||
bangumiConf?: Maybe<Scalars['Json']['output']>;
|
||||
createdAt: Scalars['String']['output'];
|
||||
credential3rd: Credential3rdConnection;
|
||||
displayName: Scalars['String']['output'];
|
||||
@@ -1766,7 +1790,7 @@ export type GetTasksQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type GetTasksQuery = { __typename?: 'Query', subscriberTasks: { __typename?: 'SubscriberTasksConnection', nodes: Array<{ __typename?: 'SubscriberTasks', id: string, 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: 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 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>;
|
||||
@@ -1783,4 +1807,4 @@ export const GetSubscriptionDetailDocument = {"kind":"Document","definitions":[{
|
||||
export const SyncSubscriptionFeedsIncrementalDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionFeedsIncremental"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionSyncOneFeedsIncremental"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"taskId"}}]}}]}}]} as unknown as DocumentNode<SyncSubscriptionFeedsIncrementalMutation, SyncSubscriptionFeedsIncrementalMutationVariables>;
|
||||
export const SyncSubscriptionFeedsFullDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionFeedsFull"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionSyncOneFeedsFull"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"taskId"}}]}}]}}]} as unknown as DocumentNode<SyncSubscriptionFeedsFullMutation, SyncSubscriptionFeedsFullMutationVariables>;
|
||||
export const SyncSubscriptionSourcesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SyncSubscriptionSources"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriptionSyncOneSources"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"taskId"}}]}}]}}]} as unknown as DocumentNode<SyncSubscriptionSourcesMutation, SyncSubscriptionSourcesMutationVariables>;
|
||||
export const GetTasksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetTasks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriberTasksFilterInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriberTasksOrderInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriberTasks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"attempts"}},{"kind":"Field","name":{"kind":"Name","value":"maxAttempts"}},{"kind":"Field","name":{"kind":"Name","value":"runAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastError"}},{"kind":"Field","name":{"kind":"Name","value":"lockAt"}},{"kind":"Field","name":{"kind":"Name","value":"lockBy"}},{"kind":"Field","name":{"kind":"Name","value":"doneAt"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}}]}},{"kind":"Field","name":{"kind":"Name","value":"paginationInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"pages"}}]}}]}}]}}]} as unknown as DocumentNode<GetTasksQuery, GetTasksQueryVariables>;
|
||||
export const GetTasksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetTasks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriberTasksFilterInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SubscriberTasksOrderInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PaginationInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"subscriberTasks"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pagination"}}},{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filters"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"nodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"job"}},{"kind":"Field","name":{"kind":"Name","value":"taskType"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"attempts"}},{"kind":"Field","name":{"kind":"Name","value":"maxAttempts"}},{"kind":"Field","name":{"kind":"Name","value":"runAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastError"}},{"kind":"Field","name":{"kind":"Name","value":"lockAt"}},{"kind":"Field","name":{"kind":"Name","value":"lockBy"}},{"kind":"Field","name":{"kind":"Name","value":"doneAt"}},{"kind":"Field","name":{"kind":"Name","value":"priority"}}]}},{"kind":"Field","name":{"kind":"Name","value":"paginationInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"pages"}}]}}]}}]}}]} as unknown as DocumentNode<GetTasksQuery, GetTasksQueryVariables>;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { AUTH_PROVIDER } from '@/infra/auth/auth.provider';
|
||||
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
|
||||
import { setContext } from '@apollo/client/link/context';
|
||||
import { Injectable, inject } from '@outposts/injection-js';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { AUTH_PROVIDER } from '../auth/auth.provider.ts';
|
||||
|
||||
@Injectable()
|
||||
export class GraphQLService {
|
||||
@@ -33,6 +33,7 @@ export class GraphQLService {
|
||||
errorPolicy: 'all',
|
||||
},
|
||||
},
|
||||
connectToDevTools: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
|
||||
query = this._apollo.query;
|
||||
|
||||
@@ -21,8 +21,8 @@ import { useAppForm } from '@/components/ui/tanstack-form';
|
||||
import { MikanSeasonEnum } from '@/domains/recorder/schema/mikan';
|
||||
import {
|
||||
INSERT_SUBSCRIPTION,
|
||||
type SubscriptionInsertForm,
|
||||
SubscriptionInsertFormSchema,
|
||||
type SubscriptionForm,
|
||||
SubscriptionFormSchema,
|
||||
} from '@/domains/recorder/schema/subscriptions';
|
||||
import { SubscriptionService } from '@/domains/recorder/services/subscription.service';
|
||||
import { useInject } from '@/infra/di/inject';
|
||||
@@ -78,11 +78,11 @@ function SubscriptionCreateRouteComponent() {
|
||||
credentialId: '',
|
||||
year: undefined,
|
||||
seasonStr: '',
|
||||
} as unknown as SubscriptionInsertForm,
|
||||
} as unknown as SubscriptionForm,
|
||||
validators: {
|
||||
onChangeAsync: SubscriptionInsertFormSchema,
|
||||
onChangeAsync: SubscriptionFormSchema,
|
||||
onChangeAsyncDebounceMs: 300,
|
||||
onSubmit: SubscriptionInsertFormSchema,
|
||||
onSubmit: SubscriptionFormSchema,
|
||||
},
|
||||
onSubmit: async (form) => {
|
||||
const input = subscriptionService.transformInsertFormToInput(form.value);
|
||||
@@ -152,9 +152,7 @@ function SubscriptionCreateRouteComponent() {
|
||||
<Select
|
||||
value={field.state.value}
|
||||
onValueChange={(value) =>
|
||||
field.handleChange(
|
||||
value as SubscriptionInsertForm['category']
|
||||
)
|
||||
field.handleChange(value as SubscriptionForm['category'])
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
|
||||
@@ -25,8 +25,8 @@ import { useAppForm } from '@/components/ui/tanstack-form';
|
||||
import { MikanSeasonEnum } from '@/domains/recorder/schema/mikan';
|
||||
import {
|
||||
GET_SUBSCRIPTION_DETAIL,
|
||||
type SubscriptionInsertForm,
|
||||
SubscriptionInsertFormSchema,
|
||||
type SubscriptionForm,
|
||||
SubscriptionFormSchema,
|
||||
UPDATE_SUBSCRIPTIONS,
|
||||
} from '@/domains/recorder/schema/subscriptions';
|
||||
import { SubscriptionService } from '@/domains/recorder/services/subscription.service';
|
||||
@@ -125,11 +125,11 @@ function FormView({
|
||||
}, [subscription, sourceUrlMeta]);
|
||||
|
||||
const form = useAppForm({
|
||||
defaultValues: defaultValues as unknown as SubscriptionInsertForm,
|
||||
defaultValues: defaultValues as unknown as SubscriptionForm,
|
||||
validators: {
|
||||
onChangeAsync: SubscriptionInsertFormSchema,
|
||||
onChangeAsync: SubscriptionFormSchema,
|
||||
onChangeAsyncDebounceMs: 300,
|
||||
onSubmit: SubscriptionInsertFormSchema,
|
||||
onSubmit: SubscriptionFormSchema,
|
||||
},
|
||||
onSubmit: async (form) => {
|
||||
const input = subscriptionService.transformInsertFormToInput(form.value);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
export const TaskActionsView = memo(() => {
|
||||
return null;
|
||||
});
|
||||
@@ -1,5 +1,34 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
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 { QueryErrorView } from '@/components/ui/query-error-view';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { GET_TASKS, type TaskDto } from '@/domains/recorder/schema/tasks';
|
||||
import type { GetTasksQuery } from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton';
|
||||
import { 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 {
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
Loader2,
|
||||
RefreshCw,
|
||||
} from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
export const Route = createFileRoute('/_app/tasks/manage')({
|
||||
component: TaskManageRouteComponent,
|
||||
@@ -9,5 +38,340 @@ export const Route = createFileRoute('/_app/tasks/manage')({
|
||||
});
|
||||
|
||||
function TaskManageRouteComponent() {
|
||||
return <div>Hello "/_app/tasks/manage"!</div>;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
|
||||
lockAt: false,
|
||||
lockBy: false,
|
||||
attempts: false,
|
||||
});
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { loading, error, data, refetch } = useQuery<GetTasksQuery>(GET_TASKS, {
|
||||
variables: {
|
||||
pagination: {
|
||||
page: {
|
||||
page: pagination.pageIndex,
|
||||
limit: pagination.pageSize,
|
||||
},
|
||||
},
|
||||
filters: {},
|
||||
orderBy: {
|
||||
runAt: 'DESC',
|
||||
},
|
||||
},
|
||||
pollInterval: 5000, // Auto-refresh every 5 seconds
|
||||
});
|
||||
|
||||
const { showSkeleton } = useDebouncedSkeleton({ loading });
|
||||
|
||||
const tasks = data?.subscriberTasks;
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const cs: ColumnDef<TaskDto>[] = [
|
||||
{
|
||||
header: 'ID',
|
||||
accessorKey: 'id',
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div
|
||||
className="max-w-[200px] truncate font-mono text-sm"
|
||||
title={row.original.id}
|
||||
>
|
||||
{row.original.id}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
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;
|
||||
}, [navigate]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: useMemo(() => (tasks?.nodes ?? []) as TaskDto[], [tasks]),
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onSortingChange: setSorting,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
pageCount: tasks?.paginationInfo?.pages,
|
||||
rowCount: tasks?.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 space-y-4 px-4">
|
||||
<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={() => refetch()} variant="outline" size="sm">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<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) => {
|
||||
const task = row.original;
|
||||
return (
|
||||
<div
|
||||
key={task.id}
|
||||
className="space-y-3 rounded-lg border bg-card p-4"
|
||||
>
|
||||
{/* Header with status and priority */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="font-mono text-muted-foreground text-xs">
|
||||
# {task.id}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Badge variant="outline">{task.taskType}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
{getStatusBadge(task.status)}
|
||||
<div className="mr-0 ml-auto">
|
||||
<DataTableRowActions
|
||||
row={row}
|
||||
getId={(r) => r.original.id}
|
||||
showDetail
|
||||
onDetail={() => {
|
||||
navigate({
|
||||
to: '/tasks/detail/$id',
|
||||
params: { id: task.id },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</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 */}
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Run at: </span>
|
||||
<span>{format(new Date(task.runAt), 'MM/dd HH:mm')}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-muted-foreground">Done: </span>
|
||||
<span>
|
||||
{task.doneAt
|
||||
? format(new Date(task.doneAt), 'MM/dd HH:mm')
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Attempts */}
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">Attempts: </span>
|
||||
<span>
|
||||
{task.attempts} / {task.maxAttempts}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Priority */}
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">Priority: </span>
|
||||
<span>{task.priority}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error if exists */}
|
||||
{task.status === 'error' && task.lastError && (
|
||||
<div className="rounded bg-destructive/10 p-2 text-destructive text-sm">
|
||||
{task.lastError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<DetailEmptyView message="No tasks found" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DataTablePagination table={table} showSelectedRowCount={false} />
|
||||
</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>;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user