Merge branch 'refs/heads/dev_1.0.0_beta2' into dev

This commit is contained in:
xlsea 2024-05-23 15:26:52 +08:00
commit 980be3e2ac
30 changed files with 447 additions and 295 deletions

2
.env
View File

@ -2,7 +2,7 @@ VITE_APP_TITLE=Snail Job
VITE_APP_DESC=A flexible, reliable, and fast platform for distributed task retry and distributed task scheduling.
VITE_APP_VERSION=3.1.0
VITE_APP_VERSION=1.0.0-beta2
# the prefix of the icon name
VITE_ICON_PREFIX=icon

61
README.md Normal file
View File

@ -0,0 +1,61 @@
<p align="center">
<a href="https://snailjob.opensnail.com">
<img alt="snail-job-Logo" src="public/favicon.svg" width="200px">
</a>
</p>
<p align="center">
🔥🔥🔥 灵活,可靠和快速的分布式任务重试和分布式任务调度平台<br> <br/>
</p>
<p align="center">
> ✅️ 可重放,可管控、为提高分布式业务系统一致性的分布式任务重试平台 <br/>
> ✅️ 支持秒级、可中断、可编排的高性能分布式任务调度平台
>
</p>
# 简介
> SnailJob 是一个灵活、可靠且高效的分布式任务重试和任务调度平台。其核心采用分区模式实现,具备高度可伸缩性和容错性的分布式系统。拥有完善的权限管理、强大的告警监控功能和友好的界面交互。欢迎大家接入并使用。
# 开源地址
- [snail-job](https://gitee.com/aizuda/snail-job.git)
- [snail-job-demo](https://gitee.com/opensnail/snail-job-demo.git)
# 预览地址
https://snailjob.opensnail.com/docs/preview.html
## 特别用户
<a href="http://aizuda.com/?from=mp" >
<img alt="snail-job-Logo" src="public/aizuda.png" width="200px">
</a>
<a href="https://plus-doc.dromara.org/#/">
<img alt="snail-job-Logo" src="public/ryp.png" width="300px" height="71">
</a>
## 相关链接
- [字节跳动: 如何优雅地重试](https://juejin.cn/post/6914091859463634951)
- [这款分布式重试组件,治好了我的重试强迫症!](https://juejin.cn/post/7249607108043145274)
- [系统简介](https://snailjob.opensnail.com/docs/introduce/preface.html)
## 快速入门
- [服务部署](https://snailjob.opensnail.com/docs/guide/service_deployment.html)
- [HelloWorld](https://snailjob.opensnail.com/docs/guide/hello_world.html)
## 期望
欢迎提出更好的意见,帮助完善 snail-job
## 版权
Aizuda/SnailJob 采用[APACHE LICENSE 2.0](https://gitee.com/aizuda/snail-job/blob/master/LICENSE)
开源协议,您在使用过程中,需要注意以下几点:
1. 不得修改产品相关代码的源码头注释和出处;
2. 不得进行简单修改包装声称是自己的产品;
3. 不得应用于危害国家安全、荣誉和利益的行为,不能以任何形式用于非法目的;

View File

@ -113,5 +113,5 @@
"*": "eslint --fix"
},
"officialWebsite": "https://snailjob.opensnail.com",
"website": "https://snailjob.opensnail.com/pages/78ba75/"
"website": "https://snailjob.opensnail.com/docs/preview.html"
}

BIN
public/aizuda.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
public/ryp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -22,8 +22,6 @@ const interval = ref<number>(props.backOff === 2 || props.backOff === 4 ? Number
/** 保存 `CRON表达式` 类型的 表达式 */
const cron = ref<string>(props.backOff === 3 ? model.value! : '* * * * * ?');
const delayLevelDesc = ref('10s,15s,30s,35s,40s,50s,1m,2m,4m,6m,8m,10m,20m,40m,1h,2h,3h,4h,5h,6h,7h,8h,9h,10h,11h,12h');
/** 监视 触发间隔 变化 */
watch(
interval,
@ -70,7 +68,6 @@ watch(
:placeholder="$t('page.retryScene.form.triggerInterval')"
clearable
/>
<NInput v-else v-model:value="delayLevelDesc" type="textarea" :autosize="{ minRows: 1, maxRows: 3 }" readonly />
</template>
<style scoped></style>

View File

@ -213,6 +213,15 @@ export const taskBatchStatusRecord: Record<Api.Common.TaskBatchStatus, App.I18n.
};
export const taskBatchStatusRecordOptions = transformRecordToNumberOption(taskBatchStatusRecord);
export const taskStatusRecord: Record<Api.Common.TaskStatus, App.I18n.I18nKey> = {
2: 'common.taskStatus.items.running',
3: 'common.taskStatus.items.success',
4: 'common.taskStatus.items.fail',
5: 'common.taskStatus.items.stop',
6: 'common.taskStatus.items.cancel'
};
export const taskStatusRecordOptions = transformRecordToNumberOption(taskStatusRecord);
export const operationReasonRecord: Record<Api.Common.OperationReason, App.I18n.I18nKey> = {
0: 'common.jobOperationReason.items.none',
1: 'common.jobOperationReason.items.taskExecutionTimeout',

View File

@ -133,6 +133,17 @@ const local: App.I18n.Schema = {
cancel: 'Cancel'
}
},
taskStatus: {
label: 'Task status',
form: 'Please enter task status',
items: {
running: 'Running',
success: 'Success',
fail: 'Fail',
stop: 'Stop',
cancel: 'Cancel'
}
},
jobOperationReason: {
label: 'Job operation reason',
form: 'Please enter job operation reason',
@ -513,7 +524,9 @@ const local: App.I18n.Schema = {
groupPartition: 'Please select Group partition',
initScene: 'Initialized scene',
collapseCommon: 'Common config',
collapseRetry: 'Retry config'
collapseRetry: 'Retry config',
groupNameRule:
'Group name: Must be between 1 and 64 characters in length. Format: numbers, letters, underscores, or hyphens.'
},
idMode: {
idWorker: 'Id Workder',
@ -866,7 +879,7 @@ const local: App.I18n.Schema = {
deadlineRequest: 'Please enter Call chain timeout(ms)',
routeKey: 'Please enter Routing strategy',
backOff: 'Please enter Backoff strategy',
sceneName2: 'Scene name: 1~64 characters. allowing: digit, letters and underscore.'
sceneName2: 'Scene name: 1~64 characters. allowing: digit, letters, underscore or hyphens..'
},
addScene: 'Add Scenes',
editScene: 'Add Scenes',
@ -1004,6 +1017,7 @@ const local: App.I18n.Schema = {
title: 'Job task list',
id: 'ID',
groupName: 'Group name',
taskStatus: 'Status',
clientInfo: 'Client address',
argsStr: 'Argument string',
resultMessage: 'Result message',

View File

@ -133,6 +133,17 @@ const local: App.I18n.Schema = {
cancel: '取消'
}
},
taskStatus: {
label: '状态',
form: '请选择状态',
items: {
running: '运行中',
success: '处理成功',
fail: '处理失败',
stop: '任务停止',
cancel: '取消'
}
},
jobOperationReason: {
label: '操作原因',
form: '请选择执行状态',
@ -521,7 +532,8 @@ const local: App.I18n.Schema = {
groupPartition: '分区',
initScene: '初始化场景',
collapseCommon: '通用配置',
collapseRetry: '重试配置'
collapseRetry: '重试配置',
groupNameRule: '组名称: 仅支持长度为:1~64位字符.格式为:数字、字母、下划线、短横线。'
},
idMode: {
idWorker: '雪花算法',
@ -874,7 +886,7 @@ const local: App.I18n.Schema = {
deadlineRequest: '请输入调用链超时时间(毫秒)',
routeKey: '请输入路由策略',
backOff: '请输入退避策略',
sceneName2: '场景名称: 仅支持长度为:1~64位字符.格式为:数字、字母、下划线。'
sceneName2: '场景名称: 仅支持长度为:1~64位字符.格式为:数字、字母、下划线和中横线。'
},
addScene: '新增场景',
editScene: '编辑场景',
@ -1011,6 +1023,7 @@ const local: App.I18n.Schema = {
title: 'JobTask 列表',
id: 'ID',
groupName: '组名称',
taskStatus: '状态',
clientInfo: '地址',
argsStr: '参数',
resultMessage: '结果',

View File

@ -170,8 +170,10 @@ async function initRoute(to: RouteLocationNormalized): Promise<RouteLocationRaw
const authStore = useAuthStore();
await authStore.getInfo();
const { data, error } = await fetchVersion();
if (!error) {
if (!error && data) {
localStg.set('version', data!);
} else {
localStg.remove('version');
}
}
}

15
src/typings/api.d.ts vendored
View File

@ -88,6 +88,9 @@ declare namespace Api {
/** 1、待处理 2、运行中 3、成功 4、失败 5、停止 6、取消 */
type TaskBatchStatus = 1 | 2 | 3 | 4 | 5 | 6;
/** 2、处理中 3、处理成功 4、处理失败、5、任务停止 6、取消 */
type TaskStatus = 2 | 3 | 4 | 5 | 6;
/**
* 1 2 3JOB已关闭 4 5 6 7 8 9 10 11 12
* 13 14
@ -197,10 +200,10 @@ declare namespace Api {
type DashboardLine = {
taskList: TaskList;
rankList: RankList[];
dashboardLineResponseDOList: DashboardLineResponseDOList[];
dashboardLineResponseDOList: DashboardLineResponseDO[];
};
type DashboardLineResponseDOList = {
type DashboardLineResponseDO = {
createDt: string;
total: number;
} & DashboardLineJob &
@ -209,7 +212,7 @@ declare namespace Api {
type DashboardLineJob = {
createDt: string;
total: number;
fail: number;
failNum: number;
stop: number;
cancel: number;
success: number;
@ -262,6 +265,8 @@ declare namespace Api {
*/
type DashboardLineMode = 'JOB' | 'WORKFLOW';
type TaskType = 'JOB' | 'RETRY' | 'WORKFLOW';
type DashboardLineParams = {
groupName?: string;
type: DashboardLineType;
@ -541,7 +546,7 @@ declare namespace Api {
/** notify-config */
type NotifyConfig = Common.CommonRecord<{
/** 组名称 */
groupName: string;
groupName: string | null;
/** 业务ID */
businessId: string | null;
/** 通知人id */
@ -995,6 +1000,8 @@ declare namespace Api {
createDt: string;
/** 任务批次 ID */
taskBatchId: string;
/** 任务状态 ID */
taskStatus: Common.TaskStatus;
}>;
/** jobTask search params */

13
src/typings/app.d.ts vendored
View File

@ -383,6 +383,17 @@ declare namespace App {
cancel: string;
};
};
taskStatus: {
label: string;
form: string;
items: {
running: string;
success: string;
fail: string;
stop: string;
cancel: string;
};
};
jobOperationReason: {
label: string;
form: string;
@ -660,6 +671,7 @@ declare namespace App {
initScene: string;
collapseCommon: string;
collapseRetry: string;
groupNameRule: string;
};
idMode: {
idWorker: string;
@ -1149,6 +1161,7 @@ declare namespace App {
title: string;
id: string;
groupName: string;
taskStatus: string;
clientInfo: string;
argsStr: string;
resultMessage: string;

View File

@ -27,10 +27,12 @@ const version = ref<string>(`v${localStg.get('version') || VITE_APP_VERSION}`);
const getVersion = async () => {
const { data, error } = await fetchVersion();
if (!error) {
if (!error && data) {
version.value = data;
localStg.set('version', data);
return;
}
localStg.remove('version');
};
getVersion();

View File

@ -1,41 +1,44 @@
<script setup lang="ts">
import { computed } from 'vue';
import { computed, ref } from 'vue';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import pkg from '~/package.json';
import { localStg } from '@/utils/storage';
const appStore = useAppStore();
const column = computed(() => (appStore.isMobile ? 1 : 2));
interface PkgJson {
name: string;
version: string;
dependencies: PkgVersionInfo[];
devDependencies: PkgVersionInfo[];
}
// interface PkgJson {
// name: string;
// dependencies: PkgVersionInfo[];
// devDependencies: PkgVersionInfo[];
// }
interface PkgVersionInfo {
name: string;
version: string;
}
// interface PkgVersionInfo {
// name: string;
// version: string;
// }
const { name, version, dependencies, devDependencies } = pkg;
const { VITE_APP_VERSION } = import.meta.env;
function transformVersionData(tuple: [string, string]): PkgVersionInfo {
const [$name, $version] = tuple;
return {
name: $name,
version: $version
};
}
// const { name, dependencies, devDependencies } = pkg;
//
// function transformVersionData(tuple: [string, string]): PkgVersionInfo {
// const [$name, $version] = tuple;
// return {
// name: $name,
// version: $version
// };
// }
const pkgJson: PkgJson = {
name,
version,
dependencies: Object.entries(dependencies).map(item => transformVersionData(item)),
devDependencies: Object.entries(devDependencies).map(item => transformVersionData(item))
};
const version = ref<string>(`${localStg.get('version') || VITE_APP_VERSION}`);
// const pkgJson: PkgJson = {
// name,
// dependencies: Object.entries(dependencies).map(item => transformVersionData(item)),
// devDependencies: Object.entries(devDependencies).map(item => transformVersionData(item))
// };
const latestBuildTime = BUILD_TIME;
</script>
@ -53,7 +56,7 @@ const latestBuildTime = BUILD_TIME;
</a>
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.about.projectInfo.version')">
<NTag type="primary">{{ pkgJson.version }}</NTag>
<NTag type="primary">{{ version }}</NTag>
</NDescriptionsItem>
<NDescriptionsItem :label="$t('page.about.projectInfo.githubLink')">
<a class="text-primary" :href="pkg.repository.githubUrl" target="_blank" rel="noopener noreferrer">

View File

@ -66,14 +66,31 @@ type RuleKey = Extract<
'groupName' | 'token' | 'groupStatus' | 'idGeneratorMode' | 'initScene' | 'groupPartition'
>;
const rules: Record<RuleKey, App.Global.FormRule> = {
groupName: defaultRequiredRule,
token: defaultRequiredRule,
groupStatus: defaultRequiredRule,
idGeneratorMode: defaultRequiredRule,
initScene: defaultRequiredRule,
groupPartition: defaultRequiredRule
};
// const rules: Record<RuleKey, App.Global.FormRule> = {
// groupName: defaultRequiredRule,
// token: defaultRequiredRule,
// groupStatus: defaultRequiredRule,
// idGeneratorMode: defaultRequiredRule,
// initScene: defaultRequiredRule,
// groupPartition: defaultRequiredRule
// };
const rules = {
groupName: [
defaultRequiredRule,
{
required: true,
pattern: /^[A-Za-z0-9_-]{1,64}$/,
trigger: 'change',
message: $t('page.groupConfig.form.groupNameRule')
}
],
token: [defaultRequiredRule],
groupStatus: [defaultRequiredRule],
idGeneratorMode: [defaultRequiredRule],
initScene: [defaultRequiredRule],
groupPartition: [defaultRequiredRule]
} satisfies Record<RuleKey, App.Global.FormRule[]>;
function handleUpdateModelWhenEdit() {
if (props.operateType === 'add') {

View File

@ -2,7 +2,7 @@
import { ref } from 'vue';
import { fetchCardCount } from '@/service/api';
import CardData from './modules/card-data.vue';
import RetryTab from './modules/retry-tab.vue';
import TaskTab from './modules/task-tab.vue';
const cardCount = ref<Api.Dashboard.CardCount>();
@ -20,7 +20,7 @@ getCardData();
<NSpace vertical :size="16">
<CardData v-model="cardCount!" />
<NCard :bordered="false" class="card-wrapper p-t-136px 2xl:p-t-0 lg:p-t-36px md:p-t-90px">
<RetryTab v-model="cardCount!" />
<TaskTab v-model="cardCount!" />
</NCard>
</NSpace>
</template>

View File

@ -91,6 +91,35 @@ interface CardData {
// eslint-disable-next-line complexity
const cardData = computed<CardData[]>(() => [
{
key: 'jobTask',
title: $t('page.home.jobTask'),
tip: $t('page.home.jobTaskTip'),
value: props.modelValue?.jobTask.totalNum ?? 0,
color: {
start: '#f5b386',
end: '#FFD6BA'
},
icon: 'ant-design:profile-outlined',
bottom: [
{
label: $t('common.success'),
value: props.modelValue?.jobTask.successNum ?? 0
},
{
label: $t('common.fail'),
value: props.modelValue?.jobTask.failNum ?? 0
},
{
label: $t('common.stop'),
value: props.modelValue?.jobTask.stopNum ?? 0
},
{
label: $t('common.cancel'),
value: props.modelValue?.jobTask.cancelNum ?? 0
}
]
},
{
key: 'retryTask',
title: $t('page.home.retryTask'),
@ -121,35 +150,6 @@ const cardData = computed<CardData[]>(() => [
}
]
},
{
key: 'jobTask',
title: $t('page.home.jobTask'),
tip: $t('page.home.jobTaskTip'),
value: props.modelValue?.jobTask.totalNum ?? 0,
color: {
start: '#f5b386',
end: '#FFD6BA'
},
icon: 'ant-design:profile-outlined',
bottom: [
{
label: $t('common.success'),
value: props.modelValue?.jobTask.successNum ?? 0
},
{
label: $t('common.fail'),
value: props.modelValue?.jobTask.failNum ?? 0
},
{
label: $t('common.stop'),
value: props.modelValue?.jobTask.stopNum ?? 0
},
{
label: $t('common.cancel'),
value: props.modelValue?.jobTask.cancelNum ?? 0
}
]
},
{
key: 'workflow',
title: $t('page.home.workflow'),
@ -268,7 +268,7 @@ function getGradientColor(color: CardData['color']) {
type="line"
color="#728bf9"
rail-color="#ebebeb"
:percentage="12.58 ?? 0"
:percentage="props.modelValue?.workFlowTask.successRate ?? 0"
indicator-text-color="#fff"
/>
<DardRetryChart v-else-if="item.key === 'retryTask'" :model-value="props.modelValue?.retryTaskBarList" />
@ -296,6 +296,6 @@ function getGradientColor(color: CardData['color']) {
}
:deep(.n-progress-icon--as-text) {
width: 53px !important;
width: 60px !important;
}
</style>

View File

@ -55,7 +55,7 @@ const { domRef, updateOptions } = useEcharts(() => ({
const getData = async () => {
await new Promise(resolve => {
setTimeout(resolve, 1);
setTimeout(resolve, 100);
});
if (!props.modelValue) {

View File

@ -5,16 +5,16 @@ import { useAppStore } from '@/store/modules/app';
import { useEcharts } from '@/hooks/common/echarts';
defineOptions({
name: 'LineRetryChart'
name: 'TaskLineChart'
});
interface Props {
type?: number;
type?: Api.Dashboard.TaskType;
modelValue: Api.Dashboard.DashboardLine;
}
const props = withDefaults(defineProps<Props>(), {
type: 0
type: 'JOB'
});
const appStore = useAppStore();
@ -32,7 +32,7 @@ const { domRef, updateOptions } = useEcharts(() => ({
},
legend: {
data:
props.type === 0
props.type === 'RETRY'
? [
$t('common.success'),
$t('common.running'),
@ -88,7 +88,7 @@ const { domRef, updateOptions } = useEcharts(() => ({
},
{
color: '#40e9c5',
name: props.type === 0 ? $t('common.running') : $t('common.fail'),
name: props.type === 'RETRY' ? $t('common.running') : $t('common.fail'),
type: 'line',
smooth: true,
stack: 'Total',
@ -118,7 +118,7 @@ const { domRef, updateOptions } = useEcharts(() => ({
},
{
color: '#b686d4',
name: props.type === 0 ? $t('page.manage.retryTask.status.maxRetryTimes') : $t('common.stop'),
name: props.type === 'RETRY' ? $t('page.manage.retryTask.status.maxRetryTimes') : $t('common.stop'),
type: 'line',
smooth: true,
stack: 'Total',
@ -148,7 +148,7 @@ const { domRef, updateOptions } = useEcharts(() => ({
},
{
color: '#ec6f6f',
name: props.type === 0 ? $t('page.manage.retryTask.status.pauseRetry') : $t('common.cancel'),
name: props.type === 'RETRY' ? $t('page.manage.retryTask.status.pauseRetry') : $t('common.cancel'),
type: 'line',
smooth: true,
stack: 'Total',
@ -190,37 +190,23 @@ const getData = () => {
opts.xAxis.data = props.modelValue?.dashboardLineResponseDOList.map(x => x.createDt);
opts.series[0].data = props.modelValue?.dashboardLineResponseDOList.map(x =>
opts.tabIndex === 0 ? x.successNum : x.success
opts.tabIndex === 'RETRY' ? x.successNum : x.success
);
opts.series[1].data = props.modelValue?.dashboardLineResponseDOList.map(x =>
opts.tabIndex === 0 ? x.runningNum : x.fail
opts.tabIndex === 'RETRY' ? x.runningNum : x.failNum
);
opts.series[2].data = props.modelValue?.dashboardLineResponseDOList.map(x =>
opts.tabIndex === 0 ? x.maxCountNum : x.stop
opts.tabIndex === 'RETRY' ? x.maxCountNum : x.stop
);
opts.series[3].data = props.modelValue?.dashboardLineResponseDOList.map(x =>
opts.tabIndex === 0 ? x.suspendNum : x.cancel
opts.tabIndex === 'RETRY' ? x.suspendNum : x.cancel
);
return opts;
});
};
watch(
() => appStore.locale,
() => {
getData();
}
);
watch(
() => props.modelValue,
() => {
getData();
}
);
watch(
() => props.type,
[() => appStore.locale, props],
() => {
getData();
},

View File

@ -7,16 +7,16 @@ import { useEcharts } from '@/hooks/common/echarts';
import { useThemeStore } from '@/store/modules/theme';
defineOptions({
name: 'PieRetryChart'
name: 'TaskPieChart'
});
interface Props {
type?: number;
type?: Api.Dashboard.TaskType;
modelValue: Api.Dashboard.CardCount;
}
const props = withDefaults(defineProps<Props>(), {
type: 0
type: 'JOB'
});
const appStore = useAppStore();
@ -94,16 +94,7 @@ function updateLocale() {
opts.tooltip.textStyle.color = originOpts.tooltip.textStyle.color;
opts.tooltip.backgroundColor = originOpts.tooltip.backgroundColor;
if (props.type === 0) {
const retryTask = props.modelValue.retryTask;
opts.series[0].data = [
{ name: $t('common.success'), value: retryTask.finishNum / retryTask.totalNum },
{ name: $t('common.running'), value: retryTask.runningNum / retryTask.totalNum },
{ name: $t('page.manage.retryTask.status.maxRetryTimes'), value: retryTask.maxCountNum / retryTask.totalNum },
{ name: $t('page.manage.retryTask.status.pauseRetry'), value: retryTask.suspendNum / retryTask.totalNum }
];
}
if (props.type === 1) {
if (props.type === 'JOB') {
const jobTask = props.modelValue.jobTask;
opts.series[0].data = [
{ name: $t('common.success'), value: jobTask.successNum / jobTask.totalNum },
@ -112,7 +103,18 @@ function updateLocale() {
{ name: $t('common.cancel'), value: jobTask.cancelNum / jobTask.totalNum }
];
}
if (props.type === 2) {
if (props.type === 'RETRY') {
const retryTask = props.modelValue.retryTask;
opts.series[0].data = [
{ name: $t('common.success'), value: retryTask.finishNum / retryTask.totalNum },
{ name: $t('common.running'), value: retryTask.runningNum / retryTask.totalNum },
{ name: $t('page.manage.retryTask.status.maxRetryTimes'), value: retryTask.maxCountNum / retryTask.totalNum },
{ name: $t('page.manage.retryTask.status.pauseRetry'), value: retryTask.suspendNum / retryTask.totalNum }
];
}
if (props.type === 'WORKFLOW') {
const workFlowTask = props.modelValue.workFlowTask;
opts.series[0].data = [
{ name: $t('common.success'), value: workFlowTask.successNum / workFlowTask.totalNum },

View File

@ -4,11 +4,11 @@ import type { DataTableColumns } from 'naive-ui';
import { $t } from '@/locales';
import { useAppStore } from '@/store/modules/app';
import { fetchAllGroupName, fetchJobLine, fetchRetryLine } from '@/service/api';
import LineRetryChart from './line-retry-chart.vue';
import PieRetryChart from './pie-retry-chart.vue';
import TaskLineChart from './task-line-chart.vue';
import TaskPieChart from './task-pie-chart.vue';
defineOptions({
name: 'RetryTab'
name: 'TaskTab'
});
interface Props {
@ -17,7 +17,7 @@ interface Props {
defineProps<Props>();
const type = ref(0);
const taskType = ref<Api.Dashboard.TaskType>('JOB');
const appStore = useAppStore();
const gap = computed(() => (appStore.isMobile ? 0 : 16));
const data = ref<Api.Dashboard.DashboardLine>();
@ -25,7 +25,8 @@ const groupOptions = ref();
const tabParams = ref<Api.Dashboard.DashboardLineParams>({
type: 'WEEK',
page: 1,
size: 6
size: 6,
mode: 'JOB'
});
const dateRange = ref<[number, number] | null>();
const formattedValue = ref<[string, string] | null>(
@ -34,15 +35,13 @@ const formattedValue = ref<[string, string] | null>(
const getData = async () => {
const { data: lineData, error } =
type.value === 0 ? await fetchRetryLine(tabParams.value) : await fetchJobLine(tabParams.value);
taskType.value === 'RETRY' ? await fetchRetryLine(tabParams.value) : await fetchJobLine(tabParams.value);
if (!error) {
data.value = lineData;
}
};
getData();
const getGroupNames = async () => {
const { data: groupNames, error } = await fetchAllGroupName();
@ -53,27 +52,17 @@ const getGroupNames = async () => {
}
};
getGroupNames();
watch(
() => tabParams.value,
() => {
getData();
},
{ deep: true }
);
const onUpdateTab = (value: string) => {
if (value === 'retryTask') {
type.value = 0;
tabParams.value.mode = undefined;
}
if (value === 'jobTask') {
type.value = 1;
taskType.value = 'JOB';
tabParams.value.mode = 'JOB';
}
if (value === 'retryTask') {
taskType.value = 'RETRY';
tabParams.value.mode = undefined;
}
if (value === 'workflow') {
type.value = 2;
taskType.value = 'WORKFLOW';
tabParams.value.mode = 'WORKFLOW';
}
};
@ -108,14 +97,14 @@ const pagination = ref({
});
const createPanels = () => [
{
name: 'retryTask',
tab: $t('page.home.retryTask')
},
{
name: 'jobTask',
tab: $t('page.home.jobTask')
},
{
name: 'retryTask',
tab: $t('page.home.retryTask')
},
{
name: 'workflow',
tab: $t('page.home.workflow')
@ -133,13 +122,13 @@ const createColumns = (): DataTableColumns<Api.Dashboard.Task> => [
title: $t('page.home.retryTab.task.run'),
key: 'run',
align: 'center',
render: row => <span class="retry-table-number">{row.run}</span>
render: row => <span class="task-table-number">{row.run}</span>
},
{
title: $t('page.home.retryTab.task.total'),
key: 'total',
align: 'center',
render: row => <span class="retry-table-number">{row.total}</span>
render: row => <span class="task-table-number">{row.total}</span>
}
];
@ -152,6 +141,17 @@ watch(
columns.value = createColumns();
}
);
watch(
() => tabParams.value,
() => {
getData();
},
{ deep: true }
);
getData();
getGroupNames();
</script>
<template>
@ -160,20 +160,20 @@ watch(
<NTabPane v-for="panel in panels" :key="panel.name" :tab="panel.tab" :name="panel.name">
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
<NGi span="24 s:24 m:16">
<LineRetryChart v-model="data!" :type="type"></LineRetryChart>
<TaskLineChart v-model="data!" :type="taskType" />
</NGi>
<NGi span="24 s:24 m:8">
<div class="retry-tab-rank">
<h4 class="retry-tab-title">{{ $t('page.home.retryTab.rank.title') }}</h4>
<ul class="retry-tab-rank__list">
<li v-for="(item, index) in data?.rankList" :key="index" class="retry-tab-rank__list--item">
<div class="task-tab-rank">
<h4 class="task-tab-title">{{ $t('page.home.retryTab.rank.title') }}</h4>
<ul class="task-tab-rank__list">
<li v-for="(item, index) in data?.rankList" :key="index" class="task-tab-rank__list--item">
<span>
<span class="retry-tab-rank__list--index">
<span class="task-tab-rank__list--index">
{{ index + 1 }}
</span>
<span>{{ item.name }}</span>
</span>
<span class="retry-tab-badge">{{ item.total }}</span>
<span class="task-tab-badge">{{ item.total }}</span>
</li>
</ul>
</div>
@ -181,7 +181,7 @@ watch(
</NGrid>
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive class="p-t-16px">
<NGi span="24 s:24 m:16">
<h4 class="retry-tab-title">{{ $t('page.home.retryTab.task.title') }}</h4>
<h4 class="task-tab-title">{{ $t('page.home.retryTab.task.title') }}</h4>
<NDivider />
<NDataTable
min-height="300px"
@ -193,9 +193,9 @@ watch(
/>
</NGi>
<NGi span="24 s:24 m:8">
<h4 class="retry-tab-title">{{ $t('page.home.retryTab.pie.title') }}</h4>
<h4 class="task-tab-title">{{ $t('page.home.retryTab.pie.title') }}</h4>
<NDivider />
<PieRetryChart v-model="modelValue!" :type="type" />
<TaskPieChart v-model="modelValue!" :type="taskType" />
</NGi>
</NGrid>
</NTabPane>
@ -225,7 +225,7 @@ watch(
</template>
<style>
.retry-table-number {
.task-table-number {
padding: 3px 7px;
background-color: #f4f4f4;
color: #555;
@ -234,19 +234,19 @@ watch(
border-radius: 4px;
}
.dark .retry-table-number {
.dark .task-table-number {
background: #2c2c2c;
color: #d6d6d6;
}
</style>
<style scoped lang="scss">
.retry-tab-title {
.task-tab-title {
font-size: 16px;
font-weight: 600;
}
.retry-tab-badge {
.task-tab-badge {
float: right;
padding: 3px 7px;
background-color: #f4f4f4;
@ -256,7 +256,7 @@ watch(
border-radius: 4px;
}
.retry-tab-rank {
.task-tab-rank {
height: 360px;
overflow: hidden;
@ -303,12 +303,12 @@ watch(
}
.dark {
.retry-tab-badge {
.task-tab-badge {
background: #2c2c2c;
color: #d6d6d6;
}
.retry-tab-rank {
.task-tab-rank {
&__list {
&--index {
color: #d6d6d6;

View File

@ -34,7 +34,7 @@ const { columnChecks, columns, data, getData, loading, mobilePagination, searchP
key: 'id',
title: $t('common.index'),
align: 'center',
width: 64,
width: 120,
render: row => {
function showDetailDrawer() {
detailData.value = row;

View File

@ -1,9 +1,13 @@
<script setup lang="tsx">
import { NButton } from 'naive-ui';
import { NButton, NTag } from 'naive-ui';
import { onBeforeUnmount, ref } from 'vue';
import { executorTypeRecord, operationReasonRecord, taskBatchStatusRecord } from '@/constants/business';
import {
executorTypeRecord,
operationReasonRecord,
taskBatchStatusRecord,
taskStatusRecord
} from '@/constants/business';
import { $t } from '@/locales';
// import { fetchGetJobBatchDetail } from '@/service/api';
import { tagColor } from '@/utils/common';
import { useTable } from '@/hooks/common/table';
import { fetchGetJobTaskList } from '@/service/api';
@ -71,6 +75,27 @@ const { columns, data, loading, mobilePagination } = useTable({
align: 'left',
minWidth: 120
},
{
key: 'taskStatus',
title: $t('page.jobBatch.jobTask.taskStatus'),
align: 'left',
minWidth: 80,
render: row => {
if (row.taskStatus === null) {
return null;
}
const label = $t(taskStatusRecord[row.taskStatus!]);
const tagMap: Record<number, NaiveUI.ThemeColor> = {
1: 'info',
2: 'info',
3: 'info',
4: 'error',
5: 'error',
6: 'error'
};
return <NTag type={tagMap[row.taskStatus!]}>{label}</NTag>;
}
},
{
key: 'clientInfo',
title: $t('page.jobBatch.jobTask.clientInfo'),

View File

@ -37,7 +37,7 @@ const { columnChecks, columns, data, getData, loading, mobilePagination, searchP
key: 'index',
title: $t('common.index'),
align: 'center',
width: 40
width: 120
},
{
key: 'jobName',

View File

@ -281,39 +281,27 @@ watch(visible, () => {
:placeholder="$t('page.jobTask.form.jobName')"
/>
</NFormItem>
<NGrid cols="2 s:1 m:2" responsive="screen" x-gap="20">
<NGi>
<NFormItem :label="$t('page.jobTask.groupName')" path="groupName">
<SelectGroup v-model:value="model.groupName" />
</NFormItem>
</NGi>
<NGi>
<NFormItem :label="$t('page.jobTask.jobStatus')" path="jobStatus">
<NRadioGroup v-model:value="model.jobStatus" name="jobStatus">
<NSpace>
<NRadio
v-for="item in enableStatusNumberOptions"
:key="item.value"
:value="item.value"
:label="$t(item.label)"
/>
</NSpace>
</NRadioGroup>
</NFormItem>
</NGi>
</NGrid>
<NGrid cols="2 s:1 m:2" responsive="screen" x-gap="20">
<NGi>
<NFormItem :label="$t('page.jobTask.executorType')" path="executorType">
<ExecutorType v-model:value="model.executorType" />
</NFormItem>
</NGi>
<NGi>
<NFormItem :label="$t('page.jobTask.executorInfo')" path="executorInfo">
<NInput v-model:value="model.executorInfo" :placeholder="$t('page.jobTask.form.executorInfo')" />
</NFormItem>
</NGi>
</NGrid>
<NFormItem :label="$t('page.jobTask.groupName')" path="groupName">
<SelectGroup v-model:value="model.groupName" />
</NFormItem>
<NFormItem :label="$t('page.jobTask.jobStatus')" path="jobStatus">
<NRadioGroup v-model:value="model.jobStatus" name="jobStatus">
<NSpace>
<NRadio
v-for="item in enableStatusNumberOptions"
:key="item.value"
:value="item.value"
:label="$t(item.label)"
/>
</NSpace>
</NRadioGroup>
</NFormItem>
<NFormItem :label="$t('page.jobTask.executorType')" path="executorType">
<ExecutorType v-model:value="model.executorType" />
</NFormItem>
<NFormItem :label="$t('page.jobTask.executorInfo')" path="executorInfo">
<NInput v-model:value="model.executorInfo" :placeholder="$t('page.jobTask.form.executorInfo')" />
</NFormItem>
<NFormItem :label="$t('page.jobTask.taskType')" path="taskType">
<TaskType v-model:value="model.taskType" :placeholder="$t('page.jobTask.form.taskType')" />
</NFormItem>

View File

@ -108,7 +108,7 @@ watch(visible, () => {
</script>
<template>
<OperateDrawer v-model="visible" :title="title">
<OperateDrawer v-model="visible" :min-size="480" :title="title">
<NTabs v-model:value="notifyTabPane" type="segment" animated>
<NTabPane :name="1" tab="钉钉" :disabled="notifyTabPane !== 1 && props.operateType === 'edit'">
<DingDingForm ref="formRef" v-model:value="model" />

View File

@ -100,7 +100,7 @@ const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
groupName: '',
groupName: null,
businessId: '',
recipientIds: [],
systemTaskType: null,
@ -267,23 +267,11 @@ watch(visible, () => {
</script>
<template>
<OperateDrawer v-model="visible" :title="title" @handle-submit="handleSubmit">
<OperateDrawer v-model="visible" :title="title" :min-size="480" @handle-submit="handleSubmit">
<NForm ref="formRef" :model="model" :rules="rules">
<NFormItem :label="$t('page.notifyConfig.groupName')" path="groupName">
<SelectGroup v-model:modelValue="model.groupName" @update:model-value="groupNameUpdate" />
</NFormItem>
<NFormItem :label="$t('page.notifyConfig.notifyStatus')" path="notifyStatus">
<NRadioGroup v-model:value="model.notifyStatus" name="notifyStatus">
<NSpace>
<NRadio
v-for="item in enableStatusNumberOptions"
:key="item.value"
:value="item.value"
:label="$t(item.label)"
/>
</NSpace>
</NRadioGroup>
</NFormItem>
<NFormItem :label="$t('page.notifyConfig.systemTaskType')" path="systemTaskType">
<NSelect
v-model:value="model.systemTaskType"
@ -341,34 +329,58 @@ watch(visible, () => {
multiple
/>
</NFormItem>
<NFormItem :label="$t('page.notifyConfig.rateLimiterStatus')" path="rateLimiterStatus">
<NRadioGroup v-model:value="model.rateLimiterStatus" name="rateLimiterStatus" :disabled="retrySceneDisable">
<NSpace>
<NRadio
v-for="item in enableStatusNumberOptions"
:key="item.value"
:value="item.value"
:label="$t(item.label)"
<NGrid cols="2 s:1 m:2" responsive="screen" x-gap="20">
<NGi>
<NFormItem :label="$t('page.notifyConfig.notifyStatus')" path="notifyStatus">
<NRadioGroup v-model:value="model.notifyStatus" name="notifyStatus">
<NSpace>
<NRadio
v-for="item in enableStatusNumberOptions"
:key="item.value"
:value="item.value"
:label="$t(item.label)"
/>
</NSpace>
</NRadioGroup>
</NFormItem>
</NGi>
<NGi>
<NFormItem :label="$t('page.notifyConfig.notifyThreshold')" path="notifyThreshold">
<NInputNumber
v-model:value="model.notifyThreshold"
:min="1"
:placeholder="$t('page.notifyConfig.form.notifyThreshold')"
:disabled="retrySceneDisable"
/>
</NSpace>
</NRadioGroup>
</NFormItem>
<NFormItem :label="$t('page.notifyConfig.rateLimiterThreshold')" path="notifyThreshold">
<NInputNumber
v-model:value="model.rateLimiterThreshold"
:min="1"
:placeholder="$t('page.notifyConfig.form.notifyThreshold')"
:disabled="retrySceneDisable"
/>
</NFormItem>
<NFormItem :label="$t('page.notifyConfig.notifyThreshold')" path="notifyThreshold">
<NInputNumber
v-model:value="model.notifyThreshold"
:min="1"
:placeholder="$t('page.notifyConfig.form.notifyThreshold')"
:disabled="retrySceneDisable"
/>
</NFormItem>
</NFormItem>
</NGi>
</NGrid>
<NGrid cols="2 s:1 m:2" responsive="screen" x-gap="20">
<NGi>
<NFormItem :label="$t('page.notifyConfig.rateLimiterStatus')" path="rateLimiterStatus">
<NRadioGroup v-model:value="model.rateLimiterStatus" name="rateLimiterStatus" :disabled="retrySceneDisable">
<NSpace>
<NRadio
v-for="item in enableStatusNumberOptions"
:key="item.value"
:value="item.value"
:label="$t(item.label)"
/>
</NSpace>
</NRadioGroup>
</NFormItem>
</NGi>
<NGi>
<NFormItem :label="$t('page.notifyConfig.rateLimiterThreshold')" path="notifyThreshold">
<NInputNumber
v-model:value="model.rateLimiterThreshold"
:min="1"
:placeholder="$t('page.notifyConfig.form.notifyThreshold')"
:disabled="retrySceneDisable"
/>
</NFormItem>
</NGi>
</NGrid>
<NFormItem :label="$t('page.notifyConfig.description')" path="description">
<NInput
v-model:value="model.description"

View File

@ -107,7 +107,7 @@ const rules = {
defaultRequiredRule,
{
required: true,
pattern: /^[A-Za-z0-9_]{1,64}$/,
pattern: /^[A-Za-z0-9_-]{1,64}$/,
trigger: 'change',
message: $t('page.retryScene.form.sceneName2')
}
@ -202,17 +202,6 @@ async function handleSubmit() {
emit('submitted');
}
function maxRetryCountUpdate(maxRetryCount: number) {
if (model.backOff !== 1) {
return;
}
let desc = '';
for (let i = 1; i <= maxRetryCount; i += 1) {
desc += `,${DelayLevel[i as keyof typeof DelayLevel]}`;
}
delayLevelDesc.value = desc.substring(1, desc.length);
}
watch(visible, () => {
if (visible.value) {
handleUpdateModelWhenEdit();
@ -220,10 +209,19 @@ watch(visible, () => {
}
});
watch(
() => model.backOff,
backOff => {
if (backOff === 1 && model.maxRetryCount > 26) {
model.maxRetryCount = 1;
}
}
);
watch(
() => model.maxRetryCount,
() => {
maxRetryCountUpdate(model.maxRetryCount);
delayLevelDesc.value = Object.values(DelayLevel).slice(0, model.maxRetryCount).join(',');
}
);
</script>
@ -240,34 +238,27 @@ watch(
:placeholder="$t('page.retryScene.form.sceneName')"
/>
</NFormItem>
<NGrid cols="2 s:1 m:2" responsive="screen" x-gap="20">
<NGi>
<NFormItem :label="$t('page.retryScene.groupName')" path="groupName">
<NSelect
v-model:value="model.groupName"
:disabled="props.operateType === 'edit'"
:placeholder="$t('page.retryScene.form.groupName')"
:options="translateOptions2(groupNameList)"
clearable
<NFormItem :label="$t('page.retryScene.groupName')" path="groupName">
<NSelect
v-model:value="model.groupName"
:disabled="props.operateType === 'edit'"
:placeholder="$t('page.retryScene.form.groupName')"
:options="translateOptions2(groupNameList)"
clearable
/>
</NFormItem>
<NFormItem :label="$t('page.retryScene.sceneStatus')" path="sceneStatus">
<NRadioGroup v-model:value="model.sceneStatus" name="sceneStatus">
<NSpace>
<NRadio
v-for="item in enableStatusNumberOptions"
:key="item.value"
:value="item.value"
:label="$t(item.label)"
/>
</NFormItem>
</NGi>
<NGi>
<NFormItem :label="$t('page.retryScene.sceneStatus')" path="sceneStatus">
<NRadioGroup v-model:value="model.sceneStatus" name="sceneStatus">
<NSpace>
<NRadio
v-for="item in enableStatusNumberOptions"
:key="item.value"
:value="item.value"
:label="$t(item.label)"
/>
</NSpace>
</NRadioGroup>
</NFormItem>
</NGi>
</NGrid>
</NSpace>
</NRadioGroup>
</NFormItem>
<NGrid cols="2 s:1 m:2" responsive="screen" x-gap="20">
<NGi>
<NFormItem :label="$t('common.routeKey.routeLabel')" path="routeKey">
@ -299,7 +290,18 @@ watch(
</NGi>
<NGi>
<NFormItem path="triggerInterval">
<SceneTriggerInterval v-model="model.triggerInterval" :back-off="model.backOff" />
<SceneTriggerInterval
v-if="model.backOff !== 1"
v-model="model.triggerInterval"
:back-off="model.backOff"
/>
<NInput
v-else
v-model:value="delayLevelDesc"
type="textarea"
:autosize="{ minRows: 1, maxRows: 3 }"
readonly
/>
<template #label>
<div class="flex-center">
{{ $t('page.retryScene.triggerInterval') }}

View File

@ -37,7 +37,7 @@ const { columns, columnChecks, data, getData, loading, mobilePagination, searchP
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64
width: 120
},
{
key: 'workflowName',
@ -131,7 +131,6 @@ const { columns, columnChecks, data, getData, loading, mobilePagination, searchP
});
const {
handleAdd,
checkedRowKeys,
onBatchDeleted
// closeDrawer
@ -172,8 +171,8 @@ function detail(id: string) {
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
:show-add="false"
:show-delete="false"
@add="handleAdd"
@delete="handleBatchDelete"
@refresh="getData"
/>

View File

@ -41,7 +41,7 @@ const { columns, columnChecks, data, getData, loading, mobilePagination, searchP
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64
width: 120
},
{
key: 'workflowName',