Compare commits

...

25 Commits

Author SHA1 Message Date
ibuler
200b83c105 perf: table search two times, one init one search 2024-07-10 19:50:10 +08:00
wangruidong
d22079446f perf: profile improvement mfa disabled 2024-07-10 14:21:06 +08:00
ibuler
d57b99b00c perf: phone code api 2024-07-09 19:24:07 +08:00
ZhaoJiSen
e7321356af Merge pull request #4197 from jumpserver/pr@dev@fix_card_style
style: Fine-tuning styles
2024-07-09 18:58:39 +08:00
zhaojisen
a0edc2c527 style: Fine-tuning styles 2024-07-09 18:54:45 +08:00
ibuler
10ebcfd64d perf: tags display inline 2024-07-09 16:08:55 +08:00
ibuler
a3fedb9697 perf: license display for community edition 2024-07-09 16:03:18 +08:00
吴小白
1eb8a18c66 fix: FromAsCasing keywords 2024-07-09 16:02:18 +08:00
wangruidong
f491c57c34 perf: display newly invited users at the top of the list 2024-07-09 15:54:31 +08:00
ZhaoJiSen
50b7f54652 Merge pull request #4193 from jumpserver/pr@dev@fix_menu_heighlight
fixed: Missing menu highlight in command group detail page
2024-07-09 11:12:00 +08:00
zhaojisen
5bf298a5bf fixed: Missing menu highlight in command group detail page 2024-07-09 11:07:01 +08:00
wangruidong
124ff9a8c2 fix: foot content allow blank 2024-07-08 19:24:34 +08:00
ZhaoJiSen
387ab4f1b3 Merge pull request #4186 from jumpserver/pr@dev@fix_item_empty
fixed: Fixed an issue where the field is empty
2024-07-08 18:55:00 +08:00
zhaojisen
d6fbdfa7ea fixed: Fixed an issue where the field is empty 2024-07-08 18:51:10 +08:00
feng626
cd4260fd8d Merge pull request #4184 from jumpserver/pr@dev@translate
perf: Translate
2024-07-05 16:37:29 +08:00
feng
baeece62d3 perf: Translate 2024-07-05 16:36:24 +08:00
wangruidong
576c8f5891 fix: update platform with update protocols 2024-07-05 15:07:10 +08:00
feng626
5e97600807 Merge pull request #4182 from jumpserver/pr@dev@translate
perf: Translate
2024-07-04 18:15:04 +08:00
feng
4e2eb3a37d perf: Translate 2024-07-04 18:13:49 +08:00
feng626
e92173f8e8 Merge pull request #4181 from jumpserver/pr@dev@applet
perf: Virtualapp and appprovider tab perm
2024-07-04 16:46:45 +08:00
feng
412d9c804e perf: Virtualapp and appprovider tab perm 2024-07-04 16:46:00 +08:00
ZhaoJiSen
b84a725d4a Merge pull request #4179 from jumpserver/pr@dev@fix_split_job
perf: split job management
2024-07-04 16:37:39 +08:00
feng626
58b39743e0 Merge pull request #4180 from jumpserver/pr@dev@apple
perf: Apple host detail perm
2024-07-04 16:33:39 +08:00
feng
7651443c25 perf: Apple host detail perm 2024-07-04 16:32:20 +08:00
zhaojisen
8d64e331d1 perf: split job management 2024-07-04 16:04:26 +08:00
43 changed files with 493 additions and 266 deletions

View File

@@ -1,4 +1,4 @@
FROM node:16.20-bullseye-slim as stage-build
FROM node:16.20-bullseye-slim AS stage-build
ARG TARGETARCH
ARG DEPENDENCIES=" \

View File

