2025-06-30 18:25:26 +00:00
|
|
|
|
import { Component, createMemo, createSignal, Show } from 'solid-js'
|
|
|
|
|
import { query } from '../graphql'
|
|
|
|
|
import { EnvVariable } from '../graphql/generated/schema'
|
|
|
|
|
import { ADMIN_UPDATE_ENV_VARIABLE_MUTATION } from '../graphql/mutations'
|
|
|
|
|
import formStyles from '../styles/Form.module.css'
|
|
|
|
|
import Button from '../ui/Button'
|
|
|
|
|
import Modal from '../ui/Modal'
|
|
|
|
|
import TextPreview from '../ui/TextPreview'
|
|
|
|
|
|
|
|
|
|
interface EnvVariableModalProps {
|
|
|
|
|
isOpen: boolean
|
|
|
|
|
variable: EnvVariable
|
|
|
|
|
onClose: () => void
|
|
|
|
|
onSave: () => void
|
|
|
|
|
onValueChange?: (value: string) => void // FIXME: no need
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const EnvVariableModal: Component<EnvVariableModalProps> = (props) => {
|
|
|
|
|
const [value, setValue] = createSignal(props.variable.value)
|
|
|
|
|
const [saving, setSaving] = createSignal(false)
|
|
|
|
|
const [error, setError] = createSignal<string | null>(null)
|
|
|
|
|
const [showFormatted, setShowFormatted] = createSignal(false)
|
|
|
|
|
|
|
|
|
|
// Определяем нужно ли использовать textarea
|
|
|
|
|
const needsTextarea = createMemo(() => {
|
|
|
|
|
const val = value()
|
|
|
|
|
return (
|
|
|
|
|
val.length > 50 ||
|
|
|
|
|
val.includes('\n') ||
|
|
|
|
|
props.variable.type === 'json' ||
|
|
|
|
|
props.variable.key.includes('URL') ||
|
|
|
|
|
props.variable.key.includes('SECRET')
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Форматируем JSON если возможно
|
|
|
|
|
const formattedValue = createMemo(() => {
|
|
|
|
|
if (props.variable.type === 'json' || (value().startsWith('{') && value().endsWith('}'))) {
|
|
|
|
|
try {
|
|
|
|
|
return JSON.stringify(JSON.parse(value()), null, 2)
|
|
|
|
|
} catch {
|
|
|
|
|
return value()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return value()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const handleSave = async () => {
|
|
|
|
|
setSaving(true)
|
|
|
|
|
setError(null)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const result = await query<{ updateEnvVariable: boolean }>(
|
|
|
|
|
`${location.origin}/graphql`,
|
|
|
|
|
ADMIN_UPDATE_ENV_VARIABLE_MUTATION,
|
|
|
|
|
{
|
|
|
|
|
key: props.variable.key,
|
|
|
|
|
value: value()
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (result?.updateEnvVariable) {
|
|
|
|
|
props.onSave()
|
|
|
|
|
} else {
|
|
|
|
|
setError('Failed to update environment variable')
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(err instanceof Error ? err.message : 'Unknown error occurred')
|
|
|
|
|
} finally {
|
|
|
|
|
setSaving(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const formatValue = () => {
|
|
|
|
|
if (props.variable.type === 'json') {
|
|
|
|
|
try {
|
|
|
|
|
const formatted = JSON.stringify(JSON.parse(value()), null, 2)
|
|
|
|
|
setValue(formatted)
|
|
|
|
|
} catch (_e) {
|
|
|
|
|
setError('Invalid JSON format')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Modal
|
|
|
|
|
isOpen={props.isOpen}
|
|
|
|
|
title={`Редактировать ${props.variable.key}`}
|
|
|
|
|
onClose={props.onClose}
|
|
|
|
|
size="large"
|
|
|
|
|
>
|
2025-07-02 19:30:21 +00:00
|
|
|
|
<div class={formStyles.modalWide}>
|
2025-06-30 18:25:26 +00:00
|
|
|
|
<form class={formStyles.form} onSubmit={(e) => e.preventDefault()}>
|
2025-07-02 19:30:21 +00:00
|
|
|
|
<div class={formStyles.fieldGroup}>
|
|
|
|
|
<label class={formStyles.label}>
|
|
|
|
|
<span class={formStyles.labelText}>
|
|
|
|
|
<span class={formStyles.labelIcon}>🔑</span>
|
|
|
|
|
Ключ
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
2025-06-30 18:25:26 +00:00
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={props.variable.key}
|
|
|
|
|
disabled
|
2025-07-02 19:30:21 +00:00
|
|
|
|
class={`${formStyles.input} ${formStyles.disabled}`}
|
2025-06-30 18:25:26 +00:00
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
<div class={formStyles.fieldGroup}>
|
|
|
|
|
<label class={formStyles.label}>
|
|
|
|
|
<span class={formStyles.labelText}>
|
|
|
|
|
<span class={formStyles.labelIcon}>💾</span>
|
|
|
|
|
Значение
|
|
|
|
|
<span class={formStyles.labelInfo}>
|
|
|
|
|
({props.variable.type}
|
|
|
|
|
{props.variable.isSecret && ', секретное'})
|
|
|
|
|
</span>
|
2025-06-30 18:25:26 +00:00
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
<Show when={needsTextarea()}>
|
2025-07-02 19:30:21 +00:00
|
|
|
|
<div class={formStyles.textareaContainer}>
|
2025-06-30 18:25:26 +00:00
|
|
|
|
<textarea
|
|
|
|
|
value={value()}
|
|
|
|
|
onInput={(e) => setValue(e.currentTarget.value)}
|
2025-07-02 19:30:21 +00:00
|
|
|
|
class={formStyles.textarea}
|
2025-06-30 18:25:26 +00:00
|
|
|
|
rows={Math.min(Math.max(value().split('\n').length + 2, 4), 15)}
|
|
|
|
|
placeholder="Введите значение переменной..."
|
|
|
|
|
/>
|
|
|
|
|
<Show when={props.variable.type === 'json'}>
|
2025-07-02 19:30:21 +00:00
|
|
|
|
<div class={formStyles.textareaActions}>
|
2025-06-30 18:25:26 +00:00
|
|
|
|
<Button
|
|
|
|
|
variant="secondary"
|
|
|
|
|
size="small"
|
|
|
|
|
onClick={formatValue}
|
|
|
|
|
title="Форматировать JSON"
|
|
|
|
|
>
|
|
|
|
|
🎨 Форматировать
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="secondary"
|
|
|
|
|
size="small"
|
|
|
|
|
onClick={() => setShowFormatted(!showFormatted())}
|
|
|
|
|
title={showFormatted() ? 'Скрыть превью' : 'Показать превью'}
|
|
|
|
|
>
|
|
|
|
|
{showFormatted() ? '👁️ Скрыть' : '👁️ Превью'}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
<Show when={!needsTextarea()}>
|
|
|
|
|
<input
|
|
|
|
|
type={props.variable.isSecret ? 'password' : 'text'}
|
|
|
|
|
value={value()}
|
|
|
|
|
onInput={(e) => setValue(e.currentTarget.value)}
|
2025-07-02 19:30:21 +00:00
|
|
|
|
class={formStyles.input}
|
2025-06-30 18:25:26 +00:00
|
|
|
|
placeholder="Введите значение переменной..."
|
|
|
|
|
/>
|
|
|
|
|
</Show>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Show when={showFormatted() && (props.variable.type === 'json' || value().startsWith('{'))}>
|
2025-07-02 19:30:21 +00:00
|
|
|
|
<div class={formStyles.fieldGroup}>
|
|
|
|
|
<label class={formStyles.label}>
|
|
|
|
|
<span class={formStyles.labelText}>
|
|
|
|
|
<span class={formStyles.labelIcon}>👁️</span>
|
|
|
|
|
Превью (форматированное)
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<div class={formStyles.codePreview}>
|
2025-06-30 18:25:26 +00:00
|
|
|
|
<TextPreview content={formattedValue()} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
<Show when={props.variable.description}>
|
2025-07-02 19:30:21 +00:00
|
|
|
|
<div class={formStyles.formHelp}>
|
2025-06-30 18:25:26 +00:00
|
|
|
|
<strong>Описание:</strong> {props.variable.description}
|
|
|
|
|
</div>
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
<Show when={error()}>
|
2025-07-02 19:30:21 +00:00
|
|
|
|
<div class={formStyles.formError}>{error()}</div>
|
2025-06-30 18:25:26 +00:00
|
|
|
|
</Show>
|
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
<div class={formStyles.formActions}>
|
2025-06-30 18:25:26 +00:00
|
|
|
|
<Button variant="secondary" onClick={props.onClose} disabled={saving()}>
|
|
|
|
|
Отменить
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="primary" onClick={handleSave} loading={saving()}>
|
|
|
|
|
Сохранить
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</Modal>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default EnvVariableModal
|