mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +08:00
refactor(approval-grants-ui): rebuild create dialog to match ToolGuard pattern
This commit is contained in:
parent
b9c5ec03d4
commit
6a754f5bfa
@ -164,96 +164,137 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Create dialog -->
|
<!--
|
||||||
<el-dialog
|
Create dialog — matches the ToolGuard "edit rule" modal pattern:
|
||||||
v-model="dialogOpen"
|
Teleport to body + native .modal-overlay / .modal / .modal-header /
|
||||||
:title="dialogWorkspaceWide ? t('approval.grant.createWorkspaceBtn') : t('approval.grant.createBtn')"
|
.modal-body / .modal-footer + .form-grid / .form-group / .form-input.
|
||||||
width="560px"
|
The workspace-wide path reuses the same modal with a red warning banner
|
||||||
:close-on-click-modal="false"
|
at the top so the destructive variant looks identifiably different
|
||||||
destroy-on-close
|
from the routine create flow.
|
||||||
>
|
-->
|
||||||
<el-alert
|
<Teleport to="body">
|
||||||
v-if="dialogWorkspaceWide"
|
<div v-if="dialogOpen" class="modal-overlay" @click.self="dialogOpen = false">
|
||||||
type="warning"
|
<div class="modal">
|
||||||
:closable="false"
|
<div class="modal-header">
|
||||||
show-icon
|
<h3>
|
||||||
class="warning-banner"
|
<el-icon v-if="dialogWorkspaceWide" :size="18" class="modal-header__icon"><Unlock /></el-icon>
|
||||||
>
|
{{ dialogWorkspaceWide
|
||||||
{{ t('approval.grant.createWorkspaceWarning') }}
|
? t('approval.grant.createWorkspaceBtn')
|
||||||
</el-alert>
|
: t('approval.grant.createBtn') }}
|
||||||
|
</h3>
|
||||||
<el-form :model="form" label-width="120px" class="grant-form">
|
<button class="modal-close" @click="dialogOpen = false">×</button>
|
||||||
<el-form-item :label="t('approval.grant.form.scopeType')">
|
</div>
|
||||||
<el-select v-model="form.scopeType" :disabled="dialogWorkspaceWide" style="width: 100%">
|
<div class="modal-body">
|
||||||
<el-option label="CONVERSATION" value="CONVERSATION" />
|
<div v-if="dialogWorkspaceWide" class="danger-banner">
|
||||||
<el-option label="AGENT" value="AGENT" />
|
<el-icon :size="16"><WarningFilled /></el-icon>
|
||||||
<el-option label="USER" value="USER" />
|
<span>{{ t('approval.grant.createWorkspaceWarning') }}</span>
|
||||||
<el-option label="WORKSPACE" value="WORKSPACE" />
|
</div>
|
||||||
</el-select>
|
<div class="form-grid">
|
||||||
</el-form-item>
|
<div class="form-group">
|
||||||
<el-form-item :label="t('approval.grant.form.scopeId')">
|
<label>{{ t('approval.grant.form.scopeType') }} <span class="required">*</span></label>
|
||||||
<el-input
|
<select
|
||||||
v-model.trim="form.scopeId"
|
v-model="form.scopeType"
|
||||||
type="text"
|
class="form-input"
|
||||||
inputmode="numeric"
|
:disabled="dialogWorkspaceWide"
|
||||||
pattern="\d*"
|
>
|
||||||
placeholder="snowflake id"
|
<option value="CONVERSATION">CONVERSATION</option>
|
||||||
/>
|
<option value="AGENT">AGENT</option>
|
||||||
</el-form-item>
|
<option value="USER">USER</option>
|
||||||
<el-form-item :label="t('approval.grant.form.toolName')">
|
<option value="WORKSPACE">WORKSPACE</option>
|
||||||
<el-input v-model.trim="form.toolName" :disabled="dialogWorkspaceWide" />
|
</select>
|
||||||
</el-form-item>
|
</div>
|
||||||
<el-form-item :label="t('approval.grant.form.ruleId')">
|
<div class="form-group">
|
||||||
<el-input v-model.trim="form.ruleId" placeholder="(optional)" />
|
<label>{{ t('approval.grant.form.scopeId') }} <span class="required">*</span></label>
|
||||||
</el-form-item>
|
<input
|
||||||
<el-form-item :label="t('approval.grant.form.maxSeverity')">
|
v-model.trim="form.scopeId"
|
||||||
<el-select v-model="form.maxSeverity" style="width: 100%">
|
type="text"
|
||||||
<el-option label="LOW" value="LOW" />
|
inputmode="numeric"
|
||||||
<el-option label="MEDIUM" value="MEDIUM" />
|
pattern="\d*"
|
||||||
<el-option label="HIGH" value="HIGH" />
|
placeholder="snowflake id"
|
||||||
</el-select>
|
class="form-input"
|
||||||
</el-form-item>
|
/>
|
||||||
<el-form-item :label="t('approval.grant.form.grantKind')">
|
</div>
|
||||||
<el-select v-model="form.grantKind" style="width: 100%">
|
<div class="form-group">
|
||||||
<el-option :label="t('approval.grant.kind.always')" value="ALWAYS" />
|
<label>{{ t('approval.grant.form.toolName') }}</label>
|
||||||
<el-option :label="t('approval.grant.kind.until')" value="UNTIL_TIMESTAMP" />
|
<input
|
||||||
<el-option :label="t('approval.grant.kind.conversationEnd')" value="UNTIL_CONVERSATION_END" />
|
v-model.trim="form.toolName"
|
||||||
</el-select>
|
class="form-input mono"
|
||||||
</el-form-item>
|
:disabled="dialogWorkspaceWide"
|
||||||
<el-form-item
|
placeholder="(empty = any tool)"
|
||||||
v-if="form.grantKind === 'UNTIL_TIMESTAMP'"
|
/>
|
||||||
:label="t('approval.grant.form.expireAt')"
|
</div>
|
||||||
>
|
<div class="form-group">
|
||||||
<el-date-picker
|
<label>{{ t('approval.grant.form.ruleId') }}</label>
|
||||||
v-model="form.expireAt"
|
<input
|
||||||
type="datetime"
|
v-model.trim="form.ruleId"
|
||||||
style="width: 100%"
|
class="form-input mono"
|
||||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
placeholder="(optional)"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</div>
|
||||||
<el-form-item :label="t('approval.grant.form.note')">
|
<div class="form-group">
|
||||||
<el-input v-model.trim="form.note" />
|
<label>{{ t('approval.grant.form.maxSeverity') }}</label>
|
||||||
</el-form-item>
|
<select v-model="form.maxSeverity" class="form-input">
|
||||||
<el-form-item
|
<option value="LOW">LOW</option>
|
||||||
v-if="requiresPassword"
|
<option value="MEDIUM">MEDIUM</option>
|
||||||
:label="t('approval.grant.form.password')"
|
<option value="HIGH">HIGH</option>
|
||||||
>
|
</select>
|
||||||
<el-input
|
</div>
|
||||||
v-model="form.password"
|
<div class="form-group">
|
||||||
type="password"
|
<label>{{ t('approval.grant.form.grantKind') }}</label>
|
||||||
show-password
|
<select v-model="form.grantKind" class="form-input">
|
||||||
:prefix-icon="Lock"
|
<option value="ALWAYS">{{ t('approval.grant.kind.always') }}</option>
|
||||||
/>
|
<option value="UNTIL_TIMESTAMP">{{ t('approval.grant.kind.until') }}</option>
|
||||||
</el-form-item>
|
<option value="UNTIL_CONVERSATION_END">{{ t('approval.grant.kind.conversationEnd') }}</option>
|
||||||
</el-form>
|
</select>
|
||||||
|
</div>
|
||||||
<template #footer>
|
<div v-if="form.grantKind === 'UNTIL_TIMESTAMP'" class="form-group">
|
||||||
<el-button @click="dialogOpen = false">{{ t('common.cancel') }}</el-button>
|
<label>{{ t('approval.grant.form.expireAt') }} <span class="required">*</span></label>
|
||||||
<el-button type="primary" :loading="creating" @click="submitCreate">
|
<input
|
||||||
{{ t('common.confirm') }}
|
v-model="form.expireAt"
|
||||||
</el-button>
|
type="datetime-local"
|
||||||
</template>
|
class="form-input"
|
||||||
</el-dialog>
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group form-group--full">
|
||||||
|
<label>{{ t('approval.grant.form.note') }}</label>
|
||||||
|
<input
|
||||||
|
v-model.trim="form.note"
|
||||||
|
class="form-input"
|
||||||
|
:placeholder="dialogWorkspaceWide ? '请说明为什么需要全工具白名单' : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="requiresPassword" class="form-group form-group--full">
|
||||||
|
<label>
|
||||||
|
{{ t('approval.grant.form.password') }}
|
||||||
|
<span class="required">*</span>
|
||||||
|
</label>
|
||||||
|
<div class="password-wrap">
|
||||||
|
<el-icon :size="14" class="password-wrap__icon"><Lock /></el-icon>
|
||||||
|
<input
|
||||||
|
v-model="form.password"
|
||||||
|
type="password"
|
||||||
|
class="form-input form-input--with-icon"
|
||||||
|
autocomplete="current-password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn-secondary" @click="dialogOpen = false">
|
||||||
|
{{ t('common.cancel') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn-primary"
|
||||||
|
:disabled="creating"
|
||||||
|
@click="submitCreate"
|
||||||
|
>
|
||||||
|
{{ creating ? t('common.processing') : t('common.confirm') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -268,6 +309,7 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
Refresh,
|
Refresh,
|
||||||
Unlock,
|
Unlock,
|
||||||
|
WarningFilled,
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import { approvalApi } from '@/api'
|
import { approvalApi } from '@/api'
|
||||||
import type {
|
import type {
|
||||||
@ -559,11 +601,145 @@ onMounted(loadGrants)
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning-banner {
|
/* ─── Modal (mirrors ToolGuard / edit-rule pattern) ──────────────────── */
|
||||||
margin-bottom: 16px;
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 2000;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
.modal {
|
||||||
|
background: var(--mc-surface-primary, #fff);
|
||||||
|
border-radius: 12px;
|
||||||
|
width: min(640px, 100%);
|
||||||
|
max-height: 88vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.18);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid var(--mc-border-light, #e5e7eb);
|
||||||
|
}
|
||||||
|
.modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--mc-text-primary, #0f172a);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.modal-header__icon {
|
||||||
|
color: var(--mc-danger, #b91c1c);
|
||||||
|
}
|
||||||
|
.modal-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--mc-text-tertiary, #94a3b8);
|
||||||
|
}
|
||||||
|
.modal-close:hover { color: var(--mc-text-primary, #0f172a); }
|
||||||
|
.modal-body {
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.modal-footer {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-top: 1px solid var(--mc-border-light, #e5e7eb);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grant-form {
|
/* Danger banner inside the workspace-wide create modal — bright red so the
|
||||||
padding-top: 4px;
|
destructive variant looks instantly different from the routine flow. */
|
||||||
|
.danger-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
background: #fef2f2;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #991b1b;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.danger-banner .el-icon { flex-shrink: 0; margin-top: 1px; }
|
||||||
|
|
||||||
|
/* Form grid layout — two columns on wide modal, one column when narrow.
|
||||||
|
form-group--full breaks across both columns (note, password). */
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 14px 16px;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.form-group--full { grid-column: 1 / -1; }
|
||||||
|
.form-group label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--mc-text-secondary, #475569);
|
||||||
|
}
|
||||||
|
.form-group label .required {
|
||||||
|
color: var(--mc-danger, #b91c1c);
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
.form-input {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid var(--mc-border-light, #e5e7eb);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--mc-surface-primary, #fff);
|
||||||
|
color: var(--mc-text-primary, #0f172a);
|
||||||
|
font-size: 13px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--mc-primary, #d97757);
|
||||||
|
}
|
||||||
|
.form-input:disabled {
|
||||||
|
background: var(--mc-surface-tertiary, #f1f5f9);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.form-input.mono {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Password input with the Lock icon as a visual prefix. */
|
||||||
|
.password-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.password-wrap__icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
color: var(--mc-text-tertiary, #94a3b8);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.form-input--with-icon { padding-left: 32px; }
|
||||||
|
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
.form-grid { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user