86 lines
2.9 KiB
Vue
86 lines
2.9 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* Глобальный индикатор активных impersonation-сессий (audit B5 / Ю-1).
|
|
*
|
|
* Размещён в AdminLayout над <RouterView> — виден на всех /admin/* страницах.
|
|
* На MVP saas-admin auth нет и реального переключения сессии нет, поэтому
|
|
* показываем счётчик ВСЕХ активных сессий (impersonationActive() =
|
|
* used_at != null AND session_ended_at == null). Polling 30 c — сессия может
|
|
* стартовать/завершиться, пока админ остаётся в админке (AdminLayout
|
|
* persistent, перемонтируется только <RouterView>).
|
|
*
|
|
* Если активных сессий 0 — компонент не рендерит ничего.
|
|
*/
|
|
import { computed, onMounted, ref } from 'vue';
|
|
import { impersonationActive, type ImpersonationActiveSession } from '../../api/admin';
|
|
import { usePolling } from '../../composables/usePolling';
|
|
import { POLLING_INTERVAL_MS } from '../../constants/polling';
|
|
|
|
const sessions = ref<ImpersonationActiveSession[]>([]);
|
|
|
|
async function load(): Promise<void> {
|
|
try {
|
|
sessions.value = await impersonationActive();
|
|
} catch {
|
|
// Баннер не критичен — ошибку детально покажет AdminImpersonationView.
|
|
// Сохраняем прежнее значение sessions, не падаем.
|
|
}
|
|
}
|
|
|
|
const count = computed(() => sessions.value.length);
|
|
|
|
const label = computed(() => {
|
|
if (count.value === 1) {
|
|
const s = sessions.value[0];
|
|
return `Активна impersonation-сессия: ${s.tenant_name ?? `тенант #${s.tenant_id}`}`;
|
|
}
|
|
return `Активны impersonation-сессии: ${count.value}`;
|
|
});
|
|
|
|
onMounted(load);
|
|
usePolling(load, { intervalMs: POLLING_INTERVAL_MS });
|
|
|
|
defineExpose({ sessions, load });
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="count > 0" class="impersonation-banner" role="status" data-testid="impersonation-banner">
|
|
<v-icon size="16" class="impersonation-banner__icon">mdi-account-switch</v-icon>
|
|
<span class="impersonation-banner__label">{{ label }}</span>
|
|
<RouterLink
|
|
to="/admin/impersonation"
|
|
class="impersonation-banner__link"
|
|
data-testid="impersonation-banner-link"
|
|
>
|
|
Открыть
|
|
</RouterLink>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.impersonation-banner {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
background: #fff4e0;
|
|
border-bottom: 1px solid #f0d8a8;
|
|
color: #8a5a00;
|
|
font-size: 13px;
|
|
padding: 8px 24px;
|
|
}
|
|
.impersonation-banner__icon {
|
|
color: #b87400;
|
|
}
|
|
.impersonation-banner__label {
|
|
flex: 1;
|
|
}
|
|
.impersonation-banner__link {
|
|
color: #0f6e56;
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
}
|
|
.impersonation-banner__link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
</style>
|