U 批量执行的历史记录,如果来自模版执行,则显示模版名称 #430

pull/462/head
vapao 2022-02-17 20:24:49 +08:00
parent 01cc1ca8c7
commit 824cf39c43
9 changed files with 54 additions and 30 deletions

View File

@ -35,6 +35,7 @@ class ExecTemplate(models.Model, ModelMixin):
class ExecHistory(models.Model, ModelMixin):
user = models.ForeignKey(User, on_delete=models.CASCADE)
template = models.ForeignKey(ExecTemplate, on_delete=models.SET_NULL, null=True)
digest = models.CharField(max_length=32, db_index=True)
interpreter = models.CharField(max_length=20)
command = models.TextField()
@ -44,6 +45,8 @@ class ExecHistory(models.Model, ModelMixin):
def to_view(self):
tmp = self.to_dict()
tmp['host_ids'] = json.loads(self.host_ids)
if hasattr(self, 'template_name'):
tmp['template_name'] = self.template_name
return tmp
class Meta:

View File

@ -4,6 +4,7 @@
from django.views.generic import View
from django_redis import get_redis_connection
from django.conf import settings
from django.db.models import F
from libs import json_response, JsonParser, Argument, human_datetime, auth
from apps.exec.models import ExecTemplate, ExecHistory
from apps.host.models import Host
@ -57,7 +58,8 @@ def do_task(request):
form, error = JsonParser(
Argument('host_ids', type=list, filter=lambda x: len(x), help='请选择执行主机'),
Argument('command', help='请输入执行命令内容'),
Argument('interpreter', default='sh')
Argument('interpreter', default='sh'),
Argument('template_id', type=int, required=False)
).parse(request.body)
if error is None:
if not has_host_perm(request.user, form.host_ids):
@ -81,7 +83,12 @@ def do_task(request):
tmp_str = f'{form.interpreter},{host_ids},{form.command}'
digest = hashlib.md5(tmp_str.encode()).hexdigest()
record = ExecHistory.objects.filter(user=request.user, digest=digest).first()
if form.template_id:
template = ExecTemplate.objects.filter(pk=form.template_id).first()
if not template or template.body != form.command:
form.template_id = None
if record:
record.template_id = form.template_id
record.updated_at = human_datetime()
record.save()
else:
@ -89,6 +96,7 @@ def do_task(request):
user=request.user,
digest=digest,
interpreter=form.interpreter,
template_id=form.template_id,
command=form.command,
host_ids=json.dumps(form.host_ids),
)
@ -98,5 +106,5 @@ def do_task(request):
@auth('exec.task.do')
def get_histories(request):
records = ExecHistory.objects.filter(user=request.user)
records = ExecHistory.objects.filter(user=request.user).annotate(template_name=F('template__name'))
return json_response([x.to_view() for x in records])

View File

