perf: cloud sync module adjustment (#3912)

* perf: cloud sync module adjustment

* perf: optimize interface settings

* fix: cloud edit - previous step and next step will failed
This commit is contained in:
jiangweidong
2024-05-13 18:39:19 +08:00
committed by GitHub
parent cd6d9e1687
commit 1c39041de2
21 changed files with 692 additions and 794 deletions

View File

@@ -0,0 +1,93 @@
<template>
<div>
<el-checkbox
v-if="hasCheckAll && options.length > 0"
v-model="checkAll"
:indeterminate="isIndeterminate"
@change="handleCheckAllChange"
>
{{ $tc('SelectAll') }}
</el-checkbox>
<el-checkbox-group v-model="checked" @change="onChange">
<el-alert v-show="tipShow" type="error"> {{ noOptionTip }}</el-alert>
<el-checkbox v-for="item in options" :key="item.value" :label="item.value">{{ item.label }}</el-checkbox>
</el-checkbox-group>
</div>
</template>
<script>
export default {
name: 'Checkbox',
props: {
value: {
type: Array,
default: () => ([])
},
url: {
type: String,
default: ''
},
processResults: {
type: Function,
default: (data) => data
},
noOptionTip: {
type: String,
default: () => ''
},
hasCheckAll: {
type: Boolean,
default: () => true
}
},
data() {
return {
checkAll: false,
tipShow: false,
isIndeterminate: false,
checked: this.value || [],
options: []
}
},
async mounted() {
await this.getOptions()
this.tipShow = this.options.length === 0 && this.noOptionTip
},
methods: {
async getOptions() {
const validateStatus = (status) => {
if (status === 403) {
return 200
}
return status
}
await this.$axios.get(this.url, { validateStatus }).then((data) => {
this.processResults(data)?.forEach((v) => {
this.options.push(v)
})
this.refreshCheckboxStatus()
})
},
refreshCheckboxStatus() {
const checkedCount = this.checked.length
this.checkAll = checkedCount === this.options.length
this.isIndeterminate = checkedCount > 0 && checkedCount < this.options.length
},
onChange() {
this.$log.debug('Current checked: ', this.checked)
this.refreshCheckboxStatus()
this.$emit('input', this.checked)
},
handleCheckAllChange(val) {
this.checked = val ? this.options.map((o) => o.value) : []
this.isIndeterminate = false
this.$emit('input', this.checked)
}
}
}
</script>
<style scoped>
</style>

View File

@@ -2,6 +2,7 @@ import Link from './Link.vue'
import Select2 from './Select2.vue'
import TagInput from './TagInput.vue'
import Switcher from './Switcher.vue'
import Checkbox from './Checkbox.vue'
import AttrInput from './AttrInput.vue'
import UploadKey from './UploadKey.vue'
import JsonEditor from './JsonEditor.vue'
@@ -23,6 +24,7 @@ import PasswordRule from './PasswordRule.vue'
export default {
Link,
Switcher,
Checkbox,
Select2,
TagInput,
AttrInput,
@@ -47,6 +49,7 @@ export default {
export {
Link,
Switcher,
Checkbox,
Select2,
TagInput,
AttrInput,

View File

@@ -60,6 +60,7 @@ const defaultDeleteCallback = function({ row, col, cellValue, reload }) {
msg += ' ?'
const title = this.$t('Info')
const performDelete = this.colActions.performDelete
const afterDelete = this.colActions.afterDelete
this.$alert(msg, title, {
type: 'warning',
confirmButtonClass: 'el-button--danger',
@@ -71,6 +72,9 @@ const defaultDeleteCallback = function({ row, col, cellValue, reload }) {
await performDelete.bind(this)({ row: row, col: col })
done()
reload()
if (afterDelete instanceof Function) {
afterDelete({ row: row, col: col })
}
this.$message.success(this.$tc('DeleteSuccessMsg'))
} finally {
instance.confirmButtonLoading = false

View File

@@ -118,10 +118,10 @@ export default {
}
.action-bar {
position: relative;
height: 30px;
line-height: 30px;
height: 0;
border: 1px solid #dcdfe6;
border-bottom: none;
z-index: 999;
.action {
position: absolute;
right: 6px;

View File

@@ -13,6 +13,7 @@ export { default as Hamburger } from './Widgets/Hamburger'
export { default as ListTable } from './Table/ListTable'
export { default as RelationCard } from './Cards/RelationCard'
export { default as Select2 } from './Form/FormFields/Select2'
export { default as Checkbox } from './Form/FormFields/Checkbox'
export { default as UploadKey } from './Form/FormFields/UploadKey.vue'
export { default as AssetSelect } from './Apps/AssetSelect'
export { default as AutomationParams } from './Apps/AutomationParams'

View File

@@ -26,7 +26,7 @@ const clouds = {
component: empty,
hidden: true,
meta: {
title: i18n.t('AccountList'),
title: i18n.t('CloudAccountList'),
permissions: ['xpack.view_account']
},
children: [
@@ -36,7 +36,7 @@ const clouds = {
hidden: true,
redirect: '/console/assets/cloud',
meta: {
title: i18n.t('AccountList'),
title: i18n.t('CloudAccountList'),
permissions: ['xpack.view_account']
}
},
@@ -46,7 +46,7 @@ const clouds = {
name: 'AccountCreate',
hidden: true,
meta: {
title: i18n.t('Create'),
title: i18n.t('CloudAccountCreate'),
action: 'create',
permissions: ['xpack.add_account']
}
@@ -57,7 +57,7 @@ const clouds = {
name: 'AccountUpdate',
hidden: true,
meta: {
title: i18n.t('Update'),
title: i18n.t('CloudAccountUpdate'),
action: 'update',
permissions: ['xpack.change_account']
}
@@ -68,62 +68,12 @@ const clouds = {
name: 'AccountDetail',
hidden: true,
meta: {
title: i18n.t('Detail'),
title: i18n.t('CloudAccountDetail'),
permissions: ['xpack.view_account']
}
}
]
},
{
path: 'sync-instance-tasks',
component: empty,
hidden: true,
meta: {
title: i18n.t('SyncInstanceTaskList'),
permissions: ['xpack.view_syncinstancetask']
},
children: [
{
path: '',
component: () => import('@/views/assets/Cloud/'),
name: 'SyncInstanceTaskList',
hidden: true,
meta: {
title: i18n.t('SyncInstanceTaskList'),
permissions: ['xpack.view_syncinstancetask']
}
},
{
path: 'create',
component: () => import('@/views/assets/Cloud/SyncInstanceTask/SyncInstanceTaskCreateUpdate'),
name: 'SyncInstanceTaskCreate',
hidden: true,
meta: {
title: i18n.t('SyncInstanceTaskCreate'),
permissions: ['xpack.add_syncinstancetask']
}
},
{
path: ':id/update',
component: () => import('@/views/assets/Cloud/SyncInstanceTask/SyncInstanceTaskCreateUpdate'),
name: 'SyncInstanceTaskUpdate',
hidden: true,
meta: {
title: i18n.t('SyncInstanceTaskUpdate'),
permissions: ['xpack.change_syncinstancetask']
}
},
{
path: ':id',
component: () => import('@/views/assets/Cloud/SyncInstanceTask/SyncInstanceTaskDetail/index'),
name: 'SyncInstanceTaskDetail',
hidden: true,
meta: {
title: i18n.t('SyncInstanceTaskDetail')
}
}
]
},
{
path: 'strategy',
component: empty,

View File

@@ -12,7 +12,7 @@ import Detail from './Detail'
import TaskExecutionList from './TaskExecutionList'
export default {
name: 'SyncInstanceTaskDetail',
name: 'AccountGatherTaskDetail',
components: {
GenericDetailPage,
TabPage,

View File

@@ -1,20 +1,50 @@
<template>
<GenericCreateUpdatePage
:initial="initial"
v-bind="$data"
/>
<Page>
<el-row :gutter="20">
<el-col :md="20" :sm="24">
<IBox>
<GenericCreateUpdateForm
v-show="activeStep===0"
ref="baseForm"
v-bind="configSettings"
/>
<GenericCreateUpdateForm
v-if="activeStep===1"
ref="strategyForm"
v-bind="strategySettings"
/>
</IBox>
</el-col>
<el-col :md="4" :sm="24">
<IBox style="height: 490px;">
<el-steps
direction="vertical"
:active="activeStep"
space="400px"
>
<el-step :title="$tc('Config')" :description="description" />
<el-step :title="$tc('Strategy')" />
</el-steps>
</IBox>
</el-col>
</el-row>
</Page>
</template>
<script>
import { GenericCreateUpdatePage } from '@/layout/components'
import { RequiredChange, specialEmojiCheck } from '@/components/Form/DataForm/rules'
import { ACCOUNT_PROVIDER_ATTRS_MAP, aliyun } from '../const'
import { UploadKey } from '@/components'
import { Checkbox, CronTab, IBox, UploadKey } from '@/components'
import { encryptPassword } from '@/utils/crypto'
import { GenericCreateUpdateForm, Page } from '@/layout/components'
import SyncInstanceTaskStrategy from './components/SyncInstanceTaskStrategy'
import { setUrlParam } from '@/utils/common'
export default {
components: {
GenericCreateUpdatePage
IBox,
Page,
GenericCreateUpdateForm
},
data() {
const vm = this
@@ -33,110 +63,233 @@ export default {
}
return {
initial: {
attrs: {
ip_group: []
accountId: '',
taskId: '',
activeStep: 0,
description: accountProviderAttrs.title,
configSettings: {
initial: {
attrs: {
ip_group: []
},
provider: this.$route.query.provider,
port: 443
},
provider: this.$route.query.provider,
port: 443
},
url: '/api/v1/xpack/cloud/accounts/',
fields: [
[this.$t('Basic'), ['name', 'provider']],
[this.$t(accountProviderAttrs.title), ['attrs']],
[this.$t('Other'), ['comment']]
],
fieldsMeta: {
name: {
rules: [RequiredChange, specialEmojiCheck]
},
attrs: {
encryptedFields: ['access_key_secret'],
fields: accountProviderAttrs.attrs,
fieldsMeta: {
// 必须放在最上面,下面特殊制定的字段才会覆盖默认
...setFieldAttrs(),
service_account_key: {
label: this.$t('ServerAccountKey'),
component: UploadKey,
el: {
toFormat: 'object'
url: '/api/v1/xpack/cloud/accounts/',
fields: [
[this.$t('Basic'), ['name', 'provider']],
[this.$t(accountProviderAttrs.title), ['attrs']],
[this.$t('Other'), ['comment']]
],
fieldsMeta: {
name: {
rules: [RequiredChange, specialEmojiCheck]
},
attrs: {
encryptedFields: ['access_key_secret'],
fields: accountProviderAttrs.attrs,
fieldsMeta: {
// 必须放在最上面,下面特殊制定的字段才会覆盖默认
...setFieldAttrs(),
service_account_key: {
label: this.$t('ServerAccountKey'),
component: UploadKey,
el: {
toFormat: 'object'
}
},
cert_file: {
label: this.$t('Certificate'),
component: UploadKey,
el: {
toFormat: 'object'
}
},
key_file: {
label: this.$t('SecretKey'),
component: UploadKey,
el: {
toFormat: 'object'
}
},
password: {
rules: this.$route.params.id ? [] : [RequiredChange]
},
public_key: {
label: this.$t('PublicKey'),
rules: this.$route.params.id ? [] : [RequiredChange]
},
private_key: {
label: this.$t('PrivateKey'),
rules: this.$route.params.id ? [] : [RequiredChange]
},
project: {
label: this.$t('Project')
}
},
cert_file: {
label: this.$t('Certificate'),
component: UploadKey,
el: {
toFormat: 'object'
}
},
key_file: {
label: this.$t('SecretKey'),
component: UploadKey,
el: {
toFormat: 'object'
}
},
password: {
rules: this.$route.params.id ? [] : [RequiredChange]
},
public_key: {
label: this.$t('PublicKey'),
rules: this.$route.params.id ? [] : [RequiredChange]
},
private_key: {
label: this.$t('PrivateKey'),
rules: this.$route.params.id ? [] : [RequiredChange]
},
project: {
label: this.$t('Project')
}
},
provider: {
rules: [RequiredChange],
el: {
disabled: true
}
}
},
provider: {
rules: [RequiredChange],
el: {
disabled: true
onPerformSuccess: (resp) => {
let url = vm.strategySettings.fieldsMeta.regions.el.url
if (!vm.accountId) {
vm.accountId = resp?.id
url = setUrlParam(url, 'account_id', vm.accountId)
vm.strategySettings.fieldsMeta.regions.el.url = url
}
}
},
updateSuccessNextRoute: { name: 'CloudCenter', params: { activeMenu: 'AccountList' }},
createSuccessNextRoute: { name: 'CloudCenter', params: { activeMenu: 'AccountList' }},
getUrl() {
const params = this.$route.params
let url = `/api/v1/xpack/cloud/accounts/`
if (params.id) {
url = `${url}${params.id}/`
}
return `${url}?provider=${accountProvider}`
},
cleanFormValue(values) {
const encryptedFields = [
'access_key_secret', 'password', 'client_secret',
'oc_password', 'sc_password'
]
const attrs = values.attrs
for (const item of encryptedFields) {
const value = attrs[item]
if (!value) {
continue
if (!vm.taskId) {
vm.taskId = resp?.task?.id
vm.strategySettings.url += `${vm.taskId}/`
}
this.activeStep = this.activeStep === 0 ? 1 : 0
},
submitBtnText: this.$t('Next'),
hasSaveContinue: false,
hasReset: false,
getUrl: () => {
let url = vm.configSettings.url
const params = vm.$route.params
const instanceId = params?.id || vm.accountId
if (instanceId) {
url = `${url}${instanceId}/`
}
return `${url}?provider=${accountProvider}`
},
submitMethod: () => { return vm.$route.params?.id || vm.accountId ? 'put' : 'post' },
cleanFormValue(values) {
const encryptedFields = [
'access_key_secret', 'password', 'client_secret',
'oc_password', 'sc_password'
]
const attrs = values.attrs
for (const item of encryptedFields) {
const value = attrs[item]
if (!value) {
continue
}
attrs[item] = encryptPassword(value)
}
if (Array.isArray(attrs.ip_group)) {
values.attrs.ip_group = attrs.ip_group.filter(Boolean)
}
return values
},
afterGetFormValue(formValue) {
if (!formValue.attrs) {
return formValue
}
attrs[item] = encryptPassword(value)
}
if (Array.isArray(attrs.ip_group)) {
values.attrs.ip_group = attrs.ip_group.filter(Boolean)
}
return values
},
afterGetFormValue(formValue) {
if (!formValue.attrs) {
return formValue
}
return formValue
},
strategySettings: {
initial: {
is_periodic: false,
interval: 24,
hostname_strategy: 'instance_name_partial_ip',
ip_network_segment_group: ['*']
},
fields: [
[this.$t('CloudSource'), ['regions']],
[this.$t('SaveSetting'), [
'hostname_strategy', 'ip_network_segment_group', 'sync_ip_type',
'is_always_update', 'fully_synchronous', 'release_assets'
]],
[this.$t('SyncStrategy'), ['strategy']],
[this.$t('Periodic'), ['is_periodic', 'interval', 'crontab']]
],
url: '/api/v1/xpack/cloud/sync-instance-tasks/',
fieldsMeta: {
hostname_strategy: {
rules: [RequiredChange],
helpTip: this.$t('HostnameStrategy')
},
is_always_update: {
type: 'switch',
label: this.$t('IsAlwaysUpdate'),
helpTip: this.$t('IsAlwaysUpdateHelpTip')
},
fully_synchronous: {
type: 'switch',
label: this.$t('FullySynchronous'),
helpTip: this.$t('FullySynchronousHelpTip')
},
release_assets: {
type: 'switch',
label: this.$t('ReleaseAssets'),
helpTips: this.$t('ReleaseAssetsHelpTips')
},
regions: {
component: Checkbox,
el: {
url: '/api/v1/xpack/cloud/regions/',
options: [],
value: [],
noOptionTip: this.$t('CloudRegionTip'),
processResults(data) {
return data.regions?.map((item) => {
return { label: item.name, value: item.id }
})
}
}
},
is_periodic: {
type: 'switch'
},
crontab: {
component: CronTab,
hidden: (formValue) => {
return formValue.is_periodic === false
},
helpText: this.$t('CrontabOfCreateUpdatePage'),
helpTextAsTip: true
},
interval: {
hidden: (formValue) => {
return formValue.is_periodic === false
},
helpText: this.$t('IntervalOfCreateUpdatePage')
},
strategy: {
label: this.$t('Strategy'),
component: SyncInstanceTaskStrategy,
helpTip: this.$t('StrategyHelpTip')
}
},
hasSaveContinue: false,
hasReset: false,
getUrl: () => { return this.strategySettings.url },
submitMethod: () => { return this.$refs.baseForm.form.task?.id ? 'put' : 'post' },
moreButtons: [
{
title: this.$t('Previous'),
callback: () => { this.activeStep = this.activeStep === 1 ? 0 : 1 }
}
],
updateSuccessNextRoute: { name: 'CloudCenter' },
createSuccessNextRoute: { name: 'CloudCenter' },
cleanFormValue(value) {
const ipNetworkSegments = value.ip_network_segment_group
const strategy = value?.strategy || []
if (!Array.isArray(ipNetworkSegments)) {
value.ip_network_segment_group = ipNetworkSegments ? ipNetworkSegments.split(',') : []
}
value.strategy = strategy.map(item => { return item.id })
const accountId = this.$route.params?.id || this.accountId
if (accountId) {
value.account = { pk: accountId }
}
return value
}
}
}
},
methods: {}
methods: {
}
}
</script>

View File

@@ -1,17 +1,33 @@
<template>
<el-row :gutter="20">
<el-col :md="16" :sm="24">
<el-col :md="15" :sm="24">
<AutoDetailCard :excludes="excludes" :object="object" :url="url" />
<AutoDetailCard :fields="detailFields" :object="object" :title="$tc('TaskDetail')" :url="url" />
</el-col>
<el-col :md="9" :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 { toSafeLocalDateStr } from '@/utils/common'
import RelationCard from '@/components/Cards/RelationCard'
import QuickActions from '@/components/QuickActions'
import { openTaskPage } from '@/utils/jms'
export default {
name: 'AccountDetail',
components: {
QuickActions, RelationCard,
AutoDetailCard
},
props: {
@@ -23,13 +39,108 @@ export default {
data() {
return {
url: `/api/v1/xpack/cloud/accounts/${this.object.id}/`,
excludes: ['attrs']
excludes: ['attrs', 'task'],
quickActions: [
{
title: this.$t('RunTaskManually'),
attrs: {
showTip: !this.object.task?.id,
tip: this.$t('ExecCloudSyncErrorMsg'),
type: 'primary',
label: this.$t('Execute'),
disabled: !this.$hasPerm('xpack.add_syncinstancetaskexecution') || !this.object.task?.id
},
callbacks: {
click: function() {
this.$axios.get(
`/api/v1/xpack/cloud/sync-instance-tasks/${this.object.task.id}/run/`
).then(res => {
openTaskPage(res['task'])
})
}.bind(this)
}
}
],
strategyRelationConfig: {
icon: 'fa-info',
title: this.$t('Strategy'),
objectsAjax: {
url: '/api/v1/xpack/cloud/strategies/',
transformOption: (item) => {
return { label: item.name, value: item.id }
}
},
hasObjectsId: this.object?.task?.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 })
}
},
detailFields: [
{
key: this.$t('Strategy'),
value: this.object?.task?.strategy?.map(item => item.name).join(', ')
},
{
key: this.$t('IPNetworkSegment'),
value: this.object?.task?.ip_network_segment_group?.join(', ')
},
{
key: this.$t('IsAlwaysUpdate'),
value: this.object?.task?.is_always_update
},
{
key: this.$t('CyclePerform'),
value: this.object?.task?.is_periodic
},
{
key: this.$t('Region'),
value: this.object?.task.regions,
formatter(row, value) {
return (<div>{
value?.map((content) => {
return <div>{ content }</div>
})}
</div>)
}
},
{
key: this.$t('ReleaseAssets'),
value: this.object?.task.release_assets
},
{
key: this.$t('DateLastSync'),
value: this.object?.task?.date_last_sync ? toSafeLocalDateStr(this.object?.task.date_created) : ''
},
{
key: this.$t('DateCreated'),
value: this.object?.task.date_created ? toSafeLocalDateStr(this.object?.task.date_created) : ''
},
'comment'
]
}
},
computed: {
cardTitle() {
return this.object.name
}
}
}
</script>

View File

@@ -8,7 +8,7 @@ import { ActionsFormatter, DateFormatter } from '@/components/Table/TableFormatt
import { openTaskPage } from '@/utils/jms'
export default {
name: 'HistoryList',
name: 'TaskHistoryList',
components: { GenericListTable },
props: {
object: {
@@ -29,7 +29,7 @@ export default {
}
},
tableConfig: {
url: `/api/v1/xpack/cloud/sync-instance-tasks/${this.object.id}/history/`,
url: `/api/v1/xpack/cloud/sync-instance-tasks/${this.object.task?.id}/history/`,
columns: [
{
prop: 'summary.new',

View File

@@ -7,7 +7,7 @@ import GenericListTable from '@/layout/components/GenericListTable/index'
import { DateFormatter } from '@/components/Table/TableFormatters'
export default {
name: 'AssetList',
name: 'TaskSyncAssetList',
components: { GenericListTable },
props: {
object: {
@@ -40,7 +40,7 @@ export default {
]
},
tableConfig: {
url: `/api/v1/xpack/cloud/sync-instance-tasks/${this.object.id}/instances/`,
url: `/api/v1/xpack/cloud/sync-instance-tasks/${this.object.task?.id}/instances/`,
hasSelection: false,
columns: [
'instance_id',

View File

@@ -8,11 +8,15 @@
<script>
import { GenericDetailPage, TabPage } from '@/layout/components'
import TaskSyncAssetList from './TaskSyncAssetList'
import TaskHistoryList from './TaskHistoryList'
import AccountDetail from './AccountDetail'
export default {
components: {
GenericDetailPage,
TaskSyncAssetList,
TaskHistoryList,
AccountDetail,
TabPage
},
@@ -20,7 +24,7 @@ export default {
const vm = this
return {
Account: {
name: '', provider: '', provider_display: '', validity_display: '', comment: '', date_created: '', created_by: ''
name: '', provider: '', provider_display: '', validity_display: '', comment: '', date_created: '', created_by: '', task: {}
},
config: {
url: `/api/v1/xpack/cloud/accounts`,
@@ -29,6 +33,16 @@ export default {
{
title: this.$t('Detail'),
name: 'AccountDetail'
},
{
title: this.$t('SyncInstanceTaskHistoryList'),
name: 'TaskHistoryList',
hidden: () => { return !this.Account.task?.id }
},
{
title: this.$t('SyncInstanceTaskHistoryAssetList'),
name: 'TaskSyncAssetList',
hidden: () => { return !this.Account.task?.id }
}
],
actions: {

View File

@@ -2,9 +2,9 @@
<div>
<GenericListTable ref="regionTable" :header-actions="headerActions" :table-config="tableConfig" />
<Dialog
:confirm-title="$tc('assets.TestConnection')"
:confirm-title="$tc('TestConnection')"
:loading-status="testLoading"
:title="$tc('assets.TestConnection')"
:title="$tc('TestConnection')"
:visible.sync="visible"
width="50"
@cancel="handleCancel()"
@@ -12,7 +12,7 @@
@confirm="handleConfirm()"
>
<el-form ref="regionForm" :model="account" label-width="auto">
<el-form-item :label="$tc('xpack.Cloud.Region')" :rules="regionRules" prop="region">
<el-form-item :label="$tc('Region')" :rules="regionRules" prop="region">
<Select2 ref="regionSelect" v-model="account.region" v-bind="select2" />
</el-form-item>
</el-form>
@@ -30,9 +30,10 @@ import rules from '@/components/Form/DataForm/rules'
import { Select2 } from '@/components/Form/FormFields'
import GenericListTable from '@/layout/components/GenericListTable'
import Dialog from '@/components/Dialog/index.vue'
import { openTaskPage } from '@/utils/jms'
export default {
name: 'AccountList',
name: 'CloudAccountList',
components: {
Dialog,
Select2,
@@ -47,7 +48,7 @@ export default {
app: 'xpack',
resource: 'account'
},
columnsExclude: ['attrs'],
columnsExclude: ['attrs', 'task'],
columnsShow: {
default: [
'name', 'provider', 'comment', 'validity', 'actions'
@@ -70,6 +71,9 @@ export default {
onUpdate: ({ row, col }) => {
vm.$router.push({ name: 'AccountUpdate', params: { id: row.id }, query: { provider: row.provider?.value }})
},
afterDelete: () => {
this.getCloudPlatforms()
},
extraActions: [
{
name: 'TestConnection',
@@ -87,6 +91,22 @@ export default {
vm.$message.error(err.response.data.msg)
})
}
},
{
title: vm.$t('RunTaskManually'),
name: 'execute',
type: 'info',
can: () => vm.$hasPerm('xpack.add_syncinstancetaskexecution'),
callback: function(data) {
const taskId = data.row.task?.id
if (taskId) {
this.$axios.get(`/api/v1/xpack/cloud/sync-instance-tasks/${taskId}/run/`).then(res => {
openTaskPage(res['task'])
})
} else {
this.$message.error(this.$t('ExecCloudSyncErrorMsg'))
}
}
}
]
}
@@ -101,117 +121,11 @@ export default {
getUrlQuery: false
},
moreCreates: {
loading: false,
callback: (option) => {
vm.$router.push({ name: 'AccountCreate', query: { provider: option.name }})
},
dropdown: [
{
name: aliyun,
title: ACCOUNT_PROVIDER_ATTRS_MAP[aliyun].title,
type: 'primary',
group: this.$t('PublicCloud'),
can: true
},
{
name: qcloud,
title: ACCOUNT_PROVIDER_ATTRS_MAP[qcloud].title,
type: 'primary',
can: true
},
{
name: qcloud_lighthouse,
title: ACCOUNT_PROVIDER_ATTRS_MAP[qcloud_lighthouse].title
},
{
name: huaweicloud,
title: ACCOUNT_PROVIDER_ATTRS_MAP[huaweicloud].title
},
{
name: baiducloud,
title: ACCOUNT_PROVIDER_ATTRS_MAP[baiducloud].title
},
{
name: jdcloud,
title: ACCOUNT_PROVIDER_ATTRS_MAP[jdcloud].title
},
{
name: kingsoftcloud,
title: ACCOUNT_PROVIDER_ATTRS_MAP[kingsoftcloud].title
},
{
name: aws_china,
title: ACCOUNT_PROVIDER_ATTRS_MAP[aws_china].title
},
{
name: aws_international,
title: ACCOUNT_PROVIDER_ATTRS_MAP[aws_international].title
},
{
name: azure,
title: ACCOUNT_PROVIDER_ATTRS_MAP[azure].title
},
{
name: azure_international,
title: ACCOUNT_PROVIDER_ATTRS_MAP[azure_international].title
},
{
name: gcp,
title: ACCOUNT_PROVIDER_ATTRS_MAP[gcp].title
},
{
name: ucloud,
title: ACCOUNT_PROVIDER_ATTRS_MAP[ucloud].title
},
{
name: volcengine,
title: ACCOUNT_PROVIDER_ATTRS_MAP[volcengine].title
},
{
name: vmware,
group: this.$t('PrivateCloud'),
title: ACCOUNT_PROVIDER_ATTRS_MAP[vmware].title
},
{
name: qingcloud_private,
title: ACCOUNT_PROVIDER_ATTRS_MAP[qingcloud_private].title
},
{
name: huaweicloud_private,
title: ACCOUNT_PROVIDER_ATTRS_MAP[huaweicloud_private].title
},
{
name: ctyun_private,
title: ACCOUNT_PROVIDER_ATTRS_MAP[ctyun_private].title
},
{
name: openstack,
title: ACCOUNT_PROVIDER_ATTRS_MAP[openstack].title
},
{
name: zstack,
title: ACCOUNT_PROVIDER_ATTRS_MAP[zstack].title
},
{
name: nutanix,
title: ACCOUNT_PROVIDER_ATTRS_MAP[nutanix].title
},
{
name: fc,
title: ACCOUNT_PROVIDER_ATTRS_MAP[fc].title
},
{
name: scp,
title: ACCOUNT_PROVIDER_ATTRS_MAP[scp].title
},
{
name: apsara_stack,
title: ACCOUNT_PROVIDER_ATTRS_MAP[apsara_stack].title
},
{
name: lan,
title: ACCOUNT_PROVIDER_ATTRS_MAP[lan].title
}
]
dropdown: []
}
},
account: {},
@@ -224,10 +138,148 @@ export default {
regionRules: [rules.Required]
}
},
mounted() {
this.getCloudPlatforms()
},
methods: {
fitCloudPlatformAttr(platforms, data, group) {
const createdPlatform = []
const uncreatedPlatform = []
platforms.map((p) => {
let created = false
for (let i = 0; i < data?.length; i++) {
if (p.name === data[i].provider.value) {
p['can'] = false
created = true
createdPlatform.push(p)
break
}
}
if (!created) {
uncreatedPlatform.push(p)
}
})
const result = uncreatedPlatform.concat(createdPlatform)
result[0].group = group
return result
},
getCloudPlatforms() {
this.headerActions.moreCreates.loading = true
const publicPlatforms = [
{
name: aliyun,
title: ACCOUNT_PROVIDER_ATTRS_MAP[aliyun].title
},
{
name: qcloud,
title: ACCOUNT_PROVIDER_ATTRS_MAP[qcloud].title
},
{
name: qcloud_lighthouse,
title: ACCOUNT_PROVIDER_ATTRS_MAP[qcloud_lighthouse].title
},
{
name: huaweicloud,
title: ACCOUNT_PROVIDER_ATTRS_MAP[huaweicloud].title
},
{
name: baiducloud,
title: ACCOUNT_PROVIDER_ATTRS_MAP[baiducloud].title
},
{
name: jdcloud,
title: ACCOUNT_PROVIDER_ATTRS_MAP[jdcloud].title
},
{
name: kingsoftcloud,
title: ACCOUNT_PROVIDER_ATTRS_MAP[kingsoftcloud].title
},
{
name: aws_china,
title: ACCOUNT_PROVIDER_ATTRS_MAP[aws_china].title
},
{
name: aws_international,
title: ACCOUNT_PROVIDER_ATTRS_MAP[aws_international].title
},
{
name: azure,
title: ACCOUNT_PROVIDER_ATTRS_MAP[azure].title
},
{
name: azure_international,
title: ACCOUNT_PROVIDER_ATTRS_MAP[azure_international].title
},
{
name: gcp,
title: ACCOUNT_PROVIDER_ATTRS_MAP[gcp].title
},
{
name: ucloud,
title: ACCOUNT_PROVIDER_ATTRS_MAP[ucloud].title
},
{
name: volcengine,
title: ACCOUNT_PROVIDER_ATTRS_MAP[volcengine].title
}
]
const privatePlatforms = [
{
name: vmware,
title: ACCOUNT_PROVIDER_ATTRS_MAP[vmware].title
},
{
name: qingcloud_private,
title: ACCOUNT_PROVIDER_ATTRS_MAP[qingcloud_private].title
},
{
name: huaweicloud_private,
title: ACCOUNT_PROVIDER_ATTRS_MAP[huaweicloud_private].title
},
{
name: ctyun_private,
title: ACCOUNT_PROVIDER_ATTRS_MAP[ctyun_private].title
},
{
name: openstack,
title: ACCOUNT_PROVIDER_ATTRS_MAP[openstack].title
},
{
name: zstack,
title: ACCOUNT_PROVIDER_ATTRS_MAP[zstack].title
},
{
name: nutanix,
title: ACCOUNT_PROVIDER_ATTRS_MAP[nutanix].title
},
{
name: fc,
title: ACCOUNT_PROVIDER_ATTRS_MAP[fc].title
},
{
name: scp,
title: ACCOUNT_PROVIDER_ATTRS_MAP[scp].title
},
{
name: apsara_stack,
title: ACCOUNT_PROVIDER_ATTRS_MAP[apsara_stack].title
},
{
name: lan,
title: ACCOUNT_PROVIDER_ATTRS_MAP[lan].title
}
]
const url = '/api/v1/xpack/cloud/accounts/?fields_size=mini'
this.$axios.get(url).then((resp) => {
const pcPlatforms = this.fitCloudPlatformAttr(publicPlatforms, resp, this.$t('PublicCloud'))
const paPlatforms = this.fitCloudPlatformAttr(privatePlatforms, resp, this.$t('PrivateCloud'))
this.headerActions.moreCreates.dropdown = pcPlatforms.concat(paPlatforms)
this.headerActions.moreCreates.loading = false
})
},
valid(status) {
if (status !== 200) {
this.$message.error(this.$t('AccountTestConnectionError'))
this.$message.error(this.$tc('TestAccountConnectionError'))
return 200
}
return status

View File

@@ -1,163 +0,0 @@
<template>
<GenericCreateUpdatePage ref="createUpdatePage" v-bind="$data" />
</template>
<script>
import { GenericCreateUpdatePage } from '@/layout/components'
import { CronTab, Select2 } from '@/components'
import rules from '@/components/Form/DataForm/rules'
import SyncInstanceTaskStrategy from './components/SyncInstanceTaskStrategy/index'
export default {
components: {
GenericCreateUpdatePage
},
data() {
const vm = this
return {
initial: {
is_periodic: false,
interval: 24,
hostname_strategy: 'instance_name_partial_ip',
ip_network_segment_group: ['*']
},
fields: [
[this.$t('Basic'), ['name']],
[this.$t('CloudSource'), ['account', 'regions']],
[this.$t('SaveSetting'), [
'hostname_strategy', 'ip_network_segment_group',
'sync_ip_type', 'is_always_update', 'fully_synchronous'
]],
[this.$t('SyncStrategy'), ['strategy']],
[this.$t('Periodic'), ['is_periodic', 'interval', 'crontab']],
[this.$t('Other'), ['comment']]
],
url: '/api/v1/xpack/cloud/sync-instance-tasks/',
fieldsMeta: {
account: {
label: this.$t('Account'),
on: {
change: ([event], updateForm) => {
vm.fieldsMeta.regions.el.ajax.url = `/api/v1/xpack/cloud/regions/?account_id=${event?.pk}`
updateForm({ regions: '' })
}
},
el: {
multiple: false,
value: [],
ajax: {
url: '/api/v1/xpack/cloud/accounts/',
transformOption: (item) => {
const label = `${item.name}(${item.provider.label})`
return { label: label, value: item.id }
}
}
}
},
hostname_strategy: {
rules: [rules.RequiredChange],
helpTip: this.$t('HostnameStrategy')
},
is_always_update: {
type: 'switch',
label: this.$t('IsAlwaysUpdate'),
helpTip: this.$t('IsAlwaysUpdateHelpTip')
},
fully_synchronous: {
type: 'switch',
label: this.$t('FullySynchronous'),
helpTip: this.$t('FullySynchronousHelpTip')
},
regions: {
component: Select2,
el: {
multiple: true,
allowCreate: true,
value: [],
ajax: {
url: '/api/v1/xpack/cloud/regions/',
processResults(data) {
const results = data.regions?.map((item) => {
return { label: item.name, value: item.id }
})
const more = !!data.next
return { results: results, pagination: more, total: data.count }
}
}
}
},
is_periodic: {
type: 'switch'
},
crontab: {
component: CronTab,
hidden: (formValue) => {
return formValue.is_periodic === false
},
helpTip: this.$t('CrontabOfCreateUpdatePage')
},
interval: {
hidden: (formValue) => {
return formValue.is_periodic === false
},
helpText: this.$t('IntervalOfCreateUpdatePage')
},
strategy: {
label: this.$t('Strategy'),
component: SyncInstanceTaskStrategy,
helpTip: this.$t('StrategyHelpTip')
}
},
updateSuccessNextRoute: { name: 'CloudCenter' },
createSuccessNextRoute: { name: 'CloudCenter' },
afterGetFormValue(formValue) {
formValue.protocols = formValue.protocols?.split(' ').map(i => {
const [name, port] = i.split('/')
return { name, port }
})
return formValue
},
cleanFormValue(value) {
const ipNetworkSegments = value.ip_network_segment_group
const strategy = value?.strategy || []
if (!Array.isArray(ipNetworkSegments)) {
value.ip_network_segment_group = ipNetworkSegments ? ipNetworkSegments.split(',') : []
}
value.strategy = strategy.map(item => { return item.id })
return value
},
onPerformError(error, method, vm) {
this.$emit('submitError', error)
const response = error.response
const data = response.data
if (response.status === 400) {
for (const key of Object.keys(data)) {
let value = data[key]
if (key === 'protocols') {
value = Object.values(data[key])
}
if (value instanceof Array) {
value = value.join(';')
}
this.$refs.form.setFieldError(key, value)
}
}
}
}
},
async mounted() {
const params = this.$route.params
// 更新获取链接
if (params.id) {
const form = await this.$refs.createUpdatePage.$refs.createUpdateForm.getFormValue()
this.fieldsMeta.regions.el.ajax.url = form.account?.id ? `/api/v1/xpack/cloud/regions/?account_id=${form.account.id}` : `/api/v1/xpack/cloud/regions/`
}
},
methods: {
}
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -1,143 +0,0 @@
<template>
<el-row :gutter="20">
<el-col :md="15" :sm="24">
<AutoDetailCard :fields="detailFields" :object="object" :url="url" />
</el-col>
<el-col :md="9" :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'
export default {
name: 'Detail',
components: {
AutoDetailCard,
QuickActions,
RelationCard
},
props: {
object: {
type: Object,
default: () => {}
}
},
data() {
return {
quickActions: [
{
title: this.$t('RunTaskManually'),
attrs: {
type: 'primary',
label: this.$t('Execute'),
disabled: !this.$hasPerm('xpack.add_syncinstancetaskexecution')
},
callbacks: {
click: function() {
this.$axios.get(
`/api/v1/xpack/cloud/sync-instance-tasks/${this.object.id}/run/`
).then(res => {
openTaskPage(res['task'])
}
)
}.bind(this)
}
}
],
strategyRelationConfig: {
icon: 'fa-info',
title: this.$t('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('Strategy'),
value: this.object.strategy?.map(item => item.name).join(', ')
},
{
key: this.$t('IPNetworkSegment'),
value: this.object.ip_network_segment_group?.join(', ')
},
'is_always_update', 'is_periodic', 'periodic_display',
{
key: this.$t('DateLastSync'),
value: this.object.date_last_sync ? toSafeLocalDateStr(this.object.date_created) : ''
},
{
key: this.$t('DateCreated'),
value: this.object.date_created ? toSafeLocalDateStr(this.object.date_created) : ''
},
{
key: this.$t('Region'),
value: this.object.regions,
formatter(row, value) {
return (<div>{
value?.map((content) => {
return <div>{ content }</div>
})}
</div>)
}
},
'comment'
]
}
},
computed: {
},
mounted() {
},
methods: {
}
}
</script>
<style lang='less' scoped>
</style>

View File

@@ -1,65 +0,0 @@
<template>
<GenericDetailPage :active-menu.sync="config.activeMenu" :object.sync="TaskDetail" v-bind="config" v-on="$listeners">
<keep-alive>
<component :is="config.activeMenu" :object="TaskDetail" />
</keep-alive>
</GenericDetailPage>
</template>
<script>
import { GenericDetailPage, TabPage } from '@/layout/components'
import detail from './detail'
import HistoryList from './HistoryList'
import AssetList from './AssetList'
export default {
name: 'SyncInstanceTaskDetail',
components: {
GenericDetailPage,
TabPage,
detail,
HistoryList,
AssetList
},
data() {
const vm = this
return {
TaskDetail: {},
config: {
activeMenu: 'detail',
url: '/api/v1/xpack/cloud/sync-instance-tasks',
actions: {
canDelete: vm.$hasPerm('xpack.delete_syncinstancetask'),
canUpdate: vm.$hasPerm('xpack.change_syncinstancetask'),
deleteSuccessRoute: 'CloudCenter'
},
submenu: [
{
title: this.$t('SyncInstanceTaskDetail'),
name: 'detail'
},
{
title: this.$t('SyncInstanceTaskHistoryList'),
name: 'HistoryList',
hidden: () => !this.$hasPerm('xpack.view_syncinstancetaskexecution')
},
{
title: this.$t('SyncInstanceTaskHistoryAssetList'),
name: 'AssetList',
hidden: () => !this.$hasPerm('xpack.view_syncinstancedetail')
}
],
hasRightSide: true
// getObjectName: function(obj) {
// return obj.hostname + '(' + obj.ip + ')'
// }
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,116 +0,0 @@
<template>
<GenericListTable :header-actions="headerActions" :table-config="tableConfig" />
</template>
<script>
import GenericListTable from '@/layout/components/GenericListTable'
import { DetailFormatter } from '@/components/Table/TableFormatters'
import { openTaskPage } from '@/utils/jms'
export default {
name: 'SyncInstanceTaskList',
components: {
GenericListTable
},
data() {
const vm = this
return {
tableConfig: {
url: '/api/v1/xpack/cloud/sync-instance-tasks/',
permissions: {
app: 'xpack',
resource: 'syncinstancetask'
},
columnsShow: {
min: ['name', 'account', 'hostname_strategy', 'actions'],
default: [
'name', 'account', 'hostname_strategy', 'protocols', 'is_periodic', 'actions'
]
},
columnsMeta: {
sync_ip_type: {
width: '120px'
},
hostname_strategy: {
width: '150px',
formatter: function(row) {
return <span>{ row.hostname_strategy.label }</span>
}
},
account: {
label: this.$t('Account'),
formatter: function(row) {
return <span>{ row.account?.name }</span>
}
},
periodic_display: {
width: '150px'
},
actions: {
formatterArgs: {
hasClone: false,
onUpdate: ({ row }) => {
this.$router.push({ name: 'SyncInstanceTaskUpdate', params: { id: row.id }})
},
extraActions: [
{
title: vm.$t('Execute'),
name: 'execute',
type: 'info',
can: () => vm.$hasPerm('xpack.add_syncinstancetaskexecution'),
callback: function(data) {
this.$axios.get(`/api/v1/xpack/cloud/sync-instance-tasks/${data.row.id}/run/`).then(res => {
openTaskPage(res['task'])
})
}
}
]
}
},
name: {
formatter: DetailFormatter,
formatterArgs: {
permissions: 'xpack.view_syncinstancedetail',
route: 'SyncInstanceTaskDetail'
}
},
history_count: {
width: '110px',
formatter: DetailFormatter,
formatterArgs: {
permissions: 'xpack.view_syncinstancetaskexecution',
route: 'SyncInstanceTaskDetail',
routeQuery: {
tab: 'HistoryList'
}
}
},
instance_count: {
formatter: DetailFormatter,
formatterArgs: {
permissions: 'xpack.view_syncinstancetask',
route: 'SyncInstanceTaskDetail',
routeQuery: {
tab: 'AssetList'
}
}
}
},
date_last_sync: {
width: '130px'
}
},
headerActions: {
hasMoreActions: false,
hasImport: false,
hasExport: false,
createRoute: 'SyncInstanceTaskCreate'
}
}
}
}
</script>
<style>
</style>

View File

@@ -13,25 +13,19 @@ export default {
data() {
return {
config: {
activeMenu: 'SyncInstanceTaskList',
activeMenu: 'CloudAccountList',
submenu: [
{
title: this.$t('SyncTask'),
name: 'SyncInstanceTaskList',
hidden: () => !this.$hasPerm('xpack.view_syncinstancetask'),
component: () => import('@/views/assets/Cloud/SyncInstanceTask/SyncInstanceTaskList.vue')
title: this.$t('CloudAccountList'),
name: 'CloudAccountList',
hidden: () => !this.$hasPerm('xpack.view_account'),
component: () => import('@/views/assets/Cloud/Account/AccountList.vue')
},
{
title: this.$t('SyncStrategy'),
name: 'StrategyList',
hidden: () => !this.$hasPerm('xpack.view_strategy'),
component: () => import('@/views/assets/Cloud/Strategy/StrategyList.vue')
},
{
title: this.$t('CloudAccountList'),
name: 'AccountList',
hidden: () => !this.$hasPerm('xpack.view_account'),
component: () => import('@/views/assets/Cloud/Account/AccountList.vue')
}
],
actions: {

View File

@@ -3,45 +3,50 @@
<div v-if="isDev" style="margin-bottom: 20px">
<div class="dz">
<el-button
v-for="tp of ['primary', 'success', 'info', 'warning', 'danger']"
v-for="(value, tp) in examples"
:key="tp"
:type="tp"
size="small"
>
{{ tp.toUpperCase() }}
{{ value }}
</el-button>
</div>
<div class="dz">
<el-button
v-for="tp of ['primary', 'success', 'info', 'warning', 'danger']"
v-for="(value, tp) in examples"
:key="tp"
:type="tp"
size="small"
disabled
>
{{ tp.toUpperCase() }}
{{ value }}
</el-button>
</div>
<div class="dz">
<el-link
v-for="tp of ['primary', 'success', 'info', 'warning', 'danger']"
v-for="(value, tp) in examples"
:key="tp"
:type="tp"
style="padding-right: 10px;"
>
<span style="padding-right: 10px">{{ tp.toUpperCase() }}</span>
{{ value }}
</el-link>
</div>
<div class="dz">
<el-radio-group v-model="dz.radio">
<el-radio :label="3">备选项1</el-radio>
<el-radio :label="6">备选项2</el-radio>
<el-radio :label="9">备选项3</el-radio>
<el-radio v-for="i in 3" :key="i" :label="$tc('Options') + ` ${i}`" />
</el-radio-group>
</div>
<el-steps :active="1" :space="200" class="dz" finish-status="error">
<el-step title="已完成" />
<el-step title="进行中" />
<el-step title="步骤 3" />
</el-steps>
<div class="dz">
<el-steps :active="1" :space="100">
<el-step
v-for="(s, i) in stepStatus"
:key="s"
:title="$tc('Step') + ` ${i+1}`"
:status="s"
/>
</el-steps>
</div>
<div class="dz" />
</div>
<IBox v-if="!loading">
@@ -77,8 +82,13 @@ export default {
data() {
return {
dz: {},
stepStatus: ['wait', 'success', 'finish', 'process', 'error'],
loading: true,
files: {},
examples: {
'primary': this.$t('Primary'), 'info': this.$t('Info'), 'warning': this.$t('Warning'),
'success': this.$t('Success'), 'danger': this.$t('Danger')
},
interfaceInfo: {},
hasSaveContinue: false,
successUrl: { name: 'Settings' },