blob: 49a9e98236181acef8f23e427150a2ac6e73bee0 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<loading-outlined v-if="loadingTable" class="main-loading-spinner" />
<div v-else>
<div style="width: 100%; display: flex; margin-bottom: 10px">
<a-button type="dashed" @click="exportRolePermissions" style="width: 100%">
<template #icon><download-outlined /></template>
{{ $t('label.export.rules') }}
</a-button>
</div>
<div v-if="updateTable" class="loading-overlay">
<loading-outlined />
</div>
<div
class="rules-list ant-list ant-list-bordered"
:class="{'rules-list--overflow-hidden' : updateTable}" >
<div class="rules-table-item ant-list-item">
<div class="rules-table__col rules-table__col--grab"></div>
<div class="rules-table__col rules-table__col--rule rules-table__col--new">
<a-auto-complete
v-focus="true"
:filterOption="filterOption"
:options="apis"
v-model:value="newRule"
@change="val => newRule = val"
:placeholder="$t('label.rule')"
:class="{'rule-dropdown-error' : newRuleSelectError}" />
</div>
<div class="rules-table__col rules-table__col--permission">
<permission-editable
:defaultValue="newRulePermission"
@onChange="onPermissionChange(null, $event)" />
</div>
<div class="rules-table__col rules-table__col--description">
<a-input v-model:value="newRuleDescription" :placeholder="$t('label.description')"></a-input>
</div>
<div class="rules-table__col rules-table__col--actions">
<tooltip-button
tooltipPlacement="bottom"
:tooltip="$t('label.save.new.rule')"
:disabled="!('createRolePermission' in $store.getters.apis)"
icon="plus-outlined"
type="primary"
@onClick="onRuleSave" />
</div>
</div>
<draggable
v-model="rules"
@change="changeOrder"
:disabled="!('updateRolePermission' in $store.getters.apis)"
handle=".drag-handle"
animation="200"
ghostClass="drag-ghost"
:component-data="{type: 'transition'}"
item-key="id">
<template #item="{element}">
<div class="rules-table-item ant-list-item">
<div class="rules-table__col rules-table__col--grab drag-handle">
<drag-outlined />
</div>
<div class="rules-table__col rules-table__col--rule">
{{ element.rule }}
</div>
<div class="rules-table__col rules-table__col--permission">
<permission-editable
:default-value="element.permission"
@onChange="onPermissionChange(element, $event)" />
</div>
<div class="rules-table__col rules-table__col--description">
<div v-if="element.description">
{{ element.description }}
</div>
<div v-else class="no-description">
{{ $t('message.no.description') }}
</div>
</div>
<div class="rules-table__col rules-table__col--actions">
<rule-delete
:disabled="!('deleteRolePermission' in $store.getters.apis)"
:record="element"
@delete="onRuleDelete(element.id)" />
</div>
</div>
</template>
</draggable>
</div>
</div>
</template>
<script>
import { api } from '@/api'
import draggable from 'vuedraggable'
import PermissionEditable from './PermissionEditable'
import RuleDelete from './RuleDelete'
import TooltipButton from '@/components/widgets/TooltipButton'
export default {
name: 'RolePermissionTab',
components: {
RuleDelete,
PermissionEditable,
draggable,
TooltipButton
},
props: {
resource: {
type: Object,
required: true
}
},
data () {
return {
loadingTable: true,
updateTable: false,
rules: null,
newRule: '',
newRulePermission: 'allow',
newRuleDescription: '',
newRuleSelectError: false,
drag: false,
apis: []
}
},
created () {
this.apis = Object.keys(this.$store.getters.apis)
.sort((a, b) => a.localeCompare(b))
.map(value => { return { value: value } })
this.fetchData()
},
watch: {
resource: {
deep: true,
handler () {
this.fetchData(() => {
this.resetNewFields()
})
}
}
},
methods: {
filterOption (input, option) {
return (
option.value.toUpperCase().indexOf(input.toUpperCase()) >= 0
)
},
resetNewFields () {
this.newRule = ''
this.newRulePermission = 'allow'
this.newRuleDescription = ''
this.newRuleSelectError = false
},
fetchData (callback = null) {
if (!this.resource.id) return
api('listRolePermissions', { roleid: this.resource.id }).then(response => {
this.rules = response.listrolepermissionsresponse.rolepermission
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loadingTable = false
this.updateTable = false
this.updateApis()
if (callback) callback()
})
},
updateApis () {
this.apis = Object.keys(this.$store.getters.apis)
.sort((a, b) => a.localeCompare(b))
var apisSupported = this.rules?.map(rule => rule.rule) || []
this.apis = this.apis.filter(api => !apisSupported.includes(api.value)).map(value => { return { value: value } })
},
changeOrder () {
this.updateTable = true
api('updateRolePermission', {}, 'POST', {
roleid: this.resource.id,
ruleorder: this.rules.map(rule => rule.id)
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.updateTable = false
this.fetchData()
})
},
onRuleDelete (key) {
this.updateTable = true
api('deleteRolePermission', { id: key }).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.updateTable = false
this.fetchData()
})
},
onPermissionChange (record, value) {
this.newRulePermission = value
if (!record) return
this.updateTable = true
api('updateRolePermission', {
roleid: this.resource.id,
ruleid: record.id,
permission: value
}).then(() => {
this.fetchData()
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.updateTable = false
})
},
onRuleSelect (value) {
this.newRule = value
},
onRuleSave () {
if (!this.newRule) {
this.newRuleSelectError = true
return
}
this.updateTable = true
api('createRolePermission', {
rule: this.newRule,
permission: this.newRulePermission,
description: this.newRuleDescription,
roleid: this.resource.id
}).then(() => {
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.resetNewFields()
this.fetchData()
this.updateTable = false
})
},
rulesDataToCsv ({ data = null, columnDelimiter = ',', lineDelimiter = '\n' }) {
if (data === null || !data.length) {
return null
}
const keys = ['rule', 'permission', 'description']
let result = ''
result += keys.join(columnDelimiter)
result += lineDelimiter
data.forEach(item => {
keys.forEach(key => {
if (item[key] === undefined) {
item[key] = ''
}
result += typeof item[key] === 'string' && item[key].includes(columnDelimiter) ? `"${item[key]}"` : item[key]
result += columnDelimiter
})
result = result.slice(0, -1)
result += lineDelimiter
})
return result
},
exportRolePermissions () {
const rulesCsvData = this.rulesDataToCsv({ data: this.rules })
const hiddenElement = document.createElement('a')
hiddenElement.href = 'data:text/csv;charset=utf-8,' + encodeURI(rulesCsvData)
hiddenElement.target = '_blank'
hiddenElement.download = this.resource.name + '_' + this.resource.type + '.csv'
hiddenElement.click()
hiddenElement.remove()
}
}
}
</script>
<style lang="scss" scoped>
.main-loading-spinner {
display: flex;
align-items: center;
justify-content: center;
font-size: 30px;
}
.role-add-btn {
margin-bottom: 15px;
}
.new-role-controls {
display: flex;
button {
&:not(:last-child) {
margin-right: 5px;
}
}
}
.rules-list {
max-height: 600px;
overflow: auto;
&--overflow-hidden {
overflow: hidden;
}
}
.rules-table {
&-item {
position: relative;
display: flex;
align-items: stretch;
padding: 0;
flex-wrap: wrap;
@media (min-width: 760px) {
flex-wrap: nowrap;
padding-right: 25px;
}
}
&__col {
display: flex;
align-items: center;
padding: 15px;
@media (min-width: 760px) {
padding: 15px 0;
&:not(:first-child) {
padding-left: 20px;
}
&:not(:last-child) {
border-right: 1px solid #e8e8e8;
padding-right: 20px;
}
}
&--grab {
position: absolute;
top: 4px;
left: 0;
width: 100%;
@media (min-width: 760px) {
position: relative;
top: auto;
width: 35px;
padding-left: 25px;
justify-content: center;
}
}
&--rule,
&--description {
word-break: break-all;
flex: 1;
width: 100%;
@media (min-width: 760px) {
width: auto;
}
}
&--rule {
padding-left: 60px;
background-color: rgba(#e6f7ff, 0.7);
@media (min-width: 760px) {
padding-left: 0;
background: none;
}
}
&--permission {
justify-content: center;
width: 100%;
.ant-select {
width: 100%;
}
@media (min-width: 760px) {
width: auto;
.ant-select {
width: auto;
}
}
}
&--actions {
max-width: 60px;
width: 100%;
padding-right: 0;
@media (min-width: 760px) {
width: auto;
max-width: 70px;
padding-right: 15px;
}
}
&--new {
padding-left: 15px;
background-color: transparent;
div {
width: 100%;
}
}
}
}
.no-description {
opacity: 0.4;
font-size: 0.7rem;
@media (min-width: 760px) {
display: none;
}
}
.drag-handle {
cursor: pointer;
}
.drag-ghost {
opacity: 0.5;
background: #f0f2f5;
}
.loading-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 5;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
color: #39A7DE;
background-color: rgba(#fff, 0.8);
}
</style>
<style lang="scss">
.rules-table__col--new {
.ant-select {
width: 100%;
}
}
.rule-dropdown-error {
.ant-input {
border-color: #ff0000
}
}
</style>