mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +00:00
WIP new WebAdmin: event rules
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
c85601146d
commit
ad80d4e475
11 changed files with 1264 additions and 1014 deletions
2
go.mod
2
go.mod
|
@ -54,7 +54,7 @@ require (
|
|||
github.com/rs/xid v1.5.0
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/sftpgo/sdk v0.1.6-0.20240114195211-3f4916cc829c
|
||||
github.com/shirou/gopsutil/v3 v3.23.12
|
||||
github.com/shirou/gopsutil/v3 v3.24.1
|
||||
github.com/spf13/afero v1.11.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
|
|
4
go.sum
4
go.sum
|
@ -352,8 +352,8 @@ github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
|||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/sftpgo/sdk v0.1.6-0.20240114195211-3f4916cc829c h1:07TYPvNbOnmKsBxjNsUr+gsILIUWflw1UYwjn1jognM=
|
||||
github.com/sftpgo/sdk v0.1.6-0.20240114195211-3f4916cc829c/go.mod h1:AWoY2YYe/P1ymfTlRER/meERQjCcZZTbgVPGcPQgaqc=
|
||||
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
|
||||
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
|
||||
github.com/shirou/gopsutil/v3 v3.24.1 h1:R3t6ondCEvmARp3wxODhXMTLC/klMa87h2PHUw5m7QI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.1/go.mod h1:UU7a2MSBQa+kW1uuDq8DeEBS8kmrnQwsv2b5O513rwU=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
|
|
|
@ -117,19 +117,19 @@ func isEventTriggerValid(trigger int) bool {
|
|||
func getTriggerTypeAsString(trigger int) string {
|
||||
switch trigger {
|
||||
case EventTriggerFsEvent:
|
||||
return "Filesystem event"
|
||||
return util.I18nTriggerFsEvent
|
||||
case EventTriggerProviderEvent:
|
||||
return "Provider event"
|
||||
return util.I18nTriggerProviderEvent
|
||||
case EventTriggerIPBlocked:
|
||||
return "IP blocked"
|
||||
return util.I18nTriggerIPBlockedEvent
|
||||
case EventTriggerCertificate:
|
||||
return "Certificate renewal"
|
||||
return util.I18nTriggerCertificateRenewEvent
|
||||
case EventTriggerOnDemand:
|
||||
return "On demand"
|
||||
return util.I18nTriggerOnDemandEvent
|
||||
case EventTriggerIDPLogin:
|
||||
return "Identity Provider login"
|
||||
return util.I18nTriggerIDPLoginEvent
|
||||
default:
|
||||
return "Schedule"
|
||||
return util.I18nTriggerScheduleEvent
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1212,17 +1212,26 @@ func (a *EventAction) getACopy() EventAction {
|
|||
func (a *EventAction) validateAssociation(trigger int, fsEvents []string) error {
|
||||
if a.Options.IsFailureAction {
|
||||
if a.Options.ExecuteSync {
|
||||
return util.NewValidationError("sync execution is not supported for failure actions")
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("sync execution is not supported for failure actions"),
|
||||
util.I18nErrorEvSyncFailureActions,
|
||||
)
|
||||
}
|
||||
}
|
||||
if a.Options.ExecuteSync {
|
||||
if trigger != EventTriggerFsEvent && trigger != EventTriggerIDPLogin {
|
||||
return util.NewValidationError("sync execution is only supported for some filesystem events and Identity Provider logins")
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("sync execution is only supported for some filesystem events and Identity Provider logins"),
|
||||
util.I18nErrorEvSyncUnsupported,
|
||||
)
|
||||
}
|
||||
if trigger == EventTriggerFsEvent {
|
||||
for _, ev := range fsEvents {
|
||||
if !util.Contains(allowedSyncFsEvents, ev) {
|
||||
return util.NewValidationError("sync execution is only supported for upload and pre-* events")
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("sync execution is only supported for upload and pre-* events"),
|
||||
util.I18nErrorEvSyncUnsupportedFs,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1379,11 +1388,14 @@ func (c *EventConditions) getACopy() EventConditions {
|
|||
|
||||
func (c *EventConditions) validateSchedules() error {
|
||||
if len(c.Schedules) == 0 {
|
||||
return util.NewValidationError("at least one schedule is required")
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("at least one schedule is required"),
|
||||
util.I18nErrorRuleScheduleRequired,
|
||||
)
|
||||
}
|
||||
for _, schedule := range c.Schedules {
|
||||
if err := schedule.validate(); err != nil {
|
||||
return err
|
||||
return util.NewI18nError(err, util.I18nErrorRuleScheduleInvalid)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
@ -1397,7 +1409,10 @@ func (c *EventConditions) validate(trigger int) error {
|
|||
c.Options.ProviderObjects = nil
|
||||
c.IDPLoginEvent = 0
|
||||
if len(c.FsEvents) == 0 {
|
||||
return util.NewValidationError("at least one filesystem event is required")
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("at least one filesystem event is required"),
|
||||
util.I18nErrorRuleFsEventRequired,
|
||||
)
|
||||
}
|
||||
for _, ev := range c.FsEvents {
|
||||
if !util.Contains(SupportedFsEvents, ev) {
|
||||
|
@ -1414,7 +1429,10 @@ func (c *EventConditions) validate(trigger int) error {
|
|||
c.Options.MaxFileSize = 0
|
||||
c.IDPLoginEvent = 0
|
||||
if len(c.ProviderEvents) == 0 {
|
||||
return util.NewValidationError("at least one provider event is required")
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("at least one provider event is required"),
|
||||
util.I18nErrorRuleProviderEventRequired,
|
||||
)
|
||||
}
|
||||
for _, ev := range c.ProviderEvents {
|
||||
if !util.Contains(SupportedProviderEvents, ev) {
|
||||
|
@ -1558,7 +1576,7 @@ func (r *EventRule) isStatusValid() bool {
|
|||
|
||||
func (r *EventRule) validate() error {
|
||||
if r.Name == "" {
|
||||
return util.NewValidationError("name is mandatory")
|
||||
return util.NewI18nError(util.NewValidationError("name is mandatory"), util.I18nErrorNameRequired)
|
||||
}
|
||||
if !r.isStatusValid() {
|
||||
return util.NewValidationError(fmt.Sprintf("invalid event rule status: %d", r.Status))
|
||||
|
@ -1570,7 +1588,7 @@ func (r *EventRule) validate() error {
|
|||
return err
|
||||
}
|
||||
if len(r.Actions) == 0 {
|
||||
return util.NewValidationError("at least one action is required")
|
||||
return util.NewI18nError(util.NewValidationError("at least one action is required"), util.I18nErrorRuleActionRequired)
|
||||
}
|
||||
actionNames := make(map[string]bool)
|
||||
actionOrders := make(map[int]bool)
|
||||
|
@ -1581,7 +1599,10 @@ func (r *EventRule) validate() error {
|
|||
return util.NewValidationError(fmt.Sprintf("invalid action at position %d, name not specified", idx))
|
||||
}
|
||||
if actionNames[r.Actions[idx].Name] {
|
||||
return util.NewValidationError(fmt.Sprintf("duplicated action %q", r.Actions[idx].Name))
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("duplicated action %q", r.Actions[idx].Name)),
|
||||
util.I18nErrorRuleDuplicateActions,
|
||||
)
|
||||
}
|
||||
if actionOrders[r.Actions[idx].Order] {
|
||||
return util.NewValidationError(fmt.Sprintf("duplicated order %d for action %q",
|
||||
|
@ -1600,7 +1621,10 @@ func (r *EventRule) validate() error {
|
|||
actionOrders[r.Actions[idx].Order] = true
|
||||
}
|
||||
if len(r.Actions) == failureActions {
|
||||
return util.NewValidationError("at least a non-failure action is required")
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError("at least a non-failure action is required"),
|
||||
util.I18nErrorRuleFailureActionsOnly,
|
||||
)
|
||||
}
|
||||
if !hasSyncAction {
|
||||
return r.validateMandatorySyncActions()
|
||||
|
@ -1614,7 +1638,13 @@ func (r *EventRule) validateMandatorySyncActions() error {
|
|||
}
|
||||
for _, ev := range r.Conditions.FsEvents {
|
||||
if util.Contains(mandatorySyncFsEvents, ev) {
|
||||
return util.NewValidationError(fmt.Sprintf("event %q requires at least a sync action", ev))
|
||||
return util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("event %q requires at least a sync action", ev)),
|
||||
util.I18nErrorRuleSyncActionRequired,
|
||||
util.I18nErrorArgs(map[string]any{
|
||||
"val": ev,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -1775,6 +1775,8 @@ func (s *httpdServer) setupWebAdminRoutes() {
|
|||
s.handleWebUpdateEventActionPost)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader).
|
||||
Delete(webAdminEventActionPath+"/{name}", deleteEventAction)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), compressor.Handler, s.refreshCookie).
|
||||
Get(webAdminEventRulesPath+jsonAPISuffix, getAllRules)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
|
||||
Get(webAdminEventRulesPath, s.handleWebGetEventRules)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
|
||||
|
|
|
@ -98,7 +98,6 @@ const (
|
|||
templateMaintenance = "maintenance.html"
|
||||
templateMFA = "mfa.html"
|
||||
templateSetup = "adminsetup.html"
|
||||
pageEventRulesTitle = "Event rules"
|
||||
defaultQueryLimit = 1000
|
||||
inversePatternType = "inverse"
|
||||
)
|
||||
|
@ -153,11 +152,6 @@ type basePage struct {
|
|||
Branding UIBranding
|
||||
}
|
||||
|
||||
type eventRulesPage struct {
|
||||
basePage
|
||||
Rules []dataprovider.EventRule
|
||||
}
|
||||
|
||||
type statusPage struct {
|
||||
basePage
|
||||
Status *ServicesStatus
|
||||
|
@ -312,7 +306,7 @@ type eventRulePage struct {
|
|||
Protocols []string
|
||||
ProviderEvents []string
|
||||
ProviderObjects []string
|
||||
Error string
|
||||
Error *util.I18nError
|
||||
Mode genericPageMode
|
||||
IsShared bool
|
||||
}
|
||||
|
@ -1105,19 +1099,19 @@ func (s *httpdServer) renderEventActionPage(w http.ResponseWriter, r *http.Reque
|
|||
}
|
||||
|
||||
func (s *httpdServer) renderEventRulePage(w http.ResponseWriter, r *http.Request, rule dataprovider.EventRule,
|
||||
mode genericPageMode, error string,
|
||||
mode genericPageMode, err error,
|
||||
) {
|
||||
actions, err := s.getWebEventActions(w, r, defaultQueryLimit, true)
|
||||
if err != nil {
|
||||
actions, errActions := s.getWebEventActions(w, r, defaultQueryLimit, true)
|
||||
if errActions != nil {
|
||||
return
|
||||
}
|
||||
var title, currentURL string
|
||||
switch mode {
|
||||
case genericPageModeAdd:
|
||||
title = "Add new event rules"
|
||||
title = util.I18nAddRuleTitle
|
||||
currentURL = webAdminEventRulePath
|
||||
case genericPageModeUpdate:
|
||||
title = "Update event rules"
|
||||
title = util.I18nUpdateRuleTitle
|
||||
currentURL = fmt.Sprintf("%v/%v", webAdminEventRulePath, url.PathEscape(rule.Name))
|
||||
}
|
||||
|
||||
|
@ -1130,7 +1124,7 @@ func (s *httpdServer) renderEventRulePage(w http.ResponseWriter, r *http.Request
|
|||
Protocols: dataprovider.SupportedRuleConditionProtocols,
|
||||
ProviderEvents: dataprovider.SupportedProviderEvents,
|
||||
ProviderObjects: dataprovider.SupporteRuleConditionProviderObjects,
|
||||
Error: error,
|
||||
Error: getI18nError(err),
|
||||
Mode: mode,
|
||||
IsShared: s.isShared > 0,
|
||||
}
|
||||
|
@ -2420,74 +2414,66 @@ func getIDPLoginEventFromPostField(r *http.Request) int {
|
|||
func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventConditions, error) {
|
||||
var schedules []dataprovider.Schedule
|
||||
var names, groupNames, roleNames, fsPaths []dataprovider.ConditionPattern
|
||||
for k := range r.Form {
|
||||
if strings.HasPrefix(k, "schedule_hour") {
|
||||
hour := strings.TrimSpace(r.Form.Get(k))
|
||||
if hour != "" {
|
||||
idx := strings.TrimPrefix(k, "schedule_hour")
|
||||
dayOfWeek := strings.TrimSpace(r.Form.Get(fmt.Sprintf("schedule_day_of_week%s", idx)))
|
||||
dayOfMonth := strings.TrimSpace(r.Form.Get(fmt.Sprintf("schedule_day_of_month%s", idx)))
|
||||
month := strings.TrimSpace(r.Form.Get(fmt.Sprintf("schedule_month%s", idx)))
|
||||
schedules = append(schedules, dataprovider.Schedule{
|
||||
Hours: hour,
|
||||
DayOfWeek: dayOfWeek,
|
||||
DayOfMonth: dayOfMonth,
|
||||
Month: month,
|
||||
})
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(k, "name_pattern") {
|
||||
pattern := strings.TrimSpace(r.Form.Get(k))
|
||||
if pattern != "" {
|
||||
idx := strings.TrimPrefix(k, "name_pattern")
|
||||
patternType := r.Form.Get(fmt.Sprintf("type_name_pattern%s", idx))
|
||||
names = append(names, dataprovider.ConditionPattern{
|
||||
Pattern: pattern,
|
||||
InverseMatch: patternType == inversePatternType,
|
||||
})
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(k, "group_name_pattern") {
|
||||
pattern := strings.TrimSpace(r.Form.Get(k))
|
||||
if pattern != "" {
|
||||
idx := strings.TrimPrefix(k, "group_name_pattern")
|
||||
patternType := r.Form.Get(fmt.Sprintf("type_group_name_pattern%s", idx))
|
||||
groupNames = append(groupNames, dataprovider.ConditionPattern{
|
||||
Pattern: pattern,
|
||||
InverseMatch: patternType == inversePatternType,
|
||||
})
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(k, "role_name_pattern") {
|
||||
pattern := strings.TrimSpace(r.Form.Get(k))
|
||||
if pattern != "" {
|
||||
idx := strings.TrimPrefix(k, "role_name_pattern")
|
||||
patternType := r.Form.Get(fmt.Sprintf("type_role_name_pattern%s", idx))
|
||||
roleNames = append(roleNames, dataprovider.ConditionPattern{
|
||||
Pattern: pattern,
|
||||
InverseMatch: patternType == inversePatternType,
|
||||
})
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(k, "fs_path_pattern") {
|
||||
pattern := strings.TrimSpace(r.Form.Get(k))
|
||||
if pattern != "" {
|
||||
idx := strings.TrimPrefix(k, "fs_path_pattern")
|
||||
patternType := r.Form.Get(fmt.Sprintf("type_fs_path_pattern%s", idx))
|
||||
fsPaths = append(fsPaths, dataprovider.ConditionPattern{
|
||||
Pattern: pattern,
|
||||
InverseMatch: patternType == inversePatternType,
|
||||
})
|
||||
}
|
||||
|
||||
scheduleHours := r.Form["schedule_hour"]
|
||||
scheduleDayOfWeeks := r.Form["schedule_day_of_week"]
|
||||
scheduleDayOfMonths := r.Form["schedule_day_of_month"]
|
||||
scheduleMonths := r.Form["schedule_month"]
|
||||
|
||||
for idx, hour := range scheduleHours {
|
||||
if hour != "" {
|
||||
schedules = append(schedules, dataprovider.Schedule{
|
||||
Hours: hour,
|
||||
DayOfWeek: scheduleDayOfWeeks[idx],
|
||||
DayOfMonth: scheduleDayOfMonths[idx],
|
||||
Month: scheduleMonths[idx],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for idx, name := range r.Form["name_pattern"] {
|
||||
if name != "" {
|
||||
names = append(names, dataprovider.ConditionPattern{
|
||||
Pattern: name,
|
||||
InverseMatch: r.Form["type_name_pattern"][idx] == inversePatternType,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for idx, name := range r.Form["group_name_pattern"] {
|
||||
if name != "" {
|
||||
groupNames = append(groupNames, dataprovider.ConditionPattern{
|
||||
Pattern: name,
|
||||
InverseMatch: r.Form["type_group_name_pattern"][idx] == inversePatternType,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for idx, name := range r.Form["role_name_pattern"] {
|
||||
if name != "" {
|
||||
roleNames = append(roleNames, dataprovider.ConditionPattern{
|
||||
Pattern: name,
|
||||
InverseMatch: r.Form["type_role_name_pattern"][idx] == inversePatternType,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for idx, name := range r.Form["fs_path_pattern"] {
|
||||
if name != "" {
|
||||
fsPaths = append(fsPaths, dataprovider.ConditionPattern{
|
||||
Pattern: name,
|
||||
InverseMatch: r.Form["type_fs_path_pattern"][idx] == inversePatternType,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
minFileSize, err := util.ParseBytes(r.Form.Get("fs_min_size"))
|
||||
if err != nil {
|
||||
return dataprovider.EventConditions{}, fmt.Errorf("invalid min file size: %w", err)
|
||||
return dataprovider.EventConditions{}, util.NewI18nError(fmt.Errorf("invalid min file size: %w", err), util.I18nErrorInvalidMinSize)
|
||||
}
|
||||
maxFileSize, err := util.ParseBytes(r.Form.Get("fs_max_size"))
|
||||
if err != nil {
|
||||
return dataprovider.EventConditions{}, fmt.Errorf("invalid max file size: %w", err)
|
||||
return dataprovider.EventConditions{}, util.NewI18nError(fmt.Errorf("invalid max file size: %w", err), util.I18nErrorInvalidMaxSize)
|
||||
}
|
||||
conditions := dataprovider.EventConditions{
|
||||
FsEvents: r.Form["fs_events"],
|
||||
|
@ -2511,38 +2497,86 @@ func getEventRuleConditionsFromPostFields(r *http.Request) (dataprovider.EventCo
|
|||
|
||||
func getEventRuleActionsFromPostFields(r *http.Request) ([]dataprovider.EventAction, error) {
|
||||
var actions []dataprovider.EventAction
|
||||
for k := range r.Form {
|
||||
if strings.HasPrefix(k, "action_name") {
|
||||
name := strings.TrimSpace(r.Form.Get(k))
|
||||
if name != "" {
|
||||
idx := strings.TrimPrefix(k, "action_name")
|
||||
order, err := strconv.Atoi(r.Form.Get(fmt.Sprintf("action_order%s", idx)))
|
||||
if err != nil {
|
||||
return actions, fmt.Errorf("invalid order: %w", err)
|
||||
}
|
||||
options := r.Form[fmt.Sprintf("action_options%s", idx)]
|
||||
actions = append(actions, dataprovider.EventAction{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: name,
|
||||
},
|
||||
Order: order + 1,
|
||||
Options: dataprovider.EventActionOptions{
|
||||
IsFailureAction: util.Contains(options, "1"),
|
||||
StopOnFailure: util.Contains(options, "2"),
|
||||
ExecuteSync: util.Contains(options, "3"),
|
||||
},
|
||||
})
|
||||
|
||||
names := r.Form["action_name"]
|
||||
orders := r.Form["action_order"]
|
||||
|
||||
for idx, name := range names {
|
||||
if name != "" {
|
||||
order, err := strconv.Atoi(orders[idx])
|
||||
if err != nil {
|
||||
return actions, fmt.Errorf("invalid order: %w", err)
|
||||
}
|
||||
options := r.Form["action_options"+strconv.Itoa(idx)]
|
||||
actions = append(actions, dataprovider.EventAction{
|
||||
BaseEventAction: dataprovider.BaseEventAction{
|
||||
Name: name,
|
||||
},
|
||||
Order: order + 1,
|
||||
Options: dataprovider.EventActionOptions{
|
||||
IsFailureAction: util.Contains(options, "1"),
|
||||
StopOnFailure: util.Contains(options, "2"),
|
||||
ExecuteSync: util.Contains(options, "3"),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return actions, nil
|
||||
}
|
||||
|
||||
func updateRepeaterFormRuleFields(r *http.Request) {
|
||||
for k := range r.Form {
|
||||
if hasPrefixAndSuffix(k, "schedules[", "][schedule_hour]") {
|
||||
base, _ := strings.CutSuffix(k, "[schedule_hour]")
|
||||
r.Form.Add("schedule_hour", strings.TrimSpace(r.Form.Get(k)))
|
||||
r.Form.Add("schedule_day_of_week", strings.TrimSpace(r.Form.Get(base+"[schedule_day_of_week]")))
|
||||
r.Form.Add("schedule_day_of_month", strings.TrimSpace(r.Form.Get(base+"[schedule_day_of_month]")))
|
||||
r.Form.Add("schedule_month", strings.TrimSpace(r.Form.Get(base+"[schedule_month]")))
|
||||
continue
|
||||
}
|
||||
if hasPrefixAndSuffix(k, "name_filters[", "][name_pattern]") {
|
||||
base, _ := strings.CutSuffix(k, "[name_pattern]")
|
||||
r.Form.Add("name_pattern", strings.TrimSpace(r.Form.Get(k)))
|
||||
r.Form.Add("type_name_pattern", strings.TrimSpace(r.Form.Get(base+"[type_name_pattern]")))
|
||||
continue
|
||||
}
|
||||
if hasPrefixAndSuffix(k, "group_name_filters[", "][group_name_pattern]") {
|
||||
base, _ := strings.CutSuffix(k, "[group_name_pattern]")
|
||||
r.Form.Add("group_name_pattern", strings.TrimSpace(r.Form.Get(k)))
|
||||
r.Form.Add("type_group_name_pattern", strings.TrimSpace(r.Form.Get(base+"[type_group_name_pattern]")))
|
||||
continue
|
||||
}
|
||||
if hasPrefixAndSuffix(k, "role_name_filters[", "][role_name_pattern]") {
|
||||
base, _ := strings.CutSuffix(k, "[role_name_pattern]")
|
||||
r.Form.Add("role_name_pattern", strings.TrimSpace(r.Form.Get(k)))
|
||||
r.Form.Add("type_role_name_pattern", strings.TrimSpace(r.Form.Get(base+"[type_role_name_pattern]")))
|
||||
continue
|
||||
}
|
||||
if hasPrefixAndSuffix(k, "path_filters[", "][fs_path_pattern]") {
|
||||
base, _ := strings.CutSuffix(k, "[fs_path_pattern]")
|
||||
r.Form.Add("fs_path_pattern", strings.TrimSpace(r.Form.Get(k)))
|
||||
r.Form.Add("type_fs_path_pattern", strings.TrimSpace(r.Form.Get(base+"[type_fs_path_pattern]")))
|
||||
continue
|
||||
}
|
||||
if hasPrefixAndSuffix(k, "actions[", "][action_name]") {
|
||||
base, _ := strings.CutSuffix(k, "[action_name]")
|
||||
order, _ := strings.CutPrefix(k, "actions[")
|
||||
order, _ = strings.CutSuffix(order, "][action_name]")
|
||||
r.Form.Add("action_name", strings.TrimSpace(r.Form.Get(k)))
|
||||
r.Form["action_options"+strconv.Itoa(len(r.Form["action_name"])-1)] = r.Form[base+"[action_options][]"]
|
||||
r.Form.Add("action_order", order)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getEventRuleFromPostFields(r *http.Request) (dataprovider.EventRule, error) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
return dataprovider.EventRule{}, util.NewI18nError(err, util.I18nErrorInvalidForm)
|
||||
}
|
||||
updateRepeaterFormRuleFields(r)
|
||||
status, err := strconv.Atoi(r.Form.Get("status"))
|
||||
if err != nil {
|
||||
return dataprovider.EventRule{}, fmt.Errorf("invalid status: %w", err)
|
||||
|
@ -3789,31 +3823,27 @@ func (s *httpdServer) handleWebUpdateEventActionPost(w http.ResponseWriter, r *h
|
|||
http.Redirect(w, r, webAdminEventActionsPath, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebGetEventRules(w http.ResponseWriter, r *http.Request) {
|
||||
func getAllRules(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
limit := defaultQueryLimit
|
||||
if _, ok := r.URL.Query()["qlimit"]; ok {
|
||||
if lim, err := strconv.Atoi(r.URL.Query().Get("qlimit")); err == nil {
|
||||
limit = lim
|
||||
}
|
||||
}
|
||||
rules := make([]dataprovider.EventRule, 0, limit)
|
||||
rules := make([]dataprovider.EventRule, 0, 10)
|
||||
for {
|
||||
res, err := dataprovider.GetEventRules(limit, len(rules), dataprovider.OrderASC)
|
||||
res, err := dataprovider.GetEventRules(defaultQueryLimit, len(rules), dataprovider.OrderASC)
|
||||
if err != nil {
|
||||
s.renderInternalServerErrorPage(w, r, err)
|
||||
sendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nError500Message), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rules = append(rules, res...)
|
||||
if len(res) < limit {
|
||||
if len(res) < defaultQueryLimit {
|
||||
break
|
||||
}
|
||||
}
|
||||
render.JSON(w, r, rules)
|
||||
}
|
||||
|
||||
data := eventRulesPage{
|
||||
basePage: s.getBasePageData(pageEventRulesTitle, webAdminEventRulesPath, r),
|
||||
Rules: rules,
|
||||
}
|
||||
func (s *httpdServer) handleWebGetEventRules(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
data := s.getBasePageData(util.I18nRulesTitle, webAdminEventRulesPath, r)
|
||||
renderAdminTemplate(w, templateEventRules, data)
|
||||
}
|
||||
|
||||
|
@ -3823,7 +3853,7 @@ func (s *httpdServer) handleWebAddEventRuleGet(w http.ResponseWriter, r *http.Re
|
|||
Status: 1,
|
||||
Trigger: dataprovider.EventTriggerFsEvent,
|
||||
}
|
||||
s.renderEventRulePage(w, r, rule, genericPageModeAdd, "")
|
||||
s.renderEventRulePage(w, r, rule, genericPageModeAdd, nil)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleWebAddEventRulePost(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -3835,7 +3865,7 @@ func (s *httpdServer) handleWebAddEventRulePost(w http.ResponseWriter, r *http.R
|
|||
}
|
||||
rule, err := getEventRuleFromPostFields(r)
|
||||
if err != nil {
|
||||
s.renderEventRulePage(w, r, rule, genericPageModeAdd, err.Error())
|
||||
s.renderEventRulePage(w, r, rule, genericPageModeAdd, err)
|
||||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
|
@ -3845,7 +3875,7 @@ func (s *httpdServer) handleWebAddEventRulePost(w http.ResponseWriter, r *http.R
|
|||
return
|
||||
}
|
||||
if err = dataprovider.AddEventRule(&rule, claims.Username, ipAddr, claims.Role); err != nil {
|
||||
s.renderEventRulePage(w, r, rule, genericPageModeAdd, err.Error())
|
||||
s.renderEventRulePage(w, r, rule, genericPageModeAdd, err)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, webAdminEventRulesPath, http.StatusSeeOther)
|
||||
|
@ -3856,7 +3886,7 @@ func (s *httpdServer) handleWebUpdateEventRuleGet(w http.ResponseWriter, r *http
|
|||
name := getURLParam(r, "name")
|
||||
rule, err := dataprovider.EventRuleExists(name)
|
||||
if err == nil {
|
||||
s.renderEventRulePage(w, r, rule, genericPageModeUpdate, "")
|
||||
s.renderEventRulePage(w, r, rule, genericPageModeUpdate, nil)
|
||||
} else if errors.Is(err, util.ErrNotFound) {
|
||||
s.renderNotFoundPage(w, r, err)
|
||||
} else {
|
||||
|
@ -3882,7 +3912,7 @@ func (s *httpdServer) handleWebUpdateEventRulePost(w http.ResponseWriter, r *htt
|
|||
}
|
||||
updatedRule, err := getEventRuleFromPostFields(r)
|
||||
if err != nil {
|
||||
s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err.Error())
|
||||
s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err)
|
||||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
|
@ -3894,7 +3924,7 @@ func (s *httpdServer) handleWebUpdateEventRulePost(w http.ResponseWriter, r *htt
|
|||
updatedRule.Name = rule.Name
|
||||
err = dataprovider.UpdateEventRule(&updatedRule, claims.Username, ipAddr, claims.Role)
|
||||
if err != nil {
|
||||
s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err.Error())
|
||||
s.renderEventRulePage(w, r, updatedRule, genericPageModeUpdate, err)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, webAdminEventRulesPath, http.StatusSeeOther)
|
||||
|
|
|
@ -69,8 +69,11 @@ const (
|
|||
I18nDefenderTitle = "title.defender"
|
||||
I18nEventsTitle = "title.logs"
|
||||
I18nActionsTitle = "title.event_actions"
|
||||
I18nRulesTitle = "title.event_rules"
|
||||
I18nAddActionTitle = "title.add_action"
|
||||
I18nUpdateActionTitle = "title.update_action"
|
||||
I18nAddRuleTitle = "title.add_rule"
|
||||
I18nUpdateRuleTitle = "title.update_rule"
|
||||
I18nStatusTitle = "status.desc"
|
||||
I18nErrorSetupInstallCode = "setup.install_code_mismatch"
|
||||
I18nInvalidAuth = "general.invalid_auth_request"
|
||||
|
@ -278,6 +281,26 @@ const (
|
|||
I18nActionFsTypeCompress = "actions.fs_types.compress"
|
||||
I18nActionFsTypeCopy = "actions.fs_types.copy"
|
||||
I18nActionFsTypeCreateDirs = "actions.fs_types.create_dirs"
|
||||
I18nTriggerFsEvent = "rules.triggers.fs_event"
|
||||
I18nTriggerProviderEvent = "rules.triggers.provider_event"
|
||||
I18nTriggerIPBlockedEvent = "rules.triggers.ip_blocked"
|
||||
I18nTriggerCertificateRenewEvent = "rules.triggers.certificate_renewal"
|
||||
I18nTriggerOnDemandEvent = "rules.triggers.on_demand"
|
||||
I18nTriggerIDPLoginEvent = "rules.triggers.idp_login"
|
||||
I18nTriggerScheduleEvent = "rules.triggers.schedule"
|
||||
I18nErrorInvalidMinSize = "rules.invalid_fs_min_size"
|
||||
I18nErrorInvalidMaxSize = "rules.invalid_fs_max_size"
|
||||
I18nErrorRuleActionRequired = "rules.action_required"
|
||||
I18nErrorRuleFsEventRequired = "rules.fs_event_required"
|
||||
I18nErrorRuleProviderEventRequired = "rules.provider_event_required"
|
||||
I18nErrorRuleScheduleRequired = "rules.schedule_required"
|
||||
I18nErrorRuleScheduleInvalid = "rules.schedule_invalid"
|
||||
I18nErrorRuleDuplicateActions = "rules.duplicate_actions"
|
||||
I18nErrorEvSyncFailureActions = "rules.sync_failure_actions"
|
||||
I18nErrorEvSyncUnsupported = "rules.sync_unsupported"
|
||||
I18nErrorEvSyncUnsupportedFs = "rules.sync_unsupported_fs_event"
|
||||
I18nErrorRuleFailureActionsOnly = "rules.only_failure_actions"
|
||||
I18nErrorRuleSyncActionRequired = "rules.sync_action_required"
|
||||
)
|
||||
|
||||
// NewI18nError returns a I18nError wrappring the provided error
|
||||
|
|
|
@ -63,7 +63,9 @@
|
|||
"add_ip_list": "Add IP list entry",
|
||||
"update_ip_list": "Update IP list entry",
|
||||
"add_action": "Add action",
|
||||
"update_action": "Update action"
|
||||
"update_action": "Update action",
|
||||
"add_rule": "Add rule",
|
||||
"update_rule": "Update rule"
|
||||
},
|
||||
"setup": {
|
||||
"desc": "To start using SFTPGo you need to create an administrator user",
|
||||
|
@ -252,7 +254,12 @@
|
|||
"timeout": "Timeout",
|
||||
"env_vars": "Environment variables",
|
||||
"hours": "Hours",
|
||||
"paths": "Paths"
|
||||
"paths": "Paths",
|
||||
"hour": "Hour",
|
||||
"day_of_week": "Day of week",
|
||||
"day_of_month": "Day of month",
|
||||
"month": "Month",
|
||||
"options": "Options"
|
||||
},
|
||||
"fs": {
|
||||
"view_file": "View file \"{{- path}}\"",
|
||||
|
@ -1003,5 +1010,63 @@
|
|||
"metadata_string": "Cloud storage metadata for the downloaded file as JSON escaped string",
|
||||
"uid": "Unique ID"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"view_manage": "View and manage rules for events",
|
||||
"trigger": "Trigger",
|
||||
"run_confirm": "Do you want to execute the selected rule?",
|
||||
"run_confirm_btn": "Yes, run",
|
||||
"run_error_generic": "Unable to run the selected rule",
|
||||
"run_ok": "Rule actions started",
|
||||
"run": "Run",
|
||||
"invalid_fs_min_size": "Invalid min size",
|
||||
"invalid_fs_max_size": "Invalid max size",
|
||||
"action_required": "At least one action is required",
|
||||
"fs_event_required": "At least one filesystem event is required",
|
||||
"provider_event_required": "At least one provider event is required",
|
||||
"schedule_required": "At least one schedule is required",
|
||||
"schedule_invalid": "Invalid schedule",
|
||||
"duplicate_actions": "Duplicate actions detected",
|
||||
"sync_failure_actions": "Synchronous execution is not supported for failure actions",
|
||||
"sync_unsupported": "Synchronous execution is only supported for some filesystem events and Identity Provider logins",
|
||||
"sync_unsupported_fs_event": "Synchronous execution is only supported for upload and pre-* filesystem events",
|
||||
"only_failure_actions": "At least a non-failure action is required",
|
||||
"sync_action_required": "Event \"{{val}}\" requires at least a synchronous action",
|
||||
"scheduler_help": "The scheduler uses UTC time. Hours: 0-23. Day of week: 0-6 (Sun-Sat). Day of month: 1-31. Month: 1-12. Asterisk (*) indicates a match for all the values of the field. e.g. every day of week, every day of month and so on",
|
||||
"concurrent_run": "Allow concurrent execution from multiple instances",
|
||||
"protocol_filters": "Protocol filters",
|
||||
"object_filters": "Object filters",
|
||||
"name_filters": "Name filters",
|
||||
"name_filters_help": "Shell-like pattern filters for usernames, folder names. For example \"user*\"\" will match names starting with \"user\". For provider events, this filter is applied to the username of the admin executing the event",
|
||||
"inverse_match": "Inverse match",
|
||||
"group_name_filters": "Group name filters",
|
||||
"group_name_filters_help": "Shell-like pattern filters for group names. For example \"group*\"\" will match group names starting with \"group\"",
|
||||
"role_name_filters": "Role name filters",
|
||||
"role_name_filters_help": "Shell-like pattern filters for role names. For example \"role*\"\" will match role names starting with \"role\"",
|
||||
"path_filters": "Path filters",
|
||||
"path_filters_help": "Shell-like pattern filters on filesystem event paths. For example \"/adir/*.txt\"\" will match paths in the \"/adir\" directory ending with \".txt\". Double asterisk is supported, for example \"/**/*.txt\" will match any file ending with \".txt\". \"/mydir/**\" will match any entry in \"/mydir\"",
|
||||
"file_size_limits": "File size limits",
|
||||
"file_size_limits_help": "0 means no limit. You can use MB/GB suffix",
|
||||
"min_size": "Minimum size",
|
||||
"max_size": "Maximum size",
|
||||
"actions_help": "One or more actions to execute. The \"Execute sync\" option is supported for \"upload\" events and required for \"pre-*\" events and Identity provider login events if the action checks the account",
|
||||
"option_failure_action": "Failure action",
|
||||
"option_stop_on_failure": "Stop on failure",
|
||||
"option_execute_sync": "Synchronous execution",
|
||||
"no_filter": "No filter means always triggering events",
|
||||
"action_placeholder": "Select an action",
|
||||
"triggers": {
|
||||
"fs_event": "Filesystem events",
|
||||
"provider_event": "Provider events",
|
||||
"ip_blocked": "IP blocked",
|
||||
"certificate_renewal": "Certificate renewal",
|
||||
"on_demand": "On demand",
|
||||
"idp_login": "Identity Provider logins",
|
||||
"schedule": "Schedules"
|
||||
},
|
||||
"idp_logins": {
|
||||
"user": "User login",
|
||||
"admin": "Admin login"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -63,7 +63,9 @@
|
|||
"add_ip_list": "Aggiungi elemento a lista IP",
|
||||
"update_ip_list": "Aggiorna elemento lista IP",
|
||||
"add_action": "Aggiungi azione",
|
||||
"update_action": "Aggiorna azione"
|
||||
"update_action": "Aggiorna azione",
|
||||
"add_rule": "Aggiungi regola",
|
||||
"update_rule": "Aggiorna regola"
|
||||
},
|
||||
"setup": {
|
||||
"desc": "Per iniziare a utilizzare SFTPGo devi creare un utente amministratore",
|
||||
|
@ -252,7 +254,12 @@
|
|||
"timeout": "Timeout",
|
||||
"env_vars": "Variabili d'ambiente",
|
||||
"hours": "Ore",
|
||||
"paths": "Percorsi"
|
||||
"paths": "Percorsi",
|
||||
"hour": "Ora",
|
||||
"day_of_week": "Giorno settimana",
|
||||
"day_of_month": "Giorno mese",
|
||||
"month": "Mese",
|
||||
"options": "Opzioni"
|
||||
},
|
||||
"fs": {
|
||||
"view_file": "Visualizza file \"{{- path}}\"",
|
||||
|
@ -1003,5 +1010,63 @@
|
|||
"metadata_string": "Metadati del Cloud Storage Provider serializzati come stringa JSON escaped per i file scaricati",
|
||||
"uid": "ID univoco"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"view_manage": "Visualizza e gestisci le regole per gli eventi",
|
||||
"trigger": "Attivazione",
|
||||
"run_confirm": "Vuoi eseguire la regola selezionata?",
|
||||
"run_confirm_btn": "Si, esegui",
|
||||
"run_error_generic": "Impossibile eseguire la regola selezionata",
|
||||
"run_ok": "Azioni delle regola avviate",
|
||||
"run": "Esegui",
|
||||
"invalid_fs_min_size": "Dimensione minima non valida",
|
||||
"invalid_fs_max_size": "Dimensione massima non valida",
|
||||
"action_required": "Almeno un'azione è obbligatoria",
|
||||
"fs_event_required": "Almeno un evento file system è obbligatorio",
|
||||
"provider_event_required": "Almeno un evento provider è obbligatorio",
|
||||
"schedule_required": "Almeno una schedulazione è obbligatoria",
|
||||
"schedule_invalid": "Schedulazione non valida",
|
||||
"duplicate_actions": "Rilevata azioni duplicate",
|
||||
"sync_failure_actions": "L'esecuzione sincrona non è supportata per le azioni su errore",
|
||||
"sync_unsupported": "L'esecuzione sincrona è supportata solo per alcuni eventi del file system e per gli accessi tramite Identity Provider",
|
||||
"sync_unsupported_fs_event": "L'esecuzione sincrona è supporta solo per gli eventi \"upload\" e \"pre-*\"",
|
||||
"only_failure_actions": "E' richiesta almeno un'azione che non venga eseguita su errore",
|
||||
"sync_action_required": "L'evento \"{{val}}\" richiede almeno un'azione da eseguire sincronamente",
|
||||
"scheduler_help": "Lo scheduler utilizza l'ora UTC. Orari: 0-23. Giorno della settimana: 0-6 (dom-sab). Giorno del mese: 1-31. Mese: 1-12. L'asterisco (*) indica una corrispondenza per tutti i valori del campo. per esempio. ogni giorno della settimana, ogni giorno del mese e così via",
|
||||
"concurrent_run": "Consentire l'esecuzione simultanea da più istanze",
|
||||
"protocol_filters": "Filtro su protocolli",
|
||||
"object_filters": "Filtro su oggetti",
|
||||
"name_filters": "Filtro su nomi",
|
||||
"name_filters_help": "Filtri per nomi utente e nomi di cartelle. Ad esempio, \"user*\"\" corrisponderà per i nomi che iniziano con \"user\". Per gli eventi del provider, questo filtro viene applicato al nome utente dell'amministratore che esegue l'evento",
|
||||
"inverse_match": "Corrispondenza inversa",
|
||||
"group_name_filters": "Filtro su nome gruppi",
|
||||
"group_name_filters_help": "Filtri per nomi dei gruppi. Ad esempio \"group*\"\" corrisponderà ai nomi dei gruppi che iniziano con \"group\"",
|
||||
"role_name_filters": "Filtri su nome ruoli",
|
||||
"role_name_filters_help": "Filtri per nomi dei ruoli. Ad esempio \"role*\"\" corrisponderà ai nomi dei gruppi che iniziano con \"role\"",
|
||||
"path_filters": "Filtri sui percorsi",
|
||||
"path_filters_help": "Filtri sui percorsi degli eventi del file system. Ad esempio \"/adir/*.txt\"\" corrisponderà ai percorsi nella directory \"/adir\" che terminano con \".txt\". È supportato il doppio asterisco, ad esempio \"/**/*. txt\" corrisponderà a qualsiasi file che termina con \".txt\". \"/mydir/**\" corrisponderà a qualsiasi voce in \"/mydir\"",
|
||||
"file_size_limits": "Filtri sulla dimensione file",
|
||||
"file_size_limits_help": "0 significa nessun limite. È possibile utilizzare il suffisso MB/GB",
|
||||
"min_size": "Dimensione min",
|
||||
"max_size": "Dimensione max",
|
||||
"actions_help": "Una o più azioni da eseguire. L'opzione \"Esecuzione sincrona\" è supportata per gli eventi di \"upload\" ed è richiesta per gli eventi \"pre-*\" e gli eventi di accesso tramite Identity provider se l'azione controlla l'account",
|
||||
"option_failure_action": "Azione su errore",
|
||||
"option_stop_on_failure": "Termina su errore",
|
||||
"option_execute_sync": "Esecuzione sincrona",
|
||||
"no_filter": "Nessun filtro significa attivare sempre gli eventi",
|
||||
"action_placeholder": "Seleziona un'azione",
|
||||
"triggers": {
|
||||
"fs_event": "Eventi file system",
|
||||
"provider_event": "Eventi provider",
|
||||
"ip_blocked": "IP bloccato",
|
||||
"certificate_renewal": "Rinnovo certificato",
|
||||
"on_demand": "Su richiesta",
|
||||
"idp_login": "Accessi tramite Identity Provider",
|
||||
"schedule": "Schedulazioni"
|
||||
},
|
||||
"idp_logins": {
|
||||
"user": "Accesso utente",
|
||||
"admin": "Accesso amministratore"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1020,13 +1020,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
|
||||
$('#idType').on("change", function(){
|
||||
onTypeChanged(this.value);
|
||||
})
|
||||
});
|
||||
|
||||
$('#idFsActionType').on("change", function(){
|
||||
onFsActionChanged(this.value);
|
||||
});
|
||||
|
||||
$('#role_form').submit(function (event) {
|
||||
$('#eventaction_form').submit(function (event) {
|
||||
let submitButton = document.querySelector('#form_submit');
|
||||
submitButton.setAttribute('data-kt-indicator', 'on');
|
||||
submitButton.disabled = true;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,326 +1,406 @@
|
|||
<!--
|
||||
Copyright (C) 2019 Nicola Murino
|
||||
Copyright (C) 2024 Nicola Murino
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, version 3.
|
||||
This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
https://keenthemes.com/products/templates-mega-bundle
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
KeenThemes HTML/CSS/JS components are allowed for use only within the
|
||||
SFTPGo product and restricted to be used in a resealable HTML template
|
||||
that can compete with KeenThemes products anyhow.
|
||||
|
||||
This WebUI is allowed for use only within the SFTPGo product and
|
||||
therefore cannot be used in derivative works/products without an
|
||||
explicit grant from the SFTPGo Team (support@sftpgo.com).
|
||||
-->
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}{{.Title}}{{end}}
|
||||
{{- define "extra_css"}}
|
||||
<link href="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css" rel="stylesheet" type="text/css"/>
|
||||
{{- end}}
|
||||
|
||||
{{define "extra_css"}}
|
||||
<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
|
||||
<link href="{{.StaticURL}}/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
|
||||
{{end}}
|
||||
|
||||
{{define "page_body"}}
|
||||
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert">
|
||||
<span id="errorTxt"></span>
|
||||
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
function dismissErrorMsg(){
|
||||
$('#errorMsg').hide();
|
||||
}
|
||||
</script>
|
||||
<div id="successMsg" class="card mb-4 border-left-success" style="display: none;">
|
||||
<div id="successTxt" class="card-body"></div>
|
||||
</div>
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">View and manage event rules</h6>
|
||||
{{- define "page_body"}}
|
||||
{{- template "errmsg" ""}}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h3 data-i18n="rules.view_manage" class="card-title section-title">View and manage event rules</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0">
|
||||
<div id="card_body" class="card-body">
|
||||
<div id="loader" class="align-items-center text-center my-10">
|
||||
<span class="spinner-border w-15px h-15px text-muted align-middle me-2"></span>
|
||||
<span data-i18n="general.loading" class="text-gray-700">Loading...</span>
|
||||
</div>
|
||||
<div id="card_content" class="d-none">
|
||||
<div class="d-flex flex-stack flex-wrap mb-5">
|
||||
<div class="d-flex align-items-center position-relative my-2">
|
||||
<i class="ki-solid ki-magnifier fs-1 position-absolute ms-6"></i>
|
||||
<input name="search" data-i18n="[placeholder]general.search" type="text" data-table-filter="search"
|
||||
class="form-control rounded-1 w-250px ps-15 me-5" placeholder="Search" />
|
||||
</div>
|
||||
<div class="d-flex justify-content-end my-2" data-table-toolbar="base">
|
||||
{{- if .LoggedUser.HasPermission "manage_event_rules"}}
|
||||
<a href="{{.EventRuleURL}}" class="btn btn-primary ms-5">
|
||||
<i class="ki-duotone ki-plus fs-2"></i>
|
||||
<span data-i18n="general.add">Add</span>
|
||||
</a>
|
||||
{{- end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Description</th>
|
||||
<th>Trigger</th>
|
||||
<th>Actions</th>
|
||||
<tr class="text-start text-muted fw-bold fs-6 gs-0">
|
||||
<th data-i18n="general.name">Name</th>
|
||||
<th data-i18n="general.status">Status</th>
|
||||
<th data-i18n="rules.trigger">Trigger</th>
|
||||
<th data-i18n="title.event_actions">Actions</th>
|
||||
<th class="min-w-100px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Rules}}
|
||||
<tr>
|
||||
<td>{{.Trigger}}</td>
|
||||
<td>{{.Name}}</td>
|
||||
<td>{{if eq .Status 1 }}Active{{else}}Inactive{{end}}</td>
|
||||
<td>{{.Description}}</td>
|
||||
<td>{{.GetTriggerAsString}}</td>
|
||||
<td>{{.GetActionsAsString}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
<tbody id="table_body" class="text-gray-800 fw-semibold"></tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{- end}}
|
||||
|
||||
{{define "dialog"}}
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">
|
||||
Confirmation required
|
||||
</h5>
|
||||
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">Do you want to delete the selected rule?</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" type="button" data-dismiss="modal">
|
||||
Cancel
|
||||
</button>
|
||||
<a class="btn btn-warning" href="#" onclick="deleteAction()">
|
||||
Delete
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{- define "extra_js"}}
|
||||
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js"></script>
|
||||
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
|
||||
function deleteAction(name) {
|
||||
ModalAlert.fire({
|
||||
text: $.t('general.delete_confirm_generic'),
|
||||
icon: "warning",
|
||||
confirmButtonText: $.t('general.delete_confirm_btn'),
|
||||
cancelButtonText: $.t('general.cancel'),
|
||||
customClass: {
|
||||
confirmButton: "btn btn-danger",
|
||||
cancelButton: 'btn btn-secondary'
|
||||
}
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed){
|
||||
$('#loading_message').text("");
|
||||
KTApp.showPageLoading();
|
||||
let path = '{{.EventRuleURL}}' + "/" + encodeURIComponent(name);
|
||||
|
||||
<div class="modal fade" id="runModal" tabindex="-1" role="dialog" aria-labelledby="runModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="runModalLabel">
|
||||
Confirmation required
|
||||
</h5>
|
||||
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">Do you want to execute the selected rule?</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" type="button" data-dismiss="modal">
|
||||
Cancel
|
||||
</button>
|
||||
<a class="btn btn-warning" href="#" onclick="runAction()">
|
||||
Run
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "extra_js"}}
|
||||
<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/buttons.colVis.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/dataTables.select.min.js"></script>
|
||||
<script src="{{.StaticURL}}/vendor/datatables/ellipsis.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
function runAction(){
|
||||
let table = $('#dataTable').DataTable();
|
||||
table.button('run:name').enable(false);
|
||||
let name = table.row({ selected: true }).data()[1];
|
||||
let path = '{{.EventRuleURL}}' + "/run/" + fixedEncodeURIComponent(name);
|
||||
$('#runModal').modal('hide');
|
||||
$('#successMsg').hide();
|
||||
$('#errorMsg').hide();
|
||||
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||
timeout: 15000,
|
||||
success: function (result) {
|
||||
$('#successTxt').text("Rule actions started");
|
||||
$('#successMsg').show();
|
||||
setTimeout(function () {
|
||||
$('#successMsg').hide();
|
||||
}, 8000);
|
||||
},
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
var txt = "Unable to run the selected rule";
|
||||
if ($xhr) {
|
||||
var json = $xhr.responseJSON;
|
||||
if (json) {
|
||||
if (json.message){
|
||||
txt += ": " + json.message;
|
||||
} else {
|
||||
txt += ": " + json.error;
|
||||
axios.delete(path, {
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{.CSRFToken}}'
|
||||
},
|
||||
validateStatus: function (status) {
|
||||
return status == 200;
|
||||
}
|
||||
}).then(function(response){
|
||||
location.reload();
|
||||
}).catch(function(error){
|
||||
KTApp.hidePageLoading();
|
||||
let errorMessage;
|
||||
if (error && error.response) {
|
||||
switch (error.response.status) {
|
||||
case 403:
|
||||
errorMessage = "general.delete_error_403";
|
||||
break;
|
||||
case 404:
|
||||
errorMessage = "general.delete_error_404";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$('#errorTxt').text(txt);
|
||||
$('#errorMsg').show();
|
||||
if (!errorMessage){
|
||||
errorMessage = "general.delete_error_generic";
|
||||
}
|
||||
ModalAlert.fire({
|
||||
text: $.t(errorMessage),
|
||||
icon: "warning",
|
||||
confirmButtonText: $.t('general.ok'),
|
||||
customClass: {
|
||||
confirmButton: "btn btn-primary"
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteAction() {
|
||||
let table = $('#dataTable').DataTable();
|
||||
table.button('delete:name').enable(false);
|
||||
let name = table.row({ selected: true }).data()[1];
|
||||
let path = '{{.EventRuleURL}}' + "/" + fixedEncodeURIComponent(name);
|
||||
$('#deleteModal').modal('hide');
|
||||
$('#errorMsg').hide();
|
||||
function runAction(name) {
|
||||
ModalAlert.fire({
|
||||
text: $.t('rules.run_confirm'),
|
||||
icon: "warning",
|
||||
confirmButtonText: $.t('rules.run_confirm_btn'),
|
||||
cancelButtonText: $.t('general.cancel'),
|
||||
customClass: {
|
||||
confirmButton: "btn btn-danger",
|
||||
cancelButton: 'btn btn-secondary'
|
||||
}
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed){
|
||||
$('#loading_message').text("");
|
||||
KTApp.showPageLoading();
|
||||
let path = '{{.EventRuleURL}}' + "/run/" + encodeURIComponent(name);
|
||||
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: 'DELETE',
|
||||
dataType: 'json',
|
||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||
timeout: 15000,
|
||||
success: function (result) {
|
||||
window.location.href = '{{.EventRulesURL}}';
|
||||
},
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
var txt = "Unable to delete the selected rule";
|
||||
if ($xhr) {
|
||||
var json = $xhr.responseJSON;
|
||||
if (json) {
|
||||
if (json.message){
|
||||
txt += ": " + json.message;
|
||||
} else {
|
||||
txt += ": " + json.error;
|
||||
}
|
||||
axios.post(path, null, {
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{.CSRFToken}}'
|
||||
},
|
||||
validateStatus: function (status) {
|
||||
return status == 202;
|
||||
}
|
||||
}
|
||||
$('#errorTxt').text(txt);
|
||||
$('#errorMsg').show();
|
||||
}).then(function(response){
|
||||
KTApp.hidePageLoading();
|
||||
ModalAlert.fire({
|
||||
text: $.t("rules.run_ok"),
|
||||
icon: "success",
|
||||
confirmButtonText: $.t('general.ok'),
|
||||
customClass: {
|
||||
confirmButton: "btn btn-primary"
|
||||
}
|
||||
});
|
||||
}).catch(function(error){
|
||||
KTApp.hidePageLoading();
|
||||
ModalAlert.fire({
|
||||
text: $.t("rules.run_error_generic"),
|
||||
icon: "warning",
|
||||
confirmButtonText: $.t('general.ok'),
|
||||
customClass: {
|
||||
confirmButton: "btn btn-primary"
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$.fn.dataTable.ext.buttons.add = {
|
||||
text: '<i class="fas fa-plus"></i>',
|
||||
name: 'add',
|
||||
titleAttr: "Add",
|
||||
action: function (e, dt, node, config) {
|
||||
window.location.href = '{{.EventRuleURL}}';
|
||||
}
|
||||
};
|
||||
var datatable = function(){
|
||||
var dt;
|
||||
|
||||
$.fn.dataTable.ext.buttons.edit = {
|
||||
text: '<i class="fas fa-pen"></i>',
|
||||
name: 'edit',
|
||||
titleAttr: "Edit",
|
||||
action: function (e, dt, node, config) {
|
||||
let name = table.row({ selected: true }).data()[1];
|
||||
let path = '{{.EventRuleURL}}' + "/" + fixedEncodeURIComponent(name);
|
||||
window.location.href = path;
|
||||
},
|
||||
enabled: false
|
||||
};
|
||||
var initDatatable = function () {
|
||||
$('#errorMsg').addClass("d-none");
|
||||
dt = $('#dataTable').DataTable({
|
||||
ajax: {
|
||||
url: "{{.EventRulesURL}}/json",
|
||||
dataSrc: "",
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
$(".dataTables_processing").hide();
|
||||
let txt = "";
|
||||
if ($xhr) {
|
||||
let json = $xhr.responseJSON;
|
||||
if (json) {
|
||||
if (json.message){
|
||||
txt = json.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!txt){
|
||||
txt = "general.error500";
|
||||
}
|
||||
setI18NData($('#errorTxt'), txt);
|
||||
$('#errorMsg').removeClass("d-none");
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
data: "name",
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
return escapeHTML(data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "status",
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
switch (data){
|
||||
case 1:
|
||||
return $.t('general.active');
|
||||
default:
|
||||
return $.t('general.inactive');
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "trigger",
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
switch (data){
|
||||
case 1:
|
||||
return $.t('rules.triggers.fs_event');
|
||||
case 2:
|
||||
return $.t('rules.triggers.provider_event');
|
||||
case 3:
|
||||
return $.t('rules.triggers.schedule');
|
||||
case 4:
|
||||
return $.t('rules.triggers.ip_blocked');
|
||||
case 5:
|
||||
return $.t('rules.triggers.certificate_renewal');
|
||||
case 6:
|
||||
return $.t('rules.triggers.on_demand');
|
||||
case 7:
|
||||
return $.t('rules.triggers.idp_login');
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "actions",
|
||||
defaultContent: [],
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
if (data){
|
||||
let actions = [];
|
||||
for (i = 0; i < data.length; i++){
|
||||
actions.push(data[i].name);
|
||||
}
|
||||
return escapeHTML(actions.join(', '));
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return "";
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "id",
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
className: 'text-end',
|
||||
render: function (data, type, row) {
|
||||
if (type === 'display') {
|
||||
let numActions = 0;
|
||||
let actions = `<button class="btn btn-light btn-active-light-primary btn-flex btn-center btn-sm rotate" data-kt-menu-trigger="click" data-kt-menu-placement="bottom-end">
|
||||
<span data-i18n="general.actions" class="fs-6">Actions</span>
|
||||
<i class="ki-duotone ki-down fs-5 ms-1 rotate-180"></i>
|
||||
</button>
|
||||
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-200px py-4" data-kt-menu="true">`;
|
||||
|
||||
$.fn.dataTable.ext.buttons.delete = {
|
||||
text: '<i class="fas fa-trash"></i>',
|
||||
name: 'delete',
|
||||
titleAttr: "Delete",
|
||||
action: function (e, dt, node, config) {
|
||||
$('#deleteModal').modal('show');
|
||||
},
|
||||
enabled: false
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.run = {
|
||||
text: '<i class="fas fa-play"></i>',
|
||||
name: 'run',
|
||||
titleAttr: "Run",
|
||||
action: function (e, dt, node, config) {
|
||||
$('#runModal').modal('show');
|
||||
},
|
||||
enabled: false
|
||||
};
|
||||
|
||||
var table = $('#dataTable').DataTable({
|
||||
"select": {
|
||||
"style": "single",
|
||||
"blurable": true
|
||||
},
|
||||
"stateSave": true,
|
||||
"stateDuration": 0,
|
||||
"buttons": [
|
||||
{
|
||||
"text": "Column visibility",
|
||||
"extend": "colvis",
|
||||
"columns": ":not(.noVis)"
|
||||
//{{- if .LoggedUser.HasPermission "manage_event_rules"}}
|
||||
numActions++;
|
||||
actions+=`<div class="menu-item px-3">
|
||||
<a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
|
||||
</div>`
|
||||
numActions++;
|
||||
if (row.trigger === 6){
|
||||
actions+=`<div class="menu-item px-3">
|
||||
<a data-i18n="rules.run" href="#" class="menu-link px-3" data-table-action="run_row">Run</a>
|
||||
</div>`
|
||||
numActions++;
|
||||
}
|
||||
actions+=`<div class="menu-item px-3">
|
||||
<a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
|
||||
</div>`
|
||||
//{{- end}}
|
||||
if (numActions > 0){
|
||||
actions+=`</div>`;
|
||||
return actions;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
},
|
||||
],
|
||||
deferRender: true,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
stateLoadParams: function (settings, data) {
|
||||
if (data.search.search){
|
||||
const filterSearch = document.querySelector('[data-table-filter="search"]');
|
||||
filterSearch.value = data.search.search;
|
||||
}
|
||||
},
|
||||
language: {
|
||||
info: $.t('datatable.info'),
|
||||
infoEmpty: $.t('datatable.info_empty'),
|
||||
infoFiltered: $.t('datatable.info_filtered'),
|
||||
loadingRecords: "",
|
||||
processing: $.t('datatable.processing'),
|
||||
zeroRecords: "",
|
||||
emptyTable: $.t('datatable.no_records')
|
||||
},
|
||||
order: [[0, 'asc']],
|
||||
initComplete: function(settings, json) {
|
||||
$('#loader').addClass("d-none");
|
||||
$('#card_content').removeClass("d-none");
|
||||
let api = $.fn.dataTable.Api(settings);
|
||||
api.columns.adjust().draw("page");
|
||||
drawAction();
|
||||
}
|
||||
],
|
||||
"columnDefs": [
|
||||
{
|
||||
"targets": [0],
|
||||
"visible": false,
|
||||
"searchable": false,
|
||||
"className": "noVis"
|
||||
},
|
||||
{
|
||||
"targets": [1,2],
|
||||
"className": "noVis"
|
||||
},
|
||||
{
|
||||
"targets": [3],
|
||||
"visible": false
|
||||
},
|
||||
{
|
||||
"targets": [5],
|
||||
"render": $.fn.dataTable.render.ellipsis(100, true)
|
||||
},
|
||||
],
|
||||
"scrollX": false,
|
||||
"scrollY": false,
|
||||
"responsive": true,
|
||||
"language": {
|
||||
"emptyTable": "No event rules defined"
|
||||
},
|
||||
"order": [[1, 'asc']]
|
||||
});
|
||||
});
|
||||
|
||||
new $.fn.dataTable.FixedHeader( table );
|
||||
dt.on('draw', drawAction);
|
||||
dt.on('column-reorder', function(e, settings, details){
|
||||
drawAction();
|
||||
});
|
||||
}
|
||||
|
||||
table.button().add(0,'run');
|
||||
table.button().add(0,'delete');
|
||||
table.button().add(0,'edit');
|
||||
table.button().add(0,'add');
|
||||
function drawAction() {
|
||||
KTMenu.createInstances();
|
||||
handleRowActions();
|
||||
$('#table_body').localize();
|
||||
}
|
||||
|
||||
table.buttons().container().appendTo('.col-md-6:eq(0)', table.table().container());
|
||||
var handleDatatableActions = function () {
|
||||
const filterSearch = $(document.querySelector('[data-table-filter="search"]'));
|
||||
filterSearch.off("keyup");
|
||||
filterSearch.on('keyup', function (e) {
|
||||
dt.rows().deselect();
|
||||
dt.search(e.target.value, true, false).draw();
|
||||
});
|
||||
}
|
||||
|
||||
table.on('select deselect', function () {
|
||||
var selectedRows = table.rows({ selected: true }).count();
|
||||
table.button('delete:name').enable(selectedRows == 1);
|
||||
table.button('edit:name').enable(selectedRows == 1);
|
||||
if (selectedRows == 1){
|
||||
table.button('run:name').enable(table.row({ selected: true }).data()[0] == 6);
|
||||
} else {
|
||||
table.button('run:name').enable(false);
|
||||
function handleRowActions() {
|
||||
const editButtons = document.querySelectorAll('[data-table-action="edit_row"]');
|
||||
editButtons.forEach(d => {
|
||||
let el = $(d);
|
||||
el.off("click");
|
||||
el.on("click", function(e){
|
||||
e.preventDefault();
|
||||
let rowData = dt.row(e.target.closest('tr')).data();
|
||||
window.location.replace('{{.EventRuleURL}}' + "/" + encodeURIComponent(rowData['name']));
|
||||
});
|
||||
});
|
||||
|
||||
const deleteButtons = document.querySelectorAll('[data-table-action="delete_row"]');
|
||||
deleteButtons.forEach(d => {
|
||||
let el = $(d);
|
||||
el.off("click");
|
||||
el.on("click", function(e){
|
||||
e.preventDefault();
|
||||
const parent = e.target.closest('tr');
|
||||
deleteAction(dt.row(parent).data()['name']);
|
||||
});
|
||||
});
|
||||
|
||||
const runButtons = document.querySelectorAll('[data-table-action="run_row"]');
|
||||
runButtons.forEach(d => {
|
||||
let el = $(d);
|
||||
el.off("click");
|
||||
el.on("click", function(e){
|
||||
e.preventDefault();
|
||||
const parent = e.target.closest('tr');
|
||||
runAction(dt.row(parent).data()['name']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
init: function () {
|
||||
initDatatable();
|
||||
handleDatatableActions();
|
||||
}
|
||||
});
|
||||
}
|
||||
}();
|
||||
|
||||
$(document).on("i18nshow", function(){
|
||||
datatable.init();
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
{{- end}}
|
Loading…
Reference in a new issue