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:
@@ -1,5 +1,5 @@
|
|||||||
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
|
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
|
||||||
|
|
||||||
APP_NAME = "Security Alert Center"
|
APP_NAME = "Security Alert Center"
|
||||||
APP_VERSION = "0.20.29"
|
APP_VERSION = "0.20.30"
|
||||||
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
|
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ from app.version import APP_NAME, APP_VERSION, APP_VERSION_LABEL
|
|||||||
|
|
||||||
|
|
||||||
def test_version_constants():
|
def test_version_constants():
|
||||||
assert APP_VERSION == "0.20.29"
|
assert APP_VERSION == "0.20.30"
|
||||||
assert APP_NAME == "Security Alert Center"
|
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"
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<template>
|
<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>
|
<h2>Залогиненные пользователи</h2>
|
||||||
<p class="muted">
|
<p class="muted">
|
||||||
Текущие сессии на хосте (Linux: loginctl, Windows: qwinsta). Требуются учётные данные admin в настройках SAC.
|
Текущие сессии на хосте (Linux: loginctl, Windows: qwinsta). Требуются учётные данные admin в настройках SAC.
|
||||||
@@ -38,22 +43,28 @@
|
|||||||
</table>
|
</table>
|
||||||
<p v-if="message" :class="messageOk ? 'success' : 'error'">{{ message }}</p>
|
<p v-if="message" :class="messageOk ? 'success' : 'error'">{{ message }}</p>
|
||||||
</template>
|
</template>
|
||||||
<button type="button" class="secondary" :disabled="loading" @click="load">
|
<div class="host-sessions-footer">
|
||||||
{{ loading ? "Загрузка…" : loaded ? "Обновить" : "Показать пользователей" }}
|
<button type="button" class="secondary" :disabled="loading" @click="load">
|
||||||
</button>
|
{{ loading ? "Загрузка…" : loaded ? "Обновить" : "Показать пользователей" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
||||||
import {
|
import {
|
||||||
fetchHostSessions,
|
fetchHostSessions,
|
||||||
terminateHostSession,
|
terminateHostSession,
|
||||||
type HostSessionItem,
|
type HostSessionItem,
|
||||||
} from "../api";
|
} 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 loading = ref(false);
|
||||||
const loaded = ref(false);
|
const loaded = ref(false);
|
||||||
const sessions = ref<HostSessionItem[]>([]);
|
const sessions = ref<HostSessionItem[]>([]);
|
||||||
@@ -61,6 +72,48 @@ const error = ref("");
|
|||||||
const message = ref("");
|
const message = ref("");
|
||||||
const messageOk = ref(true);
|
const messageOk = ref(true);
|
||||||
const terminatingId = ref<string | null>(null);
|
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() {
|
async function load() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@@ -79,6 +132,8 @@ async function load() {
|
|||||||
error.value = e instanceof Error ? e.message : "Ошибка загрузки сессий";
|
error.value = e instanceof Error ? e.message : "Ошибка загрузки сессий";
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
await nextTick();
|
||||||
|
updateMatchedHeight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,19 +155,55 @@ async function terminate(sessionId: string) {
|
|||||||
terminatingId.value = null;
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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 {
|
.host-sessions table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0.75rem 0;
|
margin: 0.75rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.host-sessions {
|
.host-sessions-footer {
|
||||||
margin-top: 0;
|
padding-top: 0.5rem;
|
||||||
}
|
|
||||||
|
|
||||||
.host-sessions button.secondary {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
||||||
export const APP_NAME = "Security Alert Center";
|
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}`;
|
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
|
||||||
|
|||||||
@@ -144,7 +144,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showAgentConfig" class="host-config-sessions-row">
|
<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>
|
<h2>Desired config (poll агента)</h2>
|
||||||
<p class="muted">Агент заберёт настройки при следующем poll. Revision: {{ host.agent_config_revision ?? 0 }}</p>
|
<p class="muted">Агент заберёт настройки при следующем poll. Revision: {{ host.agent_config_revision ?? 0 }}</p>
|
||||||
<form class="agent-config-form" @submit.prevent="saveAgentConfig">
|
<form class="agent-config-form" @submit.prevent="saveAgentConfig">
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<HostSessionsPanel :host-id="host.id" />
|
<HostSessionsPanel :host-id="host.id" :height-source="agentConfigSectionRef" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section v-if="inventory" class="host-inventory card">
|
<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 props = defineProps<{ id: string }>();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
|
const agentConfigSectionRef = ref<HTMLElement | null>(null);
|
||||||
const host = ref<HostDetail | null>(null);
|
const host = ref<HostDetail | null>(null);
|
||||||
const events = ref<EventListResponse | null>(null);
|
const events = ref<EventListResponse | null>(null);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -860,12 +861,6 @@ watch(
|
|||||||
align-items: flex-start;
|
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 {
|
.agent-config-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Reference in New Issue
Block a user