fix(frontend): match sessions panel height to agent config until loaded (0.20.30)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
PTah
2026-06-22 10:31:38 +10:00
parent ad2425b0ff
commit fc66b57e78
5 changed files with 110 additions and 24 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
APP_NAME = "Security Alert Center"
APP_VERSION = "0.20.29"
APP_VERSION = "0.20.30"
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
+2 -2
View File
@@ -4,6 +4,6 @@ from app.version import APP_NAME, APP_VERSION, APP_VERSION_LABEL
def test_version_constants():
assert APP_VERSION == "0.20.29"
assert APP_VERSION == "0.20.30"
assert APP_NAME == "Security Alert Center"
assert APP_VERSION_LABEL == "Security Alert Center v.0.20.29"
assert APP_VERSION_LABEL == "Security Alert Center v.0.20.30"
+100 -9
View File
@@ -1,5 +1,10 @@
<template>
<section class="card host-sessions">
<section
ref="rootRef"
class="card host-sessions"
:class="{ 'host-sessions--expanded': loaded }"
:style="panelMinHeight ? { minHeight: panelMinHeight } : undefined"
>
<h2>Залогиненные пользователи</h2>
<p class="muted">
Текущие сессии на хосте (Linux: loginctl, Windows: qwinsta). Требуются учётные данные admin в настройках SAC.
@@ -38,22 +43,28 @@
</table>
<p v-if="message" :class="messageOk ? 'success' : 'error'">{{ message }}</p>
</template>
<div class="host-sessions-footer">
<button type="button" class="secondary" :disabled="loading" @click="load">
{{ loading ? "Загрузка" : loaded ? "Обновить" : "Показать пользователей" }}
</button>
</div>
</section>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import {
fetchHostSessions,
terminateHostSession,
type HostSessionItem,
} from "../api";
const props = defineProps<{ hostId: number }>();
const props = defineProps<{
hostId: number;
heightSource?: HTMLElement | null;
}>();
const rootRef = ref<HTMLElement | null>(null);
const loading = ref(false);
const loaded = ref(false);
const sessions = ref<HostSessionItem[]>([]);
@@ -61,6 +72,48 @@ const error = ref("");
const message = ref("");
const messageOk = ref(true);
const terminatingId = ref<string | null>(null);
const panelMinHeight = ref<string | undefined>(undefined);
let resizeObserver: ResizeObserver | null = null;
let observedSource: HTMLElement | null = null;
function updateMatchedHeight() {
if (loaded.value) {
panelMinHeight.value = undefined;
return;
}
const source = props.heightSource;
if (!source) {
panelMinHeight.value = undefined;
return;
}
panelMinHeight.value = `${source.offsetHeight}px`;
}
function bindHeightObserver(source: HTMLElement | null | undefined) {
if (observedSource && resizeObserver) {
resizeObserver.unobserve(observedSource);
observedSource = null;
}
if (!source || typeof ResizeObserver === "undefined") {
updateMatchedHeight();
return;
}
if (!resizeObserver) {
resizeObserver = new ResizeObserver(() => updateMatchedHeight());
}
resizeObserver.observe(source);
observedSource = source;
updateMatchedHeight();
}
function resetSessionsState() {
loaded.value = false;
sessions.value = [];
error.value = "";
message.value = "";
terminatingId.value = null;
}
async function load() {
loading.value = true;
@@ -79,6 +132,8 @@ async function load() {
error.value = e instanceof Error ? e.message : "Ошибка загрузки сессий";
} finally {
loading.value = false;
await nextTick();
updateMatchedHeight();
}
}
@@ -100,19 +155,55 @@ async function terminate(sessionId: string) {
terminatingId.value = null;
}
}
watch(
() => props.heightSource,
(source) => {
bindHeightObserver(source ?? null);
},
);
watch(
() => props.hostId,
() => {
resetSessionsState();
nextTick(() => updateMatchedHeight());
},
);
watch(loaded, () => {
nextTick(() => updateMatchedHeight());
});
onMounted(() => {
bindHeightObserver(props.heightSource ?? null);
});
onUnmounted(() => {
resizeObserver?.disconnect();
resizeObserver = null;
observedSource = null;
});
</script>
<style scoped>
.host-sessions {
margin-top: 0;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.host-sessions:not(.host-sessions--expanded) .host-sessions-footer {
margin-top: auto;
}
.host-sessions table {
width: 100%;
margin: 0.75rem 0;
}
.host-sessions {
margin-top: 0;
}
.host-sessions button.secondary {
margin-top: 0.5rem;
.host-sessions-footer {
padding-top: 0.5rem;
}
</style>
+1 -1
View File
@@ -1,4 +1,4 @@
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
export const APP_NAME = "Security Alert Center";
export const APP_VERSION = "0.20.29";
export const APP_VERSION = "0.20.30";
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
+3 -8
View File
@@ -144,7 +144,7 @@
</div>
<div v-if="showAgentConfig" class="host-config-sessions-row">
<section class="card host-agent-config">
<section ref="agentConfigSectionRef" class="card host-agent-config">
<h2>Desired config (poll агента)</h2>
<p class="muted">Агент заберёт настройки при следующем poll. Revision: {{ host.agent_config_revision ?? 0 }}</p>
<form class="agent-config-form" @submit.prevent="saveAgentConfig">
@@ -167,7 +167,7 @@
</form>
</section>
<HostSessionsPanel :host-id="host.id" />
<HostSessionsPanel :host-id="host.id" :height-source="agentConfigSectionRef" />
</div>
<section v-if="inventory" class="host-inventory card">
@@ -305,6 +305,7 @@ const HOST_ACTION_FEEDBACK_MS = 30_000;
const props = defineProps<{ id: string }>();
const route = useRoute();
const agentConfigSectionRef = ref<HTMLElement | null>(null);
const host = ref<HostDetail | null>(null);
const events = ref<EventListResponse | null>(null);
const loading = ref(false);
@@ -860,12 +861,6 @@ watch(
align-items: flex-start;
}
.host-config-sessions-row > .host-agent-config,
.host-config-sessions-row > .host-sessions {
flex: 1 1 22rem;
min-width: min(100%, 22rem);
}
.agent-config-form {
display: flex;
flex-direction: column;