@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.
- package/.woodpecker/buildRelease.sh +13 -0
- package/.woodpecker/buildSlackNotify.sh +46 -0
- package/.woodpecker/release.yml +57 -0
- package/LICENSE +21 -0
- package/build.log +15 -0
- package/custom/JobInfoPopup.vue +110 -0
- package/custom/JobsList.vue +53 -0
- package/custom/NavbarJobs.vue +154 -0
- package/custom/StateToIcon.vue +40 -0
- package/custom/tsconfig.json +16 -0
- package/custom/utils.ts +9 -0
- package/dist/custom/JobInfoPopup.vue +110 -0
- package/dist/custom/JobsList.vue +53 -0
- package/dist/custom/NavbarJobs.vue +154 -0
- package/dist/custom/StateToIcon.vue +40 -0
- package/dist/custom/tsconfig.json +16 -0
- package/dist/custom/utils.ts +9 -0
- package/dist/index.js +433 -0
- package/dist/types.js +1 -0
- package/index.ts +459 -0
- package/package.json +27 -0
- package/tsconfig.json +13 -0
- package/types.ts +15 -0
|
@@ -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
|
+
}
|
package/custom/utils.ts
ADDED
|
@@ -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>
|