276 lines
10 KiB
TypeScript
276 lines
10 KiB
TypeScript
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
|