Skip to content

Commit ea7001e

Browse files
committed
progress[5]
1 parent 43b1913 commit ea7001e

5 files changed

Lines changed: 212 additions & 11 deletions

File tree

.woodpecker/buildRelease.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

.woodpecker/buildSlackNotify.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

.woodpecker/release.yml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

custom/JobInfoPopup.vue

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
<template>
2-
<div class="flex flex-col ">
3-
<div class="flex flex-col">
2+
<div class="flex flex-col w-full min-w-96">
43
<div class="flex items-center mb-1">
54
<h2 class="text-lg font-semibold">{{ job.name }}</h2>
65
<p class="ml-2 text-xs text-gray-600 h-full"> {{ getTimeAgoString(new Date(job.createdAt)) }}</p>
76
<p class="ml-auto text-gray-800 h-full"> {{ t('Progress:') }} <span class="font-semibold" >{{ job.progress }}%</span></p>
87
<StateToIcon :job="job" />
98
</div>
10-
<div class="flex items-center gap-4">
9+
<div class="flex items-center gap-4 w-full">
1110
<ProgressBar
1211
:current-value="job.progress"
1312
:max-value="100"
@@ -21,15 +20,14 @@
2120
<Button class="h-8" v-if="job.status === 'IN_PROGRESS'" @click="cancelJob"> {{ t('Cancel') }} </Button>
2221
</div>
2322
</div>
24-
{{ job.customComponent }}
25-
{{ getCustomComponent(job.customComponent) }}
2623
<component
2724
v-if="job.customComponent"
2825
class="mt-4"
2926
:is="getCustomComponent(job.customComponent)"
3027
:meta="job.customComponent"
28+
:getJobTasks="getJobTasks"
29+
:job="job"
3130
/>
32-
</div>
3331
</template>
3432

3533

@@ -41,7 +39,6 @@ import { getTimeAgoString, callAdminForthApi, getCustomComponent} from '@/utils'
4139
import { useI18n } from 'vue-i18n';
4240
import StateToIcon from './StateToIcon.vue';
4341
import { useAdminforth } from '@/adminforth';
44-
import { get } from 'node:http';
4542
4643
4744
const { t } = useI18n();
@@ -81,4 +78,32 @@ async function cancelJob() {
8178
console.error('Error canceling job:', error);
8279
}
8380
}
81+
82+
83+
84+
async function getJobTasks(limit: number = 10, offset: number = 0): Promise<{state: Record<string, any>, status: string}[]> {
85+
try {
86+
const res = await callAdminForthApi({
87+
path: `/plugin/${props.meta.pluginInstanceId}/get-tasks`,
88+
method: 'POST',
89+
body: {
90+
jobId: props.job.id,
91+
limit,
92+
offset,
93+
},
94+
});
95+
if (res.ok) {
96+
return res.tasks;
97+
} else {
98+
console.error('Error fetching job tasks:', res.error);
99+
return [];
100+
}
101+
} catch (error) {
102+
console.error('Error fetching job tasks:', error);
103+
return [];
104+
}
105+
}
106+
107+
108+
84109
</script>

