Support configuration variables on Gitea Actions (#24724)
Co-Author: @silverwind @wxiaoguang Replace: #24404 See: - [defining configuration variables for multiple workflows](https://docs.github.com/en/actions/learn-github-actions/variables#defining-configuration-variables-for-multiple-workflows) - [vars context](https://docs.github.com/en/actions/learn-github-actions/contexts#vars-context) Related to: - [x] protocol: https://gitea.com/gitea/actions-proto-def/pulls/7 - [x] act_runner: https://gitea.com/gitea/act_runner/pulls/157 - [x] act: https://gitea.com/gitea/act/pulls/43 #### Screenshoot Create Variable: ![image](https://user-images.githubusercontent.com/33891828/236758288-032b7f64-44e7-48ea-b07d-de8b8b0e3729.png) ![image](https://user-images.githubusercontent.com/33891828/236758174-5203f64c-1d0e-4737-a5b0-62061dee86f8.png) Workflow: ```yaml test_vars: runs-on: ubuntu-latest steps: - name: Print Custom Variables run: echo "${{ vars.test_key }}" - name: Try to print a non-exist var run: echo "${{ vars.NON_EXIST_VAR }}" ``` Actions Log: ![image](https://user-images.githubusercontent.com/33891828/236759075-af0c5950-368d-4758-a8ac-47a96e43b6e2.png) --- This PR just implement the org / user (depends on the owner of the current repository) and repo level variables, The Environment level variables have not been implemented. Because [Environment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#about-environments) is a module separate from `Actions`. Maybe it would be better to create a new PR to do it. --------- Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Giteabot <teabot@gitea.io>forgejo-federated-star
parent
8220e50b56
commit
35a653d7ed
@ -0,0 +1,97 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActionVariable struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
OwnerID int64 `xorm:"UNIQUE(owner_repo_name)"`
|
||||||
|
RepoID int64 `xorm:"INDEX UNIQUE(owner_repo_name)"`
|
||||||
|
Name string `xorm:"UNIQUE(owner_repo_name) NOT NULL"`
|
||||||
|
Data string `xorm:"LONGTEXT NOT NULL"`
|
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
|
||||||
|
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
db.RegisterModel(new(ActionVariable))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ActionVariable) Validate() error {
|
||||||
|
if v.OwnerID == 0 && v.RepoID == 0 {
|
||||||
|
return errors.New("the variable is not bound to any scope")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func InsertVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*ActionVariable, error) {
|
||||||
|
variable := &ActionVariable{
|
||||||
|
OwnerID: ownerID,
|
||||||
|
RepoID: repoID,
|
||||||
|
Name: strings.ToUpper(name),
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
if err := variable.Validate(); err != nil {
|
||||||
|
return variable, err
|
||||||
|
}
|
||||||
|
return variable, db.Insert(ctx, variable)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FindVariablesOpts struct {
|
||||||
|
db.ListOptions
|
||||||
|
OwnerID int64
|
||||||
|
RepoID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opts *FindVariablesOpts) toConds() builder.Cond {
|
||||||
|
cond := builder.NewCond()
|
||||||
|
if opts.OwnerID > 0 {
|
||||||
|
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
||||||
|
}
|
||||||
|
if opts.RepoID > 0 {
|
||||||
|
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||||
|
}
|
||||||
|
return cond
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindVariables(ctx context.Context, opts FindVariablesOpts) ([]*ActionVariable, error) {
|
||||||
|
var variables []*ActionVariable
|
||||||
|
sess := db.GetEngine(ctx)
|
||||||
|
if opts.PageSize != 0 {
|
||||||
|
sess = db.SetSessionPagination(sess, &opts.ListOptions)
|
||||||
|
}
|
||||||
|
return variables, sess.Where(opts.toConds()).Find(&variables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetVariableByID(ctx context.Context, variableID int64) (*ActionVariable, error) {
|
||||||
|
var variable ActionVariable
|
||||||
|
has, err := db.GetEngine(ctx).Where("id=?", variableID).Get(&variable)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, fmt.Errorf("variable with id %d: %w", variableID, util.ErrNotExist)
|
||||||
|
}
|
||||||
|
return &variable, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateVariable(ctx context.Context, variable *ActionVariable) (bool, error) {
|
||||||
|
count, err := db.GetEngine(ctx).ID(variable.ID).Cols("name", "data").
|
||||||
|
Update(&ActionVariable{
|
||||||
|
Name: variable.Name,
|
||||||
|
Data: variable.Data,
|
||||||
|
})
|
||||||
|
return count != 0, err
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_21 //nolint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateVariableTable(x *xorm.Engine) error {
|
||||||
|
type ActionVariable struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
OwnerID int64 `xorm:"UNIQUE(owner_repo_name)"`
|
||||||
|
RepoID int64 `xorm:"INDEX UNIQUE(owner_repo_name)"`
|
||||||
|
Name string `xorm:"UNIQUE(owner_repo_name) NOT NULL"`
|
||||||
|
Data string `xorm:"LONGTEXT NOT NULL"`
|
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
|
||||||
|
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.Sync(new(ActionVariable))
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/base"
|
||||||
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
shared "code.gitea.io/gitea/routers/web/shared/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tplRepoVariables base.TplName = "repo/settings/actions"
|
||||||
|
tplOrgVariables base.TplName = "org/settings/actions"
|
||||||
|
tplUserVariables base.TplName = "user/settings/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type variablesCtx struct {
|
||||||
|
OwnerID int64
|
||||||
|
RepoID int64
|
||||||
|
IsRepo bool
|
||||||
|
IsOrg bool
|
||||||
|
IsUser bool
|
||||||
|
VariablesTemplate base.TplName
|
||||||
|
RedirectLink string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) {
|
||||||
|
if ctx.Data["PageIsRepoSettings"] == true {
|
||||||
|
return &variablesCtx{
|
||||||
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
|
IsRepo: true,
|
||||||
|
VariablesTemplate: tplRepoVariables,
|
||||||
|
RedirectLink: ctx.Repo.RepoLink + "/settings/actions/variables",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Data["PageIsOrgSettings"] == true {
|
||||||
|
return &variablesCtx{
|
||||||
|
OwnerID: ctx.ContextUser.ID,
|
||||||
|
IsOrg: true,
|
||||||
|
VariablesTemplate: tplOrgVariables,
|
||||||
|
RedirectLink: ctx.Org.OrgLink + "/settings/actions/variables",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Data["PageIsUserSettings"] == true {
|
||||||
|
return &variablesCtx{
|
||||||
|
OwnerID: ctx.Doer.ID,
|
||||||
|
IsUser: true,
|
||||||
|
VariablesTemplate: tplUserVariables,
|
||||||
|
RedirectLink: setting.AppSubURL + "/user/settings/actions/variables",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("unable to set Variables context")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Variables(ctx *context.Context) {
|
||||||
|
ctx.Data["Title"] = ctx.Tr("actions.variables")
|
||||||
|
ctx.Data["PageType"] = "variables"
|
||||||
|
ctx.Data["PageIsSharedSettingsVariables"] = true
|
||||||
|
|
||||||
|
vCtx, err := getVariablesCtx(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("getVariablesCtx", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shared.SetVariablesContext(ctx, vCtx.OwnerID, vCtx.RepoID)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.HTML(http.StatusOK, vCtx.VariablesTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func VariableCreate(ctx *context.Context) {
|
||||||
|
vCtx, err := getVariablesCtx(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("getVariablesCtx", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.HasError() { // form binding validation error
|
||||||
|
ctx.JSONError(ctx.GetErrMsg())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shared.CreateVariable(ctx, vCtx.OwnerID, vCtx.RepoID, vCtx.RedirectLink)
|
||||||
|
}
|
||||||
|
|
||||||
|
func VariableUpdate(ctx *context.Context) {
|
||||||
|
vCtx, err := getVariablesCtx(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("getVariablesCtx", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.HasError() { // form binding validation error
|
||||||
|
ctx.JSONError(ctx.GetErrMsg())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shared.UpdateVariable(ctx, vCtx.RedirectLink)
|
||||||
|
}
|
||||||
|
|
||||||
|
func VariableDelete(ctx *context.Context) {
|
||||||
|
vCtx, err := getVariablesCtx(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("getVariablesCtx", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
shared.DeleteVariable(ctx, vCtx.RedirectLink)
|
||||||
|
}
|
@ -0,0 +1,128 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/web"
|
||||||
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetVariablesContext(ctx *context.Context, ownerID, repoID int64) {
|
||||||
|
variables, err := actions_model.FindVariables(ctx, actions_model.FindVariablesOpts{
|
||||||
|
OwnerID: ownerID,
|
||||||
|
RepoID: repoID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("FindVariables", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["Variables"] = variables
|
||||||
|
}
|
||||||
|
|
||||||
|
// some regular expression of `variables` and `secrets`
|
||||||
|
// reference to:
|
||||||
|
// https://docs.github.com/en/actions/learn-github-actions/variables#naming-conventions-for-configuration-variables
|
||||||
|
// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets
|
||||||
|
var (
|
||||||
|
nameRx = regexp.MustCompile("(?i)^[A-Z_][A-Z0-9_]*$")
|
||||||
|
forbiddenPrefixRx = regexp.MustCompile("(?i)^GIT(EA|HUB)_")
|
||||||
|
|
||||||
|
forbiddenEnvNameCIRx = regexp.MustCompile("(?i)^CI")
|
||||||
|
)
|
||||||
|
|
||||||
|
func NameRegexMatch(name string) error {
|
||||||
|
if !nameRx.MatchString(name) || forbiddenPrefixRx.MatchString(name) {
|
||||||
|
log.Error("Name %s, regex match error", name)
|
||||||
|
return errors.New("name has invalid character")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func envNameCIRegexMatch(name string) error {
|
||||||
|
if forbiddenEnvNameCIRx.MatchString(name) {
|
||||||
|
log.Error("Env Name cannot be ci")
|
||||||
|
return errors.New("env name cannot be ci")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateVariable(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
|
||||||
|
form := web.GetForm(ctx).(*forms.EditVariableForm)
|
||||||
|
|
||||||
|
if err := NameRegexMatch(form.Name); err != nil {
|
||||||
|
ctx.JSONError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := envNameCIRegexMatch(form.Name); err != nil {
|
||||||
|
ctx.JSONError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := actions_model.InsertVariable(ctx, ownerID, repoID, form.Name, ReserveLineBreakForTextarea(form.Data))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("InsertVariable error: %v", err)
|
||||||
|
ctx.JSONError(ctx.Tr("actions.variables.creation.failed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Flash.Success(ctx.Tr("actions.variables.creation.success", v.Name))
|
||||||
|
ctx.JSONRedirect(redirectURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateVariable(ctx *context.Context, redirectURL string) {
|
||||||
|
id := ctx.ParamsInt64(":variable_id")
|
||||||
|
form := web.GetForm(ctx).(*forms.EditVariableForm)
|
||||||
|
|
||||||
|
if err := NameRegexMatch(form.Name); err != nil {
|
||||||
|
ctx.JSONError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := envNameCIRegexMatch(form.Name); err != nil {
|
||||||
|
ctx.JSONError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := actions_model.UpdateVariable(ctx, &actions_model.ActionVariable{
|
||||||
|
ID: id,
|
||||||
|
Name: strings.ToUpper(form.Name),
|
||||||
|
Data: ReserveLineBreakForTextarea(form.Data),
|
||||||
|
})
|
||||||
|
if err != nil || !ok {
|
||||||
|
log.Error("UpdateVariable error: %v", err)
|
||||||
|
ctx.JSONError(ctx.Tr("actions.variables.update.failed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Flash.Success(ctx.Tr("actions.variables.update.success"))
|
||||||
|
ctx.JSONRedirect(redirectURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteVariable(ctx *context.Context, redirectURL string) {
|
||||||
|
id := ctx.ParamsInt64(":variable_id")
|
||||||
|
|
||||||
|
if _, err := db.DeleteByBean(ctx, &actions_model.ActionVariable{ID: id}); err != nil {
|
||||||
|
log.Error("Delete variable [%d] failed: %v", id, err)
|
||||||
|
ctx.JSONError(ctx.Tr("actions.variables.deletion.failed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Flash.Success(ctx.Tr("actions.variables.deletion.success"))
|
||||||
|
ctx.JSONRedirect(redirectURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReserveLineBreakForTextarea(input string) string {
|
||||||
|
// Since the content is from a form which is a textarea, the line endings are \r\n.
|
||||||
|
// It's a standard behavior of HTML.
|
||||||
|
// But we want to store them as \n like what GitHub does.
|
||||||
|
// And users are unlikely to really need to keep the \r.
|
||||||
|
// Other than this, we should respect the original content, even leading or trailing spaces.
|
||||||
|
return strings.ReplaceAll(input, "\r\n", "\n")
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{.locale.Tr "actions.variables.management"}}
|
||||||
|
<div class="ui right">
|
||||||
|
<button class="ui primary tiny button show-modal"
|
||||||
|
data-modal="#edit-variable-modal"
|
||||||
|
data-modal-form.action="{{.Link}}/new"
|
||||||
|
data-modal-header="{{.locale.Tr "actions.variables.creation"}}"
|
||||||
|
data-modal-dialog-variable-name=""
|
||||||
|
data-modal-dialog-variable-data=""
|
||||||
|
>
|
||||||
|
{{.locale.Tr "actions.variables.creation"}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
{{if .Variables}}
|
||||||
|
<div class="ui list">
|
||||||
|
{{range $i, $v := .Variables}}
|
||||||
|
<div class="item gt-df gt-ac gt-fw {{if gt $i 0}} gt-py-4{{end}}">
|
||||||
|
<div class="content gt-f1 gt-ellipsis">
|
||||||
|
<strong>{{$v.Name}}</strong>
|
||||||
|
<div class="print meta gt-ellipsis">{{$v.Data}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<span class="color-text-light-2 gt-mr-5">
|
||||||
|
{{$.locale.Tr "settings.added_on" (DateTime "short" $v.CreatedUnix) | Safe}}
|
||||||
|
</span>
|
||||||
|
<button class="btn interact-bg gt-p-3 show-modal"
|
||||||
|
data-tooltip-content="{{$.locale.Tr "variables.edit"}}"
|
||||||
|
data-modal="#edit-variable-modal"
|
||||||
|
data-modal-form.action="{{$.Link}}/{{$v.ID}}/edit"
|
||||||
|
data-modal-header="{{$.locale.Tr "actions.variables.edit"}}"
|
||||||
|
data-modal-dialog-variable-name="{{$v.Name}}"
|
||||||
|
data-modal-dialog-variable-data="{{$v.Data}}"
|
||||||
|
>
|
||||||
|
{{svg "octicon-pencil"}}
|
||||||
|
</button>
|
||||||
|
<button class="btn interact-bg gt-p-3 link-action"
|
||||||
|
data-tooltip-content="{{$.locale.Tr "actions.variables.deletion"}}"
|
||||||
|
data-url="{{$.Link}}/{{$v.ID}}/delete"
|
||||||
|
data-modal-confirm="{{$.locale.Tr "actions.variables.deletion.description"}}"
|
||||||
|
>
|
||||||
|
{{svg "octicon-trash"}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
{{.locale.Tr "actions.variables.none"}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{/** Edit variable dialog */}}
|
||||||
|
<div class="ui small modal" id="edit-variable-modal">
|
||||||
|
<div class="header"></div>
|
||||||
|
<form class="ui form form-fetch-action" method="post">
|
||||||
|
<div class="content">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<div class="field">
|
||||||
|
{{.locale.Tr "actions.variables.description"}}
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="dialog-variable-name">{{.locale.Tr "name"}}</label>
|
||||||
|
<input autofocus required
|
||||||
|
name="name"
|
||||||
|
id="dialog-variable-name"
|
||||||
|
value="{{.name}}"
|
||||||
|
pattern="^[a-zA-Z_][a-zA-Z0-9_]*$"
|
||||||
|
placeholder="{{.locale.Tr "secrets.creation.name_placeholder"}}"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="dialog-variable-data">{{.locale.Tr "value"}}</label>
|
||||||
|
<textarea required
|
||||||
|
name="data"
|
||||||
|
id="dialog-variable-data"
|
||||||
|
placeholder="{{.locale.Tr "secrets.creation.value_placeholder"}}"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{template "base/modal_actions_confirm" (dict "locale" $.locale "ModalButtonTypes" "confirm")}}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue