Files
portal/app/resources/js/components/admin/ImpersonationBanner.vue
T

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>