mirror of https://github.com/openspug/spug
U 批量执行的历史记录,如果来自模版执行,则显示模版名称 #430
parent
01cc1ca8c7
commit
824cf39c43
|
@ -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:
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/')
|
||||
|
|
|
@ -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]}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
})
|
Loading…
Reference in New Issue