feat(env): add global production context and safety indicators
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
* @UX_TEST: ActivityClick -> {click: activity button, expected: task drawer opens}
|
||||
*/
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import { page } from "$app/stores";
|
||||
import { activityStore } from "$lib/stores/activity.js";
|
||||
import {
|
||||
@@ -30,6 +30,12 @@
|
||||
import { toggleAssistantChat } from "$lib/stores/assistantChat.js";
|
||||
import Icon from "$lib/ui/Icon.svelte";
|
||||
import LanguageSwitcher from "$lib/ui/LanguageSwitcher.svelte";
|
||||
import {
|
||||
environmentContextStore,
|
||||
initializeEnvironmentContext,
|
||||
setSelectedEnvironment,
|
||||
selectedEnvironmentStore,
|
||||
} from "$lib/stores/environmentContext.js";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -40,6 +46,10 @@
|
||||
$: activeCount = $activityStore?.activeCount || 0;
|
||||
$: recentTasks = $activityStore?.recentTasks || [];
|
||||
$: user = $auth?.user || null;
|
||||
$: globalEnvironments = $environmentContextStore?.environments || [];
|
||||
$: globalSelectedEnvId = $environmentContextStore?.selectedEnvId || "";
|
||||
$: globalSelectedEnv = $selectedEnvironmentStore;
|
||||
$: isProdContext = Boolean(globalSelectedEnv?.is_production);
|
||||
|
||||
function toggleUserMenu(event) {
|
||||
event.stopPropagation();
|
||||
@@ -94,6 +104,14 @@
|
||||
event.stopPropagation();
|
||||
toggleMobileSidebar();
|
||||
}
|
||||
|
||||
function handleGlobalEnvironmentChange(event) {
|
||||
setSelectedEnvironment(event.target.value);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await initializeEnvironmentContext();
|
||||
});
|
||||
</script>
|
||||
|
||||
<nav
|
||||
@@ -137,6 +155,32 @@
|
||||
|
||||
<!-- Nav Actions -->
|
||||
<div class="flex items-center gap-3 md:gap-4">
|
||||
{#if globalEnvironments.length > 0}
|
||||
<div class="hidden lg:flex items-center gap-2">
|
||||
<select
|
||||
class="h-9 rounded-lg border px-3 text-sm font-medium focus:outline-none focus:ring-2
|
||||
{isProdContext
|
||||
? 'border-red-300 bg-red-50 text-red-900 focus:ring-red-200'
|
||||
: 'border-slate-300 bg-white text-slate-700 focus:ring-sky-200'}"
|
||||
value={globalSelectedEnvId}
|
||||
on:change={handleGlobalEnvironmentChange}
|
||||
aria-label={$t.dashboard?.environment || "Environment"}
|
||||
title={$t.dashboard?.environment || "Environment"}
|
||||
>
|
||||
{#each globalEnvironments as env}
|
||||
<option value={env.id}>
|
||||
{env.name}{env.is_production ? " [PROD]" : ""}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if isProdContext}
|
||||
<span class="rounded-md bg-red-600 px-2 py-1 text-xs font-bold text-white">
|
||||
PROD
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<LanguageSwitcher />
|
||||
|
||||
<!-- Assistant -->
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
"on": "On",
|
||||
"off": "Off",
|
||||
"per_page": "per page",
|
||||
"close_modal": "Close modal"
|
||||
,
|
||||
"close_modal": "Close modal",
|
||||
"prod_context_warning": "PROD context active",
|
||||
"choose_environment": "-- Choose an environment --"
|
||||
},
|
||||
"nav": {
|
||||
@@ -113,6 +113,7 @@
|
||||
"env_add": "Add Environment",
|
||||
"env_edit": "Edit Environment",
|
||||
"env_default": "Default Environment",
|
||||
"env_production": "Production environment",
|
||||
"env_test": "Test",
|
||||
"env_delete": "Delete",
|
||||
"storage_title": "File Storage Configuration",
|
||||
@@ -502,7 +503,15 @@
|
||||
"clear_failed": "Clear Failed",
|
||||
"clear_awaiting_input": "Clear Awaiting Input",
|
||||
"keys": "keys",
|
||||
"mappings": "Mappings"
|
||||
"mappings": "Mappings",
|
||||
"summary_report": "Summary report",
|
||||
"observability_warnings": "Warning",
|
||||
"open_dashboard_target": "Open dashboard in {env}",
|
||||
"open_dashboard_target_fallback": "Open dashboard",
|
||||
"show_diff": "Show diff",
|
||||
"diff_preview": "Diff preview",
|
||||
"no_diff_available": "Diff is not available",
|
||||
"summary_link_unavailable": "Deep link or diff is unavailable for this task"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Login",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"off": "Выкл",
|
||||
"per_page": "на страницу",
|
||||
"close_modal": "Закрыть модальное окно",
|
||||
"prod_context_warning": "Активен PROD-контекст",
|
||||
"choose_environment": "-- Выберите окружение --"
|
||||
},
|
||||
"nav": {
|
||||
@@ -111,6 +112,7 @@
|
||||
"env_add": "Добавить окружение",
|
||||
"env_edit": "Редактировать окружение",
|
||||
"env_default": "Окружение по умолчанию",
|
||||
"env_production": "Production окружение",
|
||||
"env_test": "Тест",
|
||||
"env_delete": "Удалить",
|
||||
"storage_title": "Настройка хранилища файлов",
|
||||
@@ -500,7 +502,15 @@
|
||||
"clear_failed": "Очистить ошибки",
|
||||
"clear_awaiting_input": "Очистить ожидающие ввода",
|
||||
"keys": "ключей",
|
||||
"mappings": "Маппинги"
|
||||
"mappings": "Маппинги",
|
||||
"summary_report": "Сводный отчет",
|
||||
"observability_warnings": "Внимание",
|
||||
"open_dashboard_target": "Открыть дашборд в {env}",
|
||||
"open_dashboard_target_fallback": "Открыть дашборд",
|
||||
"show_diff": "Показать Diff",
|
||||
"diff_preview": "Diff предпросмотр",
|
||||
"no_diff_available": "Diff недоступен",
|
||||
"summary_link_unavailable": "Ссылка или diff недоступны для этой задачи"
|
||||
},
|
||||
"auth": {
|
||||
"login": "Вход",
|
||||
|
||||
116
frontend/src/lib/stores/environmentContext.js
Normal file
116
frontend/src/lib/stores/environmentContext.js
Normal file
@@ -0,0 +1,116 @@
|
||||
// [DEF:environmentContext:Store]
|
||||
// @TIER: STANDARD
|
||||
// @PURPOSE: Global selected environment context for navigation and safety cues.
|
||||
// @LAYER: UI-State
|
||||
|
||||
import { derived, get, writable } from "svelte/store";
|
||||
import { api } from "$lib/api.js";
|
||||
|
||||
const INITIAL_STATE = {
|
||||
environments: [],
|
||||
selectedEnvId: "",
|
||||
isLoading: false,
|
||||
isLoaded: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
const SELECTED_ENV_KEY = "selected_env_id";
|
||||
const contextStore = writable(INITIAL_STATE);
|
||||
|
||||
function getStoredSelectedEnvId() {
|
||||
if (typeof window === "undefined") return "";
|
||||
return localStorage.getItem(SELECTED_ENV_KEY) || "";
|
||||
}
|
||||
|
||||
function persistSelectedEnvId(envId) {
|
||||
if (typeof window === "undefined") return;
|
||||
if (!envId) {
|
||||
localStorage.removeItem(SELECTED_ENV_KEY);
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(SELECTED_ENV_KEY, envId);
|
||||
}
|
||||
|
||||
function resolveSelectedEnvId(environments, preferredEnvId) {
|
||||
if (!Array.isArray(environments) || environments.length === 0) return "";
|
||||
if (preferredEnvId && environments.some((env) => env.id === preferredEnvId)) {
|
||||
return preferredEnvId;
|
||||
}
|
||||
const stored = getStoredSelectedEnvId();
|
||||
if (stored && environments.some((env) => env.id === stored)) {
|
||||
return stored;
|
||||
}
|
||||
return environments[0].id;
|
||||
}
|
||||
|
||||
function applySelectedEnvId(selectedEnvId) {
|
||||
contextStore.update((state) => {
|
||||
const exists = state.environments.some((env) => env.id === selectedEnvId);
|
||||
const nextSelectedEnvId = exists ? selectedEnvId : "";
|
||||
persistSelectedEnvId(nextSelectedEnvId);
|
||||
return { ...state, selectedEnvId: nextSelectedEnvId };
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshEnvironmentContext(preferredEnvId = "") {
|
||||
contextStore.update((state) => ({ ...state, isLoading: true, error: null }));
|
||||
try {
|
||||
const environments = await api.getEnvironmentsList();
|
||||
const current = get(contextStore).selectedEnvId;
|
||||
const selectedEnvId = resolveSelectedEnvId(
|
||||
environments,
|
||||
preferredEnvId || current,
|
||||
);
|
||||
persistSelectedEnvId(selectedEnvId);
|
||||
contextStore.update((state) => ({
|
||||
...state,
|
||||
environments,
|
||||
selectedEnvId,
|
||||
isLoading: false,
|
||||
isLoaded: true,
|
||||
error: null,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[environmentContext][Coherence:Failed] Failed to refresh environments",
|
||||
error,
|
||||
);
|
||||
contextStore.update((state) => ({
|
||||
...state,
|
||||
environments: [],
|
||||
selectedEnvId: "",
|
||||
isLoading: false,
|
||||
isLoaded: true,
|
||||
error: error?.message || "Failed to load environments",
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeEnvironmentContext() {
|
||||
const state = get(contextStore);
|
||||
if (state.isLoading || state.isLoaded) return;
|
||||
await refreshEnvironmentContext();
|
||||
}
|
||||
|
||||
export const environmentContextStore = {
|
||||
subscribe: contextStore.subscribe,
|
||||
};
|
||||
|
||||
export function setSelectedEnvironment(envId) {
|
||||
applySelectedEnvId(envId);
|
||||
}
|
||||
|
||||
export { refreshEnvironmentContext, initializeEnvironmentContext };
|
||||
|
||||
export const selectedEnvironmentStore = derived(
|
||||
environmentContextStore,
|
||||
($context) =>
|
||||
$context.environments.find((env) => env.id === $context.selectedEnvId) ||
|
||||
null,
|
||||
);
|
||||
|
||||
export const isProductionContextStore = derived(
|
||||
selectedEnvironmentStore,
|
||||
($selectedEnvironment) => Boolean($selectedEnvironment?.is_production),
|
||||
);
|
||||
// [/DEF:environmentContext:Store]
|
||||
Reference in New Issue
Block a user