@svadmin/ui 0.33.1 → 0.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/component-registry.svelte.d.ts +2 -0
- package/dist/components/AdminApp.svelte +18 -5
- package/dist/components/AdminApp.svelte.d.ts +7 -1
- package/dist/components/CancelTaskButton.svelte +60 -0
- package/dist/components/CancelTaskButton.svelte.d.ts +13 -0
- package/dist/components/Layout.svelte +13 -2
- package/dist/components/RetryTaskButton.svelte +60 -0
- package/dist/components/RetryTaskButton.svelte.d.ts +13 -0
- package/dist/components/TaskDetails.svelte +138 -0
- package/dist/components/TaskDetails.svelte.d.ts +19 -0
- package/dist/components/TaskList.svelte +150 -0
- package/dist/components/TaskList.svelte.d.ts +24 -0
- package/dist/components/TaskProgressBar.svelte +25 -0
- package/dist/components/TaskProgressBar.svelte.d.ts +8 -0
- package/dist/components/TaskQueueDrawer.svelte +318 -113
- package/dist/components/TaskQueueDrawer.svelte.d.ts +6 -6
- package/dist/components/TaskQueueDrawer.test.svelte.d.ts +1 -0
- package/dist/components/TaskQueueDrawer.test.svelte.js +143 -0
- package/dist/components/TaskStatusBadge.svelte +78 -0
- package/dist/components/TaskStatusBadge.svelte.d.ts +8 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -0
- package/dist/types.d.ts +0 -11
- package/package.json +21 -13
- package/src/app.css +0 -674
|
@@ -27,6 +27,8 @@ export interface ComponentRegistry {
|
|
|
27
27
|
UserMenu?: Component<any>;
|
|
28
28
|
/** Custom notification panel / bell icon in the header */
|
|
29
29
|
NotificationPanel?: Component<any>;
|
|
30
|
+
/** Custom task queue drawer / task center trigger in the header */
|
|
31
|
+
TaskQueueDrawer?: Component<any>;
|
|
30
32
|
}
|
|
31
33
|
/** Set the component registry in context (called by AdminApp). */
|
|
32
34
|
export declare function setComponentRegistry(registry: ComponentRegistry): void;
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
// Auto-inject design tokens so consumers don't need to manually import
|
|
3
3
|
import '../app.css';
|
|
4
4
|
import type { Snippet } from 'svelte';
|
|
5
|
-
import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode, RouterProvider, ThemeConfig, MenuItem } from '@svadmin/core';
|
|
6
|
-
import { setDataProvider, setAuthProvider, setResources, setLocale, setTheme, setRouterProvider, getAuthProvider, createHashRouterProvider, configureTheme } from '@svadmin/core';
|
|
5
|
+
import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode, RouterProvider, ThemeConfig, MenuItem, TaskProvider } from '@svadmin/core';
|
|
6
|
+
import { setDataProvider, setAuthProvider, setResources, setLocale, setTheme, setRouterProvider, setTaskProvider, getAuthProvider, createHashRouterProvider, configureTheme } from '@svadmin/core';
|
|
7
7
|
import { t } from '@svadmin/core/i18n';
|
|
8
8
|
import { navigate } from '@svadmin/core/router';
|
|
9
9
|
import { currentPath } from '@svadmin/core/router';
|
|
10
|
-
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
|
|
10
|
+
import { QueryClient, QueryClientProvider, type DefaultOptions } from '@tanstack/svelte-query';
|
|
11
11
|
import { setComponentRegistry, getComponentRegistry, type ComponentRegistry } from '../component-registry.svelte.js';
|
|
12
12
|
import Layout from './Layout.svelte';
|
|
13
13
|
import AutoTable from './AutoTable.svelte';
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import DevTools from './DevTools.svelte';
|
|
23
23
|
import Sidebar from './Sidebar.svelte';
|
|
24
24
|
import Header from './Header.svelte';
|
|
25
|
+
import TaskQueueDrawer from './TaskQueueDrawer.svelte';
|
|
25
26
|
import SettingsPage from './SettingsPage.svelte';
|
|
26
27
|
import { Button } from './ui/button/index.js';
|
|
27
28
|
import { Input } from './ui/input/index.js';
|
|
@@ -32,6 +33,7 @@
|
|
|
32
33
|
interface Props {
|
|
33
34
|
dataProvider: DataProvider;
|
|
34
35
|
authProvider?: AuthProvider;
|
|
36
|
+
taskProvider?: TaskProvider;
|
|
35
37
|
routerProvider?: RouterProvider;
|
|
36
38
|
resources: ResourceDefinition[];
|
|
37
39
|
locale?: string;
|
|
@@ -49,11 +51,16 @@
|
|
|
49
51
|
siteUrl?: string;
|
|
50
52
|
/** Routing strategy for Sidebar links ('hash' | 'path' | 'auto') */
|
|
51
53
|
routeMode?: 'hash' | 'path' | 'auto';
|
|
54
|
+
/** Optional TanStack Query client override. */
|
|
55
|
+
queryClient?: QueryClient;
|
|
56
|
+
/** Optional TanStack Query default options override when QueryClient is not supplied. */
|
|
57
|
+
queryClientDefaultOptions?: DefaultOptions;
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
let {
|
|
55
61
|
dataProvider,
|
|
56
62
|
authProvider,
|
|
63
|
+
taskProvider,
|
|
57
64
|
routerProvider,
|
|
58
65
|
resources,
|
|
59
66
|
locale,
|
|
@@ -66,12 +73,15 @@
|
|
|
66
73
|
menu,
|
|
67
74
|
siteUrl,
|
|
68
75
|
routeMode,
|
|
76
|
+
queryClient: providedQueryClient,
|
|
77
|
+
queryClientDefaultOptions,
|
|
69
78
|
}: Props = $props();
|
|
70
79
|
|
|
71
80
|
// Default component registry
|
|
72
81
|
const defaultComponents: ComponentRegistry = {
|
|
73
82
|
Layout, Sidebar, Header,
|
|
74
83
|
LoginPage, AutoTable, AutoForm, ShowPage,
|
|
84
|
+
TaskQueueDrawer,
|
|
75
85
|
Button: Button as unknown as ComponentRegistry['Button'],
|
|
76
86
|
Input: Input as unknown as ComponentRegistry['Input'],
|
|
77
87
|
Badge: Badge as unknown as ComponentRegistry['Badge'],
|
|
@@ -90,6 +100,7 @@
|
|
|
90
100
|
// Set up initial context synchronously so children can access resources immediately during first render
|
|
91
101
|
setDataProvider(dataProvider);
|
|
92
102
|
if (authProvider) setAuthProvider(authProvider);
|
|
103
|
+
if (taskProvider) setTaskProvider(taskProvider);
|
|
93
104
|
setResources(resources);
|
|
94
105
|
setRouterProvider(resolvedRouter);
|
|
95
106
|
if (locale) setLocale(locale);
|
|
@@ -100,6 +111,7 @@
|
|
|
100
111
|
$effect.pre(() => {
|
|
101
112
|
setDataProvider(dataProvider);
|
|
102
113
|
if (authProvider) setAuthProvider(authProvider);
|
|
114
|
+
if (taskProvider) setTaskProvider(taskProvider);
|
|
103
115
|
setResources(resources);
|
|
104
116
|
setRouterProvider(resolvedRouter);
|
|
105
117
|
if (locale) setLocale(locale);
|
|
@@ -107,11 +119,12 @@
|
|
|
107
119
|
if (defaultTheme) setTheme(defaultTheme);
|
|
108
120
|
});
|
|
109
121
|
|
|
110
|
-
const queryClient = new QueryClient({
|
|
122
|
+
const queryClient = $derived(providedQueryClient ?? new QueryClient({
|
|
111
123
|
defaultOptions: {
|
|
112
124
|
queries: { staleTime: 30_000, retry: 1 },
|
|
125
|
+
...queryClientDefaultOptions,
|
|
113
126
|
},
|
|
114
|
-
});
|
|
127
|
+
}));
|
|
115
128
|
|
|
116
129
|
// Initialize router with provider synchronously
|
|
117
130
|
initRouter(resolvedRouter);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import '../app.css';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
-
import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode, RouterProvider, ThemeConfig, MenuItem } from '@svadmin/core';
|
|
3
|
+
import type { DataProvider, AuthProvider, ResourceDefinition, ThemeMode, RouterProvider, ThemeConfig, MenuItem, TaskProvider } from '@svadmin/core';
|
|
4
|
+
import { QueryClient, type DefaultOptions } from '@tanstack/svelte-query';
|
|
4
5
|
import { type ComponentRegistry } from '../component-registry.svelte.js';
|
|
5
6
|
interface Props {
|
|
6
7
|
dataProvider: DataProvider;
|
|
7
8
|
authProvider?: AuthProvider;
|
|
9
|
+
taskProvider?: TaskProvider;
|
|
8
10
|
routerProvider?: RouterProvider;
|
|
9
11
|
resources: ResourceDefinition[];
|
|
10
12
|
locale?: string;
|
|
@@ -22,6 +24,10 @@ interface Props {
|
|
|
22
24
|
siteUrl?: string;
|
|
23
25
|
/** Routing strategy for Sidebar links ('hash' | 'path' | 'auto') */
|
|
24
26
|
routeMode?: 'hash' | 'path' | 'auto';
|
|
27
|
+
/** Optional TanStack Query client override. */
|
|
28
|
+
queryClient?: QueryClient;
|
|
29
|
+
/** Optional TanStack Query default options override when QueryClient is not supplied. */
|
|
30
|
+
queryClientDefaultOptions?: DefaultOptions;
|
|
25
31
|
}
|
|
26
32
|
declare const AdminApp: import("svelte").Component<Props, {}, "">;
|
|
27
33
|
type AdminApp = ReturnType<typeof AdminApp>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getTaskProvider } from '@svadmin/core';
|
|
3
|
+
import { useQueryClient } from '@tanstack/svelte-query';
|
|
4
|
+
import { t } from '@svadmin/core/i18n';
|
|
5
|
+
import { Button } from './ui/button/index.js';
|
|
6
|
+
import { Loader2, Ban } from '@lucide/svelte';
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
taskId,
|
|
10
|
+
taskProvider = getTaskProvider({ optional: true }),
|
|
11
|
+
onSuccess,
|
|
12
|
+
onError,
|
|
13
|
+
disabled = false,
|
|
14
|
+
variant = 'outline',
|
|
15
|
+
size = 'sm',
|
|
16
|
+
children,
|
|
17
|
+
} = $props<{
|
|
18
|
+
taskId: string;
|
|
19
|
+
taskProvider?: import('@svadmin/core').TaskProvider;
|
|
20
|
+
onSuccess?: () => void;
|
|
21
|
+
onError?: (error: unknown) => void;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
variant?: import('./ui/button/index.js').ButtonVariant;
|
|
24
|
+
size?: import('./ui/button/index.js').ButtonSize;
|
|
25
|
+
children?: import('svelte').Snippet;
|
|
26
|
+
}>();
|
|
27
|
+
|
|
28
|
+
const queryClient = useQueryClient();
|
|
29
|
+
let pending = $state(false);
|
|
30
|
+
|
|
31
|
+
async function handleCancel() {
|
|
32
|
+
if (!taskProvider?.cancel || pending) return;
|
|
33
|
+
pending = true;
|
|
34
|
+
try {
|
|
35
|
+
await taskProvider.cancel(taskId);
|
|
36
|
+
await Promise.all([
|
|
37
|
+
queryClient.invalidateQueries({ queryKey: ['taskList'] }),
|
|
38
|
+
queryClient.invalidateQueries({ queryKey: ['task', taskId] }),
|
|
39
|
+
]);
|
|
40
|
+
onSuccess?.();
|
|
41
|
+
} catch (error) {
|
|
42
|
+
onError?.(error);
|
|
43
|
+
} finally {
|
|
44
|
+
pending = false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<Button {variant} {size} onclick={handleCancel} disabled={disabled || pending || !taskProvider?.cancel}>
|
|
50
|
+
{#if pending}
|
|
51
|
+
<Loader2 class="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
|
52
|
+
{:else}
|
|
53
|
+
<Ban class="mr-1.5 h-3.5 w-3.5" />
|
|
54
|
+
{/if}
|
|
55
|
+
{#if children}
|
|
56
|
+
{@render children()}
|
|
57
|
+
{:else}
|
|
58
|
+
{t('common.cancel')}
|
|
59
|
+
{/if}
|
|
60
|
+
</Button>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
taskId: string;
|
|
3
|
+
taskProvider?: import('@svadmin/core').TaskProvider;
|
|
4
|
+
onSuccess?: () => void;
|
|
5
|
+
onError?: (error: unknown) => void;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
variant?: import('./ui/button/index.js').ButtonVariant;
|
|
8
|
+
size?: import('./ui/button/index.js').ButtonSize;
|
|
9
|
+
children?: import('svelte').Snippet;
|
|
10
|
+
};
|
|
11
|
+
declare const CancelTaskButton: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
12
|
+
type CancelTaskButton = ReturnType<typeof CancelTaskButton>;
|
|
13
|
+
export default CancelTaskButton;
|
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
import ChatDialog from './ChatDialog.svelte';
|
|
9
9
|
import TooltipButton from './TooltipButton.svelte';
|
|
10
10
|
import { t } from '@svadmin/core/i18n';
|
|
11
|
-
import { getAuthProvider } from '@svadmin/core';
|
|
12
|
-
import type { Identity, MenuItem } from '@svadmin/core';
|
|
11
|
+
import { getAuthProvider, getTaskProvider } from '@svadmin/core';
|
|
12
|
+
import type { Identity, MenuItem, TaskProvider, TaskRecord } from '@svadmin/core';
|
|
13
13
|
import { navigate } from '@svadmin/core/router';
|
|
14
14
|
import { getPath } from '../router-state.svelte.js';
|
|
15
15
|
import { Skeleton } from './ui/skeleton/index.js';
|
|
16
16
|
import { Button } from './ui/button/index.js';
|
|
17
17
|
import * as Sheet from './ui/sheet/index.js';
|
|
18
18
|
import { Menu } from '@lucide/svelte';
|
|
19
|
+
import { getComponentRegistry } from '../component-registry.svelte.js';
|
|
19
20
|
|
|
20
21
|
let commandOpen = $state(false);
|
|
21
22
|
let shortcutsOpen = $state(false);
|
|
@@ -32,6 +33,9 @@
|
|
|
32
33
|
}
|
|
33
34
|
let loading = $state(true);
|
|
34
35
|
let identity = $state<Identity | null>(null);
|
|
36
|
+
const taskProvider = getTaskProvider({ optional: true }) as TaskProvider<TaskRecord> | undefined;
|
|
37
|
+
const registry = getComponentRegistry() || {} as any;
|
|
38
|
+
const TaskQueueComponent = registry.TaskQueueDrawer;
|
|
35
39
|
|
|
36
40
|
$effect(() => {
|
|
37
41
|
let cancelled = false;
|
|
@@ -149,6 +153,13 @@
|
|
|
149
153
|
<Menu class="h-5 w-5" />
|
|
150
154
|
</TooltipButton>
|
|
151
155
|
{/snippet}
|
|
156
|
+
{#snippet rightActions()}
|
|
157
|
+
{#if taskProvider}
|
|
158
|
+
{#if TaskQueueComponent}
|
|
159
|
+
<TaskQueueComponent {taskProvider} />
|
|
160
|
+
{/if}
|
|
161
|
+
{/if}
|
|
162
|
+
{/snippet}
|
|
152
163
|
</Header>
|
|
153
164
|
|
|
154
165
|
<!-- Content area -->
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getTaskProvider } from '@svadmin/core';
|
|
3
|
+
import { useQueryClient } from '@tanstack/svelte-query';
|
|
4
|
+
import { t } from '@svadmin/core/i18n';
|
|
5
|
+
import { Button } from './ui/button/index.js';
|
|
6
|
+
import { Loader2, RotateCcw } from '@lucide/svelte';
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
taskId,
|
|
10
|
+
taskProvider = getTaskProvider({ optional: true }),
|
|
11
|
+
onSuccess,
|
|
12
|
+
onError,
|
|
13
|
+
disabled = false,
|
|
14
|
+
variant = 'outline',
|
|
15
|
+
size = 'sm',
|
|
16
|
+
children,
|
|
17
|
+
} = $props<{
|
|
18
|
+
taskId: string;
|
|
19
|
+
taskProvider?: import('@svadmin/core').TaskProvider;
|
|
20
|
+
onSuccess?: () => void;
|
|
21
|
+
onError?: (error: unknown) => void;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
variant?: import('./ui/button/index.js').ButtonVariant;
|
|
24
|
+
size?: import('./ui/button/index.js').ButtonSize;
|
|
25
|
+
children?: import('svelte').Snippet;
|
|
26
|
+
}>();
|
|
27
|
+
|
|
28
|
+
const queryClient = useQueryClient();
|
|
29
|
+
let pending = $state(false);
|
|
30
|
+
|
|
31
|
+
async function handleRetry() {
|
|
32
|
+
if (!taskProvider?.retry || pending) return;
|
|
33
|
+
pending = true;
|
|
34
|
+
try {
|
|
35
|
+
await taskProvider.retry(taskId);
|
|
36
|
+
await Promise.all([
|
|
37
|
+
queryClient.invalidateQueries({ queryKey: ['taskList'] }),
|
|
38
|
+
queryClient.invalidateQueries({ queryKey: ['task', taskId] }),
|
|
39
|
+
]);
|
|
40
|
+
onSuccess?.();
|
|
41
|
+
} catch (error) {
|
|
42
|
+
onError?.(error);
|
|
43
|
+
} finally {
|
|
44
|
+
pending = false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<Button {variant} {size} onclick={handleRetry} disabled={disabled || pending || !taskProvider?.retry}>
|
|
50
|
+
{#if pending}
|
|
51
|
+
<Loader2 class="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
|
52
|
+
{:else}
|
|
53
|
+
<RotateCcw class="mr-1.5 h-3.5 w-3.5" />
|
|
54
|
+
{/if}
|
|
55
|
+
{#if children}
|
|
56
|
+
{@render children()}
|
|
57
|
+
{:else}
|
|
58
|
+
{t('common.retry')}
|
|
59
|
+
{/if}
|
|
60
|
+
</Button>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
taskId: string;
|
|
3
|
+
taskProvider?: import('@svadmin/core').TaskProvider;
|
|
4
|
+
onSuccess?: () => void;
|
|
5
|
+
onError?: (error: unknown) => void;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
variant?: import('./ui/button/index.js').ButtonVariant;
|
|
8
|
+
size?: import('./ui/button/index.js').ButtonSize;
|
|
9
|
+
children?: import('svelte').Snippet;
|
|
10
|
+
};
|
|
11
|
+
declare const RetryTaskButton: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
12
|
+
type RetryTaskButton = ReturnType<typeof RetryTaskButton>;
|
|
13
|
+
export default RetryTaskButton;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useTask, getTaskProvider } from '@svadmin/core';
|
|
3
|
+
import { t } from '@svadmin/core/i18n';
|
|
4
|
+
import type { TaskProvider, TaskRecord } from '@svadmin/core';
|
|
5
|
+
import { Loader2 } from '@lucide/svelte';
|
|
6
|
+
import * as Card from './ui/card/index.js';
|
|
7
|
+
import { Badge } from './ui/badge/index.js';
|
|
8
|
+
import TaskStatusBadge from './TaskStatusBadge.svelte';
|
|
9
|
+
import TaskProgressBar from './TaskProgressBar.svelte';
|
|
10
|
+
import RetryTaskButton from './RetryTaskButton.svelte';
|
|
11
|
+
import CancelTaskButton from './CancelTaskButton.svelte';
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
task,
|
|
15
|
+
taskId,
|
|
16
|
+
taskProvider = getTaskProvider({ optional: true }) as TaskProvider<TaskRecord> | undefined,
|
|
17
|
+
title,
|
|
18
|
+
useProviderData = !task && !!taskId,
|
|
19
|
+
queryOptions,
|
|
20
|
+
} = $props<{
|
|
21
|
+
task?: TaskRecord;
|
|
22
|
+
taskId?: string;
|
|
23
|
+
taskProvider?: TaskProvider<TaskRecord>;
|
|
24
|
+
title?: string;
|
|
25
|
+
useProviderData?: boolean;
|
|
26
|
+
queryOptions?: {
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
staleTime?: number;
|
|
29
|
+
gcTime?: number;
|
|
30
|
+
refetchOnWindowFocus?: boolean;
|
|
31
|
+
refetchInterval?: number | false;
|
|
32
|
+
refetchIntervalInBackground?: boolean;
|
|
33
|
+
};
|
|
34
|
+
}>();
|
|
35
|
+
|
|
36
|
+
const query = useTask({
|
|
37
|
+
get taskId() {
|
|
38
|
+
return taskId;
|
|
39
|
+
},
|
|
40
|
+
get taskProvider() {
|
|
41
|
+
return taskProvider;
|
|
42
|
+
},
|
|
43
|
+
get queryOptions() {
|
|
44
|
+
return { enabled: useProviderData && !!taskId && !!taskProvider, ...queryOptions };
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const resolvedTask = $derived(task ?? query.data);
|
|
49
|
+
const resolvedTitle = $derived(title ?? t('task.detailsTitle'));
|
|
50
|
+
|
|
51
|
+
function formatDate(value: unknown) {
|
|
52
|
+
if (!value) return '—';
|
|
53
|
+
return new Date(value as string | Date).toLocaleString();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toPrettyJson(value: unknown) {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.stringify(value, null, 2);
|
|
59
|
+
} catch {
|
|
60
|
+
return String(value);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<Card.Root class="border-border/40 shadow-sm">
|
|
66
|
+
<Card.Header class="pb-3">
|
|
67
|
+
<Card.Title class="text-base">{resolvedTitle}</Card.Title>
|
|
68
|
+
<Card.Description>
|
|
69
|
+
{#if resolvedTask}
|
|
70
|
+
{String(resolvedTask.title ?? resolvedTask.name ?? resolvedTask.id)}
|
|
71
|
+
{:else}
|
|
72
|
+
{t('task.detailsSummary')}
|
|
73
|
+
{/if}
|
|
74
|
+
</Card.Description>
|
|
75
|
+
</Card.Header>
|
|
76
|
+
<Card.Content class="space-y-5 pt-0">
|
|
77
|
+
{#if useProviderData && taskProvider && query.isLoading}
|
|
78
|
+
<div class="flex h-40 items-center justify-center text-muted-foreground">
|
|
79
|
+
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
|
80
|
+
{t('task.loadingDetail')}
|
|
81
|
+
</div>
|
|
82
|
+
{:else if resolvedTask}
|
|
83
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
84
|
+
<TaskStatusBadge status={String(resolvedTask.status ?? 'pending')} />
|
|
85
|
+
<Badge variant="outline" class="font-mono text-xs">{resolvedTask.id}</Badge>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{#if typeof resolvedTask.progress === 'number'}
|
|
89
|
+
<TaskProgressBar value={Number(resolvedTask.progress)} />
|
|
90
|
+
{/if}
|
|
91
|
+
|
|
92
|
+
<div class="grid gap-4 md:grid-cols-2">
|
|
93
|
+
<div class="space-y-1">
|
|
94
|
+
<div class="text-xs uppercase tracking-wide text-muted-foreground">{t('task.createdLabel')}</div>
|
|
95
|
+
<div class="text-sm">{formatDate(resolvedTask.createdAt)}</div>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="space-y-1">
|
|
98
|
+
<div class="text-xs uppercase tracking-wide text-muted-foreground">{t('task.updatedLabel')}</div>
|
|
99
|
+
<div class="text-sm">{formatDate(resolvedTask.updatedAt)}</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
{#if resolvedTask.message}
|
|
104
|
+
<div class="space-y-1">
|
|
105
|
+
<div class="text-xs uppercase tracking-wide text-muted-foreground">{t('task.messageLabel')}</div>
|
|
106
|
+
<div class="rounded-md border border-border/60 bg-muted/30 p-3 text-sm">{String(resolvedTask.message)}</div>
|
|
107
|
+
</div>
|
|
108
|
+
{/if}
|
|
109
|
+
|
|
110
|
+
{#if resolvedTask.error}
|
|
111
|
+
<div class="space-y-1">
|
|
112
|
+
<div class="text-xs uppercase tracking-wide text-destructive">{t('task.errorLabel')}</div>
|
|
113
|
+
<pre class="overflow-auto rounded-md border border-destructive/20 bg-destructive/5 p-3 text-xs text-destructive">{toPrettyJson(resolvedTask.error)}</pre>
|
|
114
|
+
</div>
|
|
115
|
+
{/if}
|
|
116
|
+
|
|
117
|
+
<div class="space-y-1">
|
|
118
|
+
<div class="text-xs uppercase tracking-wide text-muted-foreground">{t('task.payloadLabel')}</div>
|
|
119
|
+
<pre class="overflow-auto rounded-md border border-border/60 bg-muted/20 p-3 text-xs">{toPrettyJson(resolvedTask)}</pre>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{#if taskProvider?.retry || taskProvider?.cancel}
|
|
123
|
+
<div class="flex flex-wrap gap-2 pt-2">
|
|
124
|
+
{#if taskProvider?.retry && ['failed', 'error'].includes(String(resolvedTask.status).toLowerCase())}
|
|
125
|
+
<RetryTaskButton taskId={resolvedTask.id} {taskProvider} />
|
|
126
|
+
{/if}
|
|
127
|
+
{#if taskProvider?.cancel && ['pending', 'queued', 'running', 'processing'].includes(String(resolvedTask.status).toLowerCase())}
|
|
128
|
+
<CancelTaskButton taskId={resolvedTask.id} {taskProvider} />
|
|
129
|
+
{/if}
|
|
130
|
+
</div>
|
|
131
|
+
{/if}
|
|
132
|
+
{:else}
|
|
133
|
+
<div class="flex h-32 items-center justify-center text-sm text-muted-foreground">
|
|
134
|
+
{t('task.noSelection')}
|
|
135
|
+
</div>
|
|
136
|
+
{/if}
|
|
137
|
+
</Card.Content>
|
|
138
|
+
</Card.Root>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TaskProvider, TaskRecord } from '@svadmin/core';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
task?: TaskRecord;
|
|
4
|
+
taskId?: string;
|
|
5
|
+
taskProvider?: TaskProvider<TaskRecord>;
|
|
6
|
+
title?: string;
|
|
7
|
+
useProviderData?: boolean;
|
|
8
|
+
queryOptions?: {
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
staleTime?: number;
|
|
11
|
+
gcTime?: number;
|
|
12
|
+
refetchOnWindowFocus?: boolean;
|
|
13
|
+
refetchInterval?: number | false;
|
|
14
|
+
refetchIntervalInBackground?: boolean;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
declare const TaskDetails: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
18
|
+
type TaskDetails = ReturnType<typeof TaskDetails>;
|
|
19
|
+
export default TaskDetails;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useTaskList, getTaskProvider } from '@svadmin/core';
|
|
3
|
+
import { t } from '@svadmin/core/i18n';
|
|
4
|
+
import type { TaskProvider, TaskRecord } from '@svadmin/core';
|
|
5
|
+
import { Loader2 } from '@lucide/svelte';
|
|
6
|
+
import * as Card from './ui/card/index.js';
|
|
7
|
+
import * as Table from './ui/table/index.js';
|
|
8
|
+
import { Button } from './ui/button/index.js';
|
|
9
|
+
import TaskStatusBadge from './TaskStatusBadge.svelte';
|
|
10
|
+
import TaskProgressBar from './TaskProgressBar.svelte';
|
|
11
|
+
import RetryTaskButton from './RetryTaskButton.svelte';
|
|
12
|
+
import CancelTaskButton from './CancelTaskButton.svelte';
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
tasks,
|
|
16
|
+
taskProvider = getTaskProvider({ optional: true }) as TaskProvider<TaskRecord> | undefined,
|
|
17
|
+
params,
|
|
18
|
+
dlq = false,
|
|
19
|
+
title,
|
|
20
|
+
emptyText,
|
|
21
|
+
showActions = true,
|
|
22
|
+
showProgress = true,
|
|
23
|
+
useProviderData = !tasks,
|
|
24
|
+
queryOptions,
|
|
25
|
+
onSelect,
|
|
26
|
+
} = $props<{
|
|
27
|
+
tasks?: TaskRecord[];
|
|
28
|
+
taskProvider?: TaskProvider<TaskRecord>;
|
|
29
|
+
params?: Record<string, unknown>;
|
|
30
|
+
dlq?: boolean;
|
|
31
|
+
title?: string;
|
|
32
|
+
emptyText?: string;
|
|
33
|
+
showActions?: boolean;
|
|
34
|
+
showProgress?: boolean;
|
|
35
|
+
useProviderData?: boolean;
|
|
36
|
+
queryOptions?: {
|
|
37
|
+
enabled?: boolean;
|
|
38
|
+
staleTime?: number;
|
|
39
|
+
gcTime?: number;
|
|
40
|
+
refetchOnWindowFocus?: boolean;
|
|
41
|
+
refetchInterval?: number | false;
|
|
42
|
+
refetchIntervalInBackground?: boolean;
|
|
43
|
+
};
|
|
44
|
+
onSelect?: (task: TaskRecord) => void;
|
|
45
|
+
}>();
|
|
46
|
+
|
|
47
|
+
const query = useTaskList({
|
|
48
|
+
get params() {
|
|
49
|
+
return params;
|
|
50
|
+
},
|
|
51
|
+
get dlq() {
|
|
52
|
+
return dlq;
|
|
53
|
+
},
|
|
54
|
+
get taskProvider() {
|
|
55
|
+
return taskProvider;
|
|
56
|
+
},
|
|
57
|
+
get queryOptions() {
|
|
58
|
+
return { enabled: useProviderData && !!taskProvider, ...queryOptions };
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const resolvedTasks = $derived(tasks ?? query.data?.data ?? []);
|
|
63
|
+
const resolvedTitle = $derived(title ?? (dlq ? t('task.dlqTitle') : t('task.listTitle')));
|
|
64
|
+
const resolvedEmptyText = $derived(emptyText ?? (dlq ? t('task.noDlq') : t('task.noTasks')));
|
|
65
|
+
|
|
66
|
+
function formatDate(value: unknown) {
|
|
67
|
+
if (!value) return '—';
|
|
68
|
+
return new Date(value as string | Date).toLocaleString();
|
|
69
|
+
}
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<Card.Root class="border-border/40 shadow-sm">
|
|
73
|
+
<Card.Header class="pb-3">
|
|
74
|
+
<Card.Title class="text-base">{resolvedTitle}</Card.Title>
|
|
75
|
+
<Card.Description>
|
|
76
|
+
{#if useProviderData && taskProvider}
|
|
77
|
+
{t('task.count', { count: (query.data?.total ?? resolvedTasks.length) || 0 })}
|
|
78
|
+
{:else}
|
|
79
|
+
{t('task.count', { count: resolvedTasks.length })}
|
|
80
|
+
{/if}
|
|
81
|
+
</Card.Description>
|
|
82
|
+
</Card.Header>
|
|
83
|
+
<Card.Content class="pt-0">
|
|
84
|
+
{#if useProviderData && taskProvider && query.isLoading}
|
|
85
|
+
<div class="flex h-40 items-center justify-center text-muted-foreground">
|
|
86
|
+
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
|
87
|
+
{t('task.loadingList')}
|
|
88
|
+
</div>
|
|
89
|
+
{:else}
|
|
90
|
+
<div class="overflow-auto">
|
|
91
|
+
<Table.Root>
|
|
92
|
+
<Table.Header>
|
|
93
|
+
<Table.Row>
|
|
94
|
+
<Table.Head>{t('task.taskColumn')}</Table.Head>
|
|
95
|
+
<Table.Head>{t('task.statusColumn')}</Table.Head>
|
|
96
|
+
<Table.Head>{t('task.updatedColumn')}</Table.Head>
|
|
97
|
+
{#if showActions}
|
|
98
|
+
<Table.Head class="text-right">{t('task.actionsColumn')}</Table.Head>
|
|
99
|
+
{/if}
|
|
100
|
+
</Table.Row>
|
|
101
|
+
</Table.Header>
|
|
102
|
+
<Table.Body>
|
|
103
|
+
{#each resolvedTasks as task}
|
|
104
|
+
<Table.Row class="cursor-pointer hover:bg-muted/40" onclick={() => onSelect?.(task)}>
|
|
105
|
+
<Table.Cell class="min-w-[240px]">
|
|
106
|
+
<div class="space-y-1">
|
|
107
|
+
<div class="font-medium text-foreground">{String(task.title ?? task.name ?? task.id)}</div>
|
|
108
|
+
{#if task.message}
|
|
109
|
+
<div class="text-xs text-muted-foreground">{String(task.message)}</div>
|
|
110
|
+
{/if}
|
|
111
|
+
{#if showProgress && typeof task.progress === 'number' && String(task.status).toLowerCase() === 'running'}
|
|
112
|
+
<TaskProgressBar value={Number(task.progress)} />
|
|
113
|
+
{/if}
|
|
114
|
+
</div>
|
|
115
|
+
</Table.Cell>
|
|
116
|
+
<Table.Cell>
|
|
117
|
+
<TaskStatusBadge status={String(task.status ?? 'pending')} />
|
|
118
|
+
</Table.Cell>
|
|
119
|
+
<Table.Cell class="text-xs text-muted-foreground">
|
|
120
|
+
{formatDate(task.updatedAt ?? task.createdAt)}
|
|
121
|
+
</Table.Cell>
|
|
122
|
+
{#if showActions}
|
|
123
|
+
<Table.Cell class="text-right">
|
|
124
|
+
<div class="flex justify-end gap-2">
|
|
125
|
+
{#if taskProvider?.retry && ['failed', 'error'].includes(String(task.status).toLowerCase())}
|
|
126
|
+
<RetryTaskButton taskId={task.id} {taskProvider} />
|
|
127
|
+
{/if}
|
|
128
|
+
{#if taskProvider?.cancel && ['pending', 'queued', 'running', 'processing'].includes(String(task.status).toLowerCase())}
|
|
129
|
+
<CancelTaskButton taskId={task.id} {taskProvider} />
|
|
130
|
+
{/if}
|
|
131
|
+
<Button variant="ghost" size="sm" onclick={(e) => { e.stopPropagation(); onSelect?.(task); }}>
|
|
132
|
+
{t('task.detailsAction')}
|
|
133
|
+
</Button>
|
|
134
|
+
</div>
|
|
135
|
+
</Table.Cell>
|
|
136
|
+
{/if}
|
|
137
|
+
</Table.Row>
|
|
138
|
+
{:else}
|
|
139
|
+
<Table.Row>
|
|
140
|
+
<Table.Cell colspan={showActions ? 4 : 3} class="h-24 text-center text-muted-foreground">
|
|
141
|
+
{resolvedEmptyText}
|
|
142
|
+
</Table.Cell>
|
|
143
|
+
</Table.Row>
|
|
144
|
+
{/each}
|
|
145
|
+
</Table.Body>
|
|
146
|
+
</Table.Root>
|
|
147
|
+
</div>
|
|
148
|
+
{/if}
|
|
149
|
+
</Card.Content>
|
|
150
|
+
</Card.Root>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { TaskProvider, TaskRecord } from '@svadmin/core';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
tasks?: TaskRecord[];
|
|
4
|
+
taskProvider?: TaskProvider<TaskRecord>;
|
|
5
|
+
params?: Record<string, unknown>;
|
|
6
|
+
dlq?: boolean;
|
|
7
|
+
title?: string;
|
|
8
|
+
emptyText?: string;
|
|
9
|
+
showActions?: boolean;
|
|
10
|
+
showProgress?: boolean;
|
|
11
|
+
useProviderData?: boolean;
|
|
12
|
+
queryOptions?: {
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
staleTime?: number;
|
|
15
|
+
gcTime?: number;
|
|
16
|
+
refetchOnWindowFocus?: boolean;
|
|
17
|
+
refetchInterval?: number | false;
|
|
18
|
+
refetchIntervalInBackground?: boolean;
|
|
19
|
+
};
|
|
20
|
+
onSelect?: (task: TaskRecord) => void;
|
|
21
|
+
};
|
|
22
|
+
declare const TaskList: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
23
|
+
type TaskList = ReturnType<typeof TaskList>;
|
|
24
|
+
export default TaskList;
|