core/panel/routes/env.tsx
Untone 952b294345
All checks were successful
Deploy on push / deploy (push) Successful in 6s
0.5.8-panel-upgrade-community-crud-fix
2025-06-30 21:25:26 +03:00

276 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Component, createSignal, For, Show } from 'solid-js'
import { query } from '../graphql'
import type { EnvSection, EnvVariable, Query } from '../graphql/generated/schema'
import { ADMIN_UPDATE_ENV_VARIABLE_MUTATION } from '../graphql/mutations'
import { ADMIN_GET_ENV_VARIABLES_QUERY } from '../graphql/queries'
import EnvVariableModal from '../modals/EnvVariableModal'
import styles from '../styles/Admin.module.css'
import Button from '../ui/Button'
export interface EnvRouteProps {
onError?: (error: string) => void
onSuccess?: (message: string) => void
}
const EnvRoute: Component<EnvRouteProps> = (props) => {
const [envSections, setEnvSections] = createSignal<EnvSection[]>([])
const [loading, setLoading] = createSignal(true)
const [editingVariable, setEditingVariable] = createSignal<EnvVariable | null>(null)
const [showVariableModal, setShowVariableModal] = createSignal(false)
// Состояние для показа/скрытия значений
const [shownVars, setShownVars] = createSignal<{ [key: string]: boolean }>({})
/**
* Загружает переменные окружения
*/
const loadEnvVariables = async () => {
try {
setLoading(true)
const result = await query<{ getEnvVariables: Query['getEnvVariables'] }>(
`${location.origin}/graphql`,
ADMIN_GET_ENV_VARIABLES_QUERY
)
// Важно: пустой массив [] тоже валидный результат!
if (result && Array.isArray(result.getEnvVariables)) {
setEnvSections(result.getEnvVariables)
console.log('Загружено секций переменных:', result.getEnvVariables.length)
} else {
console.warn('Неожиданный результат от getEnvVariables:', result)
setEnvSections([]) // Устанавливаем пустой массив если что-то пошло не так
}
} catch (error) {
console.error('Failed to load env variables:', error)
props.onError?.(error instanceof Error ? error.message : 'Failed to load environment variables')
setEnvSections([]) // Устанавливаем пустой массив при ошибке
} finally {
setLoading(false)
}
}
/**
* Обновляет значение переменной окружения
*/
const updateEnvVariable = async (key: string, value: string) => {
try {
const result = await query(`${location.origin}/graphql`, ADMIN_UPDATE_ENV_VARIABLE_MUTATION, {
key,
value
})
if (result && typeof result === 'object' && 'updateEnvVariable' in result) {
props.onSuccess?.(`Переменная ${key} успешно обновлена`)
await loadEnvVariables()
} else {
props.onError?.('Не удалось обновить переменную')
}
} catch (err) {
console.error('Ошибка обновления переменной:', err)
props.onError?.(err instanceof Error ? err.message : 'Ошибка при обновлении переменной')
}
}
/**
* Обработчик открытия модального окна редактирования переменной
*/
const openVariableModal = (variable: EnvVariable) => {
setEditingVariable({ ...variable })
setShowVariableModal(true)
}
/**
* Обработчик закрытия модального окна редактирования переменной
*/
const closeVariableModal = () => {
setEditingVariable(null)
setShowVariableModal(false)
}
/**
* Обработчик сохранения переменной
*/
const saveVariable = async () => {
const variable = editingVariable()
if (!variable) return
await updateEnvVariable(variable.key, variable.value)
closeVariableModal()
}
/**
* Обработчик изменения значения в модальном окне
*/
const handleVariableValueChange = (value: string) => {
const variable = editingVariable()
if (variable) {
setEditingVariable({ ...variable, value })
}
}
/**
* Переключает показ значения переменной
*/
const toggleShow = (key: string) => {
setShownVars((prev) => ({ ...prev, [key]: !prev[key] }))
}
/**
* Копирует значение в буфер обмена
*/
const CopyButton: Component<{ value: string }> = (props) => {
const handleCopy = async (e: MouseEvent) => {
e.preventDefault()
try {
await navigator.clipboard.writeText(props.value)
// Можно добавить всплывающее уведомление
} catch (err) {
alert(`Ошибка копирования: ${(err as Error).message}`)
}
}
return (
<a class="btn" title="Скопировать" type="button" style="margin-left: 6px" onClick={handleCopy}>
📋
</a>
)
}
/**
* Кнопка показать/скрыть значение переменной
*/
const ShowHideButton: Component<{ shown: boolean; onToggle: () => void }> = (props) => {
return (
<a
class="btn"
title={props.shown ? 'Скрыть' : 'Показать'}
type="button"
style="margin-left: 6px"
onClick={props.onToggle}
>
{props.shown ? '🙈' : '👁️'}
</a>
)
}
// Load env variables on mount
void loadEnvVariables()
// ВРЕМЕННО: для тестирования пустого состояния
// setTimeout(() => {
// setLoading(false)
// setEnvSections([])
// console.log('Тест: установлено пустое состояние')
// }, 1000)
return (
<div class={styles['env-variables-container']}>
<Show when={loading()}>
<div class={styles['loading']}>Загрузка переменных окружения...</div>
</Show>
<Show when={!loading() && envSections().length === 0}>
<div class={styles['empty-state']}>
<h3>Переменные окружения не найдены</h3>
<p>
Переменные окружения не настроены или не обнаружены в системе.
<br />
Вы можете добавить переменные через файл <code>.env</code> или системные переменные.
</p>
<details style="margin-top: 16px;">
<summary style="cursor: pointer; font-weight: 600;">Как добавить переменные?</summary>
<div style="margin-top: 8px; padding: 12px; background: #f8f9fa; border-radius: 6px;">
<p>
<strong>Способ 1:</strong> Через командную строку
</p>
<pre style="background: #e9ecef; padding: 8px; border-radius: 4px; font-size: 12px;">
export DEBUG=true export DB_URL="postgresql://localhost:5432/db" export
REDIS_URL="redis://localhost:6379"
</pre>
<p style="margin-top: 12px;">
<strong>Способ 2:</strong> Через файл .env
</p>
<pre style="background: #e9ecef; padding: 8px; border-radius: 4px; font-size: 12px;">
DEBUG=true DB_URL=postgresql://localhost:5432/db REDIS_URL=redis://localhost:6379
</pre>
</div>
</details>
</div>
</Show>
<Show when={!loading() && envSections().length > 0}>
<div class={styles['env-sections']}>
<For each={envSections()}>
{(section) => (
<div class={styles['env-section']}>
<h3 class={styles['section-name']}>{section.name}</h3>
<Show when={section.description}>
<p class={styles['section-description']}>{section.description}</p>
</Show>
<div class={styles['variables-list']}>
<table>
<thead>
<tr>
<th>Ключ</th>
<th>Значение</th>
<th>Описание</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<For each={section.variables}>
{(variable) => {
const shown = () => shownVars()[variable.key] || false
return (
<tr>
<td>{variable.key}</td>
<td>
{variable.isSecret && !shown()
? '••••••••'
: variable.value || <span class={styles['empty-value']}>не задано</span>}
<CopyButton value={variable.value || ''} />
{variable.isSecret && (
<ShowHideButton
shown={shown()}
onToggle={() => toggleShow(variable.key)}
/>
)}
</td>
<td>{variable.description || '-'}</td>
<td class={styles['actions']}>
<Button
variant="secondary"
size="small"
onClick={() => openVariableModal(variable)}
>
Изменить
</Button>
</td>
</tr>
)
}}
</For>
</tbody>
</table>
</div>
</div>
)}
</For>
</div>
</Show>
<Show when={editingVariable()}>
<EnvVariableModal
isOpen={showVariableModal()}
variable={editingVariable()!}
onClose={closeVariableModal}
onSave={saveVariable}
onValueChange={handleVariableValueChange}
/>
</Show>
</div>
)
}
export default EnvRoute