index.ts

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export default class extends AdminForthPlugin {
1919
private taskHandlers: Record<string, taskHandlerType> = {};
2020
private jobCustomComponents: Record<string, AdminForthComponentDeclarationFull> = {};
2121
private jobParallelLimits: Record<string, number> = {};
22+
private levelDbInstances: Record<string, Level> = {};
2223

2324
constructor(options: PluginOptions) {
2425
super(options, import.meta.url);
@@ -154,6 +155,7 @@ export default class extends AdminForthPlugin {
154155

155156
//create a level db instance for the job with name as jobId
156157
const jobLevelDb = new Level(`${this.options.levelDbPath || './background-jobs-dbs/'}job_${jobId}`, { valueEncoding: 'json' });
158+
this.levelDbInstances[jobId] = jobLevelDb;
157159

158160
const limit2 = pLimit(parrallelLimit);
159161
const createTaskRecordsPromises = tasks.map((task, index) => {
@@ -186,11 +188,10 @@ export default class extends AdminForthPlugin {
186188
afLogger.info(`Job ${jobId} was cancelled. Skipping task ${taskIndex}.`);
187189
return;
188190
}
189-
const currentJobRecord = await this.adminforth.resource(this.getResourceId()).get(Filters.EQ(this.getResourcePk(), jobId));
190-
const currentJobStatus = currentJobRecord[this.options.statusField];
191+
const currentJobStatus = await this.getLastJobStatus(jobId);
191192

192193
if (currentJobStatus === 'CANCELLED') {
193-
lastJobStatus = 'CANCELLED';
194+
lastJobStatus = currentJobStatus;
194195
afLogger.info(`Job ${jobId} was cancelled. Skipping task ${taskIndex}.`);
195196
return;
196197
}
@@ -204,21 +205,31 @@ export default class extends AdminForthPlugin {
204205
}
205206

206207
await this.setLevelDbTaskStatusField(jobLevelDb, taskIndex.toString(), 'IN_PROGRESS');
208+
this.adminforth.websocket.publish(`/background-jobs-task-update/${jobId}`, { taskIndex, status: "IN_PROGRESS" });
207209

208210
//handling the task
209211
try {
210212
await handleTask({ setTaskStateField, getTaskStateField });
211213

212214
//Set task status to completed in level db
213215
await this.setLevelDbTaskStatusField(jobLevelDb, taskIndex.toString(), 'DONE');
216+
this.adminforth.websocket.publish(`/background-jobs-task-update/${jobId}`, { taskIndex, status: "DONE" });
214217
} catch (error) {
215218
afLogger.error(`Error in handling task ${taskIndex} of job ${jobId}: ${error}`, );
216219
await this.setLevelDbTaskStatusField(jobLevelDb, taskIndex.toString(), 'FAILED');
220+
this.adminforth.websocket.publish(`/background-jobs-task-update/${jobId}`, { taskIndex, status: "FAILED" });
217221
failedTasks++;
218222
return;
219223
} finally {
220224
//Update progress
221-
completedTasks = await this.handleFinishTask(completedTasks, totalTasks, jobId);
225+
const currentJobStatus = await this.getLastJobStatus(jobId);
226+
if (currentJobStatus === 'CANCELLED') {
227+
lastJobStatus = currentJobStatus;
228+
afLogger.debug(`Job ${jobId} was cancelled during processing of task ${taskIndex}. Progress will not be updated.`);
229+
return;
230+
}
231+
232+
completedTasks = await this.handleFinishTask(completedTasks, totalTasks, jobId);
222233
}
223234
}
224235

@@ -241,6 +252,11 @@ export default class extends AdminForthPlugin {
241252
}
242253
}
243254

255+
private async getLastJobStatus(jobId: string): Promise<string> {
256+
const currentJobRecord = await this.adminforth.resource(this.getResourceId()).get(Filters.EQ(this.getResourcePk(), jobId));
257+
const currentJobStatus = currentJobRecord[this.options.statusField];
258+
return currentJobStatus;
259+
}
244260

245261
private async handleFinishTask(completedTasks: number, totalTasks: number, jobId: string, wasTaskSkipped: boolean = false) {
246262
completedTasks++;
@@ -254,11 +270,14 @@ export default class extends AdminForthPlugin {
254270
this.adminforth.websocket.publish('/background-jobs', { jobId, progress });
255271
return completedTasks;
256272
}
273+
274+
257275
private async runProcessingUnfinishedTasks(
258276
job: Record<string, any>
259277
) {
260278
const levelDbPath = `${this.options.levelDbPath || './background-jobs-dbs/'}job_${job[this.getResourcePk()]}`;
261279
const jobLevelDb = new Level(levelDbPath, { valueEncoding: 'json' });
280+
this.levelDbInstances[job[this.getResourcePk()]] = jobLevelDb;
262281
const jobHandlerName = job[this.options.jobHandlerField];
263282
const handleTask: taskHandlerType = this.taskHandlers[jobHandlerName];
264283
if (!handleTask) {
@@ -394,6 +413,47 @@ export default class extends AdminForthPlugin {
394413
}
395414
}
396415
});
416+
417+
server.endpoint({
418+
method: 'POST',
419+
path: `/plugin/${this.pluginInstanceId}/get-tasks`,
420+
handler: async ({ body }) => {
421+
const { jobId, limit, offset } = body;
422+
const levelDbPath = `${this.options.levelDbPath || './background-jobs-dbs/'}job_${jobId}`;
423+
let jobLevelDb: Level;
424+
if (this.levelDbInstances[jobId]) {
425+
jobLevelDb = this.levelDbInstances[jobId];
426+
} else {
427+
try {
428+
jobLevelDb = new Level(levelDbPath, { valueEncoding: 'json' });
429+
} catch (error) {
430+
return { ok: false, message: `Failed to access tasks for job with id ${jobId}.` };
431+
}
432+
}
433+
const tasks = [];
434+
let taskIndex = 0 + offset;
435+
while (true) {
436+
if (limit && tasks.length >= limit) {
437+
break;
438+
}
439+
const taskData = await jobLevelDb.get(taskIndex.toString());
440+
if (!taskData) {
441+
break;
442+
}
443+
let parsedTaskData: { state: Record<string, any>, status: TaskStatus };
444+
try {
445+
parsedTaskData = JSON.parse(taskData);
446+
} catch (error) {
447+
afLogger.error(`Error parsing task data for task ${taskIndex} of job ${jobId}: ${error}`);
448+
taskIndex++;
449+
continue;
450+
}
451+
tasks.push(parsedTaskData);
452+
taskIndex++;
453+
}
454+
return { ok: true, tasks };
455+
}
456+
});
397457
}
398458

399459
}

0 commit comments

Comments
 (0)