This is an automated email from the ASF dual-hosted git repository.
rohit pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack-primate.git
The following commit(s) were added to refs/heads/master by this push:
new 79f8f4a vpc: work-in-progress network ACL list rules tab (#138)
79f8f4a is described below
commit 79f8f4a1457ed43f8c9a7cecc127a0d7addc50be
Author: Ritchie Vincent <[email protected]>
AuthorDate: Mon Feb 3 11:07:09 2020 +0000
vpc: work-in-progress network ACL list rules tab (#138)
Add VPC network ACL List rules tab
Signed-off-by: Rohit Yadav <[email protected]>
Co-authored-by: Rohit Yadav <[email protected]>
---
.../{index.less => ant-overwrite/ant-form.less} | 24 +-
src/style/index.less | 1 +
src/views/network/AclListRulesTab.vue | 649 ++++++++++++++++++++-
3 files changed, 646 insertions(+), 28 deletions(-)
diff --git a/src/style/index.less b/src/style/ant-overwrite/ant-form.less
similarity index 60%
copy from src/style/index.less
copy to src/style/ant-overwrite/ant-form.less
index d3c8661..0c838c6 100644
--- a/src/style/index.less
+++ b/src/style/ant-overwrite/ant-form.less
@@ -15,24 +15,6 @@
// specific language governing permissions and limitations
// under the License.
-//* import all ## official ant ## variables; mixins and styles
-@import "~ant-design-vue/lib/style/themes/default";
-@import "~ant-design-vue/lib/style/core/index";
-
-//* import all ## custom ## variables, mixins and styles
-
-
-@import "variables/prefixes";
-
-@import "common/common";
-
-@import "layout/ant";
-@import "layout/inverted-mode";
-@import "ant-overwrite/ant-layout-header";
-@import "ant-overwrite/ant-progress";
-
-@import "frame/content";
-@import "frame/search";
-@import "frame/top-menu";
-
-@import "objects/table";
+.ant-form .ant-form-item {
+ margin-bottom: 6px;
+}
diff --git a/src/style/index.less b/src/style/index.less
index d3c8661..0cb6be4 100644
--- a/src/style/index.less
+++ b/src/style/index.less
@@ -30,6 +30,7 @@
@import "layout/inverted-mode";
@import "ant-overwrite/ant-layout-header";
@import "ant-overwrite/ant-progress";
+@import "ant-overwrite/ant-form";
@import "frame/content";
@import "frame/search";
diff --git a/src/views/network/AclListRulesTab.vue
b/src/views/network/AclListRulesTab.vue
index bb35287..6e9dd2f 100644
--- a/src/views/network/AclListRulesTab.vue
+++ b/src/views/network/AclListRulesTab.vue
@@ -17,21 +17,185 @@
<template>
<a-spin :spinning="fetchLoading">
- <a-button type="dashed" style="width: 100%" icon="plus"
@click="todo()">Add ACL: show modal/form and run this..
createNetworkACL</a-button>
- <div v-for="(acl, index) in acls" :key="index">
- {{ acl }}
+ <div style="width: 100%; display: flex">
+ <a-button type="dashed" icon="plus" style="width: 100%; margin-right:
10px" @click="openAddRuleModal">
+ {{ $t('add') }} {{ $t('aclid') }}
+ </a-button>
+
+ <a-button type="dashed" @click="exportAclList" style="width: 100%"
icon="download">
+ Export ACLs
+ </a-button>
+ </div>
+
+ <div class="list">
+ <draggable
+ v-model="acls"
+ @change="changeOrder"
+ handle=".drag-handle"
+ animation="200"
+ ghostClass="drag-ghost">
+ <transition-group type="transition">
+ <div v-for="(acl, index) in acls" :key="index" class="list__item">
+ <div class="drag-handle">
+ <a-icon type="drag"></a-icon>
+ </div>
+ <div class="list__container">
+ <div class="list__col">
+ <div class="list__label">{{ $t('number') }}</div>
+ <div>{{ acl.number }}</div>
+ </div>
+ <div class="list__col">
+ <div class="list__label">{{ $t('cidrlist') }}</div>
+ <div>{{ acl.cidrlist }}</div>
+ </div>
+ <div class="list__col">
+ <div class="list__label">{{ $t('action') }}</div>
+ <div>{{ acl.action }}</div>
+ </div>
+ <div class="list__col">
+ <div class="list__label">{{ $t('protocol') }}</div>
+ <div>{{ acl.protocol }}</div>
+ </div>
+ <div class="list__col" v-if="acl.startport">
+ <div class="list__label">{{ $t('startport') }}</div>
+ <div>{{ acl.startport }}</div>
+ </div>
+ <div class="list__col" v-if="acl.endport">
+ <div class="list__label">{{ $t('endport') }}</div>
+ <div>{{ acl.endport }}</div>
+ </div>
+ <div class="list__col" v-if="acl.icmpcode">
+ <div class="list__label">{{ $t('icmpcode') }}</div>
+ <div>{{ acl.icmpcode }}</div>
+ </div>
+ <div class="list__col" v-if="acl.icmptype">
+ <div class="list__label">{{ $t('icmptype') }}</div>
+ <div>{{ acl.icmptype }}</div>
+ </div>
+ <div class="list__col">
+ <div class="list__label">{{ $t('traffictype') }}</div>
+ <div>{{ acl.traffictype }}</div>
+ </div>
+ <div class="list__col">
+ <div class="list__label">{{ $t('reason') }}</div>
+ <div>{{ acl.reason }}</div>
+ </div>
+ </div>
+ <div class="list__actions">
+ <a-button shape="round" icon="tag" @click="() =>
openTagsModal(acl)"></a-button>
+ <a-button shape="round" icon="edit" @click="() =>
openEditRuleModal(acl)"></a-button>
+ <a-button shape="round" icon="delete" type="danger" @click="()
=> handleDeleteRule(acl.id)"></a-button>
+ </div>
+ </div>
+ </transition-group>
+ </draggable>
</div>
+
+ <a-modal title="Edit Tags" v-model="tagsModalVisible" :footer="null">
+ <a-spin v-if="tagsLoading"></a-spin>
+
+ <div v-else>
+ <a-form :form="newTagsForm" class="add-tags" @submit="handleAddTag">
+ <div class="add-tags__input">
+ <p class="add-tags__label">{{ $t('key') }}</p>
+ <a-form-item>
+ <a-input v-decorator="['key', { rules: [{ required: true,
message: 'Please specify a tag key'}] }]" />
+ </a-form-item>
+ </div>
+ <div class="add-tags__input">
+ <p class="add-tags__label">{{ $t('value') }}</p>
+ <a-form-item>
+ <a-input v-decorator="['value', { rules: [{ required: true,
message: 'Please specify a tag value'}] }]" />
+ </a-form-item>
+ </div>
+ <a-button type="primary" html-type="submit">{{ $t('label.add')
}}</a-button>
+ </a-form>
+
+ <a-divider style="margin-top: 0;"></a-divider>
+
+ <div class="tags-container">
+ <div class="tags" v-for="(tag, index) in tags" :key="index">
+ <a-tag :key="index" :closable="true" :afterClose="() =>
handleDeleteTag(tag)">
+ {{ tag.key }} = {{ tag.value }}
+ </a-tag>
+ </div>
+ </div>
+
+ <a-button class="add-tags-done" @click="tagsModalVisible = false"
type="primary">{{ $t('OK') }}</a-button>
+ </div>
+
+ </a-modal>
+ <a-modal :title="ruleModalTitle" v-model="ruleModalVisible"
@ok="handleRuleModalForm">
+ <a-form :form="ruleForm" @submit="handleRuleModalForm">
+ <a-form-item :label="$t('number')">
+ <a-input v-decorator="['number']" />
+ </a-form-item>
+ <a-form-item :label="$t('cidrlist')">
+ <a-input v-decorator="['cidr']" />
+ </a-form-item>
+ <a-form-item :label="$t('action')">
+ <a-select v-decorator="['action']">
+ <a-select-option value="allow">Allow</a-select-option>
+ <a-select-option value="deny">Deny</a-select-option>
+ </a-select>
+ </a-form-item>
+ <a-form-item :label="$t('protocol')">
+ <a-select v-decorator="['protocol']">
+ <a-select-option value="tcp">TCP</a-select-option>
+ <a-select-option value="udp">UDP</a-select-option>
+ <a-select-option value="icmp">ICMP</a-select-option>
+ <a-select-option value="all">ALL</a-select-option>
+ <a-select-option value="protocolnumber">Protocol
number</a-select-option>
+ </a-select>
+ </a-form-item>
+
+ <a-form-item v-if="ruleForm.getFieldValue('protocol') ===
'protocolnumber'" :label="$t('protocolnumber')">
+ <a-input v-decorator="['protocolnumber' , { rules: [{ required:
true, message: 'required' }]}]" />
+ </a-form-item>
+
+ <div v-if="ruleForm.getFieldValue('protocol') === 'icmp' ||
ruleForm.getFieldValue('protocol') === 'protocolnumber'">
+ <a-form-item :label="$t('icmptype')">
+ <a-input v-decorator="['icmptype']" placeholder="Please specify -1
if you want to allow all ICMP types." />
+ </a-form-item>
+ <a-form-item :label="$t('icmpcode')">
+ <a-input v-decorator="['icmpcode']" placeholder="Please specify -1
if you want to allow all ICMP types." />
+ </a-form-item>
+ </div>
+
+ <div v-if="ruleForm.getFieldValue('protocol') === 'tcp' ||
ruleForm.getFieldValue('protocol') === 'udp' ||
ruleForm.getFieldValue('protocol') === 'protocolnumber'">
+ <a-form-item :label="$t('startport')">
+ <a-input v-decorator="['startport']" />
+ </a-form-item>
+ <a-form-item :label="$t('endport')">
+ <a-input v-decorator="['endport']" />
+ </a-form-item>
+ </div>
+
+ <a-form-item :label="$t('traffictype')">
+ <a-select v-decorator="['traffictype']">
+ <a-select-option value="ingress">Ingress</a-select-option>
+ <a-select-option value="egress">Egress</a-select-option>
+ </a-select>
+ </a-form-item>
+ <a-form-item :label="$t('reason')">
+ <a-textarea
+ v-decorator="['reason']"
+ :autosize="{ minRows: 2 }"
+ placeholder="Enter the reason behind an ACL rule" />
+ </a-form-item>
+ </a-form>
+ </a-modal>
</a-spin>
</template>
<script>
import { api } from '@/api'
-import Status from '@/components/widgets/Status'
+import draggable from 'vuedraggable'
export default {
name: 'AclListRulesTab',
components: {
- Status
+ draggable
},
props: {
resource: {
@@ -46,7 +210,16 @@ export default {
data () {
return {
acls: [],
- fetchLoading: false
+ fetchLoading: false,
+ tags: [],
+ selectedAcl: null,
+ tagsModalVisible: false,
+ newTagsForm: this.$form.createForm(this),
+ ruleForm: this.$form.createForm(this),
+ tagsLoading: false,
+ ruleModalVisible: false,
+ ruleModalTitle: 'Edit rule',
+ ruleFormMode: 'edit'
}
},
mounted () {
@@ -60,10 +233,43 @@ export default {
}
},
methods: {
+ csv ({ data = null, columnDelimiter = ',', lineDelimiter = '\n' }) {
+ let result = null
+ let ctr = null
+ let keys = null
+
+ if (data === null || !data.length) {
+ return null
+ }
+
+ keys = Object.keys(data[0])
+
+ result = ''
+ result += keys.join(columnDelimiter)
+ result += lineDelimiter
+
+ data.forEach(item => {
+ ctr = 0
+ keys.forEach(key => {
+ if (ctr > 0) {
+ result += columnDelimiter
+ }
+
+ result += typeof item[key] === 'string' &&
item[key].includes(columnDelimiter) ? `"${item[key]}"` : item[key]
+ ctr++
+ })
+ result += lineDelimiter
+ })
+
+ return result
+ },
fetchData () {
this.fetchLoading = true
api('listNetworkACLs', { aclid: this.resource.id }).then(json => {
this.acls = json.listnetworkaclsresponse.networkacl
+ if (this.acls.length > 0) {
+ this.acls.sort((a, b) => a.number - b.number)
+ }
}).catch(error => {
this.$notification.error({
message: 'Request Failed',
@@ -72,11 +278,440 @@ export default {
}).finally(() => {
this.fetchLoading = false
})
+ },
+ fetchTags (acl) {
+ api('listTags', {
+ resourceId: acl.id,
+ resourceType: 'NetworkACL',
+ listAll: true
+ }).then(response => {
+ this.tags = response.listtagsresponse.tag
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.errorresponse.errortext
+ })
+ })
+ },
+ openTagsModal (acl) {
+ this.selectedAcl = acl
+ this.newTagsForm.resetFields()
+ this.fetchTags(this.selectedAcl)
+ this.tagsModalVisible = true
+ },
+ handleDeleteTag (tag) {
+ this.tagsLoading = true
+ api('deleteTags', {
+ 'tags[0].key': tag.key,
+ 'tags[0].value': tag.value,
+ resourceIds: this.selectedAcl.id,
+ resourceType: 'NetworkACL'
+ }).then(response => {
+ this.$store.dispatch('AddAsyncJob', {
+ title: `Remove tag for NetworkACL`,
+ jobid: response.deletetagsresponse.jobid,
+ status: 'progress'
+ })
+ this.$pollJob({
+ jobId: response.deletetagsresponse.jobid,
+ successMessage: `Successfully deleted tag`,
+ successMethod: () => {
+ this.fetchTags(this.selectedAcl)
+ this.tagsLoading = false
+ },
+ errorMessage: 'Failed to delete tag',
+ errorMethod: () => {
+ this.fetchTags(this.selectedAcl)
+ this.tagsLoading = false
+ },
+ loadingMessage: `Deleting tag...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.fetchTags(this.selectedAcl)
+ this.tagsLoading = false
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.deletetagsresponse.errortext
+ })
+ this.tagsLoading = false
+ })
+ },
+ handleAddTag (e) {
+ this.tagsLoading = true
+
+ e.preventDefault()
+ this.newTagsForm.validateFields((err, values) => {
+ if (err) {
+ this.tagsLoading = false
+ return
+ }
+
+ api('createTags', {
+ 'tags[0].key': values.key,
+ 'tags[0].value': values.value,
+ resourceIds: this.selectedAcl.id,
+ resourceType: 'NetworkACL'
+ }).then(response => {
+ this.$store.dispatch('AddAsyncJob', {
+ title: `Add tag for NetworkACL`,
+ jobid: response.createtagsresponse.jobid,
+ status: 'progress'
+ })
+ this.$pollJob({
+ jobId: response.createtagsresponse.jobid,
+ successMessage: `Successfully added new tag`,
+ successMethod: () => {
+ this.fetchTags(this.selectedAcl)
+ this.tagsLoading = false
+ },
+ errorMessage: 'Failed to add new tag',
+ errorMethod: () => {
+ this.fetchTags(this.selectedAcl)
+ this.tagsLoading = false
+ },
+ loadingMessage: `Adding new tag...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.fetchTags(this.selectedAcl)
+ this.tagsLoading = false
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.createtagsresponse.errortext
+ })
+ this.tagsLoading = false
+ })
+ })
+ },
+ openEditRuleModal (acl) {
+ this.ruleModalTitle = 'Edit rule'
+ this.ruleFormMode = 'edit'
+ this.ruleForm.resetFields()
+ this.ruleModalVisible = true
+ this.selectedAcl = acl
+ setTimeout(() => {
+ this.ruleForm.setFieldsValue({
+ number: acl.number,
+ cidr: acl.cidrlist,
+ action: acl.action,
+ protocol: acl.protocol,
+ startport: acl.startport,
+ endport: acl.endport,
+ traffictype: acl.traffictype,
+ reason: acl.reason
+ })
+ }, 200)
+ },
+ handleEditRule (e) {
+ e.preventDefault()
+ this.ruleForm.validateFields((err, values) => {
+ if (err) return
+
+ this.fetchLoading = true
+ this.ruleModalVisible = false
+ api('updateNetworkACLItem', {}, 'POST', {
+ id: this.selectedAcl.id,
+ cidrlist: values.cidr,
+ number: values.number,
+ protocol: values.protocol,
+ traffictype: values.traffictype,
+ action: values.action,
+ reason: values.reason,
+ startport: values.startport,
+ endport: values.endport,
+ partialupgrade: false
+ }).then(response => {
+ this.$store.dispatch('AddAsyncJob', {
+ title: `Edit ACL rule`,
+ jobid: response.createnetworkaclresponse.jobid,
+ status: 'progress'
+ })
+ this.$pollJob({
+ jobId: response.createnetworkaclresponse.jobid,
+ successMessage: `Successfully edited ACL rule`,
+ successMethod: () => {
+ this.fetchData()
+ this.fetchLoading = false
+ },
+ errorMessage: 'Failed to edit ACL rule',
+ errorMethod: () => {
+ this.fetchData()
+ this.fetchLoading = false
+ },
+ loadingMessage: `Editing ACL rule...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.fetchData()
+ this.fetchLoading = false
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.createnetworkaclresponse.errortext
+ })
+ this.fetchLoading = false
+ })
+ })
+ },
+ handleDeleteRule (id) {
+ this.fetchLoading = true
+ api('deleteNetworkACL', { id }).then(response => {
+ this.$store.dispatch('AddAsyncJob', {
+ title: `Remove ACL rule`,
+ jobid: response.deletenetworkaclresponse.jobid,
+ status: 'progress'
+ })
+ this.$pollJob({
+ jobId: response.deletenetworkaclresponse.jobid,
+ successMessage: `Successfully removed ACL rule`,
+ successMethod: () => {
+ this.fetchData()
+ this.fetchLoading = false
+ },
+ errorMessage: 'Failed to remove ACL rule',
+ errorMethod: () => {
+ this.fetchData()
+ this.fetchLoading = false
+ },
+ loadingMessage: `Removing ACL rule...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.fetchData()
+ this.fetchLoading = false
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.data.deletenetworkaclresponse.errortext
+ })
+ this.fetchLoading = false
+ })
+ },
+ handleRuleModalForm (e) {
+ if (this.ruleFormMode === 'edit') {
+ this.handleEditRule(e)
+ return
+ }
+ if (this.ruleFormMode === 'add') this.handleAddRule(e)
+ },
+ openAddRuleModal () {
+ this.ruleModalTitle = 'Add rule'
+ this.ruleModalVisible = true
+ this.ruleFormMode = 'add'
+ this.ruleForm.resetFields()
+ setTimeout(() => {
+ this.ruleForm.setFieldsValue({
+ action: 'allow',
+ protocol: 'tcp',
+ traffictype: 'ingress'
+ })
+ }, 200)
+ },
+ handleAddRule (e) {
+ e.preventDefault()
+ this.ruleForm.validateFields((err, values) => {
+ if (err) return
+
+ this.fetchLoading = true
+ this.ruleModalVisible = false
+
+ const data = {
+ aclid: this.resource.id,
+ cidrlist: values.cidr || '',
+ number: values.number || '',
+ protocol: values.protocol || '',
+ traffictype: values.traffictype || '',
+ action: values.action || '',
+ reason: values.reason || ''
+ }
+
+ if (values.protocol === 'tcp' || values.protocol === 'udp' ||
values.protocol === 'protocolnumber') {
+ data.startport = values.startport || ''
+ data.endport = values.endport || ''
+ }
+
+ if (values.protocol === 'icmp') {
+ data.icmptype = values.icmptype || -1
+ data.icmpcode = values.icmpcode || -1
+ }
+
+ if (values.protocol === 'protocolnumber') {
+ data.protocol = values.protocolnumber
+ }
+
+ api('createNetworkACL', {}, 'POST', data).then(() => {
+ this.$notification.success({
+ message: 'Success',
+ description: 'Successfully added new rule'
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: 'Request Failed',
+ description: error.response.headers['x-description']
+ })
+ }).finally(() => {
+ this.fetchLoading = false
+ this.fetchData()
+ })
+ })
+ },
+ changeOrder (e) {
+ const id = e.moved.element.id
+ let previousaclruleid = null
+ let nextaclruleid = null
+
+ if (e.moved.newIndex - 1 >= 0) previousaclruleid =
this.acls[e.moved.newIndex - 1].id
+
+ if (e.moved.newIndex + 1 < this.acls.length) nextaclruleid =
this.acls[e.moved.newIndex + 1].id
+
+ this.fetchLoading = true
+ api('moveNetworkAclItem', {
+ id,
+ previousaclruleid,
+ nextaclruleid
+ }).then(response => {
+ this.$store.dispatch('AddAsyncJob', {
+ title: `Move ACL rule order`,
+ jobid: response.moveNetworkAclItemResponse.jobid,
+ status: 'progress'
+ })
+ this.$pollJob({
+ jobId: response.moveNetworkAclItemResponse.jobid,
+ successMessage: `Successfully moved ACL rule`,
+ successMethod: () => {
+ this.fetchData()
+ this.fetchLoading = false
+ },
+ errorMessage: 'Failed to move ACL rule',
+ errorMethod: () => {
+ this.fetchData()
+ this.fetchLoading = false
+ },
+ loadingMessage: `Moving ACL rule...`,
+ catchMessage: 'Error encountered while fetching async job result',
+ catchMethod: () => {
+ this.fetchData()
+ this.fetchLoading = false
+ }
+ })
+ }).catch(error => {
+ this.$notification.error({
+ message: `Error ${error.response.status}`,
+ description: error.response.headers['x-description']
+ })
+ this.fetchLoading = false
+ })
+ },
+ exportAclList () {
+ const csvData = this.csv({ data: this.acls })
+
+ const hiddenElement = document.createElement('a')
+ hiddenElement.href = 'data:text/csv;charset=utf-8,' + encodeURI(csvData)
+ hiddenElement.target = '_blank'
+ hiddenElement.download = 'AclRules-' + this.resource.name + '-' +
this.resource.id + '.csv'
+ hiddenElement.click()
}
}
}
</script>
-<style lang="less" scoped>
+<style lang="scss" scoped>
+.list {
+
+ &__item {
+ display: flex;
+ padding-top: 20px;
+ padding-bottom: 20px;
+
+ &:not(:last-child) {
+ border-bottom: 1px solid #d9d9d9;
+ }
+
+ }
+ &__container {
+ display: flex;
+ flex-wrap: wrap;
+ width: 100%;
+ }
+
+ &__col {
+ margin-right: 20px;
+ margin-bottom: 20px;
+ flex-basis: calc(50% - 20px);
+
+ @media (min-width: 760px) {
+ flex-basis: calc(25% - 20px);
+ }
+
+ }
+
+ &__label {
+ font-weight: bold;
+ }
+
+ &__actions {
+ display: flex;
+ flex-direction: column;
+
+ button {
+ &:not(:last-child) {
+ margin-bottom: 10px;
+ }
+ }
+
+ }
+
+}
+
+.tags {
+ margin-bottom: 10px;
+}
+.add-tags {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ &__input {
+ margin-right: 10px;
+ }
+ &__label {
+ margin-bottom: 5px;
+ font-weight: bold;
+ }
+}
+.tags-container {
+ display: flex;
+ flex-wrap: wrap;
+ margin-bottom: 10px;
+}
+.add-tags-done {
+ display: block;
+ margin-left: auto;
+}
+
+.drag-handle {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ padding-right: 20px;
+}
+
+.drag-ghost {
+ opacity: 0.5;
+ background: #f0f2f5;
+}
+
+.download {
+ display: block;
+ margin-top: 10px;
+ margin-bottom: 10px;
+ margin-left: auto;
+}
</style>