diff --git a/app/api/admin_routes.py b/app/api/admin_routes.py index 9f32125..afbe127 100644 --- a/app/api/admin_routes.py +++ b/app/api/admin_routes.py @@ -41,6 +41,7 @@ SourceOut, TargetIn, TargetOut, + TargetPatch, TypeIn, TypeOut, ) @@ -276,6 +277,35 @@ return TargetOut(id=target.id, enabled=target.enabled, created_at=target.created_at, **payload.model_dump()) +@router.patch("/targets/{target_id}", response_model=TargetOut) +def patch_target( + target_id: int, + payload: TargetPatch, + user: AuthenticatedUser = Depends(require_admin), + db: Session = Depends(get_db), +) -> TargetOut: + target = db.get(ChannelTarget, target_id) + if target is None: + raise HTTPException(status_code=404, detail="Цель не найдена") + data = payload.model_dump(exclude_unset=True) + if "name" in data and (data["name"] != target.name): + dup = db.execute( + select(ChannelTarget.id).where( + ChannelTarget.channel == target.channel, ChannelTarget.name == data["name"] + ) + ).scalar_one_or_none() + if dup: + raise _conflict(f"Цель {target.channel}/{data['name']} уже есть") + for field, value in data.items(): + setattr(target, field, value) + db.commit() + db.refresh(target) + return TargetOut( + id=target.id, channel=target.channel, name=target.name, description=target.description, + config=target.config, enabled=target.enabled, created_at=target.created_at, + ) + + @router.delete("/targets/{target_id}", status_code=204) def delete_target( target_id: int, diff --git a/app/api/admin_schemas.py b/app/api/admin_schemas.py index 76e16f2..cb01c4c 100644 --- a/app/api/admin_schemas.py +++ b/app/api/admin_schemas.py @@ -76,6 +76,15 @@ created_at: datetime +class TargetPatch(BaseModel): + """Частичное обновление цели; канал не меняется (ссылки из правил).""" + + name: str | None = Field(default=None, min_length=1, max_length=120) + description: str | None = None + config: dict | None = None + enabled: bool | None = None + + # --- rules --- class RuleActionIn(BaseModel): diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 4daef8d..1b74ab5 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -175,4 +175,14 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/frontend/src/api.js b/frontend/src/api.js index f9b67d7..4dddea0 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -130,6 +130,7 @@ // Цели каналов listTargets: () => apiFetch("/api/v1/admin/targets"), createTarget: (data) => apiFetch("/api/v1/admin/targets", { method: "POST", body: JSON.stringify(data) }), + patchTarget: (id, data) => apiFetch(`/api/v1/admin/targets/${id}`, { method: "PATCH", body: JSON.stringify(data) }), deleteTarget: (id) => apiFetch(`/api/v1/admin/targets/${id}`, { method: "DELETE" }), // Правила diff --git a/frontend/src/ui.js b/frontend/src/ui.js index 4657335..b3c6dde 100644 --- a/frontend/src/ui.js +++ b/frontend/src/ui.js @@ -70,10 +70,12 @@ /** * Заголовок колонки GnTable с тултипом кита: GnTable рендерит column.label - * как есть, поэтому передаём VNode с GnTooltip вокруг подписи. + * как есть, поэтому передаём VNode с GnTooltip. Класс th-tip переключает + * панель тултипа вниз — иначе её обрезает overflow у .table-wrapper + * (см. глобальный стиль в App.vue). */ export function th(label, text) { - return h(GnTooltip, { text }, () => label); + return h(GnTooltip, { text, class: "th-tip" }, () => label); } export function truncate(text, n = 80) { diff --git a/frontend/src/views/DeliveriesView.vue b/frontend/src/views/DeliveriesView.vue index c13059e..09cee46 100644 --- a/frontend/src/views/DeliveriesView.vue +++ b/frontend/src/views/DeliveriesView.vue @@ -69,7 +69,9 @@
- +
+ +
@@ -120,8 +122,10 @@ align-items: flex-end; gap: var(--gn-space-4); } -.filters > :nth-child(3) { - margin-bottom: var(--gn-space-3); +/* вход селекта сидит на 5px выше низа form-group (margin-bottom подписи + в ките) — тем же отступом поднимаем кнопку, центры совпадают */ +.refresh-slot { + margin-bottom: var(--gn-space-5); } .event-id { font-size: 0.78em; diff --git a/frontend/src/views/RuleDetailView.vue b/frontend/src/views/RuleDetailView.vue index 9279659..bc4eecd 100644 --- a/frontend/src/views/RuleDetailView.vue +++ b/frontend/src/views/RuleDetailView.vue @@ -296,10 +296,20 @@ border-radius: 6px; background: var(--gn-surface-panel-muted); display: flex; - align-items: baseline; + flex-direction: column; + gap: var(--gn-space-2); +} +.action-head { + display: flex; + align-items: center; gap: var(--gn-space-3); flex-wrap: wrap; } +.action-body { + display: flex; + align-items: baseline; + gap: var(--gn-space-3); +} .action-num { opacity: 0.6; font-size: 0.85em; diff --git a/frontend/src/views/RulesView.vue b/frontend/src/views/RulesView.vue index d04075e..09ffbad 100644 --- a/frontend/src/views/RulesView.vue +++ b/frontend/src/views/RulesView.vue @@ -21,7 +21,7 @@ import { useToast } from "gnexus-ui-kit/vue"; import { useRouter } from "vue-router"; import { api } from "../api.js"; -import { parseList, shortJson, th, truncate } from "../ui.js"; +import { parseList, th, truncate } from "../ui.js"; const toast = useToast(); const router = useRouter(); @@ -187,17 +187,14 @@ :columns="[ { key: 'name', label: 'Правило' }, { key: 'weight', label: th('Вес', WEIGHT_TIP) }, - { key: 'conditions', label: th('Условия', 'Source, subjects, actions, priority_min и требования к payload — правило применяется, только если всё совпало') }, { key: 'throttle_seconds', label: th('Шумодав', THROTTLE_TIP) }, - { key: 'actionsList', label: th('Действия', 'Куда доставить: канал и цель. internal_log без цели пишет журнал Synapse') }, + { key: 'actionsList', label: th('Действия', 'Куда доставить: канал и цель. internal_log без цели пишет журнал Synapse. Условия — на странице правила') }, { key: 'enabled', label: 'Вкл./выкл.' }, { key: 'actions', label: '' }, ]" > -