203 lines
6.7 KiB
TypeScript
203 lines
6.7 KiB
TypeScript
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"
|
||
>
|
||
<div class={formStyles.modalWide}>
|
||
<form class={formStyles.form} onSubmit={(e) => e.preventDefault()}>
|
||
<div class={formStyles.fieldGroup}>
|
||
<label class={formStyles.label}>
|
||
<span class={formStyles.labelText}>
|
||
<span class={formStyles.labelIcon}>🔑</span>
|
||
Ключ
|
||
</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={props.variable.key}
|
||
disabled
|
||
class={`${formStyles.input} ${formStyles.disabled}`}
|
||
/>
|
||
</div>
|
||
|
||
<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>
|
||
</span>
|
||
</label>
|
||
|
||
<Show when={needsTextarea()}>
|
||
<div class={formStyles.textareaContainer}>
|
||
<textarea
|
||
value={value()}
|
||
onInput={(e) => setValue(e.currentTarget.value)}
|
||
class={formStyles.textarea}
|
||
rows={Math.min(Math.max(value().split('\n').length + 2, 4), 15)}
|
||
placeholder="Введите значение переменной..."
|
||
/>
|
||
<Show when={props.variable.type === 'json'}>
|
||
<div class={formStyles.textareaActions}>
|
||
<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)}
|
||
class={formStyles.input}
|
||
placeholder="Введите значение переменной..."
|
||
/>
|
||
</Show>
|
||
</div>
|
||
|
||
<Show when={showFormatted() && (props.variable.type === 'json' || value().startsWith('{'))}>
|
||
<div class={formStyles.fieldGroup}>
|
||
<label class={formStyles.label}>
|
||
<span class={formStyles.labelText}>
|
||
<span class={formStyles.labelIcon}>👁️</span>
|
||
Превью (форматированное)
|
||
</span>
|
||
</label>
|
||
<div class={formStyles.codePreview}>
|
||
<TextPreview content={formattedValue()} />
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={props.variable.description}>
|
||
<div class={formStyles.formHelp}>
|
||
<strong>Описание:</strong> {props.variable.description}
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={error()}>
|
||
<div class={formStyles.formError}>{error()}</div>
|
||
</Show>
|
||
|
||
<div class={formStyles.formActions}>
|
||
<Button variant="secondary" onClick={props.onClose} disabled={saving()}>
|
||
Отменить
|
||
</Button>
|
||
<Button variant="primary" onClick={handleSave} loading={saving()}>
|
||
Сохранить
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</Modal>
|
||
)
|
||
}
|
||
|
||
export default EnvVariableModal
|