@@ -217,7 +217,7 @@ export default {
.el-form-item__label {
padding: 0 30px 0 0;
line-height: 32px;
line-height: 30px;
color: var(--color-text-primary);
i {

View File

@@ -1,21 +1,20 @@
<template>
<div>
<el-input v-model="rawValue.phone" required :placeholder="$tc('InputPhone')" @input="OnInputChange">
<el-input v-model="rawValue.phone" :placeholder="$tc('InputPhone')" required @input="onInputChange">
<el-select
slot="prepend"
:placeholder="$tc('Select')"
:value="rawValue.code"
style="width: 75px;"
@change="OnChange"
style="width: 105px;"
@change="onChange"
>
<el-option
v-for="country in countries"
:key="country.value"
:key="country.name"
:label="country.value"
:value="country.value"
style="width: 200px;"
>
<span style="float: left">{{ country.name }}</span>
<span class="country-name">{{ country.name }}</span>
<span style="float: right; font-size: 13px">{{ country.value }}</span>
</el-option>
</el-select>
@@ -24,19 +23,19 @@
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'PhoneInput',
props: {
value: {
type: [Object, String],
default: () => ({ 'code': '+86', 'phone': '' })
default: null
}
},
data() {
return {
rawValue: {}
rawValue: {},
countries: [{ 'name': 'China', 'value': '+86' }]
}
},
computed: {
@@ -45,26 +44,38 @@ export default {
return ''
}
return `${this.rawValue.code}${this.rawValue.phone}`
},
countries: {
get() {
return this.publicSettings.COUNTRY_CALLING_CODES
}
},
...mapGetters(['publicSettings'])
}
},
mounted() {
this.rawValue = this.value || { code: '+86', phone: '' }
const defaults = { code: localStorage.getItem('prePhoneCode') || '+86', phone: '' }
this.rawValue = this.value || defaults
this.$axios.get('/api/v1/common/countries/').then(res => {
this.countries = res.map(item => {
return { name: `${item.flag} ${item.name}`, value: item.phone_code }
})
})
this.$emit('input', this.fullPhone)
},
methods: {
OnChange(countryCode) {
onChange(countryCode) {
this.rawValue.code = countryCode
this.OnInputChange()
this.onInputChange()
localStorage.setItem('prePhoneCode', countryCode)
},
OnInputChange() {
onInputChange() {
this.$emit('input', this.fullPhone)
}
}
}
</script>
<style scoped>
.country-name {
display: inline-block;
width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 5px;
}
</style>

View File

@@ -144,9 +144,6 @@ export default {
watch: {
choices: {
handler(value, oldValue) {
if (value?.length === oldValue?.length) {
return
}
this.loading = true
setTimeout(() => {
this.setDefaultItems(value)
@@ -345,6 +342,10 @@ export default {
.protocol-item {
display: flex;
margin: 5px 0;
&:first-of-type {
margin-top: 0;
}
}
.input-button {

View File

@@ -124,6 +124,9 @@ export default {
return this.iHasLeftActions ? 'right' : 'left'
}
},
created() {
this.$emit('done')
},
methods: {
handleTagSearch(val) {
this.searchTable(val)

View File

@@ -8,9 +8,11 @@
:selected-rows="selectedRows"
:table-url="tableUrl"
v-bind="iHeaderActions"
@done="handleActionInitialDone"
/>
<IBox class="table-content">
<AutoDataTable
v-if="actionInit"
ref="dataTable"
:config="iTableConfig"
:filter-table="filter"
@@ -73,9 +75,11 @@ export default {
return {
selectedRows: [],
init: false,
extraQuery: extraQuery,
urlUpdated: {},
isDeactivated: false
isDeactivated: false,
extraQuery: extraQuery,
actionInit: this.headerActions.has === false,
initQuery: {}
}
},
computed: {
@@ -203,13 +207,35 @@ export default {
}, 500)
},
methods: {
handleActionInitialDone() {
setTimeout(() => {
this.actionInit = true
}, 100)
},
handleSelectionChange(val) {
this.selectedRows = val
},
reloadTable() {
this.dataTable?.getList()
},
updateInitQuery(attrs) {
if (!this.actionInit) {
this.initQuery = attrs
for (const key in attrs) {
this.$set(this.extraQuery, key, attrs[key])
}
return true
}
const removeKeys = Object.keys(this.initQuery).filter(key => !attrs[key])
for (const key of removeKeys) {
this.$delete(this.extraQuery, key)
}
},
search(attrs) {
const init = this.updateInitQuery(attrs)
if (init) {
return
}
this.$log.debug('ListTable: search table', attrs)
this.$emit('TagSearch', attrs)
this.$refs.dataTable?.$refs.dataTable?.search(attrs, true)

View File

@@ -4,8 +4,8 @@
<span v-if="!iLabels || iLabels.length === 0" style="vertical-align: top;">
-
</span>
<div v-else>
<div
<span v-else class="label-wrapper">
<span
v-for="label of iLabels"
:key="label.id"
>
@@ -15,8 +15,9 @@
class="tag-formatter"
@click="handleLabelSearch(label)"
/>
</div>
</div>
<span />
</span>
</span>
</a>
<a
v-if="formatterArgs.showEditBtn"
@@ -30,15 +31,16 @@
v-if="showDialog"
:title="$tc('BindLabel')"
:visible.sync="showDialog"
class="tag-dialog"
width="600px"
@cancel="handleCancel"
@confirm="handleConfirm"
>
<el-row :gutter="1" class="tag-select">
<el-row class="tag-select">
<el-col :span="12">
<Select2 v-model="keySelect2.value" v-bind="keySelect2" @change="handleKeyChanged" />
</el-col>
<el-col :span="12">
<el-col :span="12" style="padding-left: 5px">
<Select2
v-model="valueSelect2.value"
:disabled="!keySelect2.value"
@@ -207,18 +209,11 @@ export default {
flex-wrap: wrap;
& > span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.tag-select {
::v-deep .el-input__inner::placeholder {
font-size: 13px;
}
}
.edit-btn {
visibility: hidden;
position: relative;
@@ -233,9 +228,14 @@ export default {
.label-container {
display: flex;
height: 28px;
.label-formatter-col {
overflow: hidden;
&:hover {
overflow: auto;
}
}
&:hover {
@@ -249,15 +249,23 @@ export default {
}
}
.tag-zone {
margin: 20px 0 0 0;
border: solid 1px #ebeef5;
padding: 10px;
background: #f2f2f5;
.tag-dialog {
.tag-zone {
margin: 20px 0 0 0;
border: solid 1px #ebeef5;
padding: 10px;
background: #f2f2f5;
.tag-formatter {
margin: 1px 3px;
display: inline-block;
.tag-formatter {
margin: 1px 3px;
display: inline-block;
}
}
.tag-select {
::v-deep .el-input__inner::placeholder {
font-size: 13px;
}
}
}
@@ -270,7 +278,6 @@ export default {
.tag-formatter {
margin: 2px 0;
}
.tag-tip {

View File

@@ -55,8 +55,9 @@ export default {
<style lang="scss" scoped>
.tag {
display: flex;
display: inline-block;
flex-wrap: wrap;
& > span {
overflow: hidden;
white-space: nowrap;

View File

@@ -108,7 +108,7 @@ export default {
methods: {
async handleSelectView(key, keyPath) {
const routeName = this.viewsMapper[key] || '/'
localStorage.setItem('PreView', key)
localStorage.setItem('preView', key)
// Next 之前要重置 init 状态,否则这些路由守卫就不走了
await store.dispatch('app/reset')
if (!this.tipHasRead) {

View File

@@ -42,7 +42,7 @@ export default {
return this.$t('LicenseExpired')
}
if (intervalDays < 7) {
return this.$t('LicenseWillBe') + this.licenseData.date_expired + this.$t('Expire')
return this.$t('LicenseWillBe') + ' ' + this.licenseData.date_expired + ' ' + this.$t('Expire')
}
return false
},

View File

@@ -201,7 +201,7 @@ export default [
hidden: true,
meta: {
title: i18n.t('CommandGroupDetail'),
activeMenu: ''
activeMenu: '/console/perms/acls/cmd-acls'
}
},
{

View File

@@ -586,3 +586,10 @@ li.rmenu i.fa {
color: var(--color-link);
}
}
.el-table__row {
::-webkit-scrollbar {
width: 6px; /* 设置垂直滚动条的宽度 */
height: 6px; /* 设置水平滚动条的高度 */
}
}

View File

@@ -1,7 +1,7 @@
import VueCookie from 'vue-cookie'
const CURRENT_ORG_KEY = 'jms_current_org'
const CURRENT_ROLE_KEY = 'jms_current_role'
const CURRENT_ORG_KEY = 'currentOrg'
const CURRENT_ROLE_KEY = 'currentRole'
let cookieNamePrefix = VueCookie.get('SESSION_COOKIE_NAME_PREFIX')
if (!cookieNamePrefix || ['""', "''"].indexOf(cookieNamePrefix) > -1) {
cookieNamePrefix = ''
@@ -17,7 +17,7 @@ export function setTokenToCookie(value, expires) {
}
export function getCurrentRoleLocal(username) {
const key = CURRENT_ROLE_KEY + '_' + username
const key = CURRENT_ROLE_KEY + ':' + username
const role = localStorage.getItem(key)
if (role) {
return parseInt(role) || null
@@ -26,12 +26,12 @@ export function getCurrentRoleLocal(username) {
}
export function saveCurrentRoleLocal(username, role) {
const key = CURRENT_ROLE_KEY + '_' + username
const key = CURRENT_ROLE_KEY + ':' + username
return localStorage.setItem(key, role)
}
export function getCurrentOrgLocal(username) {
const key = CURRENT_ORG_KEY + '_' + username
const key = CURRENT_ORG_KEY + ':' + username
const value = localStorage.getItem(key)
try {
return JSON.parse(value)
@@ -41,18 +41,18 @@ export function getCurrentOrgLocal(username) {
}
export function saveCurrentOrgLocal(username, org) {
const key = CURRENT_ORG_KEY + '_' + username
const key = CURRENT_ORG_KEY + ':' + username
localStorage.setItem(key, JSON.stringify(org))
VueCookie.set('X-JMS-ORG', org.id)
}
export function setPreOrgLocal(username, org) {
const key = 'PRE_ORG_' + username
const key = 'preOrg' + ':' + username
localStorage.setItem(key, JSON.stringify(org))
}
export function getPreOrgLocal(username) {
const key = 'PRE_ORG_' + username
const key = 'preOrg' + ':' + username
const value = localStorage.getItem(key)
try {
return JSON.parse(value)

View File

@@ -109,7 +109,7 @@ export function isSameView(to, from) {
export function getPropView() {
const hasPermedViews = getPermedViews()
const preView = localStorage.getItem('PreView')
const preView = localStorage.getItem('preView')
const hasPerm = hasPermedViews.indexOf(preView) > -1
if (hasPerm) {
return preView

View File

@@ -164,8 +164,8 @@ export default {
},
extraMoreActions: [
{
name: 'BatchRetry',
title: this.$t('BatchRetry'),
name: 'RetrySelected',
title: this.$t('RetrySelected'),
type: 'primary',
fa: 'fa-retweet',
can: ({ selectedRows }) => {

View File

@@ -164,8 +164,8 @@ export default {
},
extraMoreActions: [
{
name: 'BatchRetry',
title: this.$t('BatchRetry'),
name: 'RetrySelected',
title: this.$t('RetrySelected'),
type: 'primary',
fa: 'fa-retweet',
can: ({ selectedRows }) => {

View File

@@ -68,11 +68,6 @@ export default {
}
}
}
},
activated() {
setTimeout(() => {
this.$refs.listTable.reloadTable()
}, 300)
}
}
</script>

View File

@@ -32,15 +32,9 @@ export default {
title: this.$t('Basic'),
name: 'Detail'
}
],
actions: {
}
]
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -53,11 +53,6 @@ export default {
}
}
}
},
computed: {}
}
}
</script>
<style>
</style>

View File

@@ -36,8 +36,6 @@ export default {
}
}
},
mounted() {
},
methods: {
handleTabClick(tab) {
const query = _.cloneDeep(this.$route.query)

View File

@@ -47,6 +47,7 @@ export default {
return {
loading: true,
platform: {},
changePlatformID: '',
defaultConfig: {
initial: {},
platform: {},
@@ -131,7 +132,7 @@ export default {
const { defaultConfig } = this
const { node, platform } = this.$route?.query || {}
const nodesInitial = node ? [node] : []
const platformId = platform || 'Linux'
const platformId = this.changePlatformID ? this.changePlatformID : (platform || 'Linux')
const url = `/api/v1/assets/platforms/${platformId}/`
this.platform = await this.$axios.get(url)
const initial = {

View File

@@ -29,7 +29,9 @@
@click.native="createAsset(platform)"
>
<img :src="loadImage(platform)" alt="icon" class="asset-icon">
<span class="platform-name">{{ platform.name }}</span>
<el-tooltip :content="platform.name">
<span class="platform-name">{{ platform.name }}</span>
</el-tooltip>
</el-card>
</el-col>
</el-collapse-item>
@@ -195,7 +197,11 @@ export default {
margin: 5px 0;
& ::v-deep .el-card__body {
padding: 10px
padding: 10px;
flex-wrap: nowrap;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
border-left: solid 4px;
@@ -234,10 +240,12 @@ export default {
height: 1.5em;
vertical-align: -0.2em;
fill: currentColor;
overflow: hidden;
}
.platform-name {
margin-left: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@@ -29,10 +29,10 @@ export const filterSelectValues = (values) => {
function updatePlatformProtocols(vm, platformType, updateForm, isPlatformChanged = false) {
setTimeout(() => vm.init().then(() => {
const isCreate = vm?.$route?.meta.action === 'create' && vm?.$route?.query.clone_from === undefined
const need_modify = isCreate || isPlatformChanged
const platformProtocols = vm.platform.protocols
if (!need_modify) return
if (platformType === 'website') {
const need_modify = isCreate || isPlatformChanged
if (!need_modify) return
const platformProtocols = vm.platform.protocols
const setting = Array.isArray(platformProtocols) ? platformProtocols[0].setting : platformProtocols.setting
updateForm({
'autofill': setting.autofill ? setting.autofill : 'basic',
@@ -42,6 +42,8 @@ function updatePlatformProtocols(vm, platformType, updateForm, isPlatformChanged
'username_selector': setting.username_selector
})
}
vm.iConfig.fieldsMeta.protocols.el.choices = platformProtocols.map(item => ({ name: item.name, port: item.port }))
updateForm({ protocols: [] })
}), 100)
}
@@ -100,12 +102,10 @@ export const assetFieldsMeta = (vm) => {
change: ([event], updateForm) => {
const pk = event.pk
const url = window.location.href
const newURL = url.replace(/platform=[^&]*/, 'platform=' + pk)
vm.changePlatformID = pk
if (url.includes('clone')) {
updatePlatformProtocols(vm, platformType, updateForm, true)
} else {
window.history.replaceState(null, null, newURL)
vm.$nextTick(() => {
updatePlatformProtocols(vm, platformType, updateForm, true)
})

View File

@@ -430,7 +430,7 @@ export default {
}
createJob(data).then(res => {
this.executionInfo.timeCost = 0
this.executionInfo.status = 'running'
this.executionInfo.status = { value: 'running', label: this.$t('Running') }
this.currentTaskId = res.task_id
this.xtermConfig = { taskId: this.currentTaskId, type: 'shortcut_cmd' }
this.setCostTimeInterval()
@@ -451,12 +451,12 @@ export default {
})
},
setBtn() {
if (this.executionInfo.status !== 'running') {
if (this.executionInfo.status.value !== 'running') {
clearInterval(this.executionInfo.cancel)
this.toolbar.left.run.icon = 'fa fa-play'
}
this.toolbar.left.run.isVisible = this.executionInfo.status === 'running'
this.toolbar.left.stop.isVisible = this.executionInfo.status !== 'running'
this.toolbar.left.run.isVisible = this.executionInfo.status.value === 'running'
this.toolbar.left.stop.isVisible = this.executionInfo.status.value !== 'running'
}
}
}

View File

@@ -9,11 +9,11 @@
<span class="status-item">
<span>{{ $tc('Status') }}: </span>
<span
:class="{'status_success':executionInfo.status==='success',
'status_warning':executionInfo.status==='timeout',
'status_danger':executionInfo.status==='failed'
:class="{'status_success':executionInfo.status.value==='success',
'status_warning':executionInfo.status.value==='timeout',
'status_danger':executionInfo.status.value==='failed'
}"
>{{ $tc('' + executionInfo.status) }}</span>
>{{ $tc('' + executionInfo.status.label) }}</span>
</span>
<span class="status-item">
<span>{{ $tc('TimeDelta') }}: </span>
@@ -66,9 +66,7 @@ export default {
executionInfo: {
type: Object,
// eslint-disable-next-line vue/require-valid-default-prop
default: {
status: 'success'
}
default: {}
}
},
data() {

View File

@@ -1,7 +1,12 @@
<template>
<el-row :gutter="20">
<el-col :md="15" :sm="24">
<AutoDetailCard :excludes="excludes" :object="object" :url="url" />
<AutoDetailCard
:excludes="excludes"
:object="object"
:url="url"
:fields="detailFields"
/>
</el-col>
<el-col v-if="hasSummary" :md="9" :sm="24">
<IBox
@@ -82,6 +87,19 @@ export default {
excludes: [
'job', 'parameters', 'summary', 'task_id', 'timedelta'
],
detailFields: [
'task_id', 'time_cost',
{
key: this.$t('IsFinished'),
value: `${this.object.is_finished ? '是' : '否'}`
},
{
key: this.$t('IsSuccess'),
value: `${this.object.is_success ? '是' : '否'}`
},
'job_type', 'material', 'org_name',
'date_start', 'date_finished', 'date_created'
],
url: `/api/v1/ops/job-executions/${this.object.id}/`
}
},
@@ -90,6 +108,9 @@ export default {
return this.object.is_finished && Object.keys(this.object.summary).length
}
},
mounted() {
console.log(this.object)
},
methods: {}
}
</script>

View File

@@ -23,7 +23,7 @@ export default {
tableConfig: {
url: '/api/v1/ops/job-executions/',
columnsExclude: [
'summary', 'parameters'
'summary', 'parameters', 'timedelta'
],
columnsShow: {
min: ['material', 'actions'],

View File

@@ -0,0 +1,129 @@
<template>
<div>
<GenericListTable :header-actions="headerActions" :table-config="tableConfig" />
<JobRunDialog v-if="showJobRunDialog" :item="item" :visible.sync="showJobRunDialog" @submit="runJob" />
</div>
</template>
<script>
import JobRunDialog from '@/views/ops/Job/JobRunDialog'
import GenericListTable from '@/layout/components/GenericListTable'
import { openTaskPage } from '@/utils/jms'
import { ActionsFormatter, DateFormatter, DetailFormatter } from '@/components/Table/TableFormatters'
export default {
components: {
GenericListTable,
JobRunDialog
},
data() {
return {
item: {},
tableConfig: {
url: '/api/v1/ops/jobs/?type=adhoc',
columnsShow: {
min: ['name', 'actions'],
default: [
'name', 'type', 'asset_amount', 'average_time_cost',
'summary', 'date_last_run', 'actions'
]
},
columns: [
'name', 'type', 'summary', 'average_time_cost', 'asset_amount',
'date_last_run', 'comment', 'date_updated', 'date_created', 'actions'
],
columnsMeta: {
name: {
width: '140px',
formatter: DetailFormatter,
formatterArgs: {
can: true,
getRoute: ({ row }) => ({
name: 'JobDetail',
params: { id: row.id }
})
}
},
type: {
width: '96px',
formatter: (row) => {
return row.type.label
}
},
comment: {
width: '240px'
},
summary: {
label: this.$t('Summary'),
formatter: (row) => {
return row.summary['success'] + '/' + row.summary['total']
}
},
average_time_cost: {
formatter: (row) => {
return row.average_time_cost.toFixed(2) + 's'
}
},
asset_amount: {
label: this.$t('AssetsOfNumber'),
formatter: (row) => {
return row.assets.length
}
},
date_last_run: {
width: '140px',
formatter: DateFormatter
},
actions: {
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: true,
canUpdate: this.$hasPerm('ops.change_job') && !this.$store.getters.currentOrgIsRoot,
updateRoute: 'JobUpdate',
hasDelete: true,
canDelete: this.$hasPerm('ops.delete_job'),
hasClone: false,
extraActions: [
{
title: this.$t('Run'),
name: 'run',
can: this.$hasPerm('ops.add_jobexecution') && !this.$store.getters.currentOrgIsRoot,
callback: ({ row }) => {
if (row?.use_parameter_define && row?.parameters_define) {
const params = JSON.parse(row.parameters_define)
if (Object.keys(params).length > 0) {
this.item = row
this.showJobRunDialog = true
}
} else {
this.runJob(row)
}
}
}
]
}
}
}
},
headerActions: {
createRoute: 'JobCreate',
hasRefresh: true,
hasExport: false,
hasImport: false
},
showJobRunDialog: false
}
},
methods: {
runJob(row, parameters) {
this.$axios.post('/api/v1/ops/job-executions/', {
job: row.id,
parameters: parameters
}).then((resp) => {
openTaskPage(resp.task_id)
})
}
}
}
</script>

View File

@@ -0,0 +1,136 @@
<template>
<div>
<GenericListTable :header-actions="headerActions" :table-config="tableConfig" />
<JobRunDialog v-if="showJobRunDialog" :item="item" :visible.sync="showJobRunDialog" @submit="runJob" />
</div>
</template>
<script>
import JobRunDialog from '@/views/ops/Job/JobRunDialog'
import GenericListTable from '@/layout/components/GenericListTable'
import { openTaskPage } from '@/utils/jms'
import { ActionsFormatter, DateFormatter, DetailFormatter } from '@/components/Table/TableFormatters'
export default {
components: {
GenericListTable,
JobRunDialog
},
data() {
return {
item: {},
tableConfig: {
url: '/api/v1/ops/jobs/?type=playbook',
columnsShow: {
min: ['name', 'actions'],
default: [
'name', 'type', 'asset_amount', 'average_time_cost',
'summary', 'date_last_run', 'actions'
]
},
columns: [
'name', 'type', 'summary', 'average_time_cost', 'asset_amount',
'date_last_run', 'comment', 'date_updated', 'date_created', 'actions'
],
columnsMeta: {
name: {
width: '140px',
formatter: DetailFormatter,
formatterArgs: {
can: true,
getRoute: ({ row }) => ({
name: 'JobDetail',
params: { id: row.id }
})
}
},
type: {
width: '96px',
formatter: (row) => {
return row.type.label
}
},
comment: {
width: '240px'
},
summary: {
label: this.$t('Summary'),
formatter: (row) => {
return row.summary['success'] + '/' + row.summary['total']
}
},
average_time_cost: {
formatter: (row) => {
return row.average_time_cost.toFixed(2) + 's'
}
},
asset_amount: {
label: this.$t('AssetsOfNumber'),
formatter: (row) => {
return row.assets.length
}
},
date_last_run: {
width: '140px',
formatter: DateFormatter
},
actions: {
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: true,
canUpdate: this.$hasPerm('ops.change_job') && !this.$store.getters.currentOrgIsRoot,
updateRoute: 'JobUpdate',
hasDelete: true,
canDelete: this.$hasPerm('ops.delete_job'),
hasClone: false,
extraActions: [
{
title: this.$t('Run'),
name: 'run',
can: this.$hasPerm('ops.add_jobexecution') && !this.$store.getters.currentOrgIsRoot,
callback: ({ row }) => {
if (row?.use_parameter_define && row?.parameters_define) {
const params = JSON.parse(row.parameters_define)
if (Object.keys(params).length > 0) {
this.item = row
this.showJobRunDialog = true
}
} else {
this.runJob(row)
}
}
}
]
}
}
}
},
headerActions: {
hasRefresh: true,
hasExport: false,
hasImport: false,
createRoute: () => {
return {
name: 'JobCreate',
query: {
type: 'playbook'
}
}
}
},
showJobRunDialog: false
}
},
methods: {
runJob(row, parameters) {
this.$axios.post('/api/v1/ops/job-executions/', {
job: row.id,
parameters: parameters
}).then((resp) => {
openTaskPage(resp.task_id)
})
}
}
}
</script>

View File

@@ -1,151 +1,39 @@
<template>
<div>
<JobRunDialog v-if="showJobRunDialog" :item="item" :visible.sync="showJobRunDialog" @submit="runJob" />
<GenericListPage :header-actions="headerActions" :table-config="tableConfig" />
</div>
<TabPage
:active-menu.sync="activeMenu"
:submenu="submenu"
>
<component :is="activeMenu" />
</TabPage>
</template>
<script>
import GenericListPage from '@/layout/components/GenericListPage'
import { ActionsFormatter, DateFormatter, DetailFormatter } from '@/components/Table/TableFormatters'
import JobRunDialog from '@/views/ops/Job/JobRunDialog'
import { openTaskPage } from '@/utils/jms'
import Adhoc from './components/Adhoc.vue'
import Playbook from './components/PlayBook.vue'
import TabPage from '@/layout/components/TabPage/index.vue'
export default {
components: {
JobRunDialog,
GenericListPage
Adhoc,
TabPage,
Playbook
},
data() {
return {
item: {},
runtime_parameters: {},
showJobRunDialog: false,
tableConfig: {
url: '/api/v1/ops/jobs/',
columnsShow: {
min: ['name', 'actions'],
default: [
'name', 'type', 'asset_amount', 'average_time_cost',
'summary', 'date_last_run', 'actions'
]
activeMenu: 'Adhoc',
submenu: [
{
title: this.$t('AdhocManage'),
name: 'Adhoc',
hidden: () => !this.$hasPerm('ops.view_adhoc')
},
columns: [
'name', 'type', 'summary', 'average_time_cost', 'asset_amount',
'date_last_run', 'comment', 'date_updated', 'date_created', 'actions'
],
columnsMeta: {
name: {
width: '140px',
formatter: DetailFormatter,
formatterArgs: {
can: true,
getRoute: ({ row }) => ({
name: 'JobDetail',
params: { id: row.id }
})
}
},
type: {
width: '96px',
formatter: (row) => {
return row.type.label
}
},
comment: {
width: '240px'
},
summary: {
label: this.$t('Summary'),
formatter: (row) => {
return row.summary['success'] + '/' + row.summary['total']
}
},
average_time_cost: {
formatter: (row) => {
return row.average_time_cost.toFixed(2) + 's'
}
},
asset_amount: {
label: this.$t('AssetsOfNumber'),
formatter: (row) => {
return row.assets.length
}
},
date_last_run: {
width: '140px',
formatter: DateFormatter
},
actions: {
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: true,
canUpdate: this.$hasPerm('ops.change_job') && !this.$store.getters.currentOrgIsRoot,
updateRoute: 'JobUpdate',
hasDelete: true,
canDelete: this.$hasPerm('ops.delete_job'),
hasClone: false,
extraActions: [
{
title: this.$t('Run'),
name: 'run',
can: this.$hasPerm('ops.add_jobexecution') && !this.$store.getters.currentOrgIsRoot,
callback: ({ row }) => {
if (row?.use_parameter_define && row?.parameters_define) {
const params = JSON.parse(row.parameters_define)
if (Object.keys(params).length > 0) {
this.item = row
this.showJobRunDialog = true
}
} else {
this.runJob(row)
}
}
}
]
}
}
{
title: this.$t('PlaybookManage'),
name: 'Playbook',
hidden: () => !this.$hasPerm('ops.view_playbook')
}
},
headerActions: {
createRoute: 'JobCreate',
hasRefresh: true,
hasExport: false,
hasImport: false,
moreCreates: {
callback: (item) => {
this.$router.push({
name: 'JobCreate',
query: { type: item.name }
})
},
dropdown: [
{
name: 'adhoc',
title: this.$t('Command')
},
{
name: 'playbook',
title: 'Playbook'
}
]
}
}
}
},
methods: {
runJob(row, parameters) {
this.$axios.post('/api/v1/ops/job-executions/', {
job: row.id,
parameters: parameters
}).then((resp) => {
openTaskPage(resp.task_id)
})
]
}
}
}
</script>
<style>
</style>

View File

@@ -49,7 +49,3 @@ export default {
}
}
</script>
<style>
</style>

View File

@@ -36,12 +36,6 @@ export default {
}
]
}
},
mounted() {
}
}
</script>
<style scoped>
</style>

View File

@@ -42,6 +42,9 @@ export default {
component: PhoneInput
},
mfa_level: {
disabled: (formValue) => {
return formValue.mfa_level === 2
},
helpText: this.$t('MFAOfUserFirstLoginPersonalInformationImprovementPage')
},
public_key: {
@@ -91,7 +94,7 @@ export default {
methods: {
disableMFAFieldIfNeed(user) {
const adminUserIsNeed = (user?.is_superuser || user?.is_org_admin) &&
store.getters.publicSettings['SECURITY_MFA_AUTH'] === 2
store.getters.publicSettings['SECURITY_MFA_AUTH'] === 2
if (store.getters.publicSettings['SECURITY_MFA_AUTH'] === 1 || adminUserIsNeed || user?.mfa_level.value === 2) {
this.fieldsMeta['mfa_level'].disabled = true
}

View File

@@ -146,8 +146,7 @@ export default {
}
}
}
},
methods: {}
}
}
</script>

View File

@@ -65,6 +65,7 @@ export default {
label: this.$t('DisplayName'),
formatter: DetailFormatter,
formatterArgs: {
can: vm.$hasPerm('assets.view_asset'),
getTitle: ({ row }) => row.host.name,
getRoute: ({ row }) => ({
name: 'AppletHostDetail',

View File

@@ -41,14 +41,14 @@ export default {
title: this.$t('VirtualApp'),
name: 'VirtualApp',
hidden: () => {
return !store.getters.publicSettings['VIRTUAL_APP_ENABLED'] || !this.$store.getters.hasValidLicense
return !store.getters.publicSettings['VIRTUAL_APP_ENABLED'] || !this.$store.getters.hasValidLicense || !this.$hasPerm('terminal.view_virtualapp')
}
},
{
title: this.$t('AppProvider'),
name: 'AppProvider',
hidden: () => {
return !store.getters.publicSettings['VIRTUAL_APP_ENABLED'] || !this.$store.getters.hasValidLicense
return !store.getters.publicSettings['VIRTUAL_APP_ENABLED'] || !this.$store.getters.hasValidLicense || !this.$hasPerm('terminal.view_appprovider')
}
}
]

View File

@@ -234,7 +234,7 @@ export default {
} else {
value = values[key]
}
if (value) {
if (value !== undefined) {
form.append(key, value)
}
}

View File

@@ -1,7 +1,7 @@
<template>
<Page v-bind="$attrs">
<div v-if="!loading">
<el-alert v-if="!hasValidLicense" type="success">
<el-alert v-if="publicSettings.XPACK_ENABLED" type="success">
{{ this.$t('ImportLicenseTip') }}
</el-alert>
<el-row :gutter="20">
@@ -93,8 +93,22 @@ export default {
if (!this.hasValidLicense) {
return [
{
key: this.$t('License'),
key: this.$t('Version'),
value: this.$t('CommunityEdition')
},
{
key: this.$t('Expired'),
value: this.$t('Never')
},
{
key: this.$t('License'),
value: 'GPLv3'
},
{
key: 'Github',
formatter: () => {
return (<a href='https://github.com/jumpserver/jumpserver' target='_blank'> JumpServer </a>)
}
}
]
}

View File

@@ -1,6 +1,6 @@
<template>
<div>
<el-button size="mini" type="primary" icon="el-icon-setting" @click="visible = !visible"> {{ $t("Settings...") }} </el-button>
<el-button size="mini" type="primary" icon="el-icon-setting" @click="visible = !visible"> {{ $t("Setting") }} </el-button>
<Dialog
v-if="visible"
:show-cancel="false"

View File

@@ -1,6 +1,6 @@
<template>
<div>
<el-button size="mini" type="primary" icon="el-icon-setting" @click="visible=true">{{ $t('Settings...') }}</el-button>
<el-button size="mini" type="primary" icon="el-icon-setting" @click="visible=true">{{ $t('Setting') }}</el-button>
<Dialog
v-if="visible"
:destroy-on-close="true"

View File

@@ -110,7 +110,7 @@ export default {
canCreate: this.$hasPerm('orgs.add_organization'),
extraActions: [
{
title: this.$t('Settings...'),
title: this.$t('Setting'),
icon: 'el-icon-setting',
callback: () => {
this.visible = true

View File

@@ -43,7 +43,7 @@ export default {
hasMoreActions: true,
extraMoreActions: [
{
name: 'BatchApproval',
name: 'ApproveSelected',
title: this.$t('ApproveSelected'),
can: ({ selectedRows }) => { return selectedRows.length > 0 },
callback: function({ selectedRows }) {

View File

@@ -86,6 +86,7 @@ export default {
this.setting.InviteDialogVisible = false
this.$emit('close', res)
this.$store.dispatch('users/currentUserJoinNewOrg', res.users)
this.$router.push({ name: 'UserList', query: { order: '-date_updated' }})
}
}
}