feat: init cron webui
This commit is contained in:
@@ -65,11 +65,17 @@ export const AppNavMainData: NavMainGroup[] = [
|
||||
icon: ListTodo,
|
||||
children: [
|
||||
{
|
||||
title: 'Manage',
|
||||
title: 'Tasks',
|
||||
link: {
|
||||
to: '/tasks/manage',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Crons',
|
||||
link: {
|
||||
to: '/tasks/cron/manage',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
52
apps/webui/src/components/ui/container-header.tsx
Normal file
52
apps/webui/src/components/ui/container-header.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useCanGoBack, useNavigate, useRouter } from "@tanstack/react-router";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { type ReactNode, memo } from "react";
|
||||
import { Button } from "./button";
|
||||
|
||||
export interface ContainerHeaderProps {
|
||||
title: string;
|
||||
description: string;
|
||||
defaultBackTo?: string;
|
||||
actions?: ReactNode;
|
||||
}
|
||||
|
||||
export const ContainerHeader = memo(
|
||||
({ title, description, defaultBackTo, actions }: ContainerHeaderProps) => {
|
||||
const navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
const canGoBack = useCanGoBack();
|
||||
|
||||
const finalCanGoBack = canGoBack || !!defaultBackTo;
|
||||
|
||||
const handleBack = () => {
|
||||
if (canGoBack) {
|
||||
router.history.back();
|
||||
} else {
|
||||
navigate({ to: defaultBackTo });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
{finalCanGoBack && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">{title}</h1>
|
||||
<p className="mt-1 text-muted-foreground">{description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">{actions}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
58
apps/webui/src/domains/recorder/schema/cron.ts
Normal file
58
apps/webui/src/domains/recorder/schema/cron.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { GetCronsQuery } from '@/infra/graphql/gql/graphql';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_CRONS = gql`
|
||||
query GetCrons($filter: CronFilterInput!, $orderBy: CronOrderInput!, $pagination: PaginationInput!) {
|
||||
cron(pagination: $pagination, filter: $filter, orderBy: $orderBy) {
|
||||
nodes {
|
||||
id
|
||||
cronExpr
|
||||
nextRun
|
||||
lastRun
|
||||
lastError
|
||||
status
|
||||
lockedAt
|
||||
lockedBy
|
||||
createdAt
|
||||
updatedAt
|
||||
timeoutMs
|
||||
maxAttempts
|
||||
priority
|
||||
attempts
|
||||
subscriberTaskCron
|
||||
subscriberTask {
|
||||
nodes {
|
||||
id,
|
||||
job,
|
||||
taskType,
|
||||
status,
|
||||
attempts,
|
||||
maxAttempts,
|
||||
runAt,
|
||||
lastError,
|
||||
lockAt,
|
||||
lockBy,
|
||||
doneAt,
|
||||
priority,
|
||||
subscription {
|
||||
displayName
|
||||
sourceUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
paginationInfo {
|
||||
total
|
||||
pages
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export type CronDto = GetCronsQuery['cron']['nodes'][number];
|
||||
|
||||
export const DELETE_CRONS = gql`
|
||||
mutation DeleteCrons($filter: CronFilterInput!) {
|
||||
cronDelete(filter: $filter)
|
||||
}
|
||||
`;
|
||||
@@ -117,6 +117,25 @@ query GetSubscriptionDetail ($id: Int!) {
|
||||
id
|
||||
username
|
||||
}
|
||||
cron {
|
||||
nodes {
|
||||
id
|
||||
cronExpr
|
||||
nextRun
|
||||
lastRun
|
||||
lastError
|
||||
status
|
||||
lockedAt
|
||||
lockedBy
|
||||
createdAt
|
||||
updatedAt
|
||||
timeoutMs
|
||||
maxAttempts
|
||||
priority
|
||||
attempts
|
||||
subscriberTaskCron
|
||||
}
|
||||
}
|
||||
bangumi {
|
||||
nodes {
|
||||
createdAt
|
||||
|
||||
@@ -20,13 +20,15 @@ type Documents = {
|
||||
"\n mutation DeleteCredential3rd($filter: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filter)\n }\n": typeof types.DeleteCredential3rdDocument,
|
||||
"\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filter: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n": typeof types.GetCredential3rdDetailDocument,
|
||||
"\n mutation CheckCredential3rdAvailable($filter: Credential3rdFilterInput!) {\n credential3rdCheckAvailable(filter: $filter) {\n available\n }\n }\n": typeof types.CheckCredential3rdAvailableDocument,
|
||||
"\nquery GetCrons($filter: CronFilterInput!, $orderBy: CronOrderInput!, $pagination: PaginationInput!) {\n cron(pagination: $pagination, filter: $filter, orderBy: $orderBy) {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n subscriberTask {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n }\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetCronsDocument,
|
||||
"\n mutation DeleteCrons($filter: CronFilterInput!) {\n cronDelete(filter: $filter)\n }\n": typeof types.DeleteCronsDocument,
|
||||
"\n mutation InsertFeed($data: FeedsInsertInput!) {\n feedsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n feedType\n token\n }\n }\n": typeof types.InsertFeedDocument,
|
||||
"\n mutation DeleteFeed($filter: FeedsFilterInput!) {\n feedsDelete(filter: $filter)\n }\n": typeof types.DeleteFeedDocument,
|
||||
"\n query GetSubscriptions($filter: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetSubscriptionsDocument,
|
||||
"\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": typeof types.InsertSubscriptionDocument,
|
||||
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filter: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filter\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": typeof types.UpdateSubscriptionsDocument,
|
||||
"\n mutation DeleteSubscriptions($filter: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filter)\n }\n": typeof types.DeleteSubscriptionsDocument,
|
||||
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument,
|
||||
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": typeof types.GetSubscriptionDetailDocument,
|
||||
"\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": typeof types.GetTasksDocument,
|
||||
"\n mutation InsertSubscriberTask($data: SubscriberTasksInsertInput!) {\n subscriberTasksCreateOne(data: $data) {\n id\n }\n }\n": typeof types.InsertSubscriberTaskDocument,
|
||||
"\n mutation DeleteTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filter)\n }\n": typeof types.DeleteTasksDocument,
|
||||
@@ -39,13 +41,15 @@ const documents: Documents = {
|
||||
"\n mutation DeleteCredential3rd($filter: Credential3rdFilterInput!) {\n credential3rdDelete(filter: $filter)\n }\n": types.DeleteCredential3rdDocument,
|
||||
"\n query GetCredential3rdDetail($id: Int!) {\n credential3rd(filter: { id: { eq: $id } }) {\n nodes {\n id\n cookies\n username\n password\n userAgent\n createdAt\n updatedAt\n credentialType\n }\n }\n }\n": types.GetCredential3rdDetailDocument,
|
||||
"\n mutation CheckCredential3rdAvailable($filter: Credential3rdFilterInput!) {\n credential3rdCheckAvailable(filter: $filter) {\n available\n }\n }\n": types.CheckCredential3rdAvailableDocument,
|
||||
"\nquery GetCrons($filter: CronFilterInput!, $orderBy: CronOrderInput!, $pagination: PaginationInput!) {\n cron(pagination: $pagination, filter: $filter, orderBy: $orderBy) {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n subscriberTask {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n }\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetCronsDocument,
|
||||
"\n mutation DeleteCrons($filter: CronFilterInput!) {\n cronDelete(filter: $filter)\n }\n": types.DeleteCronsDocument,
|
||||
"\n mutation InsertFeed($data: FeedsInsertInput!) {\n feedsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n feedType\n token\n }\n }\n": types.InsertFeedDocument,
|
||||
"\n mutation DeleteFeed($filter: FeedsFilterInput!) {\n feedsDelete(filter: $filter)\n }\n": types.DeleteFeedDocument,
|
||||
"\n query GetSubscriptions($filter: SubscriptionsFilterInput!, $orderBy: SubscriptionsOrderInput!, $pagination: PaginationInput!) {\n subscriptions(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetSubscriptionsDocument,
|
||||
"\n mutation InsertSubscription($data: SubscriptionsInsertInput!) {\n subscriptionsCreateOne(data: $data) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n credentialId\n }\n }\n": types.InsertSubscriptionDocument,
|
||||
"\n mutation UpdateSubscriptions(\n $data: SubscriptionsUpdateInput!,\n $filter: SubscriptionsFilterInput!,\n ) {\n subscriptionsUpdate (\n data: $data\n filter: $filter\n ) {\n id\n createdAt\n updatedAt\n displayName\n category\n sourceUrl\n enabled\n }\n}\n": types.UpdateSubscriptionsDocument,
|
||||
"\n mutation DeleteSubscriptions($filter: SubscriptionsFilterInput) {\n subscriptionsDelete(filter: $filter)\n }\n": types.DeleteSubscriptionsDocument,
|
||||
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument,
|
||||
"\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n": types.GetSubscriptionDetailDocument,
|
||||
"\n query GetTasks($filter: SubscriberTasksFilterInput!, $orderBy: SubscriberTasksOrderInput!, $pagination: PaginationInput!) {\n subscriberTasks(\n pagination: $pagination\n filter: $filter\n orderBy: $orderBy\n ) {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n": types.GetTasksDocument,
|
||||
"\n mutation InsertSubscriberTask($data: SubscriberTasksInsertInput!) {\n subscriberTasksCreateOne(data: $data) {\n id\n }\n }\n": types.InsertSubscriberTaskDocument,
|
||||
"\n mutation DeleteTasks($filter: SubscriberTasksFilterInput!) {\n subscriberTasksDelete(filter: $filter)\n }\n": types.DeleteTasksDocument,
|
||||
@@ -90,6 +94,14 @@ export function gql(source: "\n query GetCredential3rdDetail($id: Int!) {\n
|
||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function gql(source: "\n mutation CheckCredential3rdAvailable($filter: Credential3rdFilterInput!) {\n credential3rdCheckAvailable(filter: $filter) {\n available\n }\n }\n"): (typeof documents)["\n mutation CheckCredential3rdAvailable($filter: Credential3rdFilterInput!) {\n credential3rdCheckAvailable(filter: $filter) {\n available\n }\n }\n"];
|
||||
/**
|
||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function gql(source: "\nquery GetCrons($filter: CronFilterInput!, $orderBy: CronOrderInput!, $pagination: PaginationInput!) {\n cron(pagination: $pagination, filter: $filter, orderBy: $orderBy) {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n subscriberTask {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n }\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"): (typeof documents)["\nquery GetCrons($filter: CronFilterInput!, $orderBy: CronOrderInput!, $pagination: PaginationInput!) {\n cron(pagination: $pagination, filter: $filter, orderBy: $orderBy) {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n subscriberTask {\n nodes {\n id,\n job,\n taskType,\n status,\n attempts,\n maxAttempts,\n runAt,\n lastError,\n lockAt,\n lockBy,\n doneAt,\n priority,\n subscription {\n displayName\n sourceUrl\n }\n }\n }\n }\n paginationInfo {\n total\n pages\n }\n }\n }\n"];
|
||||
/**
|
||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function gql(source: "\n mutation DeleteCrons($filter: CronFilterInput!) {\n cronDelete(filter: $filter)\n }\n"): (typeof documents)["\n mutation DeleteCrons($filter: CronFilterInput!) {\n cronDelete(filter: $filter)\n }\n"];
|
||||
/**
|
||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -117,7 +129,7 @@ export function gql(source: "\n mutation DeleteSubscriptions($filter: Subscri
|
||||
/**
|
||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"];
|
||||
export function gql(source: "\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"): (typeof documents)["\nquery GetSubscriptionDetail ($id: Int!) {\n subscriptions(filter: { id: {\n eq: $id\n } }) {\n nodes {\n id\n subscriberId\n displayName\n createdAt\n updatedAt\n category\n sourceUrl\n enabled\n feed {\n nodes {\n id\n createdAt\n updatedAt\n token\n feedType\n feedSource\n }\n }\n subscriberTask {\n nodes {\n id\n taskType\n status\n }\n }\n credential3rd {\n id\n username\n }\n cron {\n nodes {\n id\n cronExpr\n nextRun\n lastRun\n lastError\n status\n lockedAt\n lockedBy\n createdAt\n updatedAt\n timeoutMs\n maxAttempts\n priority\n attempts\n subscriberTaskCron\n }\n }\n bangumi {\n nodes {\n createdAt\n updatedAt\n id\n mikanBangumiId\n displayName\n season\n seasonRaw\n fansub\n mikanFansubId\n rssLink\n posterLink\n homepage\n }\n }\n }\n }\n}\n"];
|
||||
/**
|
||||
* The gql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import { guardRouteIndexAsNotFound } from '@/components/layout/app-not-found';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { Outlet } from '@tanstack/react-router';
|
||||
import { Outlet, type RouteOptions } from '@tanstack/react-router';
|
||||
|
||||
export interface BuildVirtualBranchRouteOptions {
|
||||
title: string;
|
||||
@@ -8,7 +8,11 @@ export interface BuildVirtualBranchRouteOptions {
|
||||
|
||||
export function buildVirtualBranchRouteOptions(
|
||||
options: BuildVirtualBranchRouteOptions
|
||||
) {
|
||||
): {
|
||||
beforeLoad: RouteOptions['beforeLoad'];
|
||||
staticData: RouteStateDataOption;
|
||||
component: RouteOptions['component'];
|
||||
} {
|
||||
return {
|
||||
beforeLoad: guardRouteIndexAsNotFound,
|
||||
staticData: {
|
||||
|
||||
@@ -31,11 +31,15 @@ import { Route as AppCredential3rdManageRouteImport } from './routes/_app/creden
|
||||
import { Route as AppCredential3rdCreateRouteImport } from './routes/_app/credential3rd/create'
|
||||
import { Route as AppBangumiManageRouteImport } from './routes/_app/bangumi/manage'
|
||||
import { Route as AppExploreExploreRouteImport } from './routes/_app/_explore/explore'
|
||||
import { Route as AppTasksCronRouteRouteImport } from './routes/_app/tasks/cron/route'
|
||||
import { Route as AppTasksDetailIdRouteImport } from './routes/_app/tasks/detail.$id'
|
||||
import { Route as AppTasksCronManageRouteImport } from './routes/_app/tasks/cron/manage'
|
||||
import { Route as AppSubscriptionsEditIdRouteImport } from './routes/_app/subscriptions/edit.$id'
|
||||
import { Route as AppSubscriptionsDetailIdRouteImport } from './routes/_app/subscriptions/detail.$id'
|
||||
import { Route as AppCredential3rdEditIdRouteImport } from './routes/_app/credential3rd/edit.$id'
|
||||
import { Route as AppCredential3rdDetailIdRouteImport } from './routes/_app/credential3rd/detail.$id'
|
||||
import { Route as AppTasksCronEditIdRouteImport } from './routes/_app/tasks/cron/edit.$id'
|
||||
import { Route as AppTasksCronDetailIdRouteImport } from './routes/_app/tasks/cron/detail.$id'
|
||||
|
||||
const AboutRoute = AboutRouteImport.update({
|
||||
id: '/about',
|
||||
@@ -148,11 +152,21 @@ const AppExploreExploreRoute = AppExploreExploreRouteImport.update({
|
||||
path: '/explore',
|
||||
getParentRoute: () => AppRouteRoute,
|
||||
} as any)
|
||||
const AppTasksCronRouteRoute = AppTasksCronRouteRouteImport.update({
|
||||
id: '/cron',
|
||||
path: '/cron',
|
||||
getParentRoute: () => AppTasksRouteRoute,
|
||||
} as any)
|
||||
const AppTasksDetailIdRoute = AppTasksDetailIdRouteImport.update({
|
||||
id: '/detail/$id',
|
||||
path: '/detail/$id',
|
||||
getParentRoute: () => AppTasksRouteRoute,
|
||||
} as any)
|
||||
const AppTasksCronManageRoute = AppTasksCronManageRouteImport.update({
|
||||
id: '/manage',
|
||||
path: '/manage',
|
||||
getParentRoute: () => AppTasksCronRouteRoute,
|
||||
} as any)
|
||||
const AppSubscriptionsEditIdRoute = AppSubscriptionsEditIdRouteImport.update({
|
||||
id: '/edit/$id',
|
||||
path: '/edit/$id',
|
||||
@@ -175,6 +189,16 @@ const AppCredential3rdDetailIdRoute =
|
||||
path: '/detail/$id',
|
||||
getParentRoute: () => AppCredential3rdRouteRoute,
|
||||
} as any)
|
||||
const AppTasksCronEditIdRoute = AppTasksCronEditIdRouteImport.update({
|
||||
id: '/edit/$id',
|
||||
path: '/edit/$id',
|
||||
getParentRoute: () => AppTasksCronRouteRoute,
|
||||
} as any)
|
||||
const AppTasksCronDetailIdRoute = AppTasksCronDetailIdRouteImport.update({
|
||||
id: '/detail/$id',
|
||||
path: '/detail/$id',
|
||||
getParentRoute: () => AppTasksCronRouteRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
@@ -189,6 +213,7 @@ export interface FileRoutesByFullPath {
|
||||
'/tasks': typeof AppTasksRouteRouteWithChildren
|
||||
'/auth/sign-in': typeof AuthSignInRoute
|
||||
'/auth/sign-up': typeof AuthSignUpRoute
|
||||
'/tasks/cron': typeof AppTasksCronRouteRouteWithChildren
|
||||
'/explore': typeof AppExploreExploreRoute
|
||||
'/bangumi/manage': typeof AppBangumiManageRoute
|
||||
'/credential3rd/create': typeof AppCredential3rdCreateRoute
|
||||
@@ -203,7 +228,10 @@ export interface FileRoutesByFullPath {
|
||||
'/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
|
||||
'/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
|
||||
'/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
|
||||
'/tasks/cron/manage': typeof AppTasksCronManageRoute
|
||||
'/tasks/detail/$id': typeof AppTasksDetailIdRoute
|
||||
'/tasks/cron/detail/$id': typeof AppTasksCronDetailIdRoute
|
||||
'/tasks/cron/edit/$id': typeof AppTasksCronEditIdRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
@@ -218,6 +246,7 @@ export interface FileRoutesByTo {
|
||||
'/tasks': typeof AppTasksRouteRouteWithChildren
|
||||
'/auth/sign-in': typeof AuthSignInRoute
|
||||
'/auth/sign-up': typeof AuthSignUpRoute
|
||||
'/tasks/cron': typeof AppTasksCronRouteRouteWithChildren
|
||||
'/explore': typeof AppExploreExploreRoute
|
||||
'/bangumi/manage': typeof AppBangumiManageRoute
|
||||
'/credential3rd/create': typeof AppCredential3rdCreateRoute
|
||||
@@ -232,7 +261,10 @@ export interface FileRoutesByTo {
|
||||
'/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
|
||||
'/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
|
||||
'/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
|
||||
'/tasks/cron/manage': typeof AppTasksCronManageRoute
|
||||
'/tasks/detail/$id': typeof AppTasksDetailIdRoute
|
||||
'/tasks/cron/detail/$id': typeof AppTasksCronDetailIdRoute
|
||||
'/tasks/cron/edit/$id': typeof AppTasksCronEditIdRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
@@ -248,6 +280,7 @@ export interface FileRoutesById {
|
||||
'/_app/tasks': typeof AppTasksRouteRouteWithChildren
|
||||
'/auth/sign-in': typeof AuthSignInRoute
|
||||
'/auth/sign-up': typeof AuthSignUpRoute
|
||||
'/_app/tasks/cron': typeof AppTasksCronRouteRouteWithChildren
|
||||
'/_app/_explore/explore': typeof AppExploreExploreRoute
|
||||
'/_app/bangumi/manage': typeof AppBangumiManageRoute
|
||||
'/_app/credential3rd/create': typeof AppCredential3rdCreateRoute
|
||||
@@ -262,7 +295,10 @@ export interface FileRoutesById {
|
||||
'/_app/credential3rd/edit/$id': typeof AppCredential3rdEditIdRoute
|
||||
'/_app/subscriptions/detail/$id': typeof AppSubscriptionsDetailIdRoute
|
||||
'/_app/subscriptions/edit/$id': typeof AppSubscriptionsEditIdRoute
|
||||
'/_app/tasks/cron/manage': typeof AppTasksCronManageRoute
|
||||
'/_app/tasks/detail/$id': typeof AppTasksDetailIdRoute
|
||||
'/_app/tasks/cron/detail/$id': typeof AppTasksCronDetailIdRoute
|
||||
'/_app/tasks/cron/edit/$id': typeof AppTasksCronEditIdRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
@@ -279,6 +315,7 @@ export interface FileRouteTypes {
|
||||
| '/tasks'
|
||||
| '/auth/sign-in'
|
||||
| '/auth/sign-up'
|
||||
| '/tasks/cron'
|
||||
| '/explore'
|
||||
| '/bangumi/manage'
|
||||
| '/credential3rd/create'
|
||||
@@ -293,7 +330,10 @@ export interface FileRouteTypes {
|
||||
| '/credential3rd/edit/$id'
|
||||
| '/subscriptions/detail/$id'
|
||||
| '/subscriptions/edit/$id'
|
||||
| '/tasks/cron/manage'
|
||||
| '/tasks/detail/$id'
|
||||
| '/tasks/cron/detail/$id'
|
||||
| '/tasks/cron/edit/$id'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
@@ -308,6 +348,7 @@ export interface FileRouteTypes {
|
||||
| '/tasks'
|
||||
| '/auth/sign-in'
|
||||
| '/auth/sign-up'
|
||||
| '/tasks/cron'
|
||||
| '/explore'
|
||||
| '/bangumi/manage'
|
||||
| '/credential3rd/create'
|
||||
@@ -322,7 +363,10 @@ export interface FileRouteTypes {
|
||||
| '/credential3rd/edit/$id'
|
||||
| '/subscriptions/detail/$id'
|
||||
| '/subscriptions/edit/$id'
|
||||
| '/tasks/cron/manage'
|
||||
| '/tasks/detail/$id'
|
||||
| '/tasks/cron/detail/$id'
|
||||
| '/tasks/cron/edit/$id'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
@@ -337,6 +381,7 @@ export interface FileRouteTypes {
|
||||
| '/_app/tasks'
|
||||
| '/auth/sign-in'
|
||||
| '/auth/sign-up'
|
||||
| '/_app/tasks/cron'
|
||||
| '/_app/_explore/explore'
|
||||
| '/_app/bangumi/manage'
|
||||
| '/_app/credential3rd/create'
|
||||
@@ -351,7 +396,10 @@ export interface FileRouteTypes {
|
||||
| '/_app/credential3rd/edit/$id'
|
||||
| '/_app/subscriptions/detail/$id'
|
||||
| '/_app/subscriptions/edit/$id'
|
||||
| '/_app/tasks/cron/manage'
|
||||
| '/_app/tasks/detail/$id'
|
||||
| '/_app/tasks/cron/detail/$id'
|
||||
| '/_app/tasks/cron/edit/$id'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
@@ -520,6 +568,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AppExploreExploreRouteImport
|
||||
parentRoute: typeof AppRouteRoute
|
||||
}
|
||||
'/_app/tasks/cron': {
|
||||
id: '/_app/tasks/cron'
|
||||
path: '/cron'
|
||||
fullPath: '/tasks/cron'
|
||||
preLoaderRoute: typeof AppTasksCronRouteRouteImport
|
||||
parentRoute: typeof AppTasksRouteRoute
|
||||
}
|
||||
'/_app/tasks/detail/$id': {
|
||||
id: '/_app/tasks/detail/$id'
|
||||
path: '/detail/$id'
|
||||
@@ -527,6 +582,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AppTasksDetailIdRouteImport
|
||||
parentRoute: typeof AppTasksRouteRoute
|
||||
}
|
||||
'/_app/tasks/cron/manage': {
|
||||
id: '/_app/tasks/cron/manage'
|
||||
path: '/manage'
|
||||
fullPath: '/tasks/cron/manage'
|
||||
preLoaderRoute: typeof AppTasksCronManageRouteImport
|
||||
parentRoute: typeof AppTasksCronRouteRoute
|
||||
}
|
||||
'/_app/subscriptions/edit/$id': {
|
||||
id: '/_app/subscriptions/edit/$id'
|
||||
path: '/edit/$id'
|
||||
@@ -555,6 +617,20 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AppCredential3rdDetailIdRouteImport
|
||||
parentRoute: typeof AppCredential3rdRouteRoute
|
||||
}
|
||||
'/_app/tasks/cron/edit/$id': {
|
||||
id: '/_app/tasks/cron/edit/$id'
|
||||
path: '/edit/$id'
|
||||
fullPath: '/tasks/cron/edit/$id'
|
||||
preLoaderRoute: typeof AppTasksCronEditIdRouteImport
|
||||
parentRoute: typeof AppTasksCronRouteRoute
|
||||
}
|
||||
'/_app/tasks/cron/detail/$id': {
|
||||
id: '/_app/tasks/cron/detail/$id'
|
||||
path: '/detail/$id'
|
||||
fullPath: '/tasks/cron/detail/$id'
|
||||
preLoaderRoute: typeof AppTasksCronDetailIdRouteImport
|
||||
parentRoute: typeof AppTasksCronRouteRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -630,12 +706,29 @@ const AppSubscriptionsRouteRouteWithChildren =
|
||||
AppSubscriptionsRouteRouteChildren,
|
||||
)
|
||||
|
||||
interface AppTasksCronRouteRouteChildren {
|
||||
AppTasksCronManageRoute: typeof AppTasksCronManageRoute
|
||||
AppTasksCronDetailIdRoute: typeof AppTasksCronDetailIdRoute
|
||||
AppTasksCronEditIdRoute: typeof AppTasksCronEditIdRoute
|
||||
}
|
||||
|
||||
const AppTasksCronRouteRouteChildren: AppTasksCronRouteRouteChildren = {
|
||||
AppTasksCronManageRoute: AppTasksCronManageRoute,
|
||||
AppTasksCronDetailIdRoute: AppTasksCronDetailIdRoute,
|
||||
AppTasksCronEditIdRoute: AppTasksCronEditIdRoute,
|
||||
}
|
||||
|
||||
const AppTasksCronRouteRouteWithChildren =
|
||||
AppTasksCronRouteRoute._addFileChildren(AppTasksCronRouteRouteChildren)
|
||||
|
||||
interface AppTasksRouteRouteChildren {
|
||||
AppTasksCronRouteRoute: typeof AppTasksCronRouteRouteWithChildren
|
||||
AppTasksManageRoute: typeof AppTasksManageRoute
|
||||
AppTasksDetailIdRoute: typeof AppTasksDetailIdRoute
|
||||
}
|
||||
|
||||
const AppTasksRouteRouteChildren: AppTasksRouteRouteChildren = {
|
||||
AppTasksCronRouteRoute: AppTasksCronRouteRouteWithChildren,
|
||||
AppTasksManageRoute: AppTasksManageRoute,
|
||||
AppTasksDetailIdRoute: AppTasksDetailIdRoute,
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { FormFieldErrors } from '@/components/ui/form-field-errors';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -123,14 +124,11 @@ function CredentialCreateRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-2xl py-6">
|
||||
<div className="mb-6 flex items-center gap-4">
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Create third-party credential</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Add new third-party login credential
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ContainerHeader
|
||||
title="Create third-party credential"
|
||||
description="Add new third-party login credential"
|
||||
defaultBackTo="/credential3rd/manage"
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -17,14 +18,9 @@ import { GET_CREDENTIAL_3RD_DETAIL } from '@/domains/recorder/schema/credential3
|
||||
import type { GetCredential3rdDetailQuery } from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import {
|
||||
createFileRoute,
|
||||
useCanGoBack,
|
||||
useNavigate,
|
||||
useRouter,
|
||||
} from '@tanstack/react-router';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { format } from 'date-fns/format';
|
||||
import { ArrowLeft, CheckIcon, Edit, Eye, EyeOff } from 'lucide-react';
|
||||
import { CheckIcon, Edit, Eye, EyeOff } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { Credential3rdCheckAvailableViewDialogContent } from './-check-available';
|
||||
|
||||
@@ -38,21 +34,9 @@ export const Route = createFileRoute('/_app/credential3rd/detail/$id')({
|
||||
function Credential3rdDetailRouteComponent() {
|
||||
const { id } = Route.useParams();
|
||||
const navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
const canGoBack = useCanGoBack();
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleBack = () => {
|
||||
if (canGoBack) {
|
||||
router.history.back();
|
||||
} else {
|
||||
navigate({
|
||||
to: '/credential3rd/manage',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { loading, error, data } = useQuery<GetCredential3rdDetailQuery>(
|
||||
GET_CREDENTIAL_3RD_DETAIL,
|
||||
{
|
||||
@@ -91,31 +75,17 @@ function Credential3rdDetailRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl py-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Credential detail</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
View credential #{credential.id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<ContainerHeader
|
||||
title="Credential Detail"
|
||||
description={`View credential #${credential.id}`}
|
||||
defaultBackTo="/credential3rd/manage"
|
||||
actions={
|
||||
<Button onClick={handleEnterEditMode}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -39,13 +40,8 @@ import type {
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import {
|
||||
createFileRoute,
|
||||
useCanGoBack,
|
||||
useNavigate,
|
||||
useRouter,
|
||||
} from '@tanstack/react-router';
|
||||
import { ArrowLeft, Eye, EyeOff, Save, X } from 'lucide-react';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { Eye, EyeOff, Save } from 'lucide-react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@@ -63,23 +59,10 @@ function FormView({
|
||||
credential: Credential3rdDetailDto;
|
||||
onCompleted: VoidFunction;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const togglePasswordVisibility = () => {
|
||||
setShowPassword((prev) => !prev);
|
||||
};
|
||||
const router = useRouter();
|
||||
const canGoBack = useCanGoBack();
|
||||
|
||||
const handleBack = () => {
|
||||
if (canGoBack) {
|
||||
router.history.back();
|
||||
} else {
|
||||
navigate({
|
||||
to: '/credential3rd/manage',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const [updateCredential, { loading: updating }] = useMutation<
|
||||
UpdateCredential3rdMutation,
|
||||
@@ -121,35 +104,17 @@ function FormView({
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl py-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Credential edit</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Edit credential #{credential.id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleBack} disabled={updating}>
|
||||
<X className="mr-2 h-4 w-4" />
|
||||
Back
|
||||
</Button>
|
||||
<ContainerHeader
|
||||
title="Credential Edit"
|
||||
description={`Edit credential #${credential.id}`}
|
||||
defaultBackTo={`/credential3rd/detail/${credential.id}`}
|
||||
actions={
|
||||
<Button onClick={() => form.handleSubmit()} disabled={updating}>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{updating ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
||||
import { DataTableViewOptions } from '@/components/ui/data-table-view-options';
|
||||
import { DialogTrigger } from '@/components/ui/dialog';
|
||||
@@ -297,18 +298,16 @@ function CredentialManageRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto space-y-4 rounded-md">
|
||||
<div className="flex items-center justify-between pt-4">
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Credential 3rd Management</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Manage your third-party platform login credentials
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => navigate({ to: '/credential3rd/create' })}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Credential
|
||||
</Button>
|
||||
</div>
|
||||
<ContainerHeader
|
||||
title="Credential 3rd Management"
|
||||
description="Manage your third-party platform login credentials"
|
||||
actions={
|
||||
<Button onClick={() => navigate({ to: '/credential3rd/create' })}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Credential
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center py-2">
|
||||
<DataTableViewOptions table={table} />
|
||||
</div>
|
||||
|
||||
@@ -122,6 +122,7 @@ export const SubscriptionSyncView = memo(
|
||||
export interface SubscriptionSyncDialogContentProps {
|
||||
id: number;
|
||||
onCancel?: VoidFunction;
|
||||
isCron?: boolean;
|
||||
}
|
||||
|
||||
export const SubscriptionSyncDialogContent = memo(
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { FormFieldErrors } from '@/components/ui/form-field-errors';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -96,14 +97,11 @@ function SubscriptionCreateRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-2xl py-6">
|
||||
<div className="mb-6 flex items-center gap-4">
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Create Bangumi Subscription</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Add a new bangumi subscription source
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ContainerHeader
|
||||
title="Create Bangumi Subscription"
|
||||
description="Add a new bangumi subscription source"
|
||||
defaultBackTo="/subscriptions/manage"
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Img } from '@/components/ui/img';
|
||||
@@ -33,15 +34,9 @@ import {
|
||||
SubscriptionCategoryEnum,
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import {
|
||||
createFileRoute,
|
||||
useCanGoBack,
|
||||
useNavigate,
|
||||
useRouter,
|
||||
} from '@tanstack/react-router';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Edit,
|
||||
ExternalLink,
|
||||
ListIcon,
|
||||
@@ -61,20 +56,8 @@ export const Route = createFileRoute('/_app/subscriptions/detail/$id')({
|
||||
function SubscriptionDetailRouteComponent() {
|
||||
const { id } = Route.useParams();
|
||||
const navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
const canGoBack = useCanGoBack();
|
||||
const subscriptionService = useInject(SubscriptionService);
|
||||
|
||||
const handleBack = () => {
|
||||
if (canGoBack) {
|
||||
router.history.back();
|
||||
} else {
|
||||
navigate({
|
||||
to: '/subscriptions/manage',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleReload = async () => {
|
||||
const result = await refetch();
|
||||
const error = getApolloQueryError(result);
|
||||
@@ -177,31 +160,16 @@ function SubscriptionDetailRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl py-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Subscription detail</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
View subscription #{subscription.id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<ContainerHeader
|
||||
title="Subscription Detail"
|
||||
description={`View subscription #${subscription.id}`}
|
||||
actions={
|
||||
<Button onClick={handleEnterEditMode}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
</Button>{' '}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -421,6 +389,79 @@ function SubscriptionDetailRouteComponent() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="font-medium text-sm">Associated Crons</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: '/tasks/cron/manage',
|
||||
})
|
||||
}
|
||||
>
|
||||
<ListIcon className="h-4 w-4" />
|
||||
Crons
|
||||
</Button>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<RefreshCcwIcon className="h-4 w-4" />
|
||||
Setup Cron
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<SubscriptionSyncDialogContent
|
||||
id={subscription.id}
|
||||
onCancel={handleReload}
|
||||
isCron={true}
|
||||
/>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{subscription.cron?.nodes &&
|
||||
subscription.cron.nodes.length > 0 ? (
|
||||
subscription.cron.nodes.map((task) => (
|
||||
<Card
|
||||
key={task.id}
|
||||
className="group relative cursor-pointer p-4 transition-colors hover:bg-accent/50"
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: '/tasks/cron/detail/$id',
|
||||
params: {
|
||||
id: task.id.toString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="font-medium text-sm capitalize">
|
||||
<span>{task.cronExpr}</span>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<code className="break-all rounded bg-muted px-2 py-1 font-mono text-xs">
|
||||
{task.id}
|
||||
</code>
|
||||
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{task.status}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className="col-span-full py-8 text-center text-muted-foreground">
|
||||
No associated crons now
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { FormFieldErrors } from '@/components/ui/form-field-errors';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -44,8 +45,8 @@ import {
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { ArrowLeft, Save, X } from 'lucide-react';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { Save } from 'lucide-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Credential3rdSelectContent } from './-credential3rd-select';
|
||||
@@ -68,16 +69,8 @@ function FormView({
|
||||
subscription: SubscriptionDetailDto;
|
||||
onCompleted: VoidFunction;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const subscriptionService = useInject(SubscriptionService);
|
||||
|
||||
const handleBack = () => {
|
||||
navigate({
|
||||
to: '/subscriptions/detail/$id',
|
||||
params: { id: subscription.id.toString() },
|
||||
});
|
||||
};
|
||||
|
||||
const [updateSubscription, { loading: updating }] = useMutation<
|
||||
UpdateSubscriptionsMutation,
|
||||
UpdateSubscriptionsMutationVariables
|
||||
@@ -149,35 +142,17 @@ function FormView({
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl py-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Subscription edit</h1>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Edit subscription #{subscription.id}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleBack} disabled={updating}>
|
||||
<X className="mr-2 h-4 w-4" />
|
||||
Cancel
|
||||
</Button>
|
||||
<ContainerHeader
|
||||
title="Subscription Edit"
|
||||
description={`Edit subscription #${subscription.id}`}
|
||||
defaultBackTo={`/subscriptions/detail/${subscription.id}`}
|
||||
actions={
|
||||
<Button onClick={() => form.handleSubmit()} disabled={updating}>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{updating ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
||||
import { DataTableViewOptions } from '@/components/ui/data-table-view-options';
|
||||
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
||||
@@ -277,16 +278,16 @@ function SubscriptionManageRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto space-y-4 rounded-md">
|
||||
<div className="flex items-center justify-between pt-4">
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Subscription Management</h1>
|
||||
<p className="text-muted-foreground">Manage your subscription</p>
|
||||
</div>
|
||||
<Button onClick={() => navigate({ to: '/subscriptions/create' })}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Subscription
|
||||
</Button>
|
||||
</div>
|
||||
<ContainerHeader
|
||||
title="Subscription Management"
|
||||
description="Manage your subscription"
|
||||
actions={
|
||||
<Button onClick={() => navigate({ to: '/subscriptions/create' })}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Subscription
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center py-2">
|
||||
<DataTableViewOptions table={table} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { CronStatusEnum } from '@/infra/graphql/gql/graphql';
|
||||
import { AlertCircle, CheckCircle, Clock, Loader2 } from 'lucide-react';
|
||||
|
||||
export function getStatusBadge(status: CronStatusEnum) {
|
||||
switch (status) {
|
||||
case CronStatusEnum.Completed:
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-green-100 text-green-800">
|
||||
<CheckCircle className="mr-1 h-3 w-3 capitalize" />
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
case CronStatusEnum.Running:
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-blue-100 text-blue-800">
|
||||
<Loader2 className="mr-1 h-3 w-3 animate-spin capitalize" />
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
case CronStatusEnum.Failed:
|
||||
return (
|
||||
<Badge variant="destructive">
|
||||
<AlertCircle className="mr-1 h-3 w-3 capitalize" />
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
case CronStatusEnum.Pending:
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-yellow-100 text-yellow-800">
|
||||
<Clock className="mr-1 h-3 w-3 capitalize" />
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Badge variant="outline" className="capitalize">
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_app/tasks/cron/detail/$id')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/_app/tasks/cron/detail/$id"!</div>
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_app/tasks/cron/edit/$id')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/_app/tasks/cron/edit/$id"!</div>
|
||||
}
|
||||
306
apps/webui/src/presentation/routes/_app/tasks/cron/manage.tsx
Normal file
306
apps/webui/src/presentation/routes/_app/tasks/cron/manage.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||
import { DropdownMenuActions } from '@/components/ui/dropdown-menu-actions';
|
||||
import { QueryErrorView } from '@/components/ui/query-error-view';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import {
|
||||
type CronDto,
|
||||
DELETE_CRONS,
|
||||
GET_CRONS,
|
||||
} from '@/domains/recorder/schema/cron';
|
||||
import {
|
||||
apolloErrorToMessage,
|
||||
getApolloQueryError,
|
||||
} from '@/infra/errors/apollo';
|
||||
import {
|
||||
CronStatusEnum,
|
||||
type DeleteCronsMutation,
|
||||
type DeleteCronsMutationVariables,
|
||||
type GetCronsQuery,
|
||||
type GetCronsQueryVariables,
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useDebouncedSkeleton } from '@/presentation/hooks/use-debounded-skeleton';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import {
|
||||
type ColumnDef,
|
||||
type PaginationState,
|
||||
type SortingState,
|
||||
type VisibilityState,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import { format } from 'date-fns';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { getStatusBadge } from './-status-badge';
|
||||
|
||||
export const Route = createFileRoute('/_app/tasks/cron/manage')({
|
||||
component: TaskCronManageRouteComponent,
|
||||
staticData: {
|
||||
breadcrumb: { label: 'Manage' },
|
||||
} satisfies RouteStateDataOption,
|
||||
});
|
||||
|
||||
function TaskCronManageRouteComponent() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { loading, error, data, refetch } = useQuery<
|
||||
GetCronsQuery,
|
||||
GetCronsQueryVariables
|
||||
>(GET_CRONS, {
|
||||
variables: {
|
||||
pagination: {
|
||||
page: {
|
||||
page: pagination.pageIndex,
|
||||
limit: pagination.pageSize,
|
||||
},
|
||||
},
|
||||
filter: {},
|
||||
orderBy: {
|
||||
nextRun: 'DESC',
|
||||
},
|
||||
},
|
||||
pollInterval: 5000, // Auto-refresh every 5 seconds
|
||||
});
|
||||
|
||||
const { showSkeleton } = useDebouncedSkeleton({ loading });
|
||||
|
||||
const crons = data?.cron;
|
||||
|
||||
const [deleteCron] = useMutation<
|
||||
DeleteCronsMutation,
|
||||
DeleteCronsMutationVariables
|
||||
>(DELETE_CRONS, {
|
||||
onCompleted: async () => {
|
||||
const refetchResult = await refetch();
|
||||
const error = getApolloQueryError(refetchResult);
|
||||
if (error) {
|
||||
toast.error('Failed to delete tasks', {
|
||||
description: apolloErrorToMessage(error),
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast.success('Tasks deleted');
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error('Failed to delete tasks', {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const cs: ColumnDef<CronDto>[] = [
|
||||
{
|
||||
header: 'ID',
|
||||
accessorKey: 'id',
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div
|
||||
className="max-w-[200px] truncate font-mono text-sm"
|
||||
title={row.original.id.toString()}
|
||||
>
|
||||
{row.original.id}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return cs;
|
||||
}, []);
|
||||
|
||||
const table = useReactTable({
|
||||
data: useMemo(() => (crons?.nodes ?? []) as CronDto[], [crons]),
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onSortingChange: setSorting,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
pageCount: crons?.paginationInfo?.pages,
|
||||
rowCount: crons?.paginationInfo?.total,
|
||||
enableColumnPinning: true,
|
||||
autoResetPageIndex: true,
|
||||
manualPagination: true,
|
||||
state: {
|
||||
pagination,
|
||||
sorting,
|
||||
columnVisibility,
|
||||
},
|
||||
initialState: {
|
||||
columnPinning: {
|
||||
right: ['actions'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <QueryErrorView message={error.message} onRetry={refetch} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl space-y-4 px-4">
|
||||
<ContainerHeader
|
||||
title="Crons Management"
|
||||
description="Manage your crons"
|
||||
actions={
|
||||
<Button onClick={() => refetch()} variant="outline" size="sm">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="space-y-3">
|
||||
{showSkeleton &&
|
||||
Array.from(new Array(10)).map((_, index) => (
|
||||
<Skeleton key={index} className="h-32 w-full" />
|
||||
))}
|
||||
|
||||
{!showSkeleton && table.getRowModel().rows?.length > 0 ? (
|
||||
table.getRowModel().rows.map((row, index) => {
|
||||
const cron = row.original;
|
||||
return (
|
||||
<div
|
||||
className="space-y-3 rounded-lg border bg-card p-4"
|
||||
key={`${cron.id}-${index}`}
|
||||
>
|
||||
{/* Header with status and priority */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="font-mono text-muted-foreground text-xs">
|
||||
# {cron.id}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Badge variant="outline" className="capitalize">
|
||||
{cron.cronExpr}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
{getStatusBadge(cron.status)}
|
||||
<Badge variant="outline">Priority: {cron.priority}</Badge>
|
||||
<div className="mr-0 ml-auto">
|
||||
<DropdownMenuActions
|
||||
id={cron.id}
|
||||
showDetail
|
||||
onDetail={() => {
|
||||
navigate({
|
||||
to: '/tasks/cron/detail/$id',
|
||||
params: { id: cron.id.toString() },
|
||||
});
|
||||
}}
|
||||
showDelete
|
||||
onDelete={() =>
|
||||
deleteCron({
|
||||
variables: {
|
||||
filter: {
|
||||
id: {
|
||||
eq: cron.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{cron.status === CronStatusEnum.Failed && (
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
// TODO: Retry cron
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuActions>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Time info */}
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Next run: </span>
|
||||
<span>
|
||||
{cron.nextRun
|
||||
? format(new Date(cron.nextRun), 'MM/dd HH:mm')
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-muted-foreground">Last run: </span>
|
||||
<span>
|
||||
{cron.lastRun
|
||||
? format(new Date(cron.lastRun), 'MM/dd HH:mm')
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Attempts */}
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">Attempts: </span>
|
||||
<span>
|
||||
{cron.attempts} / {cron.maxAttempts}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Lock at */}
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">Lock at: </span>
|
||||
<span>
|
||||
{cron.lockedAt
|
||||
? format(new Date(cron.lockedAt), 'MM/dd HH:mm')
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subscriber task cron */}
|
||||
{cron.subscriberTaskCron && (
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">Task:</span>
|
||||
<br />
|
||||
<span
|
||||
className="whitespace-pre-wrap"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(
|
||||
cron.subscriberTaskCron,
|
||||
null,
|
||||
2
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error if exists */}
|
||||
{cron.status === CronStatusEnum.Failed && cron.lastError && (
|
||||
<div className="rounded bg-destructive/10 p-2 text-destructive text-sm">
|
||||
{cron.lastError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<DetailEmptyView message="No tasks found" fullWidth />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DataTablePagination table={table} showSelectedRowCount={false} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { buildVirtualBranchRouteOptions } from '@/infra/routes/utils';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/_app/tasks/cron')(
|
||||
buildVirtualBranchRouteOptions({
|
||||
title: 'Cron',
|
||||
})
|
||||
);
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DetailEmptyView } from '@/components/ui/detail-empty-view';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { QueryErrorView } from '@/components/ui/query-error-view';
|
||||
@@ -24,14 +25,9 @@ import {
|
||||
} from '@/infra/graphql/gql/graphql';
|
||||
import type { RouteStateDataOption } from '@/infra/routes/traits';
|
||||
import { useMutation, useQuery } from '@apollo/client';
|
||||
import {
|
||||
createFileRoute,
|
||||
useCanGoBack,
|
||||
useNavigate,
|
||||
useRouter,
|
||||
} from '@tanstack/react-router';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { format } from 'date-fns';
|
||||
import { ArrowLeft, RefreshCw } from 'lucide-react';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { prettyTaskType } from './-pretty-task-type';
|
||||
@@ -46,19 +42,6 @@ export const Route = createFileRoute('/_app/tasks/detail/$id')({
|
||||
|
||||
function TaskDetailRouteComponent() {
|
||||
const { id } = Route.useParams();
|
||||
const navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
const canGoBack = useCanGoBack();
|
||||
|
||||
const handleBack = () => {
|
||||
if (canGoBack) {
|
||||
router.history.back();
|
||||
} else {
|
||||
navigate({
|
||||
to: '/tasks/manage',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { data, loading, error, refetch } = useQuery<
|
||||
GetTasksQuery,
|
||||
@@ -129,27 +112,17 @@ function TaskDetailRouteComponent() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-4xl py-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleBack}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<ContainerHeader
|
||||
title="Task Detail"
|
||||
description={`View task #${task.id}`}
|
||||
defaultBackTo="/tasks/manage"
|
||||
actions={
|
||||
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Task Detail</h1>
|
||||
<p className="mt-1 text-muted-foreground">View task #{task.id}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type DeleteTasksMutation,
|
||||
type DeleteTasksMutationVariables,
|
||||
type GetTasksQuery,
|
||||
type GetTasksQueryVariables,
|
||||
type RetryTasksMutation,
|
||||
type RetryTasksMutationVariables,
|
||||
SubscriberTaskStatusEnum,
|
||||
@@ -35,6 +36,7 @@ import {
|
||||
import { format } from 'date-fns';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
|
||||
import { ContainerHeader } from '@/components/ui/container-header';
|
||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
apolloErrorToMessage,
|
||||
@@ -55,18 +57,17 @@ export const Route = createFileRoute('/_app/tasks/manage')({
|
||||
function TaskManageRouteComponent() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
|
||||
lockAt: false,
|
||||
lockBy: false,
|
||||
attempts: false,
|
||||
});
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { loading, error, data, refetch } = useQuery<GetTasksQuery>(GET_TASKS, {
|
||||
const { loading, error, data, refetch } = useQuery<
|
||||
GetTasksQuery,
|
||||
GetTasksQueryVariables
|
||||
>(GET_TASKS, {
|
||||
variables: {
|
||||
pagination: {
|
||||
page: {
|
||||
@@ -172,16 +173,16 @@ function TaskManageRouteComponent() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto space-y-4 px-4">
|
||||
<div className="flex items-center justify-between pt-4">
|
||||
<div>
|
||||
<h1 className="font-bold text-2xl">Tasks Management</h1>
|
||||
<p className="text-muted-foreground">Manage your tasks</p>
|
||||
</div>
|
||||
<Button onClick={() => refetch()} variant="outline" size="sm">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="container mx-auto max-w-4xl space-y-4 px-4">
|
||||
<ContainerHeader
|
||||
title="Tasks Management"
|
||||
description="Manage your tasks"
|
||||
actions={
|
||||
<Button onClick={() => refetch()} variant="outline" size="sm">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="space-y-3">
|
||||
{showSkeleton &&
|
||||
|
||||
Reference in New Issue
Block a user