@ -32,8 +32,8 @@ class TemplateSelector extends React.Component {
handleSubmit = () => {
if (this.state.selectedRows.length > 0) {
const {host_ids, body, interpreter} = this.state.selectedRows[0]
this.props.onOk(host_ids, body, interpreter)
const tpl = this.state.selectedRows[0]
this.props.onOk(tpl)
}
this.props.onCancel()
};
@ -62,13 +62,6 @@ class TemplateSelector extends React.Component {
render() {
const {selectedRows} = this.state;
let data = store.records;
if (store.f_name) {
data = data.filter(item => item['name'].toLowerCase().includes(store.f_name.toLowerCase()))
}
if (store.f_type) {
data = data.filter(item => item['type'].toLowerCase().includes(store.f_type.toLowerCase()))
}
return (
<Modal
visible
@ -99,7 +92,7 @@ class TemplateSelector extends React.Component {
type: 'radio',
onChange: (_, selectedRows) => this.setState({selectedRows})
}}
dataSource={data}
dataSource={store.dataSource}
loading={store.isFetching}
onRow={record => {
return {

View File

@ -20,6 +20,7 @@ function TaskIndex() {
const [loading, setLoading] = useState(false)
const [interpreter, setInterpreter] = useState('sh')
const [command, setCommand] = useState('')
const [template_id, setTemplateId] = useState()
const [histories, setHistories] = useState([])
useEffect(() => {
@ -40,19 +41,21 @@ function TaskIndex() {
function handleSubmit() {
setLoading(true)
const host_ids = store.host_ids;
http.post('/api/exec/do/', {host_ids, interpreter, command: cleanCommand(command)})
const formData = {interpreter, template_id, host_ids: store.host_ids, command: cleanCommand(command)}
http.post('/api/exec/do/', formData)
.then(store.switchConsole)
.finally(() => setLoading(false))
}
function handleTemplate(host_ids, command, interpreter) {
if (host_ids.length > 0) store.host_ids = host_ids
setInterpreter(interpreter)
setCommand(command)
function handleTemplate(tpl) {
if (tpl.host_ids.length > 0) store.host_ids = tpl.host_ids
setTemplateId(tpl.id)
setInterpreter(tpl.interpreter)
setCommand(tpl.body)
}
function handleClick(item) {
setTemplateId(item.template_id)
setInterpreter(item.interpreter)
setCommand(item.command)
store.host_ids = item.host_ids
@ -110,7 +113,11 @@ function TaskIndex() {
<div key={index} className={style.item} onClick={() => handleClick(item)}>
<div className={style[item.interpreter]}>{item.interpreter.substr(0, 2)}</div>
<div className={style.number}>{item.host_ids.length}</div>
<div className={style.command}>{item.command}</div>
{item.template_name ? (
<div className={style.tpl}>{item.template_name}</div>
) : (
<div className={style.command}>{item.command}</div>
)}
<div className={style.desc}>{moment(item.updated_at).format('MM.DD HH:mm')}</div>
</div>
))}

View File

@ -1,6 +1,7 @@
.index {
display: flex;
height: calc(100vh - 218px);
min-height: 420px;
background-color: #fff;
overflow: hidden;
@ -27,6 +28,7 @@
.editor {
height: calc(100vh - 482px) !important;
min-height: 152px;
}
}
@ -95,6 +97,17 @@
margin: 0 12px;
}
.tpl {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0 12px;
background-color: #d2e7fd;
padding: 0 8px;
border-radius: 2px;
}
.desc {
color: #999;
}

View File

@ -32,20 +32,13 @@ class ComTable extends React.Component {
};
render() {
let data = store.records;
if (store.f_name) {
data = data.filter(item => item['name'].toLowerCase().includes(store.f_name.toLowerCase()))
}
if (store.f_type) {
data = data.filter(item => item['type'].toLowerCase().includes(store.f_type.toLowerCase()))
}
return (
<TableCard
tKey="et"
title="模板列表"
rowKey="id"
loading={store.isFetching}
dataSource={data}
dataSource={store.dataSource}
onReload={store.fetchRecords}
actions={[
<AuthButton

View File

@ -4,7 +4,7 @@
* Released under the AGPL-3.0 License.
*/
import { observable } from "mobx";
import http from 'libs/http';
import { http, includes } from 'libs';
class Store {
@observable records = [];
@ -16,6 +16,13 @@ class Store {
@observable f_name;
@observable f_type;
get dataSource() {
let data = this.records
if (this.f_name) data = data.filter(x => includes(x.name, this.f_name))
if (this.f_type) data = data.filter(x => includes(x.type, this.f_type))
return data
}
fetchRecords = () => {
this.isFetching = true;
http.get('/api/exec/template/')

View File

@ -167,7 +167,7 @@ export default observer(function () {
<Button disabled={!canNext()} type="link" loading={loading} onClick={handleTest}>执行测试</Button>
<span style={{color: '#888', fontSize: 12}}>Tips: 仅测试第一个监控地址</span>
</Form.Item>
{showTmp && <TemplateSelector onOk={(_, v) => store.record.extra = v} onCancel={() => setShowTmp(false)}/>}
{showTmp && <TemplateSelector onOk={({body}) => store.record.extra = body} onCancel={() => setShowTmp(false)}/>}
<Selector
visible={showSelector}
selectedRowKeys={[...store.record.targets]}

View File

@ -109,7 +109,7 @@ export default observer(function () {
<Form.Item shouldUpdate wrapperCol={{span: 14, offset: 6}}>
{() => <Button disabled={canNext()} type="primary" onClick={handleNext}>下一步</Button>}
</Form.Item>
{showTmp && <TemplateSelector onOk={(_, v) => setCommand(v)} onCancel={() => setShowTmp(false)}/>}
{showTmp && <TemplateSelector onOk={({body}) => setCommand(body)} onCancel={() => setShowTmp(false)}/>}
</Form>
)
})