feat: 云同步增加同步策略 (#3264)

* feat: 云同步逻辑更新

* feat: 云同步增加同步策略

* perf: 优化

* perf: 优化提交逻辑,平台和网域只允许设置一次

* perf: 策略抽离,有列表、详情、同步任务可便捷添加策略

* fix: 修改组件层级关系

* perf: 去掉不需要的

* perf: 优化变量名称、策略添加优先级
This commit is contained in:
jiangweidong
2023-08-07 10:33:04 +08:00
committed by GitHub
parent 202f8a357c
commit af9f715357
26 changed files with 1159 additions and 94 deletions

View File

@@ -38,11 +38,11 @@
v-if="defaultButton"
:disabled="!canSubmit"
:loading="isSubmitting"
size="small"
:size="submitBtnSize"
type="primary"
@click="submitForm('form')"
>
{{ $t('common.Submit') }}
{{ submitBtnText }}
</el-button>
</el-form-item>
</ElFormRender>
@@ -73,6 +73,16 @@ export default {
type: Boolean,
default: true
},
submitBtnSize: {
type: String,
default: 'small'
},
submitBtnText: {
type: String,
default() {
return this.$t('common.Submit')
}
},
hasSaveContinue: {
type: Boolean,
default: true

View File

@@ -0,0 +1,62 @@
<template>
<div>
<GenericCreateUpdateForm
class="attr-form"
v-bind="formConfig"
@submit="onSubmit"
/>
<DataTable :config="tableConfig" class="attr-list" />
</div>
</template>
<script>
import GenericCreateUpdateForm from '@/layout/components/GenericCreateUpdateForm'
import DataTable from '@/components/Table/DataTable/index.vue'
export default {
name: 'AttrInput',
components: { DataTable, GenericCreateUpdateForm },
props: {
formConfig: {
type: Object,
default: () => ({})
},
tableConfig: {
type: Object,
default: () => ({})
},
beforeSubmit: {
type: Function,
default: (val) => { return true }
}
},
data() {
return {}
},
methods: {
onSubmit(value) {
if (this.beforeSubmit(value)) {
const clonedValue = JSON.parse(JSON.stringify(value))
this.tableConfig.totalData.push(clonedValue)
this.$emit('submit', this.tableConfig.totalData)
}
}
}
}
</script>
<style lang="scss" scoped>
.attr-form {
>>> .el-select {
width: 100%;
}
>>> .el-form-item__content {
width: 100%;
}
>>> .form-buttons {
margin: auto;
}
}
</style>

View File

@@ -21,7 +21,7 @@
import DataForm from '@/components/Form/DataForm/index.vue'
import Dialog from '@/components/Dialog/index.vue'
import ValueField from '@/components/Form/FormFields/JSONManyToManySelect/ValueField.vue'
import { attrMatchOptions, typeMatchMapper } from './const'
import { attrMatchOptions, typeMatchMapper } from '@/components/const'
export default {
name: 'AttrFormDialog',

View File

@@ -1,25 +0,0 @@
import i18n from '@/i18n/i18n'
export const strMatchValues = ['exact', 'not', 'in', 'contains', 'startswith', 'endswith', 'regex']
export const typeMatchMapper = {
str: strMatchValues,
bool: ['exact', 'not'],
m2m: ['m2m'],
ip: [...strMatchValues, 'ip_in'],
int: [...strMatchValues, 'gte', 'lte'],
select: ['in']
}
export const attrMatchOptions = [
{ label: i18n.t('common.Equal'), value: 'exact' },
{ label: i18n.t('common.NotEqual'), value: 'not' },
{ label: i18n.t('common.MatchIn'), value: 'in' },
{ label: i18n.t('common.Contains'), value: 'contains' },
{ label: i18n.t('common.Startswith'), value: 'startswith' },
{ label: i18n.t('common.Endswith'), value: 'endswith' },
{ label: i18n.t('common.Regex'), value: 'regex' },
{ label: i18n.t('common.BelongTo'), value: 'm2m' },
{ label: i18n.t('common.IPMatch'), value: 'ip_in' },
{ label: i18n.t('common.GreatEqualThan'), value: 'gte' },
{ label: i18n.t('common.LessEqualThan'), value: 'lte' }
]

View File

@@ -43,7 +43,7 @@ import ValueFormatter from './ValueFormatter.vue'
import AttrFormDialog from './AttrFormDialog.vue'
import AttrMatchResultDialog from './AttrMatchResultDialog.vue'
import { setUrlParam } from '@/utils/common'
import { attrMatchOptions } from './const'
import { attrMatchOptions } from '@/components/const'
import { toM2MJsonParams } from '@/utils/jms'
export default {

View File

@@ -305,7 +305,7 @@ export default {
items = protocols.filter(item => allProtocolNames.indexOf(item.name) !== -1)
} else {
const defaults = choices.filter(item => (item.required || item.primary || item.default))
if (defaults.length === 0) {
if (defaults.length === 0 && choices.length !== 0) {
defaults.push(choices[0])
}
items = defaults

View File

@@ -3,6 +3,7 @@ import Text from './Text.vue'
import Select2 from './Select2.vue'
import TagInput from './TagInput.vue'
import Switcher from './Switcher.vue'
import AttrInput from './AttrInput.vue'
import UploadKey from './UploadKey.vue'
import JsonEditor from './JsonEditor.vue'
import PhoneInput from './PhoneInput.vue'
@@ -23,6 +24,7 @@ export default {
Switcher,
Select2,
TagInput,
AttrInput,
UploadKey,
JsonEditor,
UpdateToken,
@@ -44,6 +46,7 @@ export {
Switcher,
Select2,
TagInput,
AttrInput,
UploadKey,
JsonEditor,
UpdateToken,

25
src/components/const.js Normal file
View File

@@ -0,0 +1,25 @@
import i18n from '@/i18n/i18n'
export const strMatchValues = ['exact', 'not', 'in', 'contains', 'startswith', 'endswith', 'regex']
export const typeMatchMapper = {
str: strMatchValues,
bool: ['exact', 'not'],
m2m: ['m2m'],
ip: [...strMatchValues, 'ip_in'],
int: [...strMatchValues, 'gte', 'lte'],
select: ['in']
}
export const attrMatchOptions = [
{ label: i18n.t('common.Equal'), value: 'exact' },
{ label: i18n.t('common.NotEqual'), value: 'not' },
{ label: i18n.t('common.MatchIn'), value: 'in' },
{ label: i18n.t('common.Contains'), value: 'contains' },
{ label: i18n.t('common.Startswith'), value: 'startswith' },
{ label: i18n.t('common.Endswith'), value: 'endswith' },
{ label: i18n.t('common.Regex'), value: 'regex' },
{ label: i18n.t('common.BelongTo'), value: 'm2m' },
{ label: i18n.t('common.IPMatch'), value: 'ip_in' },
{ label: i18n.t('common.GreatEqualThan'), value: 'gte' },
{ label: i18n.t('common.LessEqualThan'), value: 'lte' }
]

View File

@@ -486,6 +486,20 @@
"ReLoginErr": "Login time has exceeded 5 minutes, please login again"
},
"common": {
"SyncTask": "Synchronization task",
"New": "New",
"Strategy": "Strategy",
"StrategyList": "Strategy list",
"StrategyCreate": "Create strategy",
"StrategyUpdate": "Update strategy",
"StrategyDetail": "Strategy detail",
"Rule": "Rule",
"RuleCount": "Number of conditions",
"ActionCount": "Number of actions",
"PolicyName": "Policy name",
"BasicSetting": "Basic setting",
"RuleSetting": "Rule setting",
"ActionSetting": "Action setting",
"Proxy": "Proxy",
"PublicKey": "Public key",
"PrivateKey": "Private key",
@@ -595,6 +609,7 @@
"CrontabHelpTips": "eg: Every Sunday 03:05 run <5 3 * * 0> <br>Tips:Using 5 digits linux crontab expressions<min hour day month week> (<a href='https://tool.lu/crontab/' target='_blank'>Online tools</a>) <br>Note:If both Regularly perform and Cycle perform are set, give priority to Regularly perform",
"DateEnd": "End date",
"Resource": "Resource",
"ResourceType": "Resource type",
"DateLast24Hours": "Last 24 hours",
"DateLast3Months": "Last 3 months",
"DateLastHarfYear": "Last half year",
@@ -2056,6 +2071,11 @@
"Reason": "Reason"
},
"Cloud": {
"UniqueError": "Only one of the following properties can be set",
"ExistError": "This element already exists",
"InstanceName": "Instance name",
"InstancePlatformName": "Instance platform name",
"InstanceAddress": "Instance address",
"CloudSync": "Cloud sync",
"ServerAccountKey": "Server Account Key",
"IPNetworkSegment": "Ip Network Segment",
@@ -2084,11 +2104,12 @@
"CloudCenter": "Cloud Center",
"Provider": "Provider",
"Validity": "Validity",
"SyncStrategy": "Synchronisation strategy",
"IsAlwaysUpdateHelpTips": "Whether the asset information, including Hostname, IP, Platform, and AdminUser, is updated synchronously each time a synchronization task is performed",
"SyncInstanceTaskCreate": "Create sync instance task",
"SyncInstanceTaskList": "Sync instance task list",
"SyncInstanceTaskDetail": "Sync instance task detail",
"SyncInstanceTaskUpdate": "Update sync instance task",
"SyncInstanceTaskCreate": "Create sync task",
"SyncInstanceTaskList": "Sync task list",
"SyncInstanceTaskDetail": "Sync task detail",
"SyncInstanceTaskUpdate": "Update sync task",
"SyncInstanceTaskHistoryList": "Sync task history",
"SyncInstanceTaskHistoryAssetList": "Sync instance list",
"CloudSource": "Cloud source",

View File

@@ -486,6 +486,20 @@
"ReLoginErr": "ログイン時間が 5 分を超えました。もう一度ログインしてください"
},
"common": {
"SyncTask": "同期任務です",
"New": "新筑",
"Strategy": "せんりゃく",
"StrategyList": "ポリシーリストです",
"StrategyCreate": "戦略を作ります",
"StrategyUpdate": "ポリシーを更新します",
"StrategyDetail": "戦略の詳細です",
"Rule": "ルール",
"RuleCount": "条件数です",
"ActionCount": "アクション数",
"PolicyName": "ポリシーの名前です",
"BasicSetting": "基本設定",
"RuleSetting": "条件設定です",
"ActionSetting": "動作設定です",
"Proxy": "プロキシ",
"PublicKey": "公開鍵です",
"PrivateKey": "秘密鍵です",
@@ -594,6 +608,7 @@
"CrontabHelpTips": "Eg: 毎週日曜日03:05に <5 3 * * 0> <br> ヒント: 5ビットLinux crontab式 <時分割日月曜日> (<a href = 'https:// tool.lu/crontab/' target = '_ blank'> オンラインツール </a>) <br>注意: 定期実行とサイクル実行の両方が設定されている場合は、定期実行を優先します",
"DateEnd": "終了日",
"Resource": "リソース",
"ResourceType": "リソースタイプです",
"DateLast24Hours": "最近の日",
"DateLast3Months": "直近3月",
"DateLastHarfYear": "ここ半年です",
@@ -2049,6 +2064,11 @@
"Reason": "原因"
},
"Cloud": {
"UniqueError": "以下の属性は1つしか設定できません",
"ExistError": "この元素は既に存在します",
"InstanceName": "インスタンス名です",
"InstancePlatformName": "インスタンスのプラットフォーム名です",
"InstanceAddress": "インスタンスアドレスです",
"CloudSync": "クラウド同期",
"ServerAccountKey": "サービスアカウントキー",
"IPNetworkSegment": "IPネットワークセグメント",
@@ -2081,11 +2101,12 @@
"CloudCenter": "クラウド管理センター",
"Provider": "クラウドサービス業者",
"Validity": "有効",
"SyncStrategy": "同調戦略です",
"IsAlwaysUpdateHelpTips": "同期タスクを実行するたびに、ホスト名、IP、システムプラットフォーム、管理ユーザーなど、アセットの情報を同期更新しますか?",
"SyncInstanceTaskCreate": "同期インスタンスタスク作成",
"SyncInstanceTaskList": "インスタンスのタスクリストの同期",
"SyncInstanceTaskDetail": "インスタンスタスクの詳細の同期",
"SyncInstanceTaskUpdate": "同期インスタンスタスクの更新",
"SyncInstanceTaskCreate": "同期タスク作成します",
"SyncInstanceTaskList": "同期タスクリストです",
"SyncInstanceTaskDetail": "同期任務詳細です",
"SyncInstanceTaskUpdate": "同期タスクの更新です",
"SyncInstanceTaskHistoryList": "履歴リストの同期",
"SyncInstanceTaskHistoryAssetList": "インスタンスリストの同期",
"CloudSource": "同期ソース",

View File

@@ -466,6 +466,20 @@
"ReLoginErr": "登录时长已超过 5 分钟,请重新登录"
},
"common": {
"SyncTask": "同步任务",
"New": "新建",
"Strategy": "策略",
"StrategyList": "策略列表",
"StrategyCreate": "创建策略",
"StrategyUpdate": "更新策略",
"StrategyDetail": "策略详情",
"Rule": "条件",
"RuleCount": "条件数量",
"ActionCount": "动作数量",
"PolicyName": "策略名称",
"BasicSetting": "基本设置",
"RuleSetting": "条件设置",
"ActionSetting": "动作设置",
"Proxy": "代理",
"PublicKey": "公钥",
"PrivateKey": "私钥",
@@ -623,6 +637,7 @@
"CrontabHelpTips": "eg每周日 03:05 执行 <5 3 * * 0> <br> 提示: 使用5位 Linux crontab 表达式 <分 时 日 月 星期> <a href='https://tool.lu/crontab/' target='_blank'>在线工具</a> <br>注意: 如果同时设置了定期执行和周期执行,优先使用定期执行",
"DateEnd": "结束日期",
"Resource": "资源",
"ResourceType": "资源类型",
"DateLast24Hours": "最近一天",
"DateLast3Months": "最近三月",
"DateLastHarfYear": "最近半年",
@@ -1970,6 +1985,11 @@
"AssetCount": "资产数量",
"Auditor": "审计员",
"Cloud": {
"UniqueError": "以下属性只能设置一个",
"ExistError": "这个元素已经存在",
"InstanceName": "实例名称",
"InstancePlatformName": "实例平台名称",
"InstanceAddress": "实例地址",
"LAN": "局域网",
"CloudSync": "云同步",
"ServerAccountKey": "服务账号密钥",
@@ -2002,11 +2022,12 @@
"CloudCenter": "云管中心",
"Provider": "云服务商",
"Validity": "有效",
"SyncStrategy": "同步策略",
"IsAlwaysUpdateHelpTips": "每次执行同步任务时是否同步更新资产的信息包括主机名、IP、系统平台、管理用户",
"SyncInstanceTaskCreate": "创建同步实例任务",
"SyncInstanceTaskList": "同步实例任务列表",
"SyncInstanceTaskDetail": "同步实例任务详情",
"SyncInstanceTaskUpdate": "更新同步实例任务",
"SyncInstanceTaskCreate": "创建同步任务",
"SyncInstanceTaskList": "同步任务列表",
"SyncInstanceTaskDetail": "同步任务详情",
"SyncInstanceTaskUpdate": "更新同步任务",
"SyncInstanceTaskHistoryList": "同步历史列表",
"SyncInstanceTaskHistoryAssetList": "同步实例列表",
"CloudSource": "同步源",

View File

@@ -129,6 +129,58 @@ export default [
}
}
]
},
{
path: 'strategy',
component: empty,
hidden: true,
meta: {
title: i18n.t('xpack.Cloud.Strategy'),
permissions: ['xpack.view_strategy']
},
children: [
{
path: '',
name: 'CloudStrategyList',
hidden: true,
component: () => import('@/views/assets/Cloud/'),
meta: {
title: i18n.t('xpack.Cloud.StrategyList'),
permissions: ['xpack.view_strategy']
}
},
{
path: 'create',
component: () => import('@/views/assets/Cloud/Strategy/StrategyCreateUpdate'),
name: 'CloudStrategyCreate',
hidden: true,
meta: {
title: i18n.t('common.StrategyCreate'),
action: 'create',
permissions: ['xpack.add_strategy']
}
},
{
path: ':id/update',
component: () => import('@/views/assets/Cloud/Strategy/StrategyCreateUpdate'),
name: 'CloudStrategyUpdate',
hidden: true,
meta: {
title: i18n.t('common.StrategyUpdate'),
permissions: ['xpack.change_strategy']
}
},
{
path: ':id/',
component: () => import('@/views/assets/Cloud/Strategy/StrategyDetail/index'),
name: 'CloudStrategyDetail',
hidden: true,
meta: {
title: i18n.t('common.StrategyDetail'),
permissions: ['xpack.view_strategy']
}
}
]
}
]
}

View File

@@ -80,17 +80,6 @@ export default {
password: {
rules: this.$route.params.id ? [] : [RequiredChange]
},
platform: {
el: {
multiple: false,
ajax: {
url: `/api/v1/assets/platforms/`,
transformOption: (item) => {
return { label: item.name, value: item.id }
}
}
}
},
public_key: {
label: this.$t('common.PublicKey'),
rules: this.$route.params.id ? [] : [RequiredChange]

View File

@@ -0,0 +1,50 @@
<template>
<GenericCreateUpdatePage
v-bind="$data"
/>
</template>
<script>
import { GenericCreateUpdatePage } from '@/layout/components'
import { RequiredChange, specialEmojiCheck } from '@/components/Form/DataForm/rules'
import RuleInput from './components/RuleInput'
import ActionInput from './components/ActionInput'
export default {
components: {
GenericCreateUpdatePage
},
data() {
return {
url: '/api/v1/xpack/cloud/strategies/',
fields: [
[this.$t('common.Basic'), ['name', 'priority']],
[this.$t('common.Strategy'), ['strategy_rules', 'strategy_actions']],
[this.$t('common.Other'), ['comment']]
],
fieldsMeta: {
name: {
rules: [RequiredChange, specialEmojiCheck]
},
strategy_rules: {
component: RuleInput
},
strategy_actions: {
component: ActionInput
}
},
updateSuccessNextRoute: { name: 'CloudCenter', params: { activeMenu: 'StrategyList' }},
createSuccessNextRoute: { name: 'CloudCenter', params: { activeMenu: 'StrategyList' }},
getUrl() {
const id = this.$route.params?.id
return id ? `${this.url}${id}/` : this.url
}
}
},
methods: {}
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,99 @@
<template>
<el-row :gutter="20">
<el-col :md="14" :sm="24">
<AutoDetailCard :fields="detailFields" :object="object" :url="url" />
</el-col>
</el-row>
</template>
<script>
import AutoDetailCard from '@/components/Cards/DetailCard/auto'
export default {
name: 'StrategyDetail',
components: {
AutoDetailCard
},
props: {
object: {
type: Object,
default: () => ({})
}
},
data() {
return {
url: `/api/v1/xpack/cloud/strategies/${this.object.id}/`,
detailFields: [
'name', 'priority',
{
key: this.$t('common.Rule'),
formatter: () => {
const newArr = this.object.strategy_rules || []
return (
<ul>
{
newArr.map((r, index) => {
return <li key={index}>{`${r.attr.label} ${r.match.label} ${r.value}`} </li>
})
}
</ul>
)
}
},
{
key: this.$t('common.Action'),
formatter: () => {
const newArr = this.object.strategy_actions || []
return (
<ul>
{
newArr.map((a, index) => {
return <li key={index}>{`${a.attr.label}: ${a.value.label}`} </li>
})
}
</ul>
)
}
},
'comment', 'org_name'
]
}
},
computed: {
cardTitle() {
return this.object.name
}
}
}
</script>
<style scoped>
ul {
counter-reset: my-counter;
list-style-type: none;
margin: 0;
padding: 0;
}
li {
counter-increment: my-counter;
position: relative;
padding-left: 20px;
}
li:before {
content: counter(my-counter);
display: block;
position: absolute;
left: 0;
top: 32%;
width: 14px;
height: 14px;
line-height: 12px;
text-align: center;
border: 1px solid;
border-radius: 50%;
background-color: #fff;
}
</style>

View File

@@ -0,0 +1,53 @@
<template>
<GenericDetailPage :object.sync="Account" :active-menu.sync="config.activeMenu" v-bind="config" v-on="$listeners">
<keep-alive>
<component :is="config.activeMenu" :object="Account" />
</keep-alive>
</GenericDetailPage>
</template>
<script>
import { GenericDetailPage, TabPage } from '@/layout/components'
import StrategyDetail from './StrategyDetail'
export default {
components: {
GenericDetailPage,
StrategyDetail,
TabPage
},
data() {
return {
Account: {
name: '', strategy_rules: [], strategy_actions: [], comment: ''
},
config: {
url: `/api/v1/xpack/cloud/strategies`,
activeMenu: 'StrategyDetail',
submenu: [
{
title: this.$t('common.Strategy'),
name: 'StrategyDetail'
}
],
actions: {
deleteSuccessRoute: 'CloudCenter',
updateCallback: () => {
const id = this.$route.params.id
const routeName = 'CloudStrategyUpdate'
this.$router.push({
name: routeName,
params: { id: id }
})
}
}
}
}
}
}
</script>
<style lang='scss' scoped>
</style>

View File

@@ -0,0 +1,59 @@
<template>
<GenericListTable :table-config="tableConfig" :header-actions="headerActions" />
</template>
<script type="text/jsx">
import GenericListTable from '@/layout/components/GenericListTable'
import { DetailFormatter } from '@/components/Table/TableFormatters'
export default {
name: 'StrategyList',
components: {
GenericListTable
},
data() {
return {
tableConfig: {
url: '/api/v1/xpack/cloud/strategies/',
permissions: {
app: 'xpack',
resource: 'strategy'
},
columns: ['name', 'priority', 'strategy_rules', 'strategy_actions', 'actions', 'user_actions'],
columnsMeta: {
name: {
formatter: DetailFormatter,
formatterArgs: {
route: 'CloudStrategyDetail'
}
},
strategy_rules: {
formatter: (row) => { return row.strategy_rules.length }
},
strategy_actions: {
formatter: (row) => { return row.strategy_actions.length }
},
actions: {
formatterArgs: {
updateRoute: 'CloudStrategyUpdate',
hasClone: false
}
}
}
},
headerActions: {
hasImport: false,
hasMoreActions: false,
createRoute: 'CloudStrategyCreate'
}
}
},
methods: {
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,205 @@
<template>
<AttrInput
:form-config="formConfig"
:table-config="tableConfig"
:before-submit="beforeSubmit"
@submit="onSubmit"
/>
</template>
<script>
import { AttrInput, Select2 } from '@/components/Form/FormFields'
import { Required } from '@/components/Form/DataForm/rules'
import ProtocolSelector from '@/components/Form/FormFields/ProtocolSelector'
import { resourceTypeOptions, tableFormatter } from './const'
export default {
name: 'ActionInput',
components: { AttrInput },
props: {
value: {
type: Array,
default: () => ([])
}
},
data() {
return {
resourceType: '',
globalResource: {},
globalProtocols: {},
formConfig: {
initial: { attr: '', value: '' },
inline: true,
hasSaveContinue: false,
submitBtnSize: 'mini',
submitBtnText: this.$t('common.Add'),
hasReset: false,
onSubmit: () => {},
submitMethod: () => 'post',
getUrl: () => '',
cleanFormValue(data) {
if (data?.attr !== 'platform') {
delete data.protocols
}
return data
},
fields: [['', ['attr', 'value', 'protocols']]],
fieldsMeta: {
attr: {
label: '',
component: Select2,
rules: [Required],
el: {
url: '',
clearable: false,
multiple: false,
options: resourceTypeOptions
},
on: {
change: ([val], updateForm) => {
updateForm({ value: '' })
let url
switch (val) {
case 'platform':
url = '/api/v1/assets/platforms/?category=host'
break
case 'node':
url = '/api/v1/assets/nodes/'
break
case 'domain':
url = '/api/v1/assets/domains/'
break
case 'account_template':
url = '/api/v1/accounts/account-templates/'
break
}
if (val !== 'platform') {
this.formConfig.fieldsMeta.protocols.el.hidden = true
}
this.resourceType = val
this.formConfig.fieldsMeta.value.el.ajax.url = url
}
}
},
value: {
label: '',
component: Select2,
rules: [Required],
el: {
value: [],
ajax: {
url: '',
clearable: false,
transformOption: (item) => {
let label
switch (this.resourceType) {
case 'platform':
label = item?.name
this.globalProtocols[item.id] = item.protocols
this.globalResource[item.id] = label
break
case 'account_template':
label = `${item.name}(${item.username})`
this.globalResource[item.id] = label
break
case 'node':
label = item?.full_value
this.globalResource[item.id] = label
break
default:
label = item?.name
this.globalResource[item.id] = label
}
return { label: label, value: item.id }
}
},
multiple: false
},
on: {
change: ([val], updateForm) => {
if (this.resourceType === 'platform') {
this.formConfig.fieldsMeta.protocols.el.choices = this.globalProtocols[val] || []
this.formConfig.fieldsMeta.protocols.el.hidden = false
} else {
this.formConfig.fieldsMeta.protocols.el.hidden = true
}
}
}
},
protocols: {
label: '',
component: ProtocolSelector,
el: {
hidden: true,
settingReadonly: true,
choices: []
}
}
}
},
tableConfig: {
columns: [
{ prop: 'attr', label: this.$t('common.ResourceType'), formatter: tableFormatter('resource_type') },
{ prop: 'value', label: this.$t('common.Resource'), formatter: tableFormatter('resource', () => { return this.globalResource }) },
{ prop: 'protocols', label: this.$t('common.Other'), formatter: tableFormatter('protocols') },
{ prop: 'action', label: this.$t('common.Action'), align: 'center', width: '100px', formatter: (row, col, cellValue, index) => {
return (
<div className='input-button'>
<el-button
icon='el-icon-minus'
size='mini'
style={{ 'flexShrink': 0 }}
type='danger'
onClick={ this.handleDelete(index) }
/>
</div>
)
} }
],
totalData: this.value || [],
hasPagination: false
}
}
},
methods: {
onSubmit() {
this.$emit('input', this.tableConfig.totalData)
},
beforeSubmit(data) {
let status = true
const labelMap = {
platform: this.$tc('assets.Platform'), domain: this.$tc('assets.Domain')
}
this.tableConfig.totalData.map(item => {
const iValue = item.value?.id || item.value
const iAttr = item.attr?.value || item.attr
if (iValue === data.value) {
status = false
this.$message.error(`${this.$tc('xpack.Cloud.ExistError')}`)
} else if (Object.keys(labelMap).indexOf(data?.attr) !== -1 && iAttr === data.attr) {
status = false
this.$message.error(`${this.$tc('xpack.Cloud.UniqueError')}: ${labelMap[data.attr]}`)
}
})
return status
},
handleDelete(index) {
return () => {
const item = this.tableConfig.totalData.splice(index, 1)
this.$axios.delete(`/api/v1/xpack/cloud/strategy-actions/${item[0]?.id}/`)
this.$message.success(this.$tc('common.deleteSuccessMsg'))
}
}
}
}
</script>
<style lang="scss" scoped>
>>> .el-form-item:nth-child(-n+3) {
width: 43.5%;
}
>>> .el-form-item:last-child {
width: 6%;
}
</style>

View File

@@ -0,0 +1,117 @@
<template>
<AttrInput
:form-config="formConfig"
:table-config="tableConfig"
@submit="onSubmit"
/>
</template>
<script>
import { AttrInput, Select2 } from '@/components/Form/FormFields'
import { Required } from '@/components/Form/DataForm/rules'
import { instanceAttrOptions, tableFormatter } from './const'
import { attrMatchOptions, strMatchValues } from '@/components/const'
export default {
name: 'RuleInput',
components: { AttrInput },
props: {
value: {
type: Array,
default: () => ([])
}
},
data() {
return {
formConfig: {
initial: { attr: '', match: '', value: '' },
inline: true,
hasSaveContinue: false,
submitBtnSize: 'mini',
submitBtnText: this.$t('common.Add'),
hasReset: false,
onSubmit: () => {},
submitMethod: () => 'post',
getUrl: () => '',
fields: [['', ['attr', 'match', 'value']]],
fieldsMeta: {
attr: {
label: '',
component: Select2,
rules: [Required],
el: {
value: [],
multiple: false,
clearable: false,
options: instanceAttrOptions
}
},
match: {
label: '',
component: Select2,
rules: [Required],
el: {
value: [],
multiple: false,
clearable: false,
options: attrMatchOptions.filter((option) => {
if (strMatchValues.indexOf(option.value) !== -1 && option.value !== 'in') {
return option
}
})
}
},
value: {
component: 'el-input',
rules: [Required]
}
}
},
tableConfig: {
columns: [
{ prop: 'attr', label: this.$t('common.AttrName'), formatter: tableFormatter('attr') },
{ prop: 'match', label: this.$t('common.Match'), formatter: tableFormatter('match') },
{ prop: 'value', label: this.$t('common.AttrValue'), formatter: tableFormatter('value') },
{ prop: 'action', label: this.$t('common.Action'), align: 'center', width: '100px', formatter: (row, col, cellValue, index) => {
return (
<div className='input-button'>
<el-button
icon='el-icon-minus'
size='mini'
style={{ 'flexShrink': 0 }}
type='danger'
onClick={ this.handleDelete(index) }
/>
</div>
)
} }
],
totalData: this.value || [],
hasPagination: false
}
}
},
methods: {
onSubmit() {
this.$emit('input', this.tableConfig.totalData)
},
handleDelete(index) {
return () => {
const item = this.tableConfig.totalData.splice(index, 1)
this.$axios.delete(`/api/v1/xpack/cloud/strategy-rules/${item[0]?.id}/`)
this.$message.success(this.$tc('common.deleteSuccessMsg'))
}
}
}
}
</script>
<style lang="scss" scoped>
>>> .el-form-item:nth-child(-n+4) {
width: 29%;
}
>>> .el-form-item:last-child {
width: 6%;
}
</style>

View File

@@ -0,0 +1,44 @@
import i18n from '@/i18n/i18n'
import { attrMatchOptions } from '@/components/const'
export const resourceTypeOptions = [
{ label: i18n.t('assets.Platform'), value: 'platform' },
{ label: i18n.t('assets.Node'), value: 'node' },
{ label: i18n.t('assets.Domain'), value: 'domain' },
{ label: i18n.t('accounts.AccountTemplate'), value: 'account_template' }
]
export const instanceAttrOptions = [
{ label: i18n.t('xpack.Cloud.InstanceName'), value: 'instance_name' },
{ label: i18n.t('xpack.Cloud.InstancePlatformName'), value: 'instance_platform' },
{ label: i18n.t('xpack.Cloud.InstanceAddress'), value: 'instance_address' }
]
export const tableFormatter = (colName, getResourceLabel) => {
return (row, col, cellValue) => {
const globalResource = {}
const value = cellValue
if (value?.label) { return value.label }
switch (colName) {
case 'attr':
return instanceAttrOptions.find(attr => attr.value === value)?.label || value
case 'resource_type':
return resourceTypeOptions.find(attr => attr.value === value)?.label || value
case 'match':
return attrMatchOptions.find(opt => opt.value === value).label || value
case 'value':
return Array.isArray(value) ? value.join(', ') : value
case 'resource':
if (typeof getResourceLabel === 'function') {
Object.assign(globalResource, getResourceLabel())
}
return globalResource[value] || value
case 'protocols':
return Array.isArray(value) ? value.map(p => { return `${p.name}/${p.port}` }).join(', ') : ''
case 'count':
return value?.length || 0
default:
return value
}
}
}

View File

@@ -6,7 +6,7 @@
import { GenericCreateUpdatePage } from '@/layout/components'
import { CronTab, Select2 } from '@/components'
import rules from '@/components/Form/DataForm/rules'
import ProtocolSelector from '@/components/Form/FormFields/ProtocolSelector'
import SyncInstanceTaskStrategy from './components/SyncInstanceTaskStrategy/index'
export default {
components: {
@@ -24,7 +24,8 @@ export default {
fields: [
[this.$t('common.Basic'), ['name']],
[this.$t('xpack.Cloud.CloudSource'), ['account', 'regions']],
[this.$t('xpack.Cloud.SaveSetting'), ['hostname_strategy', 'node', 'protocols', 'ip_network_segment_group', 'sync_ip_type', 'is_always_update']],
[this.$t('xpack.Cloud.SaveSetting'), ['hostname_strategy', 'ip_network_segment_group', 'sync_ip_type', 'is_always_update']],
[this.$t('common.Strategy'), ['strategy']],
[this.$t('xpack.Timer'), ['is_periodic', 'crontab', 'interval']],
[this.$t('common.Other'), ['comment']]
],
@@ -49,31 +50,6 @@ export default {
rules: [rules.RequiredChange],
helpText: this.$t('xpack.Cloud.HostnameStrategy')
},
node: {
rules: [rules.RequiredChange],
el: {
multiple: false,
value: [],
ajax: {
url: '/api/v1/assets/nodes/',
transformOption: (item) => {
return { label: item.full_value, value: item.id }
}
}
}
},
protocols: {
component: ProtocolSelector,
el: {
showSetting: () => { return false },
choices: [
{ 'name': 'ssh', 'port': 22, 'primary': true, 'default': false, 'required': false },
{ 'name': 'telnet', 'port': 23, 'primary': false, 'default': false, 'required': false },
{ 'name': 'vnc', 'port': 5900, 'primary': false, 'default': false, 'required': false },
{ 'name': 'rdp', 'port': 3389, 'primary': false, 'default': false, 'required': false }
]
}
},
is_always_update: {
type: 'switch',
label: this.$t('xpack.Cloud.IsAlwaysUpdate'),
@@ -88,7 +64,7 @@ export default {
ajax: {
url: '/api/v1/xpack/cloud/regions/',
processResults(data) {
const results = data.regions.map((item) => {
const results = data.regions?.map((item) => {
return { label: item.name, value: item.id }
})
const more = !!data.next
@@ -114,6 +90,10 @@ export default {
return formValue.is_periodic === false
},
helpText: this.$t('xpack.HelpText.IntervalOfCreateUpdatePage')
},
strategy: {
label: this.$t('common.Strategy'),
component: SyncInstanceTaskStrategy
}
},
updateSuccessNextRoute: { name: 'CloudCenter' },
@@ -123,20 +103,15 @@ export default {
const [name, port] = i.split('/')
return { name, port }
})
return formValue
},
cleanFormValue(value) {
let protocols = ''
const ipNetworkSegments = value.ip_network_segment_group
const strategy = value?.strategy || []
if (!Array.isArray(ipNetworkSegments)) {
value.ip_network_segment_group = ipNetworkSegments ? ipNetworkSegments.split(',') : []
}
if (value.protocols.length > 0) {
protocols = value.protocols.map(i => (i.name + '/' + i.port)).join(' ')
}
value.protocols = protocols
value.strategy = strategy.map(item => { return item.id })
return value
},
onPerformError(error, method, vm) {

View File

@@ -5,12 +5,20 @@
</el-col>
<el-col :md="10" :sm="24">
<QuickActions :actions="quickActions" type="primary" />
<RelationCard
ref="StrategyRelation"
v-perms="'xpack.change_strategy'"
style="margin-top: 15px"
type="info"
v-bind="strategyRelationConfig"
/>
</el-col>
</el-row>
</template>
<script>
import AutoDetailCard from '@/components/Cards/DetailCard/auto'
import RelationCard from '@/components/Cards/RelationCard'
import QuickActions from '@/components/QuickActions'
import { toSafeLocalDateStr } from '@/utils/common'
import { openTaskPage } from '@/utils/jms'
@@ -19,7 +27,8 @@ export default {
name: 'Detail',
components: {
AutoDetailCard,
QuickActions
QuickActions,
RelationCard
},
props: {
object: {
@@ -47,14 +56,57 @@ export default {
)
}.bind(this)
}
},
{
title: this.$t('common.Strategy'),
attrs: {
type: 'primary',
label: this.$t('xpack.Execute'),
disabled: !this.$hasPerm('xpack.add_syncinstancetaskexecution')
}
}
],
strategyRelationConfig: {
icon: 'fa-info',
title: this.$t('common.Strategy'),
objectsAjax: {
url: '/api/v1/xpack/cloud/strategies/',
transformOption: (item) => {
return { label: item.name, value: item.id }
}
},
hasObjectsId: this.object.strategy?.map(i => i.id) || [],
performAdd: (items) => {
const newData = []
const value = this.$refs.StrategyRelation.iHasObjects
value.map(v => {
newData.push(v.value)
})
const relationUrl = `/api/v1/xpack/cloud/sync-instance-tasks/${this.object.id}/`
items.map(v => {
newData.push(v.value)
})
return this.$axios.patch(relationUrl, { strategy: newData })
},
performDelete: (item) => {
const itemId = item.value
const newData = []
const value = this.$refs.StrategyRelation.iHasObjects
value.map(v => {
if (v.value !== itemId) {
newData.push(v.value)
}
})
const relationUrl = `/api/v1/xpack/cloud/sync-instance-tasks/${this.object.id}/`
return this.$axios.patch(relationUrl, { strategy: newData })
}
},
url: `/api/v1/xpack/cloud/accounts/${this.object.id}`,
detailFields: [
'name', 'account_display', 'node_display',
{
key: this.$t('assets.Protocols'),
value: this.object.protocols
key: this.$t('common.Strategy'),
value: this.object.strategy?.map(item => item.name).join(', ')
},
{
key: this.$t('xpack.Cloud.IPNetworkSegment'),
@@ -87,7 +139,6 @@ export default {
computed: {
},
mounted() {
},
methods: {

View File

@@ -0,0 +1,101 @@
<template>
<Dialog
:destroy-on-close="true"
:show-buttons="false"
:close-on-click-modal="false"
:title="$tc('common.Strategy')"
width="80%"
v-bind="$attrs"
v-on="$listeners"
>
<GenericCreateUpdateForm v-bind="$data" />
</Dialog>
</template>
<script>
import GenericCreateUpdateForm from '@/layout/components/GenericCreateUpdateForm'
import Dialog from '@/components/Dialog/index.vue'
import RuleInput from '@/views/assets/Cloud/Strategy/components/RuleInput'
import ActionInput from '@/views/assets/Cloud/Strategy/components/ActionInput'
export default {
name: 'AttrDialog',
components: { Dialog, GenericCreateUpdateForm },
props: {
value: {
type: Object,
default: () => ({ name: '', strategy_rules: [], strategy_actions: [] })
},
tableConfig: {
type: Object,
default: () => ({})
}
},
data() {
return {
object: this.getObject(),
fields: [
[this.$t('common.BasicSetting'), ['name', 'priority']],
[this.$t('common.RuleSetting'), ['strategy_rules']],
[this.$t('common.ActionSetting'), ['strategy_actions']]
],
fieldsMeta: {
strategy_rules: {
label: this.$t('common.Rule'),
component: RuleInput
},
strategy_actions: {
label: this.$t('common.Action'),
component: ActionInput
}
},
hasSaveContinue: false,
onPerformSuccess: (instance) => {
const index = this.tableConfig.totalData.findIndex(x => x.id === instance.id)
if (index !== -1) {
this.tableConfig.totalData.splice(index, 1, instance)
} else {
this.tableConfig.totalData.push(instance)
}
this.$emit('confirm')
this.$emit('update:visible', false)
},
getUrl: () => {
const url = '/api/v1/xpack/cloud/strategies/'
return this.object?.id ? `${url}${this.object.id}/` : url
},
submitMethod: () => {
return this.object?.id ? 'put' : 'post'
}
}
},
methods: {
getObject() {
if (this.value?.id) {
return {
id: this.value.id, name: this.value.name,
strategy_rules: this.value.strategy_rules, strategy_actions: this.value.strategy_actions
}
}
return {}
},
handleAttrDelete(type, index) {
return () => {
const item = this.fieldsMeta[type].el.tableConfig.totalData.splice(index, 1)
if (item[0]?.id) {
this.$axios.delete(`/api/v1/xpack/cloud/${type}/${item[0]?.id}/`)
}
this.$message.success(this.$tc('common.deleteSuccessMsg'))
this.object[type] = this.fieldsMeta[type].el.tableConfig.totalData
}
}
}
}
</script>
<style lang="scss" scoped>
.el-form ::v-deep .el-form {
margin-top: -15px;
}
</style>

View File

@@ -0,0 +1,126 @@
<template>
<div>
<Select2 v-bind="select2" @change="handleInput" />
<DataTable :config="tableConfig" />
<AttrDialog
v-if="visible"
:value="attrValue"
:visible.sync="visible"
:table-config="tableConfig"
@confirm="onAttrDialogConfirm"
/>
<el-button type="primary" size="mini" @click="handleCreate">{{ this.$t('common.New') }}</el-button>
</div>
</template>
<script>
import DataTable from '@/components/Table/DataTable/index.vue'
import Select2 from '@/components/Form/FormFields/Select2.vue'
import { tableFormatter } from '@/views/assets/Cloud/Strategy/components/const'
import AttrDialog from './AttrDialog.vue'
export default {
name: 'SyncInstanceTaskStrategy',
components: { DataTable, AttrDialog, Select2 },
props: {
totalData: {
type: Array,
default: () => []
},
value: {
type: Array,
default: () => []
}
},
data() {
return {
attrValue: { name: '', priority: 50, strategy_rules: [], strategy_actions: [] },
strategy: {},
visible: false,
tableConfig: {
columns: [
{ prop: 'name', label: this.$t('common.PolicyName') },
{ prop: 'priority', label: this.$t('acl.priority') },
{ prop: 'strategy_rules', label: this.$t('common.RuleCount'), formatter: tableFormatter('count') },
{ prop: 'strategy_actions', label: this.$t('common.ActionCount'), formatter: tableFormatter('count') },
{ prop: 'action', label: this.$t('common.Action'), align: 'center', width: '100px', formatter: (row, col, cellValue, index) => {
return (
<div className='input-button'>
<el-button
icon='el-icon-edit'
size='mini'
style={{ 'flexShrink': 0 }}
type='primary'
onClick={this.handleAttrEdit({ row, col, cellValue, index })}
/>
<el-button
icon='el-icon-minus'
size='mini'
style={{ 'flexShrink': 0 }}
type='danger'
onClick={this.handleAttrDelete({ row, col, cellValue, index })}
/>
</div>
)
} }
],
totalData: this.value,
hasPagination: false
},
select2: {
url: '/api/v1/xpack/cloud/strategies/',
multiple: false,
ajax: {
transformOption: (item) => {
this.strategy[item.id] = {
name: item.name, priority: item.priority, strategy_rules: item.strategy_rules, strategy_actions: item.strategy_actions
}
return { label: item.name, value: item.id }
}
}
}
}
},
methods: {
handleInput(value) {
let status = true
const totalData = this.tableConfig.totalData
const data = this.strategy[value]
for (let i = 0; i < totalData.length; i++) {
if (totalData[i].id === value) {
status = false
this.$message.error(`${this.$tc('xpack.Cloud.ExistError')}`)
break
}
}
if (status) {
data['id'] = value
this.tableConfig.totalData.push(data)
}
},
handleCreate() {
this.attrValue = { name: '', priority: 50, strategy_rules: [], strategy_actions: [] }
this.visible = true
},
onAttrDialogConfirm() {
this.$emit('input', this.tableConfig.totalData)
},
handleAttrDelete({ index }) {
return () => {
this.tableConfig.totalData.splice(index, 1)
}
},
handleAttrEdit({ row, index }) {
return () => {
this.$axios.get(`/api/v1/xpack/cloud/strategies/${row?.id}/`).then((data) => {
this.attrValue = data
this.visible = true
})
}
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -127,6 +127,6 @@ export const ACCOUNT_PROVIDER_ATTRS_MAP = {
[lan]: {
name: lan,
title: i18n.t('xpack.Cloud.LAN'),
attrs: ['ip_group', 'test_port', 'test_timeout', 'platform', 'hostname_prefix']
attrs: ['ip_group', 'test_port', 'test_timeout', 'hostname_prefix']
}
}

View File

@@ -16,11 +16,17 @@ export default {
activeMenu: 'SyncInstanceTaskList',
submenu: [
{
title: this.$t('xpack.Cloud.SyncInstanceTaskList'),
title: this.$t('common.SyncTask'),
name: 'SyncInstanceTaskList',
hidden: () => !this.$hasPerm('xpack.view_syncinstancetask'),
component: () => import('@/views/assets/Cloud/SyncInstanceTask/SyncInstanceTaskList.vue')
},
{
title: this.$t('xpack.Cloud.SyncStrategy'),
name: 'StrategyList',
hidden: () => !this.$hasPerm('xpack.view_strategy'),
component: () => import('@/views/assets/Cloud/Strategy/StrategyList.vue')
},
{
title: this.$t('xpack.Cloud.AccountList'),
name: 'AccountList',