@adminforth/background-jobs 1.0.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.
@@ -0,0 +1,13 @@
1
+
2
+ #!/bin/bash
3
+
4
+ # write npm run output both to console and to build.log
5
+ npm run build 2>&1 | tee build.log
6
+ build_status=${PIPESTATUS[0]}
7
+
8
+ # if exist status from the npm run build is not 0
9
+ # then exit with the status code from the npm run build
10
+ if [ $build_status -ne 0 ]; then
11
+ echo "Build failed. Exiting with status code $build_status"
12
+ exit $build_status
13
+ fi
@@ -0,0 +1,46 @@
1
+ #!/bin/sh
2
+
3
+ set -x
4
+
5
+ COMMIT_SHORT_SHA=$(echo $CI_COMMIT_SHA | cut -c1-8)
6
+
7
+ STATUS=${1}
8
+
9
+
10
+ if [ "$STATUS" = "success" ]; then
11
+ MESSAGE="Did a build without issues on \`$CI_REPO_NAME/$CI_COMMIT_BRANCH\`. Commit: _${CI_COMMIT_MESSAGE}_ (<$CI_COMMIT_URL|$COMMIT_SHORT_SHA>)"
12
+
13
+ curl -s -X POST -H "Content-Type: application/json" -d '{
14
+ "username": "'"$CI_COMMIT_AUTHOR"'",
15
+ "icon_url": "'"$CI_COMMIT_AUTHOR_AVATAR"'",
16
+ "attachments": [
17
+ {
18
+ "mrkdwn_in": ["text", "pretext"],
19
+ "color": "#36a64f",
20
+ "text": "'"$MESSAGE"'"
21
+ }
22
+ ]
23
+ }' "$DEVELOPERS_SLACK_WEBHOOK"
24
+ exit 0
25
+ fi
26
+ export BUILD_LOG=$(cat ./build.log)
27
+
28
+ BUILD_LOG=$(echo $BUILD_LOG | sed 's/"/\\"/g')
29
+
30
+ MESSAGE="Broke \`$CI_REPO_NAME/$CI_COMMIT_BRANCH\` with commit _${CI_COMMIT_MESSAGE}_ (<$CI_COMMIT_URL|$COMMIT_SHORT_SHA>)"
31
+ CODE_BLOCK="\`\`\`$BUILD_LOG\n\`\`\`"
32
+
33
+ echo "Sending slack message to developers $MESSAGE"
34
+ # Send the message
35
+ curl -sS -X POST -H "Content-Type: application/json" -d '{
36
+ "username": "'"$CI_COMMIT_AUTHOR"'",
37
+ "icon_url": "'"$CI_COMMIT_AUTHOR_AVATAR"'",
38
+ "attachments": [
39
+ {
40
+ "mrkdwn_in": ["text", "pretext"],
41
+ "color": "#8A1C12",
42
+ "text": "'"$CODE_BLOCK"'",
43
+ "pretext": "'"$MESSAGE"'"
44
+ }
45
+ ]
46
+ }' "$DEVELOPERS_SLACK_WEBHOOK" 2>&1
@@ -0,0 +1,57 @@
1
+ clone:
2
+ git:
3
+ image: woodpeckerci/plugin-git
4
+ settings:
5
+ partial: false
6
+ depth: 5
7
+
8
+ steps:
9
+ init-secrets:
10
+ when:
11
+ - event: push
12
+ image: infisical/cli
13
+ environment:
14
+ INFISICAL_TOKEN:
15
+ from_secret: VAULT_TOKEN
16
+ commands:
17
+ - infisical export --domain https://vault.devforth.io/api --format=dotenv-export --env="prod" > /woodpecker/deploy.vault.env
18
+
19
+ build:
20
+ image: node:20
21
+ when:
22
+ - event: push
23
+ commands:
24
+ - apt update && apt install -y rsync
25
+ - . /woodpecker/deploy.vault.env
26
+ - npm clean-install
27
+ - /bin/bash ./.woodpecker/buildRelease.sh
28
+ - npm audit signatures
29
+
30
+ release:
31
+ image: node:20
32
+ when:
33
+ - event:
34
+ - push
35
+ branch:
36
+ - main
37
+ commands:
38
+ - . /woodpecker/deploy.vault.env
39
+ - npx semantic-release
40
+
41
+ slack-on-failure:
42
+ image: curlimages/curl
43
+ when:
44
+ - event: push
45
+ status: [failure]
46
+ commands:
47
+ - . /woodpecker/deploy.vault.env
48
+ - /bin/sh ./.woodpecker/buildSlackNotify.sh failure
49
+
50
+ slack-on-success:
51
+ image: curlimages/curl
52
+ when:
53
+ - event: push
54
+ status: [success]
55
+ commands:
56
+ - . /woodpecker/deploy.vault.env
57
+ - /bin/sh ./.woodpecker/buildSlackNotify.sh success
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Devforth.io
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/build.log ADDED
@@ -0,0 +1,15 @@
1
+
2
+ > @adminforth/background-jobs@1.0.1 build
3
+ > tsc && rsync -av --exclude 'node_modules' custom dist/
4
+
5
+ sending incremental file list
6
+ custom/
7
+ custom/JobInfoPopup.vue
8
+ custom/JobsList.vue
9
+ custom/NavbarJobs.vue
10
+ custom/StateToIcon.vue
11
+ custom/tsconfig.json
12
+ custom/utils.ts
13
+
14
+ sent 11,198 bytes received 134 bytes 22,664.00 bytes/sec
15
+ total size is 10,720 speedup is 0.95
@@ -0,0 +1,110 @@
1
+ <template>
2
+ <div class="flex flex-col w-full min-w-96">
3
+ <div class="flex items-center mb-1">
4
+ <h2 class="text-lg font-semibold">{{ job.name }}</h2>
5
+ <p class="ml-2 text-xs text-gray-600 h-full"> {{ getTimeAgoString(new Date(job.createdAt)) }}</p>
6
+ <p class="ml-auto text-gray-800 h-full"> {{ t('Progress:') }} <span class="font-semibold" >{{ job.progress }}%</span></p>
7
+ <StateToIcon :job="job" />
8
+ </div>
9
+ <div class="flex items-center gap-4 w-full">
10
+ <ProgressBar
11
+ :current-value="job.progress"
12
+ :max-value="100"
13
+ :min-value="0"
14
+ :showAnimation="job.status === 'IN_PROGRESS'"
15
+ :showLabels="false"
16
+ :showValues="false"
17
+ :show-progress="false"
18
+ :height="6"
19
+ />
20
+ <Button class="h-8" v-if="job.status === 'IN_PROGRESS'" @click="cancelJob"> {{ t('Cancel') }} </Button>
21
+ </div>
22
+ </div>
23
+ <component
24
+ v-if="job.customComponent"
25
+ class="mt-4"
26
+ :is="getCustomComponent(job.customComponent)"
27
+ :meta="job.customComponent"
28
+ :getJobTasks="getJobTasks"
29
+ :job="job"
30
+ />
31
+ </template>
32
+
33
+
34
+
35
+
36
+ <script setup lang="ts">
37
+ import type { IJob } from './utils';
38
+ import { ProgressBar, Button } from '@/afcl';
39
+ import { getTimeAgoString, callAdminForthApi, getCustomComponent} from '@/utils';
40
+ import { useI18n } from 'vue-i18n';
41
+ import StateToIcon from './StateToIcon.vue';
42
+ import { useAdminforth } from '@/adminforth';
43
+
44
+
45
+ const { t } = useI18n();
46
+
47
+ const adminforth = useAdminforth();
48
+
49
+ const props = defineProps<{
50
+ job: IJob;
51
+ meta: {
52
+ pluginInstanceId: string;
53
+ };
54
+ }>();
55
+
56
+ async function cancelJob() {
57
+ // Implement job cancellation logic here
58
+ const isConfirmed = await adminforth.confirm({ message: t('Are you sure you want to cancel this job?') });
59
+ if (!isConfirmed) {
60
+ return;
61
+ }
62
+ const failedToCancelText = t('Failed to cancel job');
63
+ console.log(`Canceling job with ID: ${props.job.id}`);
64
+ try {
65
+ const res = await callAdminForthApi({
66
+ path: `/plugin/${props.meta.pluginInstanceId}/cancel-job`,
67
+ method: 'POST',
68
+ body: {
69
+ jobId: props.job.id,
70
+ },
71
+ });
72
+ if (res.ok) {
73
+ adminforth.alert({ message: t('Job cancelled successfully'), variant: 'success' });
74
+ } else {
75
+ adminforth.alert({ message: failedToCancelText, variant: 'danger' });
76
+ }
77
+ } catch (error) {
78
+ adminforth.alert({ message: failedToCancelText, variant: 'danger' });
79
+ console.error('Error canceling job:', error);
80
+ }
81
+ }
82
+
83
+
84
+
85
+ async function getJobTasks(limit: number = 10, offset: number = 0): Promise<{state: Record<string, any>, status: string}[]> {
86
+ try {
87
+ const res = await callAdminForthApi({
88
+ path: `/plugin/${props.meta.pluginInstanceId}/get-tasks`,
89
+ method: 'POST',
90
+ body: {
91
+ jobId: props.job.id,
92
+ limit,
93
+ offset,
94
+ },
95
+ });
96
+ if (res.ok) {
97
+ return res.tasks;
98
+ } else {
99
+ console.error('Error fetching job tasks:', res.error);
100
+ return [];
101
+ }
102
+ } catch (error) {
103
+ console.error('Error fetching job tasks:', error);
104
+ return [];
105
+ }
106
+ }
107
+
108
+
109
+
110
+ </script>
@@ -0,0 +1,53 @@
1
+ <template>
2
+ <div class="w-1vw md:w-64 bg-white border border-gray-200 rounded-md">
3
+ <Modal v-for="job in props.jobs" :key="job.id" :beforeOpenFunction="props.closeDropdown">
4
+ <template #trigger>
5
+ <div class="flex items-center w-full px-4 py-3 border-b border-gray-200 hover:bg-gray-50 transition-colors">
6
+ <div class="flex flex-col w-full max-w-48">
7
+ <p class="flex gap-2 items-end justify-between text-nowrap">
8
+ <span class="text-sm h-full text truncate">{{ job.name }}</span>
9
+ <span class="text-xs text-gray-600">{{ getTimeAgoString(new Date(job.createdAt)) }}</span>
10
+ </p>
11
+ <ProgressBar
12
+ class="mt-1"
13
+ :current-value="job.progress"
14
+ :max-value="100"
15
+ :min-value="0"
16
+ :showAnimation="job.status === 'IN_PROGRESS'"
17
+ :showLabels="false"
18
+ :showValues="false"
19
+ :show-progress="false"
20
+ />
21
+ </div>
22
+ <StateToIcon :job="job" />
23
+ </div>
24
+ </template>
25
+ <div>
26
+ <JobInfoPopup
27
+ :job="job"
28
+ :meta="meta"
29
+ />
30
+ </div>
31
+ </Modal>
32
+
33
+ </div>
34
+ </template>
35
+
36
+
37
+ <script setup lang="ts">
38
+ import type { IJob } from './utils';
39
+ import { getTimeAgoString } from '@/utils';
40
+ import { ProgressBar, Modal } from '@/afcl';
41
+ import JobInfoPopup from './JobInfoPopup.vue';
42
+ import StateToIcon from './StateToIcon.vue';
43
+
44
+ const props = defineProps<{
45
+ jobs: IJob[];
46
+ closeDropdown: () => void;
47
+ meta: {
48
+ pluginInstanceId: string;
49
+ };
50
+ }>();
51
+
52
+
53
+ </script>
@@ -0,0 +1,154 @@
1
+ <template>
2
+ <div ref="dropdownRef">
3
+ <div class="cursor-pointer hover:scale-110 transition-transform" @click="isDropdownOpen = !isDropdownOpen">
4
+ <div v-if="isAlLeastOneJobRunning" class="relative">
5
+ <div class="loader "></div>
6
+ <div class="absolute -bottom-1 -right-1 rounded-full bg-lightPrimary w-4 h-4 text-xs flex items-center justify-center text-white"> {{ jobsCount }}</div>
7
+ </div>
8
+ <div class="flex items-center justify-center" v-else-if="jobs.length > 0">
9
+ <Tooltip>
10
+ <IconCheckCircleOutline class="w-8 h-8 text-green-500" />
11
+ <template #tooltip>
12
+ {{ t('All jobs completed') }}
13
+ </template>
14
+ </Tooltip>
15
+ </div>
16
+ </div>
17
+ <Transition
18
+ enter-active-class="transition ease-out duration-200"
19
+ enter-from-class="opacity-0 scale-95"
20
+ enter-to-class="opacity-100 scale-100"
21
+ leave-active-class="transition ease-in duration-150"
22
+ leave-from-class="opacity-100 scale-100"
23
+ leave-to-class="opacity-0 scale-95"
24
+ >
25
+ <div v-show="isDropdownOpen" class="absolute right-28 top-14 md:top-12 rounded z-10 overflow-y-auto max-h-96 ">
26
+ <JobsList
27
+ :closeDropdown="() => isDropdownOpen = false"
28
+ :jobs="jobs"
29
+ :meta="meta"
30
+ />
31
+ </div>
32
+ </Transition>
33
+ </div>
34
+
35
+
36
+ </template>
37
+
38
+
39
+
40
+ <script setup lang="ts">
41
+ import type { AdminUser } from 'adminforth';
42
+ import { onMounted, onUnmounted, ref, computed } from 'vue';
43
+ import { IconCheckCircleOutline } from '@iconify-prerendered/vue-flowbite';
44
+ import { Tooltip, Modal } from '@/afcl';
45
+ import { useI18n } from 'vue-i18n';
46
+ import JobsList from './JobsList.vue';
47
+ import type { IJob } from './utils';
48
+ import { callAdminForthApi } from '@/utils';
49
+ import websocket from '@/websocket';
50
+ import { onClickOutside } from '@vueuse/core'
51
+
52
+ const { t } = useI18n();
53
+
54
+ const props = defineProps<{
55
+ meta: {
56
+ pluginInstanceId: string;
57
+ };
58
+ adminUser: AdminUser;
59
+ }>();
60
+
61
+ const isDropdownOpen = ref(false);
62
+ const jobs = ref<IJob[]>([]);
63
+ const dropdownRef = ref<HTMLElement | null>(null);
64
+
65
+ onClickOutside(dropdownRef, () => {
66
+ isDropdownOpen.value = false;
67
+ });
68
+
69
+ const isAlLeastOneJobRunning = computed(() => {
70
+ return jobs.value.some(job => job.status === 'IN_PROGRESS');
71
+ })
72
+
73
+ const jobsCount = computed(() => {
74
+ return jobs.value.filter(job => job.status === 'IN_PROGRESS').length;
75
+ })
76
+
77
+
78
+
79
+ onMounted(async () => {
80
+ websocket.subscribe('/background-jobs', (data) => {
81
+ const jobIndex = jobs.value.findIndex(job => job.id === data.jobId);
82
+ if (jobIndex !== -1) {
83
+ if (data.status) {
84
+ jobs.value[jobIndex].status = data.status;
85
+ }
86
+ if (data.progress !== undefined) {
87
+ jobs.value[jobIndex].progress = data.progress;
88
+ }
89
+ } else {
90
+ jobs.value.unshift({
91
+ id: data.jobId,
92
+ name: data.name || 'Unknown Job',
93
+ status: data.status || 'IN_PROGRESS',
94
+ progress: data.progress || 0,
95
+ createdAt: data.createdAt || new Date().toISOString(),
96
+ customComponent: data.customComponent,
97
+ });
98
+ }
99
+ });
100
+
101
+
102
+ try {
103
+ const res = await callAdminForthApi({
104
+ path: `/plugin/${props.meta.pluginInstanceId}/get-list-of-jobs`,
105
+ method: 'POST',
106
+ });
107
+ jobs.value = res.jobs;
108
+ } catch (error) {
109
+ console.error('Error fetching jobs:', error);
110
+ }
111
+ });
112
+
113
+
114
+ onUnmounted(() => {
115
+ websocket.unsubscribe('/background-jobs');
116
+ });
117
+
118
+ </script>
119
+
120
+
121
+ <style scoped lang="scss">
122
+ .loader {
123
+ width: 28px;
124
+ aspect-ratio: 1;
125
+ border-radius: 50%;
126
+ --spinner-color: #1a56db;
127
+
128
+ background:
129
+ conic-gradient(
130
+ from 120deg,
131
+ var(--spinner-color) 0deg 40deg,
132
+ transparent 40deg
133
+ ),
134
+
135
+ conic-gradient(#ccc 0deg 360deg);
136
+
137
+ -webkit-mask: radial-gradient(
138
+ farthest-side,
139
+ transparent calc(100% - 6px),
140
+ #000 calc(100% - 5px)
141
+ );
142
+ mask: radial-gradient(
143
+ farthest-side,
144
+ transparent calc(100% - 6px),
145
+ #000 calc(100% - 5px)
146
+ );
147
+
148
+ animation: stepRotate 2s infinite;
149
+ }
150
+
151
+ @keyframes stepRotate {
152
+ to { transform: rotate(1turn); }
153
+ }
154
+ </style>
@@ -0,0 +1,40 @@
1
+ <template>
2
+ <Tooltip v-if="job.status === 'IN_PROGRESS'">
3
+ <Spinner class="w-5 h-5 ml-2" />
4
+ <template #tooltip>
5
+ {{ t('In progress') }}
6
+ </template>
7
+ </Tooltip>
8
+ <Tooltip v-else-if="job.status === 'DONE'">
9
+ <IconCheckCircleOutline class="w-6 h-6 ml-2 text-green-500" />
10
+ <template #tooltip>
11
+ {{ t('Done') }}
12
+ </template>
13
+ </Tooltip>
14
+ <Tooltip v-else-if="job.status === 'CANCELLED'">
15
+ <IconCloseCircleOutline class="w-6 h-6 ml-2 text-red-500" />
16
+ <template #tooltip>
17
+ {{ t('Cancelled') }}
18
+ </template>
19
+ </Tooltip>
20
+ <Tooltip v-else-if="job.status === 'DONE_WITH_ERRORS'">
21
+ <IconExclamationCircleOutline class="w-6 h-6 ml-2 text-yellow-500" />
22
+ <template #tooltip>
23
+ {{ t('Done with errors') }}
24
+ </template>
25
+ </Tooltip>
26
+ </template>
27
+
28
+
29
+ <script setup lang="ts">
30
+ import type { IJob } from './utils';
31
+ import { IconCheckCircleOutline, IconCloseCircleOutline, IconExclamationCircleOutline } from '@iconify-prerendered/vue-flowbite';
32
+ import { Spinner, Tooltip } from '@/afcl';
33
+ import { useI18n } from 'vue-i18n';
34
+
35
+ const { t } = useI18n();
36
+
37
+ const props = defineProps<{
38
+ job: IJob;
39
+ }>();
40
+ </script>
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".", // This should point to your project root
4
+ "paths": {
5
+ "@/*": [
6
+ "../node_modules/adminforth/dist/spa/src/*"
7
+ ],
8
+ "*": [
9
+ "../node_modules/adminforth/dist/spa/node_modules/*"
10
+ ],
11
+ "@@/*": [
12
+ "."
13
+ ]
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,9 @@
1
+ import type { AdminForthComponentDeclarationFull } from "adminforth";
2
+ export interface IJob {
3
+ id: string;
4
+ name: string;
5
+ status: 'IN_PROGRESS' | 'DONE' | 'DONE_WITH_ERRORS' | 'CANCELLED';
6
+ progress: number; // 0 to 100
7
+ createdAt: Date;
8
+ customComponent?: AdminForthComponentDeclarationFull;
9
+ }
@@ -0,0 +1,110 @@
1
+ <template>
2
+ <div class="flex flex-col w-full min-w-96">
3
+ <div class="flex items-center mb-1">
4
+ <h2 class="text-lg font-semibold">{{ job.name }}</h2>
5
+ <p class="ml-2 text-xs text-gray-600 h-full"> {{ getTimeAgoString(new Date(job.createdAt)) }}</p>
6
+ <p class="ml-auto text-gray-800 h-full"> {{ t('Progress:') }} <span class="font-semibold" >{{ job.progress }}%</span></p>
7
+ <StateToIcon :job="job" />
8
+ </div>
9
+ <div class="flex items-center gap-4 w-full">
10
+ <ProgressBar
11
+ :current-value="job.progress"
12
+ :max-value="100"
13
+ :min-value="0"
14
+ :showAnimation="job.status === 'IN_PROGRESS'"
15
+ :showLabels="false"
16
+ :showValues="false"
17
+ :show-progress="false"
18
+ :height="6"
19
+ />
20
+ <Button class="h-8" v-if="job.status === 'IN_PROGRESS'" @click="cancelJob"> {{ t('Cancel') }} </Button>
21
+ </div>
22
+ </div>
23
+ <component
24
+ v-if="job.customComponent"
25
+ class="mt-4"
26
+ :is="getCustomComponent(job.customComponent)"
27
+ :meta="job.customComponent"
28
+ :getJobTasks="getJobTasks"
29
+ :job="job"
30
+ />
31
+ </template>
32
+
33
+
34
+
35
+
36
+ <script setup lang="ts">
37
+ import type { IJob } from './utils';
38
+ import { ProgressBar, Button } from '@/afcl';
39
+ import { getTimeAgoString, callAdminForthApi, getCustomComponent} from '@/utils';
40
+ import { useI18n } from 'vue-i18n';
41
+ import StateToIcon from './StateToIcon.vue';
42
+ import { useAdminforth } from '@/adminforth';
43
+
44
+
45
+ const { t } = useI18n();
46
+
47
+ const adminforth = useAdminforth();
48
+
49
+ const props = defineProps<{
50
+ job: IJob;
51
+ meta: {
52
+ pluginInstanceId: string;
53
+ };
54
+ }>();
55
+
56
+ async function cancelJob() {
57
+ // Implement job cancellation logic here
58
+ const isConfirmed = await adminforth.confirm({ message: t('Are you sure you want to cancel this job?') });
59
+ if (!isConfirmed) {
60
+ return;
61
+ }
62
+ const failedToCancelText = t('Failed to cancel job');
63
+ console.log(`Canceling job with ID: ${props.job.id}`);
64
+ try {
65
+ const res = await callAdminForthApi({
66
+ path: `/plugin/${props.meta.pluginInstanceId}/cancel-job`,
67
+ method: 'POST',
68
+ body: {
69
+ jobId: props.job.id,
70
+ },
71
+ });
72
+ if (res.ok) {
73
+ adminforth.alert({ message: t('Job cancelled successfully'), variant: 'success' });
74
+ } else {
75
+ adminforth.alert({ message: failedToCancelText, variant: 'danger' });
76
+ }
77
+ } catch (error) {
78
+ adminforth.alert({ message: failedToCancelText, variant: 'danger' });
79
+ console.error('Error canceling job:', error);
80
+ }
81
+ }
82
+
83
+
84
+
85
+ async function getJobTasks(limit: number = 10, offset: number = 0): Promise<{state: Record<string, any>, status: string}[]> {
86
+ try {
87
+ const res = await callAdminForthApi({
88
+ path: `/plugin/${props.meta.pluginInstanceId}/get-tasks`,
89
+ method: 'POST',
90
+ body: {
91
+ jobId: props.job.id,
92
+ limit,
93
+ offset,
94
+ },
95
+ });
96
+ if (res.ok) {
97
+ return res.tasks;
98
+ } else {
99
+ console.error('Error fetching job tasks:', res.error);
100
+ return [];
101
+ }
102
+ } catch (error) {
103
+ console.error('Error fetching job tasks:', error);
104
+ return [];
105
+ }
106
+ }
107
+
108
+
109
+
110
+ </script>