diff --git a/Readme.md b/Readme.md index 5a7cebd..588df6d 100644 --- a/Readme.md +++ b/Readme.md @@ -99,13 +99,231 @@ used_protocol={{if .is_secure}}https:\\{{else}}http:\\{{end}} ## Окна В приложении прописаны несколько типов окон, также приложение максимально адаптировано под малый размер окна терминала. Минимальный размер окна терминала рассчитан на 60*20, при значениях меньше контент обрезается. Добавлен функционал переноса строк и прокручивание контента. ### Окно листа -Лист это первое окно, которое появляется при запуске. Лист нужен для выбора, какое приложение нужно будет настроить. Окно делится на 2 части, список приложений слева и короткое описание приложения справа. Переключаться между пунктами можно на стрелки вниз и вверх. При переключении автоматически меняется описание в блоке справа. -![list base image](public/list_base_image.png) +Лист это первое окно, которое появляется при запуске. Лист нужен для выбора, какое приложение нужно будет настроить. Окно делится на 2 части, список приложений слева и короткое описание приложения справа. Переключаться между пунктами можно на стрелки вниз и вверх. При переключении автоматически меняется описание в блоке справа. +![list base image](public/list_base_image.png) Если приложений в списке слишком много, то они автоматически переносятся на следующую страницу, с отображением текущей страницы. Для быстрого переключения между страницами можно использовать стрелки влево и вправо. -![list pagination example1.png](public/list_pagination_example1.png) -![list pagination example2.png](public/list_pagination_example2.png) -![list pagination example3.png](public/list_pagination_example3.png) -Для выбора приложения, используется клавиша enter, при её нажатии. Появляется модальное окно, где нужно подтвердить выбор приложения для настройки -![list modal confirm.png](public/list_modal_confirm.png) +![list pagination example1](public/list_pagination_example1.png) +![list pagination example2](public/list_pagination_example2.png) +![list pagination example3](public/list_pagination_example3.png) +Для выбора приложения, используется клавиша enter, при её нажатии. Появляется [модальное окно](Readme.md#модальное-окно-подтверждения-выбора-настраиваемого-приложения) подтверждения выбора настраиваемого приложения, где нужно подтвердить выбор приложения для настройки -### Окно секции/Формы +### Окно секции +Секция это блок посвященный конкретному разделу настройки приложений. В секции может находиться форма с полями, более подробное описание приложения или таблица переменных и их значений. +У секции могут быть условия отображения, что позволяет пропустить или задействовать определенные этапы настройки в зависимости от значений переменных или данных в системе. +Секция About предназначена для вывода подробной информации о проекте. По задумке эта секция должна идти первой, но если прописать её в других местах или несколько раз, система не выдаст ошибку. Если Разработчик сочтет нужным, он может добавлять About перед каждой секцией формы, для подробного объяснения каждого поля. +![section about_repko](public/section_about_repko.png) +При переходе на следующую секцию, если у текущей секции прописаны действия запускается поток. В нем последовательно обрабатывается каждое действие, чье условие выполняется. Во время обработки появляется [модальное окно](Readme.md#модальное-окно-ожидания-выполнения-действий) ожидания выполненения действий. +Когда обработка закончится, открывается либо следующая секция, либо если действие завершилось с ошибкой, показывается [модальное окно](Readme.md#модальное-окно-предупреждения-об-ошибке) предупреждения об ошибке, а после окно логирования действий. + +Секция Формы предназначена для предоставления различных полей для значений настройки приложения. Это могут быть простые поля ввода данных, раскрывающийся список для выбора из определенных опций, чекбокс для подтверждения или список выбора файла или директории. Каждое поле также может использовать условия для отображения. +![section field condition1](public/section_field_condition1.png) +![section field condition2](public/section_field_condition2.png) +Каждое поле также поддерживает валидацию. Что позволяет заранее узнать о возможных ошибках при настройке. Важно, в текущей версии GoSetup ошибка валидации не запрещает использовать введенное значение. +![section field validation error](public/section_field_validation_error.png) + +Секция Final предназначена для вывода вводимых данных после настройки. В этот момент пользователь может проверить все ли вводимые им данные верны. +![section final1](public/section_final1.png) +![section final2](public/section_final2.png) +После нажатия клавиши enter запускаются последние действия, если они выполнились успешно, то появится [модальное окно](Readme.md#модальное-окно-завершения-настройки) завершения настройки. При его нажатии настройка завершается и приложения закрывается, возвращая статус код 0. + +### Модальные окна +Модальны окна предназначены для вывода сообщений на экран и получения подтверждений действий пользователя. Есть следующие модальные окна. +#### Модальное окно подтверждения выбора настраиваемого приложения +![list modal confirm](public/list_modal_confirm.png) +#### Модальное окно подтверждения возврата к списку приложений +![modal back to list](public/modal_back_to_list.png) +#### Модальное окно подтверждения выхода +![modal exit](public/modal_exit.png) +#### Модальное окно завершения настройки +![section modal end](public/section_modal_end.png) +#### Модальное окно ожидания выполнения действий +![section modal loading](public/section_modal_loading.png) +#### Модальное окно предупреждения об ошибке +![section modal error action](public/section_modal_error_action.png) + +### Окно логирования +Для просмотра результатов выполняемых действий секции, предусмотрено окно Логов, куда по мере выполнения действия добавляются сообщения результаты. Окно открывается автоматически если во время выполнения произошла ошибка, также есть горячая клавиша ["ctrl+l"](Readme.md#горячие-клавиши), что скрывает или отображает окно. +![section log action1](public/section_log_action1.png) +![section log action2](public/section_log_action2.png) +![section log action3](public/section_log_action3.png) +![section log action4](public/section_log_action4.png) +В случае когда действие завершается ошибкой, то сообщение выглядит следующим образом. +![section log error](public/section_log_error.png) + +## Горячие клавиши +Для работы с приложением заранее определен ряд клавиш: +- Клавиши движения. Эти клавиши предназначены для смены фокуса между элементами. К ним относятся + - up, shift+tab - перемещают фокус выше, на прошлый элемент + - down, tab - перемещают фокус ниже, на следующий элемент + - left - перемещают фокус левее + - right - перемещают фокус правее + - home - прокрутка к старту области + - end - прокрутка к окончанию области +- Клавиши взаимодействия. Эти клавиши отвечают за работу непосредственно с процессом настройки. К ним относятся + - enter - подтверждает действие, например открытие поля, перехода на следующую секцию или модального окна + - ctrl+l - открывает или закрывает окно логирования + - esc - закрывает открытое поля списка, если фокус не был на открытом поле списка, то появляется [модальное окно](Readme.md#модальное-окно-подтверждения-возврата-к-списку-приложений) подтверждения возврата к списку приложений + - ctrl+c, ctrl+q - открывает [модальное окно](Readme.md#модальное-окно-подтверждения-выхода) подтверждения выхода + +## Дополнительно, Важное +В текущей итерации не учтен ряд элементов. Это могут быть вопросы реализации, правил поведения приложения, удобство пользования. + +## Собственные функции для expr +Библиотека [expr](https://github.com/expr-lang/expr) имеет ряд готовых функций их можно просмотреть [здесь](https://github.com/expr-lang/expr/blob/master/builtin/builtin.go). Также для были определенны собственные функции, которые можно посмотреть в файле [здесь](setup/utils/utils.go#L153) +```go +// DefineExprCustomFunctions возвращает собственные функции expr +func DefineExprCustomFunctions() []expr.Option { + return []expr.Option{ + //вычисляет значение по заданному регулярному выражению + expr.Function("regex", + func(args ...interface{}) (interface{}, error) { + if len(args) != 3 { + return "", fmt.Errorf("regex() ожидает 3 аргумента") + } + input := fmt.Sprintf("%v", args[0]) + pattern := fmt.Sprintf("%v", args[1]) + repl := fmt.Sprintf("%v", args[2]) + re, err := regexp.Compile(pattern) + if err != nil { + return input, nil + } + return re.ReplaceAllString(input, repl), nil + }, new(func(string, string, string) string), + ), + //проверяет совпадение переданного значения и указанного регулярного выражения + expr.Function("regex_match", + func(args ...interface{}) (interface{}, error) { + if len(args) != 2 { + return false, fmt.Errorf("regex_match() ожидает 2 аргумента") + } + input := fmt.Sprintf("%v", args[0]) + pattern := fmt.Sprintf("%v", args[1]) + + re, err := regexp.Compile(pattern) + if err != nil { + return false, fmt.Errorf("некорректный regex %s, err:%v", pattern, err) + } + return re.MatchString(input), nil + + }, new(func(string, string) bool), + ), + //возвращает собственный текст ошибки + expr.Function("error", + func(args ...interface{}) (interface{}, error) { + if len(args) == 0 { + return nil, fmt.Errorf("error() требует сообщения") + } + return nil, fmt.Errorf("%v", args[0]) + }, + new(func(string) error), + ), + //возвращает тело ответа по указанному адресу + expr.Function("http_get", + func(args ...interface{}) (interface{}, error) { + if len(args) == 0 { + return "", nil + } + url := fmt.Sprintf("%v", args[0]) + resp, err := http.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + return string(body), nil + }, + new(func(string) string), + ), + //возвращает результат выполнения команды из оболочки + expr.Function("shell", + func(args ...interface{}) (interface{}, error) { + if len(args) == 0 { + return "", nil + } + cmd := fmt.Sprintf("%v", args[0]) + parts := strings.Fields(cmd) + if len(parts) == 0 { + return "", nil + } + out, err := exec.Command(parts[0], parts[1:]...).CombinedOutput() + if err != nil { + return "", err + } + return strings.TrimSpace(string(out)), nil + }, + new(func(string) error), + ), + //возвращает значение указанной переменной из env + expr.Function("env", + func(args ...interface{}) (interface{}, error) { + if len(args) == 0 { + return "", nil + } + return os.Getenv(fmt.Sprintf("%v", args[0])), nil + }, new(func(string) error), + ), + //проверяет наличие файла в системе + expr.Function("file_exists", + func(args ...interface{}) (interface{}, error) { + if len(args) == 0 { + return false, nil + } + path := fmt.Sprintf("%v", args[0]) + fileInfo, err := os.Stat(path) + if err == nil && !fileInfo.IsDir() { + return true, nil + } else if err == nil && fileInfo.IsDir() { + return false, fmt.Errorf("file is dir") + } + if errors.Is(err, fs.ErrNotExist) { + return false, nil + } else { + return false, err + } + }, new(func(string) bool), + ), + //проверяет наличие директории в системе + expr.Function("dir_exists", + func(args ...interface{}) (interface{}, error) { + if len(args) == 0 { + return false, nil + } + path := fmt.Sprintf("%v", args[0]) + fileInfo, err := os.Stat(path) + if err == nil && fileInfo.IsDir() { + return true, nil + } else if err == nil && !fileInfo.IsDir() { + return false, fmt.Errorf("file is not a dir") + } + if errors.Is(err, fs.ErrNotExist) { + return false, nil + } else { + return false, err + } + }, new(func(string) bool), + ), + //проверяет доступность порта + expr.Function("port_free", + func(args ...interface{}) (interface{}, error) { + if len(args) == 0 { + return false, nil + } + port := fmt.Sprintf("%v", args[0]) + cmd := exec.Command("ss", "-ltn", "sport =: "+port) + return cmd.Run() != nil, nil + }, new(func(string) error), + ), + //проверяет наличие пользователя в системе + expr.Function("user_exists", + func(args ...interface{}) (interface{}, error) { + if len(args) == 0 { + return false, nil + } + name := fmt.Sprintf("%v", args[0]) + cmd := exec.Command("id", "-u", name) + return cmd.Run() == nil, nil + }, new(func(string) bool), + ), + } +} +``` diff --git a/confs/repko.yaml b/confs/repko.yaml index ec999db..5ce90c1 100644 --- a/confs/repko.yaml +++ b/confs/repko.yaml @@ -81,7 +81,7 @@ variables: default: "Repko Repository" - name: "gpg_note" type: "string" - default: "noe" + default: "note" - name: "gpg_mail" type: "string" default: "admin@repko.com" @@ -105,11 +105,14 @@ sections: type: "about" description: "Описание приложения" paragraphs: - - "# Repko - собственный сервер репозитория для debian систем. " - - "## Возможности: \n - Встроенная система аутентификации (локальные пользователи или LDAP).\n - Подпись gpg ключом.\n - Удобный веб интерфейс" - - "## Бла бла бла. \n lorem ipsum прочее прочее прочее прочее" - - "### Бла бла бла бла. \n lorem ipsum прочее прочее прочее прочее" - - "## Бла бла бла бла. \n lorem ipsum прочее прочее прочее прочее" + - "# Repko - инновационное решение для создания и управления репозиториями Debian на собственном хостинге." + - "Разработан на языке GO,обеспечивает высокую производительность и надежность, подходит как для небольших проектов, так и для корпоративных сред." + - "## Ключевые особенности:" + - "- Гибкость хранения метаданных: поддержка PostgreSQl и SQLite" + - "- Автоматическая генерация debian структуры" + - "- Автоматическая подпись файлов Release" + - "- Высокая производительность и минимальное потребление ресурсов" + - "- Современный веб-интерфейс на Next.js" actions: - name: "Create directories" type: "command" @@ -155,7 +158,7 @@ sections: actions: - name: "create_user" type: "command" - command: "echo 'adduser --system --group --no-create-home --disabled-password --shell /bin/false {{.repko_user}}'" + command: "adduser --system --group --no-create-home --disabled-password --shell /bin/false {{.repko_user}}" condition: "user_type=='Create'" run-as: "root" require-packages: @@ -177,18 +180,6 @@ sections: type: "command" command: "chmod -R 700 /var/lib/repko/gnupg" error: "Ошибка установки прав 700" - - name: "Set files ownership" - type: "command" - command: "chown {{.repko_user}}:{{.repko_user}} /opt/repko/repko /opt/repko/.env /etc/repko/config.ini" - error: "Ошибка установка владельца для файлов" - - name: "Set rights 750" - type: "command" - command: "chmod 750 /opt/repko/repko" - error: "Ошибка установки прав 750" - - name: "Set rights 640" - type: "command" - command: "chmod 640 /opt/repko/.env /etc/repko/config.ini" - error: "Ошибка установки прав 640" - name: "domain" description: "Настройка используемых доменов" fields: @@ -414,7 +405,7 @@ sections: actions: - name: "generate gpg key" type: "command" - command: "sudo -u {{.repko_user}} gpg --homedir /var/lib/repko/gnupg --batch --passphrase '' --quick-generate-key '{.gpg_full_name} ({.gpg_note}) <{.gpg_mail}>' rsa4096" + command: "sudo -u {{.repko_user}} gpg --homedir /var/lib/repko/gnupg --batch --passphrase '' --quick-generate-key '{{.gpg_full_name}} ({{.gpg_note}}) <{{.gpg_mail}}>' rsa4096" error: "Не удалось создать ключ" - name: "get gpg key id" type: "command" @@ -423,7 +414,7 @@ sections: assign-to: "gpg_key_id" - name: "export gpg key" type: "command" - command: "sudo -u {{.repko_user}} gpg --homedir /var/lib/repko/gnupg --armor --export {{.gpg_key_id}} > /usr/share/keyrings/repko.asc" + command: "sudo -u {{.repko_user}} gpg --homedir /var/lib/repko/gnupg --armor --export '{{.gpg_key_id}}!' 2>/dev/null > /usr/share/keyrings/repko.asc" error: "Не удалось экспортировать gpg ключ" - name: "write gpg key id" type: "command" @@ -525,4 +516,16 @@ sections: type: "file_template" file-source: "/opt/repko/donors/config.json" file-target: "/var/www/repko/web/config.json" - error: "Ошибка при создании файла repko web config" \ No newline at end of file + error: "Ошибка при создании файла repko web config" + - name: "Set files ownership" + type: "command" + command: "chown {{.repko_user}}:{{.repko_user}} /opt/repko/repko /opt/repko/.env /etc/repko/config.ini" + error: "Ошибка установка владельца для файлов" + - name: "Set rights 750" + type: "command" + command: "chmod 750 /opt/repko/repko" + error: "Ошибка установки прав 750" + - name: "Set rights 640" + type: "command" + command: "chmod 640 /opt/repko/.env /etc/repko/config.ini" + error: "Ошибка установки прав 640" \ No newline at end of file diff --git a/public/modal_back_to_list.png b/public/modal_back_to_list.png new file mode 100644 index 0000000..83a5cdc Binary files /dev/null and b/public/modal_back_to_list.png differ diff --git a/public/modal_exit.png b/public/modal_exit.png new file mode 100644 index 0000000..d32db1a Binary files /dev/null and b/public/modal_exit.png differ diff --git a/public/section_about_repko.png b/public/section_about_repko.png new file mode 100644 index 0000000..5d37f03 Binary files /dev/null and b/public/section_about_repko.png differ diff --git a/public/section_field_condition1.png b/public/section_field_condition1.png new file mode 100644 index 0000000..2213233 Binary files /dev/null and b/public/section_field_condition1.png differ diff --git a/public/section_field_condition2.png b/public/section_field_condition2.png new file mode 100644 index 0000000..5e76df7 Binary files /dev/null and b/public/section_field_condition2.png differ diff --git a/public/section_field_validation_error.png b/public/section_field_validation_error.png new file mode 100644 index 0000000..b28895e Binary files /dev/null and b/public/section_field_validation_error.png differ diff --git a/public/section_final1.png b/public/section_final1.png new file mode 100644 index 0000000..1cd1a7d Binary files /dev/null and b/public/section_final1.png differ diff --git a/public/section_final2.png b/public/section_final2.png new file mode 100644 index 0000000..2034249 Binary files /dev/null and b/public/section_final2.png differ diff --git a/public/section_log_action1.png b/public/section_log_action1.png new file mode 100644 index 0000000..67e245f Binary files /dev/null and b/public/section_log_action1.png differ diff --git a/public/section_log_action2.png b/public/section_log_action2.png new file mode 100644 index 0000000..47b1b30 Binary files /dev/null and b/public/section_log_action2.png differ diff --git a/public/section_log_action3.png b/public/section_log_action3.png new file mode 100644 index 0000000..6bac9a5 Binary files /dev/null and b/public/section_log_action3.png differ diff --git a/public/section_log_action4.png b/public/section_log_action4.png new file mode 100644 index 0000000..63b9591 Binary files /dev/null and b/public/section_log_action4.png differ diff --git a/public/section_log_error.png b/public/section_log_error.png new file mode 100644 index 0000000..a277af6 Binary files /dev/null and b/public/section_log_error.png differ diff --git a/public/section_modal_end.png b/public/section_modal_end.png new file mode 100644 index 0000000..c18974f Binary files /dev/null and b/public/section_modal_end.png differ diff --git a/public/section_modal_error_action.png b/public/section_modal_error_action.png new file mode 100644 index 0000000..50b7b7a Binary files /dev/null and b/public/section_modal_error_action.png differ diff --git a/public/section_modal_loading.png b/public/section_modal_loading.png new file mode 100644 index 0000000..7a939a0 Binary files /dev/null and b/public/section_modal_loading.png differ diff --git a/setup/form.go b/setup/form.go index f705527..7d556da 100644 --- a/setup/form.go +++ b/setup/form.go @@ -438,7 +438,7 @@ func simpleAboutRender(paragraphs []string, width int) string { if i != len(paragraphs)-1 { switch strokeType { case "normal", "quote", "bullet": - finalStroke += "\n" + finalStroke += "\n\n" case "header1", "header2", "header3": finalStroke += "\n\n" } diff --git a/setup/utils/utils.go b/setup/utils/utils.go index 948055e..2de001a 100644 --- a/setup/utils/utils.go +++ b/setup/utils/utils.go @@ -179,7 +179,7 @@ func DefineExprCustomFunctions() []expr.Option { re, err := regexp.Compile(pattern) if err != nil { - return false, fmt.Errorf("некоректный regex %s, err:%v", pattern, err) + return false, fmt.Errorf("некорректный regex %s, err:%v", pattern, err) } return re.MatchString(input), nil