Compare commits

...

28 Commits

Author SHA1 Message Date
ibuler
7dfca604c2 perf: update draw create update 2024-12-16 14:41:52 +08:00
ibuler
b4abcd4c90 perf: update draw 2024-12-12 19:03:03 +08:00
ibuler
d62c87b858 Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-12 09:44:28 +08:00
wangruidong
743187b4b3 fix: risk check perms 2024-12-11 16:36:38 +08:00
ibuler
5c1f24af6a perf: update template 2024-12-11 11:39:27 +08:00
ibuler
9477bfa2c1 Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-11 10:09:12 +08:00
ibuler
1ba58f476f perf: update async 2024-12-11 10:09:07 +08:00
feng
02600d7a1b perf: Remove push account extra api 2024-12-10 19:36:06 +08:00
ibuler
2126c92e07 perf: update status 2024-12-10 16:51:22 +08:00
ibuler
af774a8835 Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-10 15:46:44 +08:00
ibuler
b93fad1f91 perf: update check 2024-12-10 15:46:38 +08:00
zhaojisen
ef43be0cb7 perf: Add drawer 2024-12-09 18:52:46 +08:00
ibuler
8b6eea0267 Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-09 17:10:36 +08:00
ibuler
d0da22738f perf: update handler 2024-12-09 17:10:30 +08:00
wangruidong
18eda16851 perf: delete gather account 2024-12-09 16:17:39 +08:00
feng
610e9b9efa perf: Account push 2024-12-09 11:19:18 +08:00
ibuler
a0c7d60719 perf: update table actions 2024-12-09 09:38:49 +08:00
zhaojisen
60ba0d8f02 perf: drawer realize 2024-12-06 19:06:09 +08:00
ibuler
b9afb05f1b perf: update risk batch selected 2024-12-06 19:00:41 +08:00
ibuler
fcf9ea2b79 Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-06 11:38:40 +08:00
ibuler
430b1117c9 perf: update quick filter style 2024-12-06 11:38:27 +08:00
zhaojisen
a65023c8f7 style: Optimized style 2024-12-06 11:36:20 +08:00
zhaojisen
d6de85ffdd perf:Universal Drawer action assembly 2024-12-05 19:35:34 +08:00
ibuler
5f11d8b54f Merge branch 'pam' of github.com:jumpserver/lina into pam 2024-12-05 19:10:16 +08:00
ibuler
c7ce602d4c perf: update filter 2024-12-05 19:09:52 +08:00
zhaojisen
d0988da277 perf:Universal Drawer assembly 2024-12-05 18:10:40 +08:00
ibuler
7f13ef35a7 perf: update appliations 2024-12-05 16:07:58 +08:00
ibuler
44348de4ab perf: update card table 2024-12-05 11:15:35 +08:00
73 changed files with 1955 additions and 1053 deletions

View File

@@ -21,8 +21,8 @@
<AccountCreateUpdate
v-if="showAddDialog"
:account="account"
:origin="origin"
:asset="iAsset"
:origin="origin"
:title="accountCreateUpdateTitle"
:visible.sync="showAddDialog"
@add="addAccountSuccess"
@@ -31,9 +31,9 @@
<AccountCreateUpdate
v-if="showAddTemplateDialog"
:account="account"
:origin="origin"
:add-template="true"
:asset="iAsset"
:origin="origin"
:title="accountCreateByTemplateTitle"
:visible.sync="showAddTemplateDialog"
@add="addAccountSuccess"
@@ -200,13 +200,13 @@ export default {
{
label: '僵尸账号',
filter: {
risk: 'zombie'
risk: 'long_time_no_login'
}
},
{
label: '幽灵账号',
filter: {
risk: 'ghost'
risk: 'new_found'
}
},
{
@@ -299,7 +299,7 @@ export default {
columnsMeta: {
name: {
width: '120px',
formatter: function(row) {
formatter: (row) => {
const to = {
name: 'AssetAccountDetail',
params: { id: row.id }
@@ -333,7 +333,7 @@ export default {
}
},
asset: {
formatter: function(row) {
formatter: (row) => {
const to = {
name: 'AssetDetail',
params: { id: row.asset.id }

View File

@@ -5,8 +5,8 @@
</div>
<slot>
<h3 class="no-margins ">
<span class="num" @click="handleClick">
{{ iCount }}
<span v-async="iCount" class="num" @click="handleClick">
-
</span>
</h3>
</slot>
@@ -27,7 +27,7 @@ export default {
default: () => ({})
},
count: {
type: [Number, String],
type: [Number, String, Promise],
default: 0
},
route: {
@@ -49,7 +49,8 @@ export default {
},
computed: {
iCount() {
return this.body.count || this.count
const count = this.body.count || this.count
return count
},
iRoute() {
return this.body.route || this.route

View File

@@ -10,10 +10,9 @@
trigger="click"
@command="handleDropdownCallback"
>
<el-button :size="size" class="more-action" v-bind="cleanButtonAction(action)">
<span v-if="action.icon && !action.icon.startsWith('el-')" class="pre-icon">
<i v-if="action.icon.startsWith('fa')" :class="'fa fa-fw ' + action.icon" />
<svg-icon v-else :icon-class="action.icon" />
<el-button :class="action.name" :size="size" class="more-action" v-bind="{...cleanButtonAction(action), icon: ''}">
<span v-if="action.icon" class="pre-icon">
<Icon :icon="action.icon" />
</span>
<span v-if="action.title">
{{ action.title }}<i class="el-icon-arrow-down el-icon--right" />
@@ -44,8 +43,7 @@
v-bind="{...option, icon: ''}"
>
<span v-if="option.icon" class="pre-icon">
<i v-if="option.icon.startsWith('fa')" :class="'fa fa-fw ' + option.icon" />
<svg-icon v-else :icon-class="option.icon" />
<Icon :icon="option.icon" />
</span>
{{ option.title }}
</el-dropdown-item>
@@ -57,16 +55,16 @@
<el-button
v-else
:key="action.name"
:class="action.name"
:size="size"
class="action-item"
v-bind="{...cleanButtonAction(action), icon: action.icon && action.icon.startsWith('el-') ? action.icon : ''}"
v-bind="{...cleanButtonAction(action), icon: ''}"
@click="handleClick(action)"
>
<el-tooltip :content="action.tip" :disabled="!action.tip" :open-delay="500" placement="top">
<span>
<span v-if="action.icon && !action.icon.startsWith('el-')" style="vertical-align: initial">
<i v-if="action.icon.startsWith('fa')" :class="'fa ' + action.icon" />
<svg-icon v-else :icon-class="action.icon" />
<span v-if="action.icon" style="vertical-align: initial">
<Icon :icon="action.icon" />
</span>
{{ action.title }}
</span>
@@ -78,9 +76,13 @@
<script>
import { toSentenceCase } from '@/utils/common'
import Icon from '@/components/Widgets/Icon/index.vue'
export default {
name: 'DataActions',
components: {
Icon
},
props: {
grouped: {
type: Boolean,

View File

@@ -8,6 +8,7 @@
:before-close="handleClose"
:visible.sync="dialog"
:wrapper-closable="true"
:append-to-body="true"
>
<div class="drawer__content">
<slot name="default" />

View File

@@ -24,7 +24,6 @@
</el-dropdown>
</el-col>
</el-row>
<el-divider />
<el-row :gutter="20" class="panel-content">
<el-col :span="6" class="panel-image">
<el-image :src="imageUrl" fit="contain" />

View File

@@ -1,7 +1,7 @@
<template>
<div class="panel-item">
<div class="item-label">{{ title }}</div>
<div class="text-info" :title="content">{{ content }}</div>
<span class="item-label">{{ title }} </span>
<span :title="content" class="text-info">{{ content || '' }}</span>
</div>
</template>

View File

@@ -0,0 +1,44 @@
<template>
<CardTable
ref="table"
:columns="3"
:table-config="tableConfig"
v-bind="$attrs"
>
<template v-slot:default="slotProps">
<CardPanel :object="slotProps.item" :table-config="tableConfig" v-bind="subComponentProps" />
</template>
</CardTable>
</template>
<script type="text/jsx">
import CardTable from '@/components/Table/CardTable/index.vue'
import CardPanel from './CardPanel.vue'
export default {
name: 'SmallCard',
components: {
CardPanel,
CardTable
},
props: {
tableConfig: {
type: Object,
default: () => ({})
},
subComponentProps: {
type: Object,
default: () => ({})
}
},
data() {
return {
}
},
methods: {
reloadTable() {
this.$refs.table.reloadTable()
}
}
}
</script>

View File

@@ -1,32 +0,0 @@
<template>
<div>
<CardTable
ref="table"
:columns="3"
:sub-component="subComponent"
v-bind="$attrs"
/>
</div>
</template>
<script type="text/jsx">
import CardTable from '@/components/Table/CardTable'
import CardPanel from './components/CardPanel.vue'
export default {
name: 'SmallCard',
components: {
CardTable
},
data() {
return {
subComponent: CardPanel
}
},
methods: {
reloadTable() {
this.$refs.table.reloadTable()
}
}
}
</script>

View File

@@ -10,7 +10,7 @@
<IBox v-if="totalData.length === 0">
<el-empty :description="$t('NoData')" :image-size="200" class="no-data" style="padding: 20px" />
</IBox>
<el-col v-for="(d, index) in totalData" :key="index" :lg="24 / columns" :md="12" :sm="24" style="min-width: 335px;">
<el-col v-for="(d, index) in totalData" :key="index" :lg="8" :md="12" :sm="24" class="el-col">
<el-card
:body-style="{ 'text-align': 'center', 'padding': '15px' }"
:class="{'is-disabled': isDisabled(d)}"
@@ -19,15 +19,7 @@
@click.native="onView(d)"
>
<keep-alive>
<component
:is="subComponent"
v-if="subComponent"
:object="d"
:table-config="tableConfig"
v-bind="subComponentProps"
@refresh="getList"
/>
<slot v-else :index="index" :item="d">
<slot :index="index" :item="d">
<span v-if="d.edition === 'enterprise'" class="enterprise">
{{ $t('Enterprise') }}
</span>
@@ -386,6 +378,10 @@ export default {
border-top: 1px solid #e7eaec;
}
.el-col {
min-width: 330px;
}
.no-data {
display: flex;
flex-direction: column;

View File

@@ -88,7 +88,7 @@ export default {
},
moreActionsTitle: {
type: String,
default: null
default: ''
},
moreCreates: {
type: Object,

View File

@@ -1,47 +1,53 @@
<template>
<div v-if="filters || summary" :class="isExpand ? 'expand': 'shrink' " class="quick-filter">
<div v-show="isExpand" class="quick-filter-wrap">
<div v-if="filters" class="quick-filter-zone">
<div v-for="category in iFilters" :key="category.label" class="item-zone">
<div>
<h5>{{ category.label }}</h5>
<div class="filter-options">
<span
v-for="option in category.options"
:key="option.label"
:class="option.active ? 'active' : ''"
class="item"
@click="handleFilterClick(option)"
>
{{ option.label }}
<i class="el-icon-circle-check" />
</span>
<div v-show="isExpand">
<div v-if="filters || summary" :class="isExpand ? 'expand': 'shrink' " class="quick-filter">
<div v-show="isExpand" class="quick-filter-wrap">
<div v-if="filters" class="quick-filter-zone">
<div v-for="category in iFilters" :key="category.label" class="item-zone">
<div>
<h5>{{ category.label }}</h5>
<div class="filter-options">
<span
v-for="option in category.options"
:key="option.label"
:class="option.active ? 'active' : ''"
class="item"
@click="handleFilterClick(option)"
>
{{ option.label }}
<span v-if="option.hasCount">
(<span v-async="getCount(option)">-</span>)
</span>
<!-- <i class="el-icon-circle-check" />-->
</span>
</div>
</div>
</div>
</div>
<div v-if="summary" class="summary-zone">
<span v-for="item of iSummary" :key="item.title" class="summary-block">
<SummaryCard
:class="item.active ? 'active' : ''"
:count="getCount(item)"
:title="item.title"
@click="handleFilterClick(item)"
/>
</span>
</div>
</div>
<div v-if="summary" class="summary-zone">
<span v-for="item of iSummary" :key="item.title" class="summary-block">
<SummaryCard
:class="item.active ? 'active' : ''"
:count="item.count"
:title="item.title"
@click="handleFilterClick(item)"
/>
</span>
</div>
</div>
<div class="expand-bar-wrap">
<div class="expand-bar" @click="toggle">
<i :class="isExpand ? 'expand': 'shrink' " class="fa fa-angle-double-up" />
<span v-show="!isExpand"> 展开过滤器 </span>
<div class="expand-bar-wrap">
<div class="expand-bar" @click="toggle">
<i :class="isExpand ? 'expand': 'shrink' " class="fa fa-angle-double-up" />
<span v-show="!isExpand"> 展开过滤器 </span>
</div>
</div>
</div>
</div>
</template>
<script>
import SummaryCard from '@/components/Cards/SummaryCard'
import SummaryCard from '@/components/Cards/SummaryCard/index.vue'
import { setUrlParam } from '@/utils/common'
export default {
name: 'QuickFilter',
@@ -58,26 +64,49 @@ export default {
expand: {
type: Boolean,
default: true
},
tableUrl: {
type: String,
default: ''
}
},
data() {
return {
isExpand: this.expand,
iFilters: this.cleanFilters(),
iSummary: this.cleanSummary(),
filtered: {},
activeFilters: []
}
},
computed: {},
watch: {
isExpand(val) {
this.$emit('expand', val)
computed: {
isExpand: {
set(val) {
this.$emit('update:expand', val)
},
get() {
return this.expand
}
}
},
mounted() {
},
methods: {
async getCount(item) {
if (item.count) {
return item.count
}
if (!item.filter) {
return '-'
}
let url = this.tableUrl
for (const [k, v] of Object.entries({ ...item.filter, limit: 1 })) {
url = setUrlParam(url, k, v)
}
const res = await this.$axios.get(url, { raw: 1 })
item.count = res.data.count
console.log('............get count: ', item.count)
return item.count
},
cleanSummary() {
if (!this.summary) {
return []

View File

@@ -98,11 +98,23 @@ export default {
canBulkUpdate: {
type: [Boolean, Function, String],
default: false
},
hasQuickFilter: defaultTrue,
quickFilterExpand: {
type: Boolean,
default: true
}
},
data() {
return {
defaultRightSideActions: [
{
name: 'actionFilter',
icon: 'filter',
tip: this.$t('Filter'),
has: this.hasQuickFilter,
callback: this.handleFilterClick.bind(this)
},
{
name: 'actionSetting',
icon: 'system-setting',
@@ -159,6 +171,9 @@ export default {
}
},
methods: {
handleFilterClick() {
this.$emit('update:quick-filter-expand', !this.quickFilterExpand)
},
handleTagSearch(val) {
this.searchTable(val)
},

View File

@@ -1,9 +1,18 @@
<template>
<div>
<QuickFilter :filters="quickFilters" :summary="quickSummary" @filter="filter" />
<QuickFilter
:expand.sync="filterExpand"
:filters="quickFilters"
:summary="quickSummary"
:table-url="tableUrl"
@filter="filter"
/>
<TableAction
v-if="hasActions"
:class="{'filter-expand': filterExpand}"
:date-pick="handleDateChange"
:has-quick-filter="iHasQuickFilter"
:quick-filter-expand.sync="filterExpand"
:reload-table="reloadTable"
:search-table="search"
:selected-rows="selectedRows"
@@ -32,7 +41,7 @@ import IBox from '../../IBox/index.vue'
import TableAction from './TableAction/index.vue'
import Emitter from '@/mixins/emitter'
import AutoDataTable from '../AutoDataTable/index.vue'
import QuickFilter from './QuickFilter.vue'
import QuickFilter from './TableAction/QuickFilter.vue'
import { getDayEnd, getDaysAgo } from '@/utils/time'
export default {
@@ -90,13 +99,18 @@ export default {
isDeactivated: false,
extraQuery: extraQuery,
actionInit: this.headerActions.has === false,
initQuery: {}
initQuery: {},
filterExpand: true
}
},
computed: {
...mapGetters(['currentOrgIsRoot']),
iHasQuickFilter() {
const has = this.quickFilters && this.quickFilters.length > 0
return !!has
},
dataTable() {
return this.$refs.dataTable.$refs.dataTable
return this.$refs.dataTable?.$refs.dataTable
},
iHeaderActions() {
// 如果路由中锁定了 root 组织,就不在检查 root 组织下是否可以创建等
@@ -219,6 +233,9 @@ export default {
})
},
methods: {
handleFilterExpandChanged(expand) {
this.filterExpand = expand
},
handleQuickFilter(option) {
if (option.route) {
this.$router.push(option.route)
@@ -312,6 +329,11 @@ export default {
</script>
<style lang="scss" scoped>
.filter-expand {
&::v-deep button.actionFilter {
background-color: rgb(0, 0, 0, 0.08) !important;
}
}
.table-content {
margin-top: 10px;

View File

@@ -10,8 +10,8 @@
</template>
<script>
import ActionsGroup from '@/components/ActionsGroup/index.vue'
import BaseFormatter from './base.vue'
import ActionsGroup from '@/components/ActionsGroup/index.vue'
const defaultPerformDelete = function({ row, col }) {
const id = row.id
@@ -23,7 +23,9 @@ const defaultPerformDelete = function({ row, col }) {
const defaultUpdateCallback = function({ row, col }) {
const id = row.id
let route = { params: { id: id }}
const updateRoute = this.colActions.updateRoute
if (typeof updateRoute === 'object') {
@@ -33,6 +35,7 @@ const defaultUpdateCallback = function({ row, col }) {
} else {
route.name = updateRoute
}
this.$router.push(route)
}
@@ -106,7 +109,7 @@ export default {
onUpdate: defaultUpdateCallback,
onDelete: defaultDeleteCallback,
onClone: defaultCloneCallback,
extraActions: [] // format see defaultActions
extraActions: []
}
}
}
@@ -146,8 +149,8 @@ export default {
colActions: colActions,
defaultActions: defaultActions,
extraActions: colActions.extraActions,
moreActionsTitle: ''
// moreActionsTitle: colActions.moreActionsTitle || null
moreActionsTitle: ''
}
},
computed: {

View File

@@ -0,0 +1,56 @@
<script>
import BaseFormatter from './base.vue'
import { copy } from '@/utils/common'
export default {
name: 'CopyableFormatter',
extends: BaseFormatter,
props: {
formatterArgsDefault: {
type: Object,
default() {
return {
shadow: false,
getText: ({ cellValue }) => cellValue
}
}
}
},
data() {
return {
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
}
},
computed: {
iCellValue() {
if (this.formatterArgs.shadow) {
return '*'.repeat(6)
} else {
return this.cellValue
}
}
},
methods: {
async copy() {
const text = await this.formatterArgs.getText({ cellValue: this.cellValue, row: this.row })
copy(text)
}
}
}
</script>
<template>
<span class="copyable">
{{ iCellValue }} <i class="el-icon-copy-document copy" @click="copy()" />
</span>
</template>
<style lang="scss" scoped>
.copy {
cursor: pointer;
&:hover {
color: var(--color-primary);
}
}
</style>

View File

@@ -6,7 +6,7 @@
:disabled="disabled"
:type="col.type || 'info'"
class="detail"
@click="goDetail"
@click="handleClick"
>
<slot>
{{ iTitle }}
@@ -30,6 +30,7 @@ export default {
getRoute: null,
routeQuery: null,
can: true,
onClick: null,
openInNewPage: false,
removeColorOnClick: false,
getTitle({ col, row, cellValue }) {
@@ -46,6 +47,9 @@ export default {
const formatterArgs = Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
return {
linkClicked: false,
showTableDetailDrawer: false,
drawerTitle: '',
currentTemplate: null,
formatterArgs: formatterArgs
}
},
@@ -73,6 +77,17 @@ export default {
}
},
methods: {
handleClick() {
if (this.formatterArgs.onClick) {
this.formatterArgs.onClick({
col: this.col,
row: this.row,
cellValue: this.cellValue
})
} else {
this.goDetail()
}
},
getDetailRoute() {
// const defaultRoute = this.$route.name.replace('List', 'Detail')
let route = this.formatterArgs.route
@@ -107,12 +122,12 @@ export default {
const detailRoute = this.getDetailRoute()
if (this.formatterArgs.openInNewPage) {
this.linkClicked = this.formatterArgs.removeColorOnClick
const { href } = this.$router.resolve(detailRoute)
window.open(href, '_blank')
} else {
this.$router.push(detailRoute)
this.linkClicked = this.formatterArgs.removeColorOnClick
return window.open(href, '_blank')
}
this.$router.push(detailRoute)
}
}
}
@@ -142,4 +157,8 @@ export default {
width: 28px;
height: 28px;
}
::v-deep .go-back {
display: none;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<span class="conform-td">
<span v-if="iValue === '0'">
<span v-if="iValue === statusMap.pending">
<el-dropdown trigger="click" @command="handleRisk">
<el-button class="confirm action" size="mini">
<i class="fa fa-check" />
@@ -13,12 +13,12 @@
</el-dropdown>
<el-tooltip :content="$tc('Ignore')" :open-delay="400">
<el-button class="ignore action" size="mini">
<svg-icon icon-class="ignore" />
<svg-icon icon-class="ignore" @click="handleRisk('ignore')" />
</el-button>
</el-tooltip>
</span>
<el-tooltip v-else :content="iLabel" :open-delay="400" class="platform-status">
<span v-if="iValue === '1' ">
<span v-if="iValue === statusMap.confirmed ">
<i class="fa fa-check color-primary" />
</span>
<span v-else>
@@ -33,6 +33,11 @@
import BaseFormatter from './base.vue'
import ProcessingDialog from '@/components/Dialog/ProcessingDialog.vue'
const statusMap = {
pending: '0',
confirmed: '1',
ignored: '2'
}
export default {
name: 'ConfirmOrIgnoreFormatter',
components: { ProcessingDialog },
@@ -56,7 +61,8 @@ export default {
data() {
return {
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs),
processing: false
processing: false,
statusMap: statusMap
}
},
computed: {
@@ -94,7 +100,10 @@ export default {
if (cmd === 'add_account') {
this.row.present = true
}
this.row.status = { 'value': '1' }
if (cmd === 'ignore') {
this.row.status = { 'value': statusMap.ignored }
}
this.row.status = { 'value': statusMap.confirmed }
}).finally(() => {
setTimeout(() => {
this.processing = false
@@ -117,6 +126,11 @@ export default {
name: 'add_account',
label: this.$t('Add account'),
has: !this.row.present
},
{
name: 'add_account_after_change_password',
label: this.$t('Add account after changing password'),
has: !this.row.present
}
]
return actions.filter(action => {

View File

@@ -0,0 +1,26 @@
<template>
<span v-if="loading" v-loading="loading" class="loading" />
</template>
<script>
export default {
name: 'Index',
props: {
loading: {
type: Boolean,
default: true
}
},
data() {
return {
}
}
}
</script>
<style lang="scss" scoped>
.loading {
margin-top: 20px;
display: block;
}
</style>

23
src/directive/async.js Normal file
View File

@@ -0,0 +1,23 @@
import Vue from 'vue'
Vue.directive('async', {
async bind(el, binding) {
const { value: asyncFn, arg } = binding
if (typeof asyncFn === 'function') {
const result = await asyncFn(arg)
el.innerText = result
}
},
async update(el, binding) {
const { value: asyncFn, arg } = binding
if (typeof asyncFn === 'function') {
const result = await asyncFn(arg)
el.innerText = result
} else if (typeof asyncFn === 'object' && asyncFn.then) {
const result = await asyncFn
el.innerText = result
} else {
el.innerText = asyncFn
}
}
})

View File

@@ -1 +1,2 @@
import './permission'
import './async'

View File

@@ -5,3 +5,12 @@ Vue.filter('date', function(value) {
return toSafeLocalDateStr(value)
})
Vue.filter('async', async(asyncFn, args) => {
if (typeof asyncFn === 'function') {
return await asyncFn(args)
} else if (typeof asyncFn === 'object' && asyncFn.then) {
return await asyncFn
} else {
return asyncFn
}
})

View File

@@ -14,6 +14,7 @@ router.beforeEach(async(to, from, next) => {
// start progress bar
// NProgress.start()
try {
await store.dispatch('common/cleanDrawerActionMeta')
await startup({ to, from, next })
next()
} catch (e) {

View File

@@ -133,8 +133,13 @@ export default {
submitMethod: {
type: [Function, String],
default: function() {
const params = this.$route.params
if (params.id) {
const cloneFrom = this.getCloneId()
console.log('Clone from: ', cloneFrom)
if (cloneFrom) {
return 'post'
}
const objectId = this.getUpdateId()
if (objectId) {
return 'put'
} else {
return 'post'
@@ -145,13 +150,13 @@ export default {
getUrl: {
type: Function,
default: function() {
const params = this.$route.params
const objectId = this.getUpdateId()
let url = this.url
if (params.id) {
url = getUpdateObjURL(url, params.id)
if (objectId) {
url = getUpdateObjURL(url, objectId)
}
const clone_from = this.$route.query['clone_from']
const clone_from = this.getCloneId()
const query = clone_from ? `clone_from=${clone_from}` : ''
if (query) {
if (url.indexOf('?') === -1) {
@@ -204,7 +209,6 @@ export default {
type: Function,
default(res, method, vm, addContinue) {
const route = this.getNextRoute(res, method)
if (!(route.params && route.params.id)) {
route['params'] = deepmerge(route['params'] || {}, { 'id': res.id })
}
@@ -213,10 +217,18 @@ export default {
this.$emit('submitSuccess', res)
this.emitPerformSuccessMsg(method, res, addContinue)
if (!addContinue) {
if (addContinue) {
return
}
if (!vm.drawer) {
if (this.$router.currentRoute.name !== route?.name) {
setTimeout(() => this.$router.push(route), 100)
}
} else {
console.log('Reload table clsonse dr')
this.$emit('close-drawer')
this.$emit('reload-table')
}
}
},
@@ -268,7 +280,11 @@ export default {
form: {},
loading: true,
isSubmitting: false,
clone: false
clone: false,
drawer: false,
action: '',
actionId: '',
row: {}
}
},
computed: {
@@ -297,19 +313,47 @@ export default {
}
},
async created() {
this.$log.debug('Object init is: ', this.object)
const drawActionMeta = await this.$store.dispatch('common/getDrawerActionMeta')
if (drawActionMeta) {
this.drawer = true
this.action = drawActionMeta.action
this.row = drawActionMeta.row
this.actionId = this.row?.id
}
this.$log.debug('Object init is: ', this.object, this.method)
console.log('Action: ', this.action, this.actionId)
this.loading = true
try {
const values = await this.getFormValue()
this.$log.debug('Final object is: ', values)
const formValue = Object.assign(this.form, values)
this.form = this.afterGetFormValue(formValue)
console.log('Form: ', this.form)
} finally {
this.loading = false
}
},
methods: {
getUpdateId() {
if (this.actionId && this.action === 'update') {
return this.actionId
} else {
return this.$route.params.id
}
},
getAction() {
return this.action
},
getCloneId() {
if (this.actionId && this.action === 'clone') {
return this.actionId
} else {
return this.$route.query['clone_from']
}
},
isUpdateMethod() {
console.log('This method: ', this.method)
return ['put', 'patch'].indexOf(this.method.toLowerCase()) > -1
},
encryptFields(values) {
@@ -352,27 +396,34 @@ export default {
}, 200)
})
},
async getUpdateForm() {
},
async getCloneForm(cloneFrom) {
const [curUrl, query] = this.url.split('?')
const url = `${curUrl}${cloneFrom}/${query ? ('?' + query) : ''}`
const object = await this.getObjectDetail(url)
let name = ''
let attr = ''
if (object['name']) {
name = object['name']
attr = 'name'
} else if (object['hostname']) {
name = object['hostname']
attr = 'hostname'
}
object[attr] = name + '-' + this.cloneNameSuffix.toString()
return object
},
async getFormValue() {
const cloneFrom = this.$route.query['clone_from']
if ((!this.isUpdateMethod() && !cloneFrom) || !this.needGetObjectDetail) {
const cloneFrom = this.getCloneId()
const objectId = this.getUpdateId()
if ((!objectId && !cloneFrom) || !this.needGetObjectDetail) {
return Object.assign(this.form, this.initial)
}
let object = this.object
if (!object || Object.keys(object).length === 0) {
if (cloneFrom) {
const [curUrl, query] = this.url.split('?')
const url = `${curUrl}${cloneFrom}/${query ? ('?' + query) : ''}`
object = await this.getObjectDetail(url)
let name = ''
let attr = ''
if (object['name']) {
name = object['name']
attr = 'name'
} else if (object['hostname']) {
name = object['hostname']
attr = 'hostname'
}
object[attr] = name + '-' + this.cloneNameSuffix.toString()
object = await this.getCloneForm(cloneFrom)
} else {
object = await this.getObjectDetail(this.iUrl)
}
@@ -393,7 +444,7 @@ export default {
</script>
<style scoped>
.ibox ::v-deep .el-card__body {
padding-top: 30px;
}
.ibox ::v-deep .el-card__body {
padding-top: 30px;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<Page v-bind="$attrs">
<Page :class="{drawer: drawer}" v-bind="$attrs">
<IBox>
<GenericCreateUpdateForm ref="createUpdateForm" v-bind="$attrs" v-on="$listeners" />
</IBox>
@@ -14,6 +14,18 @@ export default {
name: 'GenericCreateUpdatePage',
components: {
Page, IBox, GenericCreateUpdateForm
},
data() {
return {
drawer: false
}
},
mounted() {
this.$store.dispatch('common/getDrawerActionMeta').then((res) => {
if (res.action) {
this.drawer = true
}
})
}
}
</script>
@@ -27,4 +39,8 @@ export default {
padding-bottom: 60px;
}
}
.drawer ::v-deep .page-heading {
display: none;
}
</style>

View File

@@ -35,7 +35,6 @@ export default {
props: {
url: {
type: String,
required: false,
default: ''
},
object: {
@@ -98,37 +97,34 @@ export default {
},
data() {
const vm = this
const detailApiUrl = (function() {
if (vm.url) {
return `${vm.url}/${vm.$route.params.id}/`
} else {
return getApiPath(vm)
}
}())
const defaultActions = {
// Delete button
canDelete: vm.$hasCurrentResAction('delete'),
hasDelete: true,
deleteCallback: function(item) {
vm.defaultDelete(item)
},
deleteApiUrl: detailApiUrl,
deleteSuccessRoute: this.$route.name.replace('Detail', 'List'),
// Update button
canUpdate: () => {
return !vm.currentOrgIsRoot && vm.$hasCurrentResAction('change')
},
hasUpdate: true,
updateCallback: function(item) {
this.defaultUpdate(item)
},
updateRoute: this.$route.name.replace('Detail', 'Update')
}
return {
detailApiUrl,
defaultActions,
loading: true,
drawer: '',
action: '',
actionId: '',
validActions: Object.assign(defaultActions, this.actions)
}
},
computed: {
...mapGetters(['currentOrgIsRoot']),
pageActions() {
@@ -139,7 +135,7 @@ export default {
icon: 'el-icon-edit-outline',
size: 'small',
can: this.validActions.canUpdate,
has: this.validActions.hasUpdate,
has: this.validActions.hasUpdate && !this.drawer,
callback: this.validActions.updateCallback.bind(this)
},
{
@@ -178,20 +174,53 @@ export default {
return [...this.submenu, activity]
}
},
async mounted() {
async created() {
try {
this.loading = true
await this.checkDrawer()
await this.getObject()
} finally {
this.loading = false
}
},
methods: {
async checkDrawer() {
const drawActionMeta = await this.$store.dispatch('common/getDrawerActionMeta')
if (drawActionMeta) {
this.drawer = true
this.row = drawActionMeta.row
this.actionId = this.row?.id
}
},
getDetailUrl() {
const vm = this
let objectId = ''
if (this.actionId) {
objectId = this.actionId
} else {
objectId = vm.$route.params.id
}
if (vm.url) {
return `${vm.url}/${objectId}/`
} else {
return getApiPath(vm, objectId)
}
},
afterDelete() {
if (this.drawer) {
this.$emit('close-drawer')
this.$emit('detail-delete-success')
this.$emit('reload-table')
} else {
this.$message.success(this.$tc('DeleteSuccessMsg'))
this.$router.push({ name: this.validActions.deleteSuccessRoute })
}
},
defaultDelete() {
const msg = this.$t('DeleteWarningMsg') + ' ' + this.iTitle + ' ?'
const title = this.$t('Info')
const performDelete = () => {
const url = this.validActions.deleteApiUrl
const url = this.getDetailUrl()
this.$log.debug('Start perform delete: ', url)
return this.$axios.delete(url)
}
@@ -206,8 +235,7 @@ export default {
try {
await performDelete.bind(this)()
done()
this.$message.success(this.$tc('DeleteSuccessMsg'))
this.$router.push({ name: this.validActions.deleteSuccessRoute })
this.afterDelete()
} catch (error) {
const errorDetail = error?.response?.data?.detail || ''
if (errorDetail) {
@@ -238,13 +266,13 @@ export default {
},
getObject() {
// 兼容之前的 detailApiUrl
const url = this.validActions.detailApiUrl || this.detailApiUrl
const url = this.getDetailUrl()
return this.$axios.get(url, { disableFlashErrorMsg: true }).then(data => {
this.$emit('update:object', data)
this.$emit('getObjectDone', data)
}).catch(error => {
if (error.response && error.response.status === 404) {
const msg = this.$t('ObjectNotFoundOrDeletedMsg')
const msg = this.$tc('ObjectNotFoundOrDeletedMsg')
this.$message.error(msg)
} else {
flashErrorMsg({ error, response: error.response })
@@ -261,7 +289,7 @@ export default {
</script>
<style scoped>
.header-buttons {
z-index: 999;
}
.header-buttons {
z-index: 999;
}
</style>

View File

@@ -0,0 +1,86 @@
<template>
<el-drawer
:size="drawerSize"
:title="title"
:visible.sync="iVisible"
append-to-body
class="form-drawer"
destroy-on-close
>
<component
:is="component"
v-bind="props"
@close="closeDrawer"
v-on="$listeners"
@close-drawer="iVisible=false"
/>
</el-drawer>
</template>
<script>
export default {
props: {
visible: {
type: Boolean,
required: true
},
title: {
type: String,
default: ''
},
component: {
type: [String, Function],
required: true
},
props: {
type: Object,
default: () => ({})
},
action: {
type: String,
default: ''
}
},
computed: {
iVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
},
drawerSize() {
const width = window.innerWidth
if (width >= 768) return '800px'
return '90%'
}
},
methods: {
closeDrawer() {
this.iVisible = false
}
}
}
</script>
<style lang="scss" scoped>
.form-drawer {
/* 可自定义样式 */
::v-deep {
.el-card.ibox {
//border: none;
}
.el-drawer__header {
//border-bottom: 1px solid #EBEEF5;
margin-bottom: 10px;
padding-top: 10px;
font-size: 16px;
font-weight: 500;
color: var(--color-text-primary);
}
}
}
</style>

View File

@@ -0,0 +1,183 @@
<template>
<div>
<Page v-bind="$attrs">
<GenericListTable
ref="ListTable"
:header-actions="iHeaderActions"
:table-config="iTableConfig"
v-bind="$attrs"
/>
</Page>
<Drawer
v-if="drawerVisible"
:action="action"
:component="drawerComponent"
:props="drawerProps"
:title="drawerTitle"
:visible.sync="drawerVisible"
@reload-table="reloadTable"
/>
</div>
</template>
<script>
import Page from '@/layout/components/Page'
import GenericListTable from '@/layout/components/GenericListTable'
import Drawer from './Drawer'
import { toSentenceCase } from '@/utils/common'
const drawerType = [String, Function]
export default {
name: 'GenericListPage',
components: {
Page, GenericListTable, Drawer
},
props: {
detailDrawer: {
type: drawerType,
default: ''
},
createDrawer: {
type: drawerType,
default: ''
},
updateDrawer: {
type: drawerType,
default: ''
},
tableConfig: {
type: Object,
required: true
},
headerActions: {
type: Object,
required: true
}
},
data() {
return {
visible: false,
drawerVisible: false,
drawerComponent: '',
drawerProps: {},
iHeaderActions: {},
iTableConfig: {},
action: '',
iCreateDrawer: this.createDrawer,
iUpdateDrawer: this.updateDrawer,
iDetailDrawer: this.detailDrawer
}
},
computed: {
drawerTitle() {
let title = this.title || this.$route.meta.title
if (!title) {
title = this.$t('NoTitle')
}
title = toSentenceCase(this.action) + ' ' + title.toLowerCase()
return title
}
},
watch: {
drawerVisible(val) {
if (!val) {
this.$store.dispatch('common/cleanDrawerActionMeta')
}
}
},
mounted() {
if (!this.createDrawer) {
this.iCreateDrawer = this.getDefaultDrawer('create')
}
if (!this.updateDrawer) {
this.iUpdateDrawer = this.getDefaultDrawer('update')
}
if (!this.detailDrawer) {
this.iDetailDrawer = this.getDefaultDrawer('detail')
}
},
created() {
this.iHeaderActions = {
...this.headerActions,
onCreate: this.onCreate
}
this.iTableConfig = {
...this.tableConfig
}
_.set(this.iTableConfig, 'columnsMeta.actions.formatterArgs.onUpdate', this.onUpdate)
_.set(this.iTableConfig, 'columnsMeta.actions.formatterArgs.onClone', this.onClone)
_.set(this.iTableConfig, 'columnsMeta.name.formatterArgs.onClick', this.onDetail)
},
methods: {
getDefaultDrawer(action) {
const route = this.$route.name
const actionRouteName = route.replace('List', toSentenceCase(action))
return this.getRouteNameComponent(actionRouteName)
},
getRouteNameComponent(name) {
const routes = this.$router.resolve({ name: name })
if (!routes) {
return
}
const matched = routes.resolved.matched.filter(item => item.name === name && item.components)
if (matched.length === 0) {
return
}
if (matched[0] && matched[0].components?.default) {
return matched[0].components.default
}
},
onCreate() {
this.action = 'create'
this.drawerComponent = this.iCreateDrawer
this.$store.dispatch('common/setDrawerActionMeta', {
action: 'create'
}).then(() => {
this.drawerVisible = true
})
},
reloadTable() {
this.$refs.ListTable.reloadTable()
},
onClone({ row, col }) {
this.drawerComponent = this.iCreateDrawer
this.action = 'clone'
this.$store.dispatch('common/setDrawerActionMeta', {
action: 'clone',
row: row,
col: col
}).then(() => {
this.drawerVisible = true
})
},
onUpdate({ row, col }) {
this.action = 'update'
let updateDrawer = this.iUpdateDrawer
if (!updateDrawer) {
updateDrawer = this.iCreateDrawer
}
this.drawerComponent = updateDrawer
this.$store.dispatch('common/setDrawerActionMeta', {
action: 'update',
row: row,
col: col
}).then(() => {
this.drawerVisible = true
})
},
onDetail({ row, cellValue }) {
this.action = 'detail'
this.drawerComponent = this.iDetailDrawer
this.$store.dispatch('common/setDrawerActionMeta', {
action: 'detail',
row: row,
cellValue: cellValue
}).then(() => {
this.drawerVisible = true
})
}
}
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div>
<slot name="globalNotification">
<SqlQueryTip v-if="debug" />
<SqlQueryTip v-if="debug && !inDrawer" />
<LicenseRelatedTip v-else />
<PasswordExpireTip />
</slot>
@@ -22,6 +22,7 @@
import LicenseRelatedTip from './LicenseRelatedTip'
import PasswordExpireTip from './PasswordExpireTip'
import SqlQueryTip from './SqlQueryTip'
import { mapGetters } from 'vuex'
export default {
name: 'PageHeading',
@@ -40,6 +41,9 @@ export default {
return {
debug: process.env.NODE_ENV === 'development'
}
},
computed: {
...mapGetters(['inDrawer'])
}
}
</script>

View File

@@ -3,6 +3,7 @@
<TagsView />
<PageHeading v-if="iTitle || helpMessage" :help-msg="helpMessage" class="disabled-when-print">
<el-button
v-if="!inDrawer"
:disabled="gobackDisabled"
class="go-back"
icon="el-icon-back"
@@ -46,6 +47,7 @@ import PageContent from './PageContent'
import UserConfirmDialog from '@/components/Apps/UserConfirmDialog/index.vue'
import TagsView from '../TagsView/index.vue'
import { toSentenceCase } from '@/utils/common'
import { mapGetters } from 'vuex'
export default {
name: 'Page',
@@ -81,6 +83,7 @@ export default {
}
},
computed: {
...mapGetters(['inDrawer']),
iTitle() {
let title = this.title || this.$route.meta.title
if (!title) {

View File

@@ -16,11 +16,10 @@
<el-tab-pane
:key="item.name"
:disabled="item.disabled"
:label-content="item.labelContent"
:name="item.name"
>
<span slot="label">
<i v-if="item.icon" :class="item.icon" class="fa pre-icon " />
<Icon v-if="item.icon" :icon="item.icon" class="pre-icon" />
{{ toSentenceCase(item.title) }}
<slot :tab="item.name" name="badge" />
<el-tooltip
@@ -60,12 +59,14 @@
<script>
import Page from '../Page/'
import Icon from '@/components/Widgets/Icon'
import { toSentenceCase } from '@/utils/common'
export default {
name: 'TabPage',
components: {
Page
Page,
Icon
},
props: {
submenu: {

View File

@@ -67,57 +67,57 @@ export default [
}
}
]
},
{
path: 'account-template',
component: empty,
redirect: {
name: 'AccountTemplateList'
},
meta: {
title: i18n.t('AccountTemplate'),
app: 'accounts',
icon: 'template',
permissions: ['accounts.view_accounttemplate']
},
children: [
{
path: '',
name: 'AccountTemplateList',
component: () => import('@/views/accounts/AccountTemplate/AccountTemplateList'),
meta: {
menuTitle: i18n.t('MenuAccountTemplates'),
title: i18n.t('AccountTemplateList'),
permissions: ['accounts.view_accounttemplate']
}
},
{
path: 'create',
component: () => import('@/views/accounts/AccountTemplate/AccountTemplateCreateUpdate.vue'),
name: 'AccountTemplateCreate',
meta: {
title: i18n.t('CreateAccountTemplate'),
action: 'create'
},
hidden: true
},
{
path: ':id/update',
component: () => import('@/views/accounts/AccountTemplate/AccountTemplateCreateUpdate.vue'),
name: 'AccountTemplateUpdate',
meta: {
title: i18n.t('UpdateAccountTemplate'),
action: 'update'
},
hidden: true
}
// {
// path: ':id',
// component: () => import('@/views/accounts/AccountTemplate/AccountTemplateDetail/Application.vue'),
// name: 'AccountTemplateDetail',
// meta: { title: i18n.t('AccountTemplate') },
// hidden: true
// }
]
}
// {
// path: 'account-template',
// component: empty,
// redirect: {
// name: 'AccountTemplateList'
// },
// meta: {
// title: i18n.t('AccountTemplate'),
// app: 'accounts',
// icon: 'template',
// permissions: ['accounts.view_accounttemplate']
// },
// children: [
// {
// path: '',
// name: 'AccountTemplateList',
// component: () => import('@/views/accounts/AccountTemplate/AccountTemplateList'),
// meta: {
// menuTitle: i18n.t('MenuAccountTemplates'),
// title: i18n.t('AccountTemplateList'),
// permissions: ['accounts.view_accounttemplate']
// }
// },
// {
// path: 'create',
// component: () => import('@/views/accounts/AccountTemplate/AccountTemplateCreateUpdate.vue'),
// name: 'AccountTemplateCreate',
// meta: {
// title: i18n.t('CreateAccountTemplate'),
// action: 'create'
// },
// hidden: true
// },
// {
// path: ':id/update',
// component: () => import('@/views/accounts/AccountTemplate/AccountTemplateCreateUpdate.vue'),
// name: 'AccountTemplateUpdate',
// meta: {
// title: i18n.t('UpdateAccountTemplate'),
// action: 'update'
// },
// hidden: true
// },
// {
// path: ':id',
// component: () => import('@/views/accounts/AccountTemplate/AccountTemplateDetail/Application.vue'),
// name: 'AccountTemplateDetail',
// meta: { title: i18n.t('AccountTemplate') },
// hidden: true
// }
// ]
// }
]

View File

@@ -159,6 +159,82 @@ export default [
}
]
},
{
path: 'account-push',
name: 'AccountPush',
component: empty,
redirect: {
name: 'AccountPushList'
},
meta: {
app: 'accounts',
name: 'BaseAccountPushList',
resource: 'pushaccountautomation',
icon: 'change-password'
},
children: [
{
path: '',
component: () => import('@/views/accounts/AccountPush/index.vue'),
name: 'AccountPushList',
meta: {
title: i18n.t('AccountPushList'),
menuTitle: i18n.t('AccountPushList'),
permissions: ['accounts.view_pushaccountautomation']
}
},
{
path: 'create',
component: () => import('@/views/accounts/AccountPush/AccountPushCreateUpdate.vue'),
name: 'AccountPushCreate',
hidden: true,
meta: {
title: i18n.t('AccountPushCreate'),
permissions: ['accounts.add_pushaccountautomation']
}
},
{
path: ':id/update',
component: () => import('@/views/accounts/AccountPush/AccountPushCreateUpdate.vue'),
name: 'AccountPushUpdate',
hidden: true,
meta: {
title: i18n.t('AccountPushUpdate'),
permissions: ['accounts.change_pushaccountautomation']
}
},
{
path: ':id',
component: () => import('@/views/accounts/AccountPush/AccountPushDetail/index.vue'),
name: 'AccountPushDetail',
hidden: true,
meta: {
title: i18n.t('AccountPushList'),
permissions: ['accounts.view_pushaccountautomation']
}
},
{
path: 'executions',
component: () => import('@/views/accounts/AccountPush/AccountPushExecutionList.vue'),
name: 'AccountPushExecutionList',
hidden: true,
meta: {
title: i18n.t('ExecutionList'),
permissions: ['accounts.view_pushaccountexecution']
}
},
{
path: 'executions/:id',
component: () => import('@/views/accounts/AccountPush/AccountPushExecutionDetail/index.vue'),
name: 'AccountPushExecutionDetail',
hidden: true,
meta: {
title: i18n.t('ExecutionDetail'),
permissions: ['accounts.view_pushaccountexecution']
}
}
]
},
{
path: 'account-backup',
component: empty,

View File

@@ -48,7 +48,7 @@ export default [
{
path: ':id',
component: () => import('@/views/pam/Integration/ApplicationDetail/index.vue'),
name: 'ApplicationDetail',
name: 'IntegrationApplicationDetail',
hidden: true,
meta: {
title: i18n.t('ApplicationDetail'),

View File

@@ -18,7 +18,8 @@ export default [
component: () => import('@/views/pam/RiskDetect/index.vue'),
name: 'AccountCheckList',
meta: {
title: i18n.t('RiskDetection')
title: i18n.t('RiskDetection'),
permissions: ['accounts.view_accountrisk']
}
},
{
@@ -27,7 +28,8 @@ export default [
name: 'AccountCheckCreateUpdate',
hidden: true,
meta: {
title: i18n.t('AccountCheckCreate')
title: i18n.t('AccountCheckCreate'),
permissions: ['accounts.view_accountrisk']
}
}
]

View File

@@ -31,6 +31,7 @@ const getters = {
hasValidLicense: state => state.settings.hasValidLicense,
isSystemAdmin: state => state.users.profile.system_roles.some(i => (i?.id === '00000000-0000-0000-0000-000000000001')),
sqlQueryCounter: state => state.common.sqlQueryCounter,
showSqlQueryCounter: state => state.common.showSqlQueryCounter
showSqlQueryCounter: state => state.common.showSqlQueryCounter,
inDrawer: state => state.common.inDrawer
}
export default getters

View File

@@ -7,7 +7,9 @@ const getDefaultState = () => {
isRouterAlive: true,
sqlQueryCounter: [],
showSqlQueryCounter: true,
confirmDialogVisible: false
confirmDialogVisible: false,
drawerActionMeta: {},
inDrawer: false
}
}
@@ -85,6 +87,17 @@ const actions = {
},
showSqlQueryCounter({ commit, state }, show) {
state.showSqlQueryCounter = show
},
setDrawerActionMeta({ commit, state }, meta) {
state.drawerActionMeta = meta
state.inDrawer = true
},
getDrawerActionMeta({ commit, state }) {
return state.drawerActionMeta
},
cleanDrawerActionMeta({ commit, state }) {
state.drawerActionMeta = {}
state.inDrawer = false
}
}

View File

@@ -3,7 +3,7 @@ import { message } from '@/utils/message'
const _ = require('lodash')
export function getApiPath(that) {
export function getApiPath(that, objectId) {
let pagePath = that.$route.path
const pagePathArray = pagePath.split('/')
if (pagePathArray.indexOf('orgs') !== -1) {
@@ -11,6 +11,9 @@ export function getApiPath(that) {
} else if (pagePathArray.indexOf('gathered-user') !== -1 || pagePathArray.indexOf('change-auth-plan') !== -1) {
pagePathArray[pagePathArray.indexOf('accounts')] = 'xpack'
}
if (pagePathArray.indexOf(objectId) === -1) {
pagePathArray.push(objectId)
}
if (pagePathArray.indexOf('tickets') !== -1) {
// ticket ...
pagePath = pagePathArray.slice(1, pagePathArray.length).join('/')
@@ -310,4 +313,5 @@ export function toSentenceCase(string) {
}).join(' ')
return s[0].toUpperCase() + s.slice(1)
}
export { BASE_URL }

View File

@@ -1,25 +1,36 @@
<template>
<el-row :gutter="20">
<el-col :md="15" :sm="24" class="auto-detail-card">
<AutoDetailCard :object="object" v-bind="detail" />
</el-col>
<el-col :md="9" :sm="24" class="quick-actions">
<QuickActions :actions="quickActions" type="primary" />
<ViewSecret
v-if="showViewSecretDialog"
:account="object"
:url="secretUrl"
:visible.sync="showViewSecretDialog"
/>
<AutomationParamsForm
:has-button="false"
:method="pushAccountMethod"
:visible.sync="autoPushVisible"
@canSetting="onCanSetting"
@submit="onSubmit"
/>
</el-col>
</el-row>
<div>
<el-row :gutter="20">
<el-col :md="15" :sm="24" class="auto-detail-card">
<AutoDetailCard :object="object" v-bind="detail" />
</el-col>
<el-col :md="9" :sm="24" class="quick-actions">
<QuickActions :actions="quickActions" type="primary" />
<ViewSecret
v-if="showViewSecretDialog"
:account="object"
:url="secretUrl"
:visible.sync="showViewSecretDialog"
/>
<AutomationParamsForm
:has-button="false"
:method="pushAccountMethod"
:visible.sync="autoPushVisible"
@canSetting="onCanSetting"
@submit="onSubmit"
/>
</el-col>
</el-row>
<el-drawer
size="50%"
:with-header="false"
:append-to-body="true"
:visible.sync="pamDrawerShow"
>
<component :is="drawerRefName" />
</el-drawer>
</div>
</template>
<script>
@@ -28,12 +39,14 @@ import QuickActions from '@/components/QuickActions/index.vue'
import ViewSecret from '@/components/Apps/AccountListTable/ViewSecret.vue'
import { openTaskPage } from '@/utils/jms'
import AutomationParamsForm from '@/views/assets/Platform/AutomationParamsSetting.vue'
import AssetDetail from '@/views/assets/Asset/AssetDetail'
export default {
name: 'Detail',
components: {
AutoDetailCard,
QuickActions,
AssetDetail,
AutomationParamsForm,
ViewSecret
},
@@ -47,6 +60,8 @@ export default {
data() {
const vm = this
return {
pamDrawerShow: false,
drawerRefName: null,
needSetAutoPushParams: false,
autoPushVisible: false,
secretUrl: `/api/v1/accounts/account-secrets/${this.object.id}/`,
@@ -213,6 +228,22 @@ export default {
name: 'AssetDetail',
params: { id: this.object.asset.id }
}
console.log(this.$route)
if (this.$route.params.type === 'pam') {
return (
<span style={{ color: '#1c84c6', cursor: 'pointer' }} onClick={() => {
this.pamDrawerShow = true
this.$route.params.id = this.object.asset.id
this.drawerRefName = 'AssetDetail'
}}>
{value.name}
</span>
)
}
return <router-link to={route}>{value?.name}</router-link>
},
su_from: (item, value) => {

View File

@@ -13,6 +13,7 @@ import AccountBackupExecutionList
from '@/views/accounts/AccountBackup/AccountBackupExecution/AccountBackupExecutionList.vue'
export default {
name: 'AccountBackupDetail',
components: {
GenericDetailPage,
AccountBackupInfo,
@@ -39,7 +40,3 @@ export default {
}
}
</script>
<style scoped>
</style>

View File

@@ -1,20 +1,33 @@
<template>
<GenericListTable :header-actions="headerActions" :table-config="tableConfig" />
<div>
<GenericListTable :header-actions="headerActions" :table-config="tableConfig" />
<Drawer v-if="showTableUpdateDrawer" :title="drawerTitle" @close-drawer="showTableUpdateDrawer = !showTableUpdateDrawer">
<component :is="currentTemplate" />
</Drawer>
</div>
</template>
<script>
import { GenericListTable } from '@/layout/components'
import { ArrayFormatter, DetailFormatter } from '@/components/Table/TableFormatters'
import { openTaskPage } from '@/utils/jms'
import Drawer from '@/components/Drawer/index.vue'
export default {
name: 'AccountBackupList',
components: {
GenericListTable
Drawer,
GenericListTable,
AccountBackupUpdate: () => import('@/views/accounts/AccountBackup/AccountBackupCreateUpdate.vue'),
AccountBackupCreate: () => import('@/views/accounts/AccountBackup/AccountBackupCreateUpdate.vue')
},
data() {
const vm = this
return {
drawerTitle: '',
showTableUpdateDrawer: false,
currentTemplate: null,
tableConfig: {
url: '/api/v1/accounts/account-backup-plans/',
permissions: {
@@ -36,7 +49,13 @@ export default {
name: {
formatter: DetailFormatter,
formatterArgs: {
route: 'AccountBackupDetail'
isPam: true,
route: 'AccountBackupDetail',
getRoute: ({ row }) => ({
name: 'AccountBackupDetail',
params: { id: row.id },
query: { type: 'pam' }
})
}
},
types: {
@@ -68,7 +87,14 @@ export default {
vm.$router.push({ name: 'AccountBackupCreate', query: { clone_from: row.id }})
},
onUpdate: ({ row }) => {
vm.$router.push({ name: 'AccountBackupUpdate', params: { id: row.id }})
this.$route.params.id = row.id
// 解决表单详情中的跳转
this.$route.query.type = 'pam'
this.currentTemplate = 'AccountBackupUpdate'
this.drawerTitle = this.$t('AccountBackupUpdate')
this.showTableUpdateDrawer = true
},
extraActions: [
{
@@ -101,6 +127,11 @@ export default {
return {
name: 'AccountBackupCreate'
}
},
onCreate: () => {
this.currentTemplate = 'AccountBackupCreate'
this.drawerTitle = this.$t('AccountBackupCreate')
this.showTableUpdateDrawer = true
}
}
}

View File

@@ -58,12 +58,10 @@ export default {
{
id: 'discover',
icon: 'discovery',
name: '发现账号',
name: this.$t('DiscoverAccounts'),
callback: (node) => {
console.log('Discovery it: ', node)
this.discoveryDialog.asset = node.id
this.discoveryDialog.visible = true
// this.discoveryDialog.asset = node.data
}
}
]
@@ -71,58 +69,25 @@ export default {
quickSummary: [
{
title: '最近一周发现',
count: 10,
hasCount: true,
filter: {
name: 'admin'
'days': '7'
}
},
{
title: '最近一月发现',
count: 321,
filter: {
username: 'admin'
'days': '30'
}
},
{
title: '待确认',
count: 544,
filter: {
username: 'admin'
status: '0'
}
}
],
quickFilters: [
{
label: '快速过滤',
options: [
{
label: '未同步到资产',
value: ''
},
{
label: this.$t('最近一个月'),
value: ''
}
]
},
{
label: this.$t('最近发现'),
options: [
{
label: '最近一天 (20)',
value: ''
},
{
label: '最近一周 (300)',
value: ''
},
{
label: '最近一个月 (600)',
value: ''
}
]
}
],
quickFilters: [],
tableConfig: gatherAccountTableConfig(this),
headerActions: gatherAccountHeaderActions(this)
}

View File

@@ -1,20 +1,34 @@
<template>
<GenericListTable :header-actions="headerActions" :table-config="tableConfig" />
<div>
<GenericListTable :header-actions="headerActions" :table-config="tableConfig" />
<Drawer v-if="showTableUpdateDrawer" :title="drawerTitle" @close-drawer="showTableUpdateDrawer = !showTableUpdateDrawer">
<component :is="currentTemplate" />
</Drawer>
</div>
</template>
<script>
import { GenericListTable } from '@/layout/components'
import { DetailFormatter } from '@/components/Table/TableFormatters'
import { openTaskPage } from '@/utils/jms'
import Drawer from '@/components/Drawer/index.vue'
export default {
name: 'AccountDiscoverTaskList',
components: {
GenericListTable
Drawer,
GenericListTable,
AccountDiscoverTaskCreate: () => import('@/views/accounts/AccountDiscover/TaskCreateUpdate'),
AccountDiscoverTaskUpdate: () => import('@/views/accounts/AccountDiscover/TaskCreateUpdate')
},
data() {
const vm = this
return {
showViewSecretDialog: false,
showTableUpdateDrawer: false,
currentTemplate: null,
drawerTitle: '',
tableConfig: {
name: 'AccountDiscoverTaskList',
url: '/api/v1/accounts/gather-account-automations/',
@@ -37,10 +51,11 @@ export default {
name: {
formatter: DetailFormatter,
formatterArgs: {
route: 'AccountDiscoverTaskDetail',
routeQuery: {
tab: 'AccountDiscoverTaskDetail'
}
getRoute: ({ row }) => ({
name: 'AccountDiscoverTaskDetail',
params: { id: row.id },
query: { type: 'pam' }
})
}
},
nodes: {
@@ -94,7 +109,15 @@ export default {
})
}
}
]
],
onUpdate: ({ row }) => {
this.$route.params.id = row.id
this.$route.query.type = 'pam'
this.currentTemplate = 'AccountDiscoverTaskUpdate'
this.showTableUpdateDrawer = true
}
}
}
}
@@ -107,14 +130,15 @@ export default {
createRoute: 'AccountDiscoverTaskCreate',
searchConfig: {
getUrlQuery: false
},
onCreate: ({ row }) => {
this.$route.query.type = 'pam'
this.currentTemplate = 'AccountDiscoverTaskCreate'
this.showTableUpdateDrawer = true
}
}
}
},
watch: {
// $route(to, from) {
// this.$router.go(0)
// }
}
}
</script>

View File

@@ -119,7 +119,15 @@ export default {
})
},
handleConfirm() {
this.$axios.delete('/api/v1/accounts/gathered-accounts/', {
params: {
username: this.account.username,
asset: this.account.asset.id
}
}).then(res => {
this.$message.success('删除成功')
this.iVisible = false
})
}
}
}

View File

@@ -8,6 +8,7 @@ import i18n from '@/i18n/i18n'
import { periodicMeta } from '@/components/const'
export default {
name: 'AccountDiscoverTaskCreate',
components: {
GenericCreateUpdatePage
},

View File

@@ -1,16 +1,26 @@
<template>
<GenericListTable :header-actions="headerActions" :table-config="tableConfig" />
<div>
<GenericListTable :header-actions="headerActions" :table-config="tableConfig" />
<Drawer v-if="showTableUpdateDrawer" :title="drawerTitle" @close-drawer="showTableUpdateDrawer = !showTableUpdateDrawer">
<component :is="currentTemplate" />
</Drawer>
</div>
</template>
<script>
import Drawer from '@/components/Drawer/index.vue'
import GenericListTable from '@/layout/components/GenericListTable/index.vue'
import { openTaskPage } from '@/utils/jms'
import { DetailFormatter } from '@/components/Table/TableFormatters'
export default {
name: 'AccountDiscoverTaskExecutionList',
components: {
GenericListTable
Drawer,
GenericListTable,
AccountDiscoverExecutionDetail: () => import('@/views/accounts/AccountDiscover/ExecutionDetail/index.vue')
},
props: {
object: {
@@ -21,6 +31,9 @@ export default {
},
data() {
return {
showTableUpdateDrawer: false,
currentTemplate: null,
drawerTitle: '',
tableConfig: {
url: '/api/v1/accounts/gather-account-executions/',
columns: [
@@ -41,7 +54,8 @@ export default {
getTitle: ({ row }) => row.snapshot.name,
getRoute: ({ row }) => ({
name: 'AccountDiscoverTaskDetail',
params: { id: row.automation }
params: { id: row.automation },
query: { type: 'pam' }
})
},
id: ({ row }) => row.automation
@@ -74,8 +88,10 @@ export default {
name: 'detail',
title: this.$t('Detail'),
type: 'info',
callback: function({ row }) {
return this.$router.push({ name: 'AccountDiscoverExecutionDetail', params: { id: row.id }})
callback: ({ row }) => {
this.handleDetailCallback(row)
// return this.$router.push({ name: 'AccountDiscoverExecutionDetail', params: { id: row.id }})
}
},
{
@@ -118,6 +134,16 @@ export default {
if (automation_id !== undefined) {
this.tableConfig.url = `${this.tableConfig.url}?automation_id=${automation_id}`
}
},
methods: {
handleDetailCallback(row) {
this.$route.params.id = row.id
this.$route.query.type = 'pam'
this.currentTemplate = 'AccountDiscoverExecutionDetail'
this.showTableUpdateDrawer = true
}
}
}
</script>

View File

@@ -7,6 +7,7 @@ import { GenericCreateUpdatePage } from '@/layout/components'
import { getChangeSecretFields } from '@/views/accounts/AccountChangeSecret/fields'
import { AssetSelect, AutomationParams } from '@/components'
import { periodicMeta } from '@/components/const'
import { TagInput } from '@/components/Form/FormFields'
export default {
name: 'AccountPushCreateUpdate',
@@ -92,6 +93,10 @@ export default {
readonly: true
}
},
accounts: {
component: TagInput,
helpText: this.$t('If the account already exists, the ciphertext of the account will be used for push; if the account does not exist, the account will be created according to the filled-in ciphertext and then pushed.')
},
params: {
component: AutomationParams,
label: this.$t('PushParams'),

View File

@@ -1,194 +0,0 @@
<template>
<div>
<RecordViewSecret
v-if="showViewSecretDialog"
:url="secretUrl"
:visible.sync="showViewSecretDialog"
/>
<GenericListTable :header-actions="headerActions" :table-config="tableConfig" />
</div>
</template>
<script>
import GenericListTable from '@/layout/components/GenericListTable/index.vue'
import { ActionsFormatter, DetailFormatter } from '@/components/Table/TableFormatters'
import { openTaskPage } from '@/utils/jms'
import RecordViewSecret from '@/components/Apps/ChangeSecret/RecordViewSecret.vue'
export default {
name: 'AccountPushExecutionTaskList',
components: {
RecordViewSecret,
GenericListTable
},
props: {
object: {
type: Object,
required: true,
default: () => ({})
}
},
data() {
const vm = this
return {
secretUrl: '',
showViewSecretDialog: false,
tableConfig: {
url: `/api/v1/accounts/push-account-records/?execution_id=${this.object.id}`,
columns: [
'asset', 'account', 'date_finished', 'is_success', 'error', 'actions'
],
columnsMeta: {
asset: {
label: this.$t('Asset'),
formatter: DetailFormatter,
formatterArgs: {
can: this.$hasPerm('assets.view_asset'),
getTitle({ row }) {
return row.asset.name
},
getRoute({ row }) {
return {
name: 'AssetDetail',
params: { id: row.asset.id }
}
}
}
},
account: {
label: this.$t('Username'),
formatter: DetailFormatter,
formatterArgs: {
can: this.$hasPerm('accounts.view_account'),
getTitle({ row }) {
return row.account.username
},
getRoute({ row }) {
return {
name: 'AssetAccountDetail',
params: { id: row.account.id }
}
}
}
},
is_success: {
label: this.$t('Success'),
formatter: (row) => {
if (row.status === 'pending') {
return <i Class='fa fa fa-spinner fa-spin'/>
}
if (row.is_success) {
return <i Class='fa fa-check text-primary'/>
}
return <i Class='fa fa-times text-danger'/>
}
},
actions: {
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: false,
hasDelete: false,
hasClone: false,
moreActionsTitle: this.$t('More'),
extraActions: [
{
name: 'View',
title: this.$t('View'),
type: 'primary',
callback: ({ row }) => {
// debugger
vm.secretUrl = `/api/v1/accounts/change-secret-records/${row.id}/secret/`
vm.showViewSecretDialog = false
setTimeout(() => {
vm.showViewSecretDialog = true
})
}
},
{
name: 'Retry',
title: this.$t('Retry'),
can: this.$hasPerm('accounts.add_changesecretexecution'),
type: 'primary',
callback: ({ row }) => {
this.$axios.post(
'/api/v1/accounts/push-account-records/execute/',
{ record_ids: [row.id] }
).then(res => {
openTaskPage(res['task'])
})
}
}
]
}
}
}
},
headerActions: {
hasSearch: true,
hasRefresh: true,
hasLeftActions: true,
hasRightActions: true,
hasExport: false,
hasImport: false,
hasCreate: false,
hasBulkDelete: false,
hasBulkUpdate: false,
searchConfig: {
exclude: ['id', 'status', 'execution'],
options: [
{
label: this.$t('Asset'),
value: 'asset_name'
},
{
label: this.$t('Accounts'),
value: 'account_username'
},
{
value: 'status',
label: this.$t('Status'),
type: 'choice',
children: [
{
default: true,
value: 'success',
label: this.$t('Success')
},
{
value: 'failed',
label: this.$t('Failed')
}
]
}
]
},
extraMoreActions: [
{
name: 'RetrySelected',
title: this.$t('RetrySelected'),
type: 'primary',
fa: 'fa-retweet',
can: ({ selectedRows }) => {
return selectedRows.length > 0 && vm.$hasPerm('accounts.add_changesecretexecution')
},
callback: function({ selectedRows }) {
const ids = selectedRows.map(v => {
return v.id
})
this.$axios.post(
'/api/v1/accounts/change-secret-records/execute/',
{ record_ids: ids }).then(res => {
openTaskPage(res['task'])
})
}.bind(this)
}
]
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -9,13 +9,11 @@
<script>
import { GenericDetailPage } from '@/layout/components'
import AccountPushExecutionInfo from './AccountPushExecutionInfo.vue'
import AccountPushExecutionTaskList from './AccountPushExecutionTaskList.vue'
export default {
components: {
GenericDetailPage,
AccountPushExecutionInfo,
AccountPushExecutionTaskList
AccountPushExecutionInfo
},
data() {
return {
@@ -32,11 +30,6 @@ export default {
title: this.$t('Basic'),
name: 'AccountPushExecutionInfo',
hidden: () => !this.$hasPerm('accounts.view_pushaccountexecution')
},
{
title: this.$t('TaskList'),
name: 'AccountPushExecutionTaskList',
hidden: () => !this.$hasPerm('accounts.view_changesecretrecord')
}
],
getTitle: this.getExecutionTitle

View File

@@ -22,7 +22,7 @@ export default {
data() {
return {
tableConfig: {
url: '/api/v1/accounts/push-account-executions/?' + `${this.object.id ? 'automation_id=' + this.object.id : ''}`,
url: '/api/v1/accounts/push-account-executions',
columns: [
'automation', 'push_user_name', 'asset_amount', 'node_amount', 'status',
'trigger', 'date_start', 'date_finished', 'actions'
@@ -94,7 +94,16 @@ export default {
type: 'info',
can: this.$hasPerm('accounts.view_pushaccountexecution'),
callback: function({ row }) {
return this.$router.push({ name: 'AccountCheckExecutionDetail', params: { id: row.id }})
return this.$router.push({ name: 'AccountPushExecutionDetail', params: { id: row.id }})
}
},
{
name: 'report',
title: this.$t('Report'),
type: 'success',
can: this.$hasPerm('accounts.view_pushaccountexecution'),
callback: function({ row }) {
window.open(`/api/v1/accounts/push-account-executions/${row.id}/report/`)
}
}
]

View File

@@ -1,20 +1,33 @@
<template>
<GenericListTable ref="listTable" :header-actions="headerActions" :table-config="tableConfig" />
<div>
<GenericListTable ref="listTable" :header-actions="headerActions" :table-config="tableConfig" />
<Drawer v-if="showTableUpdateDrawer" :title="drawerTitle" @close-drawer="showTableUpdateDrawer = !showTableUpdateDrawer">
<component :is="currentTemplate" />
</Drawer>
</div>
</template>
<script>
import { DetailFormatter } from '@/components/Table/TableFormatters'
import { ActionsFormatter, DetailFormatter } from '@/components/Table/TableFormatters'
import { openTaskPage } from '@/utils/jms'
import { GenericListTable } from '@/layout/components'
import Drawer from '@/components/Drawer/index.vue'
export default {
name: 'AccountPushList',
components: {
GenericListTable
Drawer,
GenericListTable,
AccountPushUpdate: () => import('@/views/accounts/AccountPush/AccountPushCreateUpdate.vue'),
AccountPushCreate: () => import('@/views/accounts/AccountPush/AccountPushCreateUpdate.vue')
},
data() {
const vm = this
return {
drawerTitle: '',
showTableUpdateDrawer: false,
currentTemplate: null,
tableConfig: {
url: '/api/v1/accounts/push-account-automations/',
columns: [
@@ -32,7 +45,12 @@ export default {
name: {
formatter: DetailFormatter,
formatterArgs: {
route: 'AccountCheckDetail'
isPam: true,
getRoute: ({ row }) => ({
name: 'AccountPushDetail',
params: { id: row.id },
query: { type: 'pam' }
})
}
},
accounts: {
@@ -81,7 +99,20 @@ export default {
}
},
actions: {
formatter: ActionsFormatter,
formatterArgs: {
isPam: true,
updateRoute: 'AccountPushUpdate',
onUpdate: ({ row }) => {
this.$route.params.id = row.id
// 解决表单详情中的跳转
this.$route.query.type = 'pam'
this.currentTemplate = 'AccountPushUpdate'
this.drawerTitle = this.$t('AccountPushUpdate')
this.showTableUpdateDrawer = true
},
extraActions: [
{
title: vm.$t('Execute'),
@@ -107,10 +138,16 @@ export default {
}
}
},
helpMsg: this.$t('WebHelpMessage'),
headerActions: {
hasRefresh: true,
hasExport: false,
hasImport: false
hasImport: false,
onCreate: () => {
this.currentTemplate = 'AccountPushCreate'
this.drawerTitle = this.$t('AccountPushCreate')
this.showTableUpdateDrawer = true
}
}
}
}

View File

@@ -1,100 +1,20 @@
<template>
<div>
<GenericListPage :header-actions="headerActions" :table-config="tableConfig" />
<ViewSecret
v-if="showViewSecretDialog"
:account="account"
:show-password-record="false"
:url="secretUrl"
type="template"
:visible.sync="showViewSecretDialog"
/>
</div>
<Page>
<AccountTemplateTable />
</Page>
</template>
<script>
import { GenericListPage } from '@/layout/components'
import { ActionsFormatter } from '@/components/Table/TableFormatters'
import ViewSecret from '@/components/Apps/AccountListTable/ViewSecret'
import AccountTemplateTable from './AccountTemplateTable.vue'
import { Page } from '@/layout/components'
export default {
name: 'AccountTemplateList',
components: {
GenericListPage,
ViewSecret
Page, AccountTemplateTable
},
data() {
const vm = this
return {
showViewSecretDialog: false,
account: {},
secretUrl: '',
tableConfig: {
url: '/api/v1/accounts/account-templates/',
columns: null,
columnsExclude: ['spec_info', 'password_rules', 'push_params'],
columnsShow: {
min: ['name', 'actions'],
default: ['name', 'username', 'secret_type', 'has_secret', 'privileged', 'actions']
},
columnsMeta: {
privileged: {
width: '120px',
formatterArgs: {
showText: false,
showFalse: false
}
},
has_secret: {
formatterArgs: {
showFalse: false,
showText: false
}
},
actions: {
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: true,
hasDelete: true,
hasClone: this.hasClone,
moreActionsTitle: this.$t('More'),
extraActions: [
{
name: 'View',
title: this.$t('View'),
can: this.$hasPerm('accounts.view_accounttemplatesecret'),
type: 'primary',
callback: ({ row }) => {
vm.secretUrl = `/api/v1/accounts/account-template-secrets/${row.id}/`
vm.account = row
vm.showViewSecretDialog = false
setTimeout(() => {
vm.showViewSecretDialog = true
})
}
}
]
}
}
}
},
headerActions: {
hasRefresh: true,
hasExport: this.$hasPerm('accounts.view_accounttemplatesecret'),
hasMoreActions: false,
hasLabelSearch: true,
exportOptions: {
url: '/api/v1/accounts/account-template-secrets/',
mfaVerifyRequired: true,
tips: this.$t('AccountExportTips')
},
createRoute: () => {
return {
name: 'AccountTemplateCreate'
}
}
}
}
return {}
}
}
</script>

View File

@@ -0,0 +1,100 @@
<template>
<div>
<ListTable :header-actions="headerActions" :table-config="tableConfig" />
<ViewSecret
v-if="showViewSecretDialog"
:account="account"
:show-password-record="false"
:url="secretUrl"
:visible.sync="showViewSecretDialog"
type="template"
/>
</div>
</template>
<script>
import { ActionsFormatter } from '@/components/Table/TableFormatters'
import ViewSecret from '@/components/Apps/AccountListTable/ViewSecret'
import ListTable from '@/components/Table/ListTable/index.vue'
export default {
name: 'AccountTemplateTable',
components: {
ListTable,
ViewSecret
},
data() {
const vm = this
return {
showViewSecretDialog: false,
account: {},
secretUrl: '',
tableConfig: {
url: '/api/v1/accounts/account-templates/',
columns: null,
columnsExclude: ['spec_info', 'password_rules', 'push_params'],
columnsShow: {
min: ['name', 'actions'],
default: ['name', 'username', 'secret_type', 'has_secret', 'privileged', 'actions']
},
columnsMeta: {
privileged: {
width: '120px',
formatterArgs: {
showText: false,
showFalse: false
}
},
has_secret: {
formatterArgs: {
showFalse: false,
showText: false
}
},
actions: {
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: true,
hasDelete: true,
hasClone: this.hasClone,
moreActionsTitle: this.$t('More'),
extraActions: [
{
name: 'View',
title: this.$t('View'),
can: this.$hasPerm('accounts.view_accounttemplatesecret'),
type: 'primary',
callback: ({ row }) => {
vm.secretUrl = `/api/v1/accounts/account-template-secrets/${row.id}/`
vm.account = row
vm.showViewSecretDialog = false
setTimeout(() => {
vm.showViewSecretDialog = true
})
}
}
]
}
}
}
},
headerActions: {
hasRefresh: true,
hasExport: this.$hasPerm('accounts.view_accounttemplatesecret'),
hasMoreActions: false,
hasLabelSearch: true,
exportOptions: {
url: '/api/v1/accounts/account-template-secrets/',
mfaVerifyRequired: true,
tips: this.$t('AccountExportTips')
},
createRoute: () => {
return {
name: 'AccountTemplateCreate'
}
}
}
}
}
}
</script>

View File

@@ -59,7 +59,6 @@
</div>
<el-drawer
direction="btt"
size="50%"
:with-header="false"
:append-to-body="true"

View File

@@ -54,7 +54,9 @@ export default {
},
columnsMeta: {
name: {
formatter: vm.$hasPerm('users.view_user') ? DetailFormatter : '',
formatter: (row) => {
return vm.$hasPerm('users.view_user') ? DetailFormatter : ''
},
formatterArgs: {
route: 'UserDetail'
}

View File

@@ -66,7 +66,7 @@ export default {
},
url: {
get() {
return `/api/v1/accounts/gathered-accounts/discover/?asset_id=${this.asset}`
return `/api/v1/accounts/gather-account-executions/adhoc/?asset_id=${this.asset}`
}
}
},
@@ -84,7 +84,6 @@ export default {
},
methods: {
onVisibleChange() {
},
onIframeLoad() {
this.loading = false

View File

@@ -1,7 +1,7 @@
<template>
<div>
<SmallCard ref="table" v-bind="$data" />
<CreateDialog v-if="visible" v-bind="providerConfig" :visible.sync="visible" />
<CreateDialog v-if="visible" :visible.sync="visible" v-bind="providerConfig" />
<Dialog
v-if="updateVisible"
:destroy-on-close="true"
@@ -40,7 +40,7 @@ import {
qcloud, qcloud_lighthouse, qingcloud_private, scp, ucloud, vmware, volcengine, zstack
} from '../const'
import CreateDialog from './components/CreateDialog.vue'
import SmallCard from '@/components/Table/CardTable/SmallCard.vue'
import SmallCard from '@/components/Table/CardTable/DataCardTable/index.vue'
import { ACCOUNT_PROVIDER_ATTRS_MAP } from '@/views/assets/Cloud/const'
import Dialog from '@/components/Dialog/index.vue'
import AssetPanel from './components/AssetPanel.vue'

View File

@@ -1,58 +1,49 @@
<template>
<Page>
<div v-if="this.$hasPerm('accounts.view_changesecretautomation')">
<div>
<SwitchDate class="switch-date" @change="onChange" />
</div>
<el-row type="flex">
<el-col :span="18">
<CardSummary class="card-summary" :days="days" />
</el-col>
<el-col :span="6">
<DataSummary class="data-summary" :days="days" style="margin-left: 1rem" />
</el-col>
</el-row>
<el-row type="flex" :gutter="24">
<el-col :span="14">
<FailedAccountSummary :days="days" class="failed-account-summary" />
</el-col>
<el-col :span="10">
<AccountSummary class="account-summary" :days="days" />
</el-col>
</el-row>
<div v-if="this.$hasPerm('accounts.view_changesecretautomation')">
<div>
<SwitchDate class="switch-date" @change="onChange" />
</div>
<Page403 v-else />
</Page>
<el-row type="flex">
<el-col :span="18">
<CardSummary :days="days" class="card-summary" />
</el-col>
<el-col :span="6">
<DataSummary :days="days" class="data-summary" style="margin-left: 1rem" />
</el-col>
</el-row>
<el-row :gutter="24" type="flex">
<el-col :span="14">
<FailedAccountSummary :days="days" class="failed-account-summary" />
</el-col>
<el-col :span="10">
<AccountSummary :days="days" class="account-summary" />
</el-col>
</el-row>
</div>
</template>
<script>
import { Page } from '@/layout/components'
import SwitchDate from '../components/SwitchDate'
import FailedAccountSummary from './FailedAccountSummary.vue'
import DataSummary from './DataSummary.vue'
import CardSummary from './CardSummary.vue'
import AccountSummary from './AccountSummary.vue'
import Page403 from '@/views/403'
export default {
components: {
AccountSummary,
Page,
SwitchDate,
DataSummary,
FailedAccountSummary,
CardSummary,
Page403
CardSummary
},
data() {
return {

View File

@@ -1,31 +1,20 @@
<template>
<div>
<AccountListTable ref="table" v-bind="tableConfig" :origin="'pam'" />
<Drawer v-if="showTableDetailDrawer" :title="drawerTitle" @close-drawer="showTableDetailDrawer = !showTableDetailDrawer">
<component :is="currentTemplate" />
</Drawer>
</div>
</template>
<script>
import Drawer from '@/components/Drawer/index.vue'
import AssetDetail from '@/views/assets/Asset/AssetDetail'
import { DetailFormatter } from '@/components/Table/TableFormatters'
import AccountListTable from '@/components/Apps/AccountListTable/AccountList.vue'
import AssetAccountDetail from '@/views/accounts/Account/AccountDetail/index.vue'
export default {
name: 'AssetAccountList',
components: {
Drawer,
AssetDetail,
AccountListTable,
AssetAccountDetail
AccountListTable
},
data() {
return {
showTableDetailDrawer: false,
currentTemplate: null,
drawerTitle: '',
tableConfig: {
url: '/api/v1/accounts/accounts/',
@@ -33,39 +22,28 @@ export default {
hasImport: true,
columnsMeta: {
name: {
formatter: (row) => {
return (
<span style={{ color: '#1c84c6', cursor: 'pointer' }} onClick={() => {
this.$route.params.id = row.id
this.currentTemplate = 'AssetAccountDetail'
this.showTableDetailDrawer = true
this.drawerTitle = this.$t('AssetAccountDetail')
}}>
{row.name}
</span>
)
}
formatterArgs: {
can: true,
isPam: true,
getRoute: ({ row }) => ({
name: 'AssetAccountList',
params: { id: row.id },
query: { type: 'pam' }
})
},
formatter: DetailFormatter
},
asset: {
formatter: (row) => {
return (
this.$hasPerm('assets.view_asset') ? (
<span
style={{ color: '#1c84c6', cursor: 'pointer' }}
onClick={() => {
this.$route.params.id = row.asset.id
this.currentTemplate = 'AssetDetail'
this.showTableDetailDrawer = true
this.drawerTitle = this.$t('AssetDetail')
}}
>
{row.name}
</span>
) : (
<span>{row.asset ? row.asset.name : ''}</span>
)
)
formatter: DetailFormatter,
formatterArgs: {
isPam: true,
can: this.$hasPerm('assets.view_asset'),
getTitle: ({ row }) => row.asset.name,
getRoute: ({ row }) => ({
name: 'AssetDetail',
params: { id: row.asset.id },
query: { type: 'pam', tab: 'Basic' }
})
}
},
connect: {
@@ -138,24 +116,4 @@ export default {
.asset-user-table {
padding-left: 20px;
}
::v-deep .page.tab-page {
.page-heading .el-row--flex {
flex-wrap: wrap;
.page-heading-left .el-button {
display: none;
}
}
//.page-content {
// height: 100% !important;
// overflow-x: unset;
//
// .tab-page-content {
// height: calc(100% - 120px);
// }
//}
}
</style>

View File

@@ -1,14 +1,6 @@
<template>
<div>
<GenericListPage :header-actions="headerActions" :table-config="tableConfig" />
<ViewSecret
v-if="showViewSecretDialog"
:account="account"
:show-password-record="false"
:url="secretUrl"
type="template"
:visible.sync="showViewSecretDialog"
/>
<AccountTemplateTable />
<Drawer v-if="showTableUpdateDrawer" :title="drawerTitle" @close-drawer="showTableUpdateDrawer = !showTableUpdateDrawer">
<component :is="currentTemplate" />
@@ -17,120 +9,24 @@
</template>
<script>
import { GenericListPage } from '@/layout/components'
import { ActionsFormatter } from '@/components/Table/TableFormatters'
import Drawer from '@/components/Drawer/index.vue'
import ViewSecret from '@/components/Apps/AccountListTable/ViewSecret'
import AccountTemplateDetail from '@/views/accounts/AccountTemplate/AccountTemplateDetail/index.vue'
import AccountTemplateUpdate from '@/views/accounts/AccountTemplate/AccountTemplateCreateUpdate.vue'
import AccountTemplateTable from '@/views/accounts/AccountTemplate/AccountTemplateTable.vue'
// import AccountTemplateDetail from '@/views/accounts/AccountTemplate/AccountTemplateDetail/index.vue'
export default {
name: 'AccountTemplateList',
components: {
AccountTemplateTable,
Drawer,
ViewSecret,
GenericListPage,
AccountTemplateUpdate,
AccountTemplateDetail
AccountTemplateUpdate: () => import('@/views/accounts/AccountTemplate/AccountTemplateCreateUpdate.vue')
},
data() {
const vm = this
return {
showViewSecretDialog: false,
showTableUpdateDrawer: false,
currentTemplate: null,
showTableUpdateDrawer: false,
drawerTitle: '',
account: {},
secretUrl: '',
tableConfig: {
url: '/api/v1/accounts/account-templates/',
columns: null,
columnsExclude: ['spec_info', 'password_rules', 'push_params'],
columnsShow: {
min: ['name', 'actions'],
default: ['name', 'username', 'secret_type', 'has_secret', 'privileged', 'actions']
},
columnsMeta: {
name: {
formatter: (row) => {
return (
<span style={{ color: '#1c84c6', cursor: 'pointer' }} onClick={() => {
this.$route.params.id = row.id
this.currentTemplate = 'AccountTemplateDetail'
this.showTableUpdateDrawer = true
this.drawerTitle = this.$t('AccountTemplate')
}}>
{row.name}
</span>
)
}
},
privileged: {
width: '120px',
formatterArgs: {
showText: false,
showFalse: false
}
},
has_secret: {
formatterArgs: {
showFalse: false,
showText: false
}
},
actions: {
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: true,
hasDelete: true,
hasClone: this.hasClone,
moreActionsTitle: this.$t('More'),
extraActions: [
{
name: 'View',
title: this.$t('View'),
can: this.$hasPerm('accounts.view_accounttemplatesecret'),
type: 'primary',
callback: ({ row }) => {
vm.secretUrl = `/api/v1/accounts/account-template-secrets/${row.id}/`
vm.account = row
vm.showViewSecretDialog = false
setTimeout(() => {
vm.showViewSecretDialog = true
})
}
}
],
onUpdate: ({ row }) => {
this.$route.params.id = row.id
this.currentTemplate = 'AccountTemplateUpdate'
this.drawerTitle = this.$t('UpdateAccountTemplate')
this.showTableUpdateDrawer = true
}
}
}
}
},
headerActions: {
hasRefresh: true,
hasExport: this.$hasPerm('accounts.view_accounttemplatesecret'),
hasMoreActions: false,
hasLabelSearch: true,
exportOptions: {
url: '/api/v1/accounts/account-template-secrets/',
mfaVerifyRequired: true,
tips: this.$t('AccountExportTips')
},
createRoute: () => {
return {
name: 'AccountTemplateCreate',
isPam: true
}
}
}
secretUrl: ''
}
}
}

View File

@@ -9,24 +9,21 @@
<script>
import Drawer from '@/components/Drawer/index.vue'
import AssetDetail from '@/views/assets/Asset/AssetDetail'
import BaseList from '@/views/assets/Asset/AssetList/components/BaseList'
import HostUpdate from '@/views/assets/Asset/AssetCreateUpdate/HostCreateUpdate.vue'
import { ActionsFormatter } from '@/components/Table/TableFormatters'
import { ActionsFormatter, DetailFormatter } from '@/components/Table/TableFormatters'
export default {
components: {
Drawer,
BaseList,
HostUpdate,
AssetDetail
HostUpdate: () => import('@/views/assets/Asset/AssetCreateUpdate/HostCreateUpdate.vue')
},
data() {
return {
drawerTitle: '',
currentTemplate: null,
showTableUpdateDrawer: false,
currentTemplate: null,
config: {
url: '/api/v1/assets/assets/',
category: 'all'
@@ -35,40 +32,35 @@ export default {
columnsExclude: ['date_verified'],
columnsMeta: {
name: {
formatter: (row) => {
return (
<span style={{ color: '#1c84c6', cursor: 'pointer' }} onClick={() => {
this.$route.params.id = row.id
this.$route.query.tab = 'Basic'
this.currentTemplate = 'AssetDetail'
this.showTableUpdateDrawer = true
this.drawerTitle = this.$t('AssetDetail')
}}>
{row.name}
</span>
)
}
formatterArgs: {
can: true,
isPam: true,
getRoute: ({ row }) => ({
name: 'AssetDetail',
params: { id: row.id },
query: { type: 'pam' }
})
},
formatter: DetailFormatter
},
accounts_amount: {
formatter: (row) => {
return (
<span style={{ color: '#1c84c6', cursor: 'pointer' }} onClick={() => {
this.$route.params.id = row.id
this.$route.query.tab = 'Account'
this.currentTemplate = 'AssetDetail'
this.showTableUpdateDrawer = true
this.drawerTitle = this.$t('AssetDetail')
}}>
{row.name}
</span>
)
}
formatterArgs: {
can: true,
isPam: true,
getTitle: ({ row }) => row.accounts_amount,
getRoute: ({ row }) => ({
name: 'AssetDetail',
params: { id: row.id },
query: { type: 'pam', tab: 'Account' }
})
},
formatter: DetailFormatter
},
actions: {
formatter: ActionsFormatter,
formatterArgs: {
isPam: true,
updateRoute: 'HostUpdate',
onUpdate: ({ row }) => {
this.$route.params.id = row.id
@@ -80,7 +72,6 @@ export default {
this.showTableUpdateDrawer = true
}
}
}
}
}

View File

@@ -37,7 +37,7 @@ export default {
{
name: 'AccountTemplateList',
title: this.$t('AccountTemplate'),
icon: 'fa-copy',
icon: 'template',
component: () => import('@/views/pam/Account/AccountTemplate.vue')
}
]

View File

@@ -1,51 +1,118 @@
<template>
<div>
<SmallCard ref="table" v-bind="$data" />
<ListTable ref="table" v-bind="$data" />
<Drawer v-if="showTableUpdateDrawer" :title="drawerTitle" @close-drawer="showTableUpdateDrawer = !showTableUpdateDrawer">
<component :is="currentTemplate" />
</Drawer>
</div>
</template>
<script type="text/jsx">
import SmallCard from '@/components/Table/CardTable/SmallCard.vue'
import { toSafeLocalDateStr } from '@/utils/time'
import Drawer from '@/components/Drawer/index.vue'
import ListTable from '@/components/Table/ListTable/index.vue'
import CopyableFormatter from '@/components/Table/TableFormatters/CopyableFormatter.vue'
import { ActionsFormatter, DetailFormatter } from '@/components/Table/TableFormatters'
export default {
name: 'CloudAccountList',
components: {
SmallCard
Drawer,
ListTable,
IntegrationApplicationUpdate: () => import('@/views/pam/Integration/ApplicationCreateUpdate.vue'),
IntegrationApplicationCreate: () => import('@/views/pam/Integration/ApplicationCreateUpdate.vue')
},
data() {
const vm = this
return {
drawerTitle: '',
showTableUpdateDrawer: false,
currentTemplate: null,
tableConfig: {
url: '/api/v1/accounts/integration-applications/',
columnsMeta: {
id: {
width: '300px',
formatter: CopyableFormatter
},
logo: {
width: '80px',
formatter: (row) => {
return (
<img src={row.logo} alt={row.name}
style='width: 40px; height: 40px; border-radius: 50%;'
/>
)
}
},
accounts: {
width: '100px',
formatter: (row) => {
return row.accounts_amount
}
},
name: {
formatterArgs: {
isPam: true,
getRoute: ({ row }) => ({
name: 'IntegrationApplicationDetail',
params: { id: row.id },
query: { type: 'pam' }
})
},
formatter: DetailFormatter
},
secret: {
label: this.$t('Secret'),
formatter: CopyableFormatter,
formatterArgs: {
shadow: true,
getText: async function({ row }) {
const app = await vm.$axios.get(`/api/v1/accounts/integration-applications/${row.id}/secret/`)
return app.secret
}
}
},
actions: {
formatter: ActionsFormatter,
formatterArgs: {
isPam: true,
updateRoute: 'IntegrationApplicationUpdate',
onUpdate: ({ row }) => {
this.$route.params.id = row.id
// 解决表单详情中的跳转
this.$route.query.type = 'pam'
this.currentTemplate = 'IntegrationApplicationUpdate'
this.drawerTitle = this.$t('IntegrationApplicationUpdate')
this.showTableUpdateDrawer = true
}
}
}
},
columnsExtra: ['secret'],
columnsShow: {
default: [
'logo', 'id', 'secret', 'name', 'accounts', 'date_last_used', 'active'
]
},
permissions: { app: 'accounts', resource: 'integrationapplication' }
},
headerActions: {
hasImport: false,
hasExport: false,
hasColumnSetting: false,
hasMoreActions: false,
searchConfig: {
getUrlQuery: false
}
},
subComponentProps: {
getImage: (obj) => {
return obj.logo
},
getInfos: (obj) => {
return [
{ title: `${this.$tc('RelevantApp')} ID`, content: obj.id },
{ title: this.$tc('DataLastUsed'), content: toSafeLocalDateStr(obj.date_last_used) },
{ title: this.$tc('AccountAmount'), content: obj.accounts_amount },
{ title: this.$tc('Comment'), content: obj.comment || this.$tc('Nothing') }
]
},
handleUpdate: (obj) => {
this.$router.push({ name: 'IntegrationApplicationUpdate', params: { id: obj.id }})
onCreate: () => {
this.currentTemplate = 'IntegrationApplicationCreate'
this.drawerTitle = this.$t('IntegrationApplicationCreate')
this.showTableUpdateDrawer = true
}
}
}
},
methods: {}
}
}
</script>

View File

@@ -3,25 +3,36 @@
<AssetTreeTable
ref="AssetTreeTable"
:header-actions="headerActions"
:quick-filters="quickFilters"
:quick-summary="quickSummary"
:table-config="tableConfig"
:tree-setting="treeSetting"
/>
<BatchResolveDialog :visible.sync="batchResolveDialog.visible" v-bind="batchResolveDialog" />
<RiskScanDialog v-if="detectDialog.visible" :asset="detectDialog.asset" :visible.sync="detectDialog.visible" />
</div>
</template>
<script>
import AssetTreeTable from '@/components/Apps/AssetTreeTable/index.vue'
import RiskHandleFormatter from './RiskHandlerFormatter/index.vue'
import BatchResolveDialog from '@/views/pam/RiskDetect/RiskHandlerFormatter/BatchResolveDialog.vue'
import RiskScanDialog from './RiskScanDialog.vue'
export default {
components: {
RiskScanDialog,
BatchResolveDialog,
AssetTreeTable
},
data() {
const vm = this
return {
gatherAccounts: [],
scanVisible: false,
detectDialog: {
visible: false,
asset: ''
},
treeSetting: {
showMenu: true,
showRefresh: true,
@@ -37,42 +48,39 @@ export default {
id: 'check',
name: this.$t('Check'),
icon: 'scan',
callback: () => {}
callback: (node) => {
vm.detectDialog.asset = node.id
setTimeout(() => {
vm.detectDialog.visible = true
}, 100)
}
}
]
},
quickFilters: [
quickSummary: [
{
label: '快速过滤',
options: [
{
label: '未同步到资产',
value: ''
},
{
label: this.$t('最近一个月'),
value: ''
}
]
title: '最近一周发现',
filter: {
'days': '7'
}
},
{
label: this.$t('最近发现'),
options: [
{
label: '最近一天 (20)',
value: ''
},
{
label: '最近一周 (300)',
value: ''
},
{
label: '最近一个月 (600)',
value: ''
}
]
title: '最近一月发现',
filter: {
'days': '30'
}
},
{
title: '待处理',
filter: {
status: '0'
}
}
],
batchResolveDialog: {
visible: false,
risks: []
},
tableConfig: {
url: '/api/v1/accounts/account-risks/',
columns: [
@@ -115,7 +123,21 @@ export default {
}
},
headerActions: {
hasLeftActions: false
hasCreate: false,
extraMoreActions: [
{
name: 'resolveSelected',
title: this.$t('ResolveSelected'),
icon: 'el-icon-check',
callback: function({ selectedRows }) {
vm.batchResolveDialog.risks = selectedRows
vm.batchResolveDialog.visible = true
},
can: function({ selectedRows }) {
return selectedRows.length > 0
}
}
]
}
}
},

View File

@@ -0,0 +1,174 @@
<template>
<Dialog :destroy-on-close="true" :show-buttons="false" :title="$tc('ResolveSelected')" :visible.sync="iVisible">
<div v-if="iVisible">
<el-form class="el-form">
<el-form-item class="risk-select" prop="selected">
<el-select v-model="riskSelected" :placeholder="$t('Select risk')">
<el-option
v-for="item in riskTypes"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<HandleDropdown
:cell-value="fakeCell"
:changed="changed"
:row="fakeRow"
:rows="tableConfig.totalData"
:value="1"
class="risk-handler"
@processDone="handleProcessDone"
/>
</el-form>
<DataTable ref="table" :config="tableConfig" />
</div>
</Dialog>
</template>
<script>
import Dialog from '@/components/Dialog/index.vue'
import DataTable from '@/components/Table/DataTable/index.vue'
import HandleDropdown from './index.vue'
export default {
name: 'BatchResolveDialog',
components: { DataTable, Dialog, HandleDropdown },
props: {
visible: {
type: Boolean,
default: false
},
risks: {
type: Array,
default: () => []
}
},
data() {
return {
changed: false,
riskSelected: '',
fakeRow: {
id: '',
risk: {}
},
fakeCell: {
value: '0',
label: this.$t('Pending')
},
tableConfig: {
totalData: [],
columns: [
{
prop: 'asset',
label: this.$t('Asset'),
formatter: (row) => row.asset.name
},
{
prop: 'username',
label: this.$t('Username')
},
{
prop: 'risk',
label: this.$t('Risk'),
formatter: (row) => row.risk.label
},
{
prop: 'status',
label: this.$t('Status'),
formatter: (row) => row.status.label
}
]
}
}
},
computed: {
iVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
},
riskTypes() {
const types = {}
for (const item of this.unconfirmedRisks) {
if (!types[item.risk.value]) {
types[item.risk.value] = item.risk.label
}
}
return Object.keys(types).map(key => ({ value: key, label: types[key] }))
},
unconfirmedRisks() {
return this.risks.filter(item => item.status.value === '0')
},
dataTable() {
return this.$refs.table.$refs.table
},
pageSize() {
return this.dataTable.size
},
dataTableCurrentPage() {
return this.dataTable.page
}
},
watch: {
riskSelected(val) {
if (val) {
this.tableConfig.totalData = this.unconfirmedRisks.filter(item => item.risk.value === this.riskSelected)
} else {
this.tableConfig.totalData = this.unconfirmedRisks.filter(item => item.status.value === '0')
}
this.fakeRow.risk = {
value: this.riskSelected
}
this.changed = true
setTimeout(() => {
this.changed = false
}, 200)
}
},
mounted() {
this.tableConfig.totalData = this.unconfirmedRisks
},
methods: {
handleProcessDone({ index, row }) {
const page = this.dataTable.page
const size = this.dataTable.size
const offset = Math.floor(index / size)
if (page < offset + 1) {
this.dataTable.gotoNextPage()
}
}
}
}
</script>
<style lang="scss" scoped>
.el-form {
::v-deep .el-form-item {
margin-bottom: 5px;
}
.risk-select {
display: inline-block;
::v-deep .el-form-item__content {
width: 100%;
}
}
.risk-handler {
margin-left: 10px;
::v-deep button {
padding: 8px;
}
}
}
</style>

View File

@@ -9,18 +9,21 @@
@open="handleOpen"
>
<div class="drawer-body">
<el-timeline :reverse="true">
<el-timeline-item
v-for="detail in row.details"
:key="detail.datetime"
:icon="getDetailIcon(detail)"
:timestamp="formatTimestamp(detail.datetime)"
:type="getDetailType(detail)"
placement="top"
>
<span v-html="handleDetail(row, detail)" />
</el-timeline-item>
</el-timeline>
<div v-for="r in iRows" :key="r.id">
<div class="host-username">{{ r.asset ? r.asset.name : r }} - {{ r.username }}</div>
<el-timeline :reverse="true">
<el-timeline-item
v-for="detail in r.details"
:key="detail.datetime"
:icon="getDetailIcon(detail)"
:timestamp="formatTimestamp(detail.datetime)"
:type="getDetailType(detail)"
placement="top"
>
<span v-html="handleDetail(r, detail)" />
</el-timeline-item>
</el-timeline>
</div>
</div>
<div v-if="showButtons" class="drawer-footer">
<span class="buttons">
@@ -49,6 +52,10 @@ export default {
showButtons: {
type: Boolean,
default: true
},
rows: {
type: Array,
default: () => []
}
},
data() {
@@ -70,6 +77,13 @@ export default {
acc[cur.name] = cur
return acc
}, {})
},
iRows() {
if (this.rows.length === 0) {
return [this.row]
} else {
return this.rows
}
}
},
mounted() {
@@ -142,9 +156,18 @@ ${detail.diff}
height: calc(100% - 40px - 40px);
overflow: auto;
::v-deep .el-drawer__body {
overflow: auto;
}
::v-deep pre {
overflow: auto;
}
.host-username {
margin-left: 40px;
margin-bottom: 10px;
}
}
.drawer-footer {

View File

@@ -1,11 +1,11 @@
import i18n from '@/i18n/i18n'
export const riskActions = [
{
name: 'disable_remote',
label: i18n.t('Disable remote'),
has: ['long_time_no_login', 'new_found']
},
// {
// name: 'disable_remote',
// label: i18n.t('Disable remote'),
// has: ['long_time_no_login', 'new_found']
// },
{
name: 'delete_remote',
label: i18n.t('Delete Account'),
@@ -21,6 +21,22 @@ export const riskActions = [
label: i18n.t('Add to Account'),
has: ['new_found'],
disabled: async function() {
if (!this.row.username) {
return false
}
const url = `/api/v1/accounts/accounts/?username=${this.row.username}&asset=${this.row.asset.id}`
const data = await this.$axios.get(url)
return data.length > 0
}
},
{
name: 'add_account_after_change_password',
label: i18n.t('Add account after change password'),
has: ['new_found'],
disabled: async function() {
if (!this.row.username) {
return false
}
const url = `/api/v1/accounts/accounts/?username=${this.row.username}&asset=${this.row.asset.id}`
const data = await this.$axios.get(url)
return data.length > 0

View File

@@ -29,7 +29,7 @@
{{ iLabel }}
</el-button>
</el-tooltip>
<ReviewDraw :row="row" :show-buttons="reviewButtons" :visible.sync="reviewDrawer" />
<ReviewDraw :row="row" :rows="rows" :show-buttons="reviewButtons" :visible.sync="reviewDrawer" />
<ProcessingDialog :visible="processing" />
</span>
</template>
@@ -38,6 +38,7 @@ import BaseFormatter from '@/components/Table/TableFormatters/base.vue'
import ReviewDraw from '@/views/pam/RiskDetect/RiskHandlerFormatter/ReviewDraw.vue'
import ProcessingDialog from '@/components/Dialog/ProcessingDialog.vue'
import { riskActions } from './const'
import { sleep } from '@/utils/time'
export default {
name: 'RiskSummaryFormatter',
@@ -47,6 +48,14 @@ export default {
formatterArgsDefault: {
type: Object,
default: () => ({})
},
changed: {
type: Boolean,
default: false
},
rows: {
type: Array,
default: () => []
}
},
data() {
@@ -77,6 +86,11 @@ export default {
}
}
},
watch: {
changed() {
this.handleVisibleChange(true)
}
},
methods: {
showDetail() {
this.reviewButtons = false
@@ -87,27 +101,32 @@ export default {
this.reviewDrawer = true
},
async handleCommon(cmd) {
const data = {
username: this.row.username,
asset: this.row.asset.id,
risk: this.row.risk.value,
action: cmd
let rows = this.rows
if (this.rows.length === 0) {
rows = [this.row]
this.processing = true
}
this.processing = true
this.$axios.post(`/api/v1/accounts/account-risks/handle/`, data).then(() => {
if (cmd !== 'ignore') {
this.row.status = { value: '1', label: this.$t('Confirmed') }
} else {
this.row.status = { value: '2', label: this.$t('Ignored') }
for (const [i, row] of Object.entries(rows)) {
const data = {
username: row.username,
asset: row.asset.id,
risk: row.risk.value,
action: cmd
}
}).finally(() => {
setTimeout(() => {
this.processing = false
this.$axios.get(`/api/v1/accounts/account-risks/${this.row.id}/`).then((res) => {
Object.assign(this.row, res)
})
}, 500)
})
row.status = { value: '3', label: this.$t('Processing') }
await this.$axios.post(`/api/v1/accounts/account-risks/handle/`, data)
await sleep(100)
if (cmd !== 'ignore') {
row.status = { value: '1', label: this.$t('Confirmed') }
} else {
row.status = { value: '2', label: this.$t('Ignored') }
}
this.$emit('processDone', { index: i, row })
}
setTimeout(() => {
this.processing = false
}, 500)
},
handleRisk(cmd) {
if (cmd === 'review') {
@@ -125,9 +144,9 @@ export default {
},
async handleVisibleChange(visible) {
if (!visible) {
return
return false
}
if (this.actions.length === 0) {
if (this.actions.length === 0 || this.changed === true) {
this.actions = await this.getActions()
}
return this.actions.length > 0

View File

@@ -0,0 +1,62 @@
<template>
<Dialog
v-if="iVisible"
:destroy-on-close="true"
:show-cancel="false"
:show-confirm="false"
:title="$tc('Detecting')"
:visible.sync="iVisible"
top="35vh"
width="80%"
@close="loading=true"
>
<span v-if="loading" v-loading="loading" class="loading" />
<iframe :src="url" frameborder="0" @load="onIframeLoad" />
</Dialog>
</template>
<script>
import Dialog from '@/components/Dialog'
export default {
name: 'RiskScanDialog',
components: { Dialog },
props: {
visible: {
type: Boolean,
default: false
},
asset: {
type: String,
default: ''
}
},
data() {
return {
loading: true,
url: `/api/v1/accounts/check-account-executions/adhoc/?asset_id=${this.asset}`
}
},
computed: {
iVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
},
methods: {
onIframeLoad() {
this.loading = false
}
}
}
</script>
<style lang="scss" scoped>
iframe {
width: 100%;
height: 500px;
}
</style>

View File

@@ -21,7 +21,6 @@ export default {
return {
group: { name: '', comment: '', users: [] },
config: {
url: '/api/v1/users/groups',
activeMenu: 'GroupInfo',
submenu: [
{

View File

@@ -1,5 +1,11 @@
<template>
<GenericCreateUpdatePage v-if="!loading" class="user-create-update" v-bind="$data" @getObjectDone="afterGetUser" />
<GenericCreateUpdatePage
v-if="!loading"
class="user-create-update"
v-bind="$data"
@getObjectDone="afterGetUser"
v-on="$listeners"
/>
</template>
<script>
@@ -164,14 +170,6 @@ export default {
el: {}
}
},
submitMethod() {
const params = this.$route.params
if (params.id) {
return 'put'
} else {
return 'post'
}
},
afterGetFormValue(obj) {
if (obj?.id) {
obj.org_roles = obj.org_roles?.map(({ id }) => id)

View File

@@ -263,9 +263,9 @@ export default {
</script>
<style scoped>
.mfa-setting ::v-deep .el-slider__runway {
margin-top: 0;
margin-bottom: 0;
}
.mfa-setting ::v-deep .el-slider__runway {
margin-top: 0;
margin-bottom: 0;
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div>
<GenericListPage
<GenericListDrawerPage
ref="GenericListPage"
:header-actions="headerActions"
:quick-filters="quickFilters"
@@ -19,7 +19,8 @@
<script>
import { mapGetters } from 'vuex'
import { GenericListPage, GenericUpdateFormDialog } from '@/layout/components'
import { GenericUpdateFormDialog } from '@/layout/components'
import GenericListDrawerPage from '@/layout/components/GenericListDrawerPage/index.vue'
import { createSourceIdCache } from '@/api/common'
import { getDayFuture } from '@/utils/time'
import InviteUsersDialog from './components/InviteUsersDialog'
@@ -28,7 +29,7 @@ import AmountFormatter from '@/components/Table/TableFormatters/AmountFormatter.
export default {
components: {
InviteUsersDialog,
GenericListPage,
GenericListDrawerPage,
GenericUpdateFormDialog
},
data() {
@@ -43,6 +44,8 @@ export default {
return !vm.currentOrgIsRoot
}
return {
createDrawer: () => import('@/views/users/User/UserCreateUpdate.vue'),
detailDrawer: () => import('@/views/users/User/UserDetail/index.vue'),
quickFilters: [
{
label: '快速筛选',

177
yarn.lock
View File

@@ -1086,6 +1086,11 @@
dependencies:
katex "^0.16.0"
"@trysound/sax@0.2.0":
version "0.2.0"
resolved "https://registry.npmmirror.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
"@types/glob@^7.1.1":
version "7.2.0"
resolved "https://registry.npmmirror.com/@types/glob/-/glob-7.2.0.tgz"
@@ -3339,6 +3344,11 @@ commander@^6.1.0, commander@^6.2.0:
resolved "https://registry.npmmirror.com/commander/-/commander-6.2.1.tgz"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
commander@^7.2.0:
version "7.2.0"
resolved "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
commander@^8.3.0:
version "8.3.0"
resolved "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz"
@@ -3738,6 +3748,17 @@ css-select@^4.1.3:
domutils "^2.8.0"
nth-check "^2.0.1"
css-select@^5.1.0:
version "5.1.0"
resolved "https://registry.npmmirror.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6"
integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==
dependencies:
boolbase "^1.0.0"
css-what "^6.1.0"
domhandler "^5.0.2"
domutils "^3.0.1"
nth-check "^2.0.1"
css-selector-tokenizer@^0.7.0:
version "0.7.3"
resolved "https://registry.npmmirror.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz"
@@ -3746,22 +3767,6 @@ css-selector-tokenizer@^0.7.0:
cssesc "^3.0.0"
fastparse "^1.1.2"
css-tree@1.0.0-alpha.28:
version "1.0.0-alpha.28"
resolved "https://registry.npmmirror.com/css-tree/-/css-tree-1.0.0-alpha.28.tgz"
integrity sha512-joNNW1gCp3qFFzj4St6zk+Wh/NBv0vM5YbEreZk0SD4S23S+1xBKb6cLDg2uj4P4k/GUMlIm6cKIDqIG+vdt0w==
dependencies:
mdn-data "~1.1.0"
source-map "^0.5.3"
css-tree@1.0.0-alpha.29:
version "1.0.0-alpha.29"
resolved "https://registry.npmmirror.com/css-tree/-/css-tree-1.0.0-alpha.29.tgz"
integrity sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==
dependencies:
mdn-data "~1.1.0"
source-map "^0.5.3"
css-tree@1.0.0-alpha.37:
version "1.0.0-alpha.37"
resolved "https://registry.npmmirror.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz"
@@ -3778,17 +3783,28 @@ css-tree@^1.1.2:
mdn-data "2.0.14"
source-map "^0.6.1"
css-url-regex@^1.1.0:
version "1.1.0"
resolved "https://registry.npmmirror.com/css-url-regex/-/css-url-regex-1.1.0.tgz"
integrity sha512-hLKuvifwoKvwqpctblTp0BovBuOXzxof8JgkA8zeqxxL+vcynHQjtIqqlFfQI1gEAZAjbqKm9gFTa88fxTAX4g==
css-tree@^2.3.1:
version "2.3.1"
resolved "https://registry.npmmirror.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20"
integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==
dependencies:
mdn-data "2.0.30"
source-map-js "^1.0.1"
css-tree@~2.2.0:
version "2.2.1"
resolved "https://registry.npmmirror.com/css-tree/-/css-tree-2.2.1.tgz#36115d382d60afd271e377f9c5f67d02bd48c032"
integrity sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==
dependencies:
mdn-data "2.0.28"
source-map-js "^1.0.1"
css-what@^3.2.1:
version "3.4.2"
resolved "https://registry.npmmirror.com/css-what/-/css-what-3.4.2.tgz"
integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==
css-what@^6.0.1:
css-what@^6.0.1, css-what@^6.1.0:
version "6.1.0"
resolved "https://registry.npmmirror.com/css-what/-/css-what-6.1.0.tgz"
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
@@ -3886,13 +3902,6 @@ cssnano@^4.0.0, cssnano@^4.1.10:
is-resolvable "^1.0.0"
postcss "^7.0.0"
csso@^3.5.1:
version "3.5.1"
resolved "https://registry.npmmirror.com/csso/-/csso-3.5.1.tgz"
integrity sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==
dependencies:
css-tree "1.0.0-alpha.29"
csso@^4.0.2:
version "4.2.0"
resolved "https://registry.npmmirror.com/csso/-/csso-4.2.0.tgz"
@@ -3900,6 +3909,13 @@ csso@^4.0.2:
dependencies:
css-tree "^1.1.2"
csso@^5.0.5:
version "5.0.5"
resolved "https://registry.npmmirror.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6"
integrity sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==
dependencies:
css-tree "~2.2.0"
cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
version "0.3.8"
resolved "https://registry.npmmirror.com/cssom/-/cssom-0.3.8.tgz"
@@ -4295,6 +4311,15 @@ dom-serializer@^1.0.1:
domhandler "^4.2.0"
entities "^2.0.0"
dom-serializer@^2.0.0:
version "2.0.0"
resolved "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"
integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==
dependencies:
domelementtype "^2.3.0"
domhandler "^5.0.2"
entities "^4.2.0"
domain-browser@^1.1.1:
version "1.2.0"
resolved "https://registry.npmmirror.com/domain-browser/-/domain-browser-1.2.0.tgz"
@@ -4305,7 +4330,7 @@ domelementtype@1, domelementtype@^1.3.1:
resolved "https://registry.npmmirror.com/domelementtype/-/domelementtype-1.3.1.tgz"
integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
domelementtype@^2.0.1, domelementtype@^2.2.0:
domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0:
version "2.3.0"
resolved "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz"
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
@@ -4331,6 +4356,13 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.2.2, domhandler@^4.3.1:
dependencies:
domelementtype "^2.2.0"
domhandler@^5.0.2, domhandler@^5.0.3:
version "5.0.3"
resolved "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31"
integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==
dependencies:
domelementtype "^2.3.0"
dompurify@^3.1.6:
version "3.1.6"
resolved "https://registry.npmmirror.com/dompurify/-/dompurify-3.1.6.tgz"
@@ -4358,6 +4390,15 @@ domutils@^2.5.2, domutils@^2.8.0:
domelementtype "^2.2.0"
domhandler "^4.2.0"
domutils@^3.0.1:
version "3.1.0"
resolved "https://registry.npmmirror.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e"
integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==
dependencies:
dom-serializer "^2.0.0"
domelementtype "^2.3.0"
domhandler "^5.0.3"
dot-object@^2.1.4:
version "2.1.4"
resolved "https://registry.npmmirror.com/dot-object/-/dot-object-2.1.4.tgz"
@@ -4554,6 +4595,11 @@ entities@^3.0.1, entities@~3.0.1:
resolved "https://registry.npmmirror.com/entities/-/entities-3.0.1.tgz"
integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==
entities@^4.2.0:
version "4.5.0"
resolved "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
env-paths@^2.2.0:
version "2.2.1"
resolved "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz"
@@ -8509,16 +8555,21 @@ mdn-data@2.0.14:
resolved "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.14.tgz"
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
mdn-data@2.0.28:
version "2.0.28"
resolved "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba"
integrity sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==
mdn-data@2.0.30:
version "2.0.30"
resolved "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc"
integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==
mdn-data@2.0.4:
version "2.0.4"
resolved "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.4.tgz"
integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==
mdn-data@~1.1.0:
version "1.1.4"
resolved "https://registry.npmmirror.com/mdn-data/-/mdn-data-1.1.4.tgz"
integrity sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==
mdurl@^1.0.1, mdurl@~1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz"
@@ -9227,7 +9278,7 @@ npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1:
resolved "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz"
integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==
npm-package-arg@^8.0.0, npm-package-arg@^8.0.1, npm-package-arg@^8.1.0, npm-package-arg@^8.1.2, npm-package-arg@^8.1.4, npm-package-arg@^8.1.5:
npm-package-arg@^8.0.0, npm-package-arg@^8.0.1, npm-package-arg@^8.1.0, npm-package-arg@^8.1.1, npm-package-arg@^8.1.2, npm-package-arg@^8.1.5:
version "8.1.5"
resolved "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.5.tgz"
integrity sha512-LhgZrg0n0VgvzVdSm1oiZworPbTxYHUJCgtsJW8mGvlDpxTM1vSJc3m5QZeUkhAHIzbz3VCHd/R4osi1L1Tg/Q==
@@ -9733,7 +9784,7 @@ p-try@^2.0.0:
resolved "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
pacote@^11.1.11, pacote@^11.2.6, pacote@^11.3.1, pacote@^11.3.4, pacote@^11.3.5:
pacote@^11.1.11, pacote@^11.2.6, pacote@^11.3.0, pacote@^11.3.1, pacote@^11.3.5:
version "11.3.5"
resolved "https://registry.npmjs.org/pacote/-/pacote-11.3.5.tgz"
integrity sha512-fT375Yczn4zi+6Hkk2TBe1x1sP8FgFsEIZ2/iWaXY2r/NkhDJfxbcn5paz1+RTFCyNf+dPnaoBDJoAxXSU8Bkg==
@@ -11812,6 +11863,11 @@ source-list-map@^2.0.0:
resolved "https://registry.npmmirror.com/source-list-map/-/source-list-map-2.0.1.tgz"
integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
source-map-js@^1.0.1:
version "1.2.1"
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz"
@@ -12060,7 +12116,7 @@ string-length@^2.0.0:
astral-regex "^1.0.0"
strip-ansi "^4.0.0"
"string-width-cjs@npm:string-width@^4.2.0":
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -12078,15 +12134,6 @@ string-width@^1.0.1:
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
version "2.1.1"
resolved "https://registry.npmmirror.com/string-width/-/string-width-2.1.1.tgz"
@@ -12187,7 +12234,7 @@ stringify-package@^1.0.1:
resolved "https://registry.npmjs.org/stringify-package/-/stringify-package-1.0.1.tgz"
integrity sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -12215,13 +12262,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1, strip-ansi@^7.1.0:
version "7.1.0"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz"
@@ -12363,25 +12403,18 @@ svg-tags@^1.0.0:
resolved "https://registry.npmmirror.com/svg-tags/-/svg-tags-1.0.0.tgz"
integrity sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==
svgo@1.2.2:
version "1.2.2"
resolved "https://registry.npmmirror.com/svgo/-/svgo-1.2.2.tgz"
integrity sha512-rAfulcwp2D9jjdGu+0CuqlrAUin6bBWrpoqXWwKDZZZJfXcUXQSxLJOFJCQCSA0x0pP2U0TxSlJu2ROq5Bq6qA==
svgo@1.2.4:
version "3.3.2"
resolved "https://registry.npmmirror.com/svgo/-/svgo-3.3.2.tgz#ad58002652dffbb5986fc9716afe52d869ecbda8"
integrity sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==
dependencies:
chalk "^2.4.1"
coa "^2.0.2"
css-select "^2.0.0"
css-select-base-adapter "^0.1.1"
css-tree "1.0.0-alpha.28"
css-url-regex "^1.1.0"
csso "^3.5.1"
js-yaml "^3.13.1"
mkdirp "~0.5.1"
object.values "^1.1.0"
sax "~1.2.4"
stable "^0.1.8"
unquote "~1.1.1"
util.promisify "~1.0.0"
"@trysound/sax" "0.2.0"
commander "^7.2.0"
css-select "^5.1.0"
css-tree "^2.3.1"
css-what "^6.1.0"
csso "^5.0.5"
picocolors "^1.0.0"
svgo@^1.0.0:
version "1.3.2"