mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-25 09:00:27 +00:00
WIP new WebAdmin: connections page
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
73b2573b14
commit
8648351fc7
13 changed files with 413 additions and 347 deletions
14
go.mod
14
go.mod
|
@ -10,16 +10,16 @@ require (
|
|||
github.com/alexedwards/argon2id v1.0.0
|
||||
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964
|
||||
github.com/aws/aws-sdk-go-v2 v1.24.1
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.4
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.15
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.5
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.12
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.13
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.19.6
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7
|
||||
github.com/bmatcuk/doublestar/v4 v4.6.1
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.3.5
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.3.6
|
||||
github.com/coreos/go-oidc/v3 v3.9.0
|
||||
github.com/drakkan/webdav v0.0.0-20230227175313-32996838bcd8
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
|
||||
|
@ -74,7 +74,7 @@ require (
|
|||
golang.org/x/sys v0.16.0
|
||||
golang.org/x/term v0.16.0
|
||||
golang.org/x/time v0.5.0
|
||||
google.golang.org/api v0.156.0
|
||||
google.golang.org/api v0.157.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
)
|
||||
|
||||
|
@ -94,7 +94,7 @@ require (
|
|||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect
|
||||
github.com/aws/smithy-go v1.19.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
|
@ -164,7 +164,7 @@ require (
|
|||
go.opentelemetry.io/otel/metric v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.22.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
|
|
28
go.sum
28
go.sum
|
@ -37,14 +37,14 @@ github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3
|
|||
github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.4 h1:Juj7LhtxNudNUlfX22K5AnLafO+v4eq9PA3VWSCIQs4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.4/go.mod h1:tioqQ7wvxMYnTDpoTTLHhV3Zh+z261i/f2oz+ds8eNI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.15 h1:P0/m1LU08MF2kRzx4P//+7lNjiJod1z4xI2WpWhdpTQ=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.15/go.mod h1:pgtMCf7Dx4GWw5EpHOTc2Sy17LIP0A0N2C9nQ83pQ/0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.5 h1:lodGSevz7d+kkFJodfauThRxK9mdJbyutUxGq1NNhvw=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.5/go.mod h1:DxHrz6diQJOc9EwDslVRh84VjjrE17g+pVZXUeSxaDU=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.12 h1:0FMZy36RSYvcvVzEf1xbNdebLHZewW40QWP+P8jCMVk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.12/go.mod h1:+chyahvarkb3HibkNei9IQEM9P5cWD5w2kgXCa3Hh0I=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.13 h1:8Nt4LBUEKV0FxLBO2BmRzDKax3hp2LRMKySMBwL4vMc=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.13/go.mod h1:t5QEDu/FBJJM4kslbQlTSpYtnhoWDNmHSsgQojIxE0o=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
|
||||
|
@ -67,8 +67,8 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0 h1:PJTdBMsyvra6FtED7JZtDpQrIAflY
|
|||
github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0/go.mod h1:4qXHrG1Ne3VGIMZPCB8OjH/pLFO94sKABIusjh0KWPU=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2 h1:A5sGOT/mukuU+4At1vkSIWAN8tPwPCoYZBp7aruR540=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2/go.mod h1:qutL00aW8GSo2D0I6UEOqMvRS3ZyuBrOC1BLe5D2jPc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.6 h1:dGrs+Q/WzhsiUKh82SfTVN66QzyulXuMDTV/G8ZxOac=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.6/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=
|
||||
|
@ -93,8 +93,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
|||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k=
|
||||
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.3.5 h1:Khtm8K6fTTz/ZCWPzU9Ne3aOW9VyAnj4qIPCJgKtwK0=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.3.5/go.mod h1:1wNJ45eSXW9AnOc3skntW9ZUZz6gxrQK3cOj3rK+BC8=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.3.6 h1:Wlv9TzkrG9V7i6u8dEtmXPrBzvfFp+CgJNs696rAajM=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.3.6/go.mod h1:1wNJ45eSXW9AnOc3skntW9ZUZz6gxrQK3cOj3rK+BC8=
|
||||
github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
|
||||
github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
|
@ -428,8 +428,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
|
|||
gocloud.dev v0.36.0 h1:q5zoXux4xkOZP473e1EZbG8Gq9f0vlg1VNH5Du/ybus=
|
||||
gocloud.dev v0.36.0/go.mod h1:bLxah6JQVKBaIxzsr5BQLYB4IYdWHkMZdzCXlo6F0gg=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
|
||||
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
|
@ -522,8 +522,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
|||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/api v0.156.0 h1:yloYcGbBtVYjLKQe4enCunxvwn3s2w/XPrrhVf6MsvQ=
|
||||
google.golang.org/api v0.156.0/go.mod h1:bUSmn4KFO0Q+69zo9CNIDp4Psi6BqM0np0CbzKRSiSY=
|
||||
google.golang.org/api v0.157.0 h1:ORAeqmbrrozeyw5NjnMxh7peHO0UzV4wWYSwZeCUb20=
|
||||
google.golang.org/api v0.157.0/go.mod h1:+z4v4ufbZ1WEpld6yMGHyggs+PmAHiaLNj5ytP3N01g=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
|
|
|
@ -475,24 +475,6 @@ type ConnectionTransfer struct {
|
|||
DLSize int64 `json:"-"`
|
||||
}
|
||||
|
||||
func (t *ConnectionTransfer) getConnectionTransferAsString() string {
|
||||
result := ""
|
||||
switch t.OperationType {
|
||||
case operationUpload:
|
||||
result += "UL "
|
||||
case operationDownload:
|
||||
result += "DL "
|
||||
}
|
||||
result += fmt.Sprintf("%q ", t.VirtualPath)
|
||||
if t.Size > 0 {
|
||||
elapsed := time.Since(util.GetTimeFromMsecSinceEpoch(t.StartTime))
|
||||
speed := float64(t.Size) / float64(util.GetTimeAsMsSinceEpoch(time.Now())-t.StartTime)
|
||||
result += fmt.Sprintf("Size: %s Elapsed: %s Speed: \"%.1f KB/s\"", util.ByteCountIEC(t.Size),
|
||||
util.GetDurationAsString(elapsed), speed)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MetadataConfig defines how to handle metadata for cloud storage backends
|
||||
type MetadataConfig struct {
|
||||
// If not zero the metadata will be read before downloads and will be
|
||||
|
@ -1254,6 +1236,7 @@ func (conns *ActiveConnections) GetStats(role string) []ConnectionStatus {
|
|||
RemoteAddress: c.GetRemoteAddress(),
|
||||
ConnectionTime: util.GetTimeAsMsSinceEpoch(c.GetConnectionTime()),
|
||||
LastActivity: util.GetTimeAsMsSinceEpoch(c.GetLastActivity()),
|
||||
CurrentTime: util.GetTimeAsMsSinceEpoch(time.Now()),
|
||||
Protocol: c.GetProtocol(),
|
||||
Command: c.GetCommand(),
|
||||
Transfers: c.GetTransfers(),
|
||||
|
@ -1279,6 +1262,8 @@ type ConnectionStatus struct {
|
|||
ConnectionTime int64 `json:"connection_time"`
|
||||
// Last activity as unix timestamp in milliseconds
|
||||
LastActivity int64 `json:"last_activity"`
|
||||
// Current time as unix timestamp in milliseconds
|
||||
CurrentTime int64 `json:"current_time"`
|
||||
// Protocol for this connection
|
||||
Protocol string `json:"protocol"`
|
||||
// active uploads/downloads
|
||||
|
@ -1289,45 +1274,6 @@ type ConnectionStatus struct {
|
|||
Node string `json:"node,omitempty"`
|
||||
}
|
||||
|
||||
// GetConnectionDuration returns the connection duration as string
|
||||
func (c *ConnectionStatus) GetConnectionDuration() string {
|
||||
elapsed := time.Since(util.GetTimeFromMsecSinceEpoch(c.ConnectionTime))
|
||||
return util.GetDurationAsString(elapsed)
|
||||
}
|
||||
|
||||
// GetConnectionInfo returns connection info.
|
||||
// Protocol,Client Version and RemoteAddress are returned.
|
||||
func (c *ConnectionStatus) GetConnectionInfo() string {
|
||||
var result strings.Builder
|
||||
|
||||
result.WriteString(fmt.Sprintf("%v. Client: %q From: %q", c.Protocol, c.ClientVersion, c.RemoteAddress))
|
||||
|
||||
if c.Command == "" {
|
||||
return result.String()
|
||||
}
|
||||
|
||||
switch c.Protocol {
|
||||
case ProtocolSSH, ProtocolFTP:
|
||||
result.WriteString(fmt.Sprintf(". Command: %q", c.Command))
|
||||
case ProtocolWebDAV:
|
||||
result.WriteString(fmt.Sprintf(". Method: %q", c.Command))
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// GetTransfersAsString returns the active transfers as string
|
||||
func (c *ConnectionStatus) GetTransfersAsString() string {
|
||||
result := ""
|
||||
for _, t := range c.Transfers {
|
||||
if result != "" {
|
||||
result += ". "
|
||||
}
|
||||
result += t.getConnectionTransferAsString()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ActiveQuotaScan defines an active quota scan for a user
|
||||
type ActiveQuotaScan struct {
|
||||
// Username to which the quota scan refers
|
||||
|
|
|
@ -23,7 +23,6 @@ import (
|
|||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -892,23 +891,10 @@ func TestConnectionStatus(t *testing.T) {
|
|||
assert.Len(t, stats, 3)
|
||||
for _, stat := range stats {
|
||||
assert.Equal(t, stat.Username, username)
|
||||
assert.True(t, strings.HasPrefix(stat.GetConnectionInfo(), stat.Protocol))
|
||||
assert.True(t, strings.HasPrefix(stat.GetConnectionDuration(), "00:"))
|
||||
if stat.ConnectionID == "SFTP_id1" {
|
||||
assert.Len(t, stat.Transfers, 2)
|
||||
assert.Greater(t, len(stat.GetTransfersAsString()), 0)
|
||||
for _, tr := range stat.Transfers {
|
||||
if tr.OperationType == operationDownload {
|
||||
assert.True(t, strings.HasPrefix(tr.getConnectionTransferAsString(), "DL"))
|
||||
} else if tr.OperationType == operationUpload {
|
||||
assert.True(t, strings.HasPrefix(tr.getConnectionTransferAsString(), "UL"))
|
||||
}
|
||||
}
|
||||
} else if stat.ConnectionID == "DAV_id3" {
|
||||
assert.Len(t, stat.Transfers, 1)
|
||||
assert.Greater(t, len(stat.GetTransfersAsString()), 0)
|
||||
} else {
|
||||
assert.Equal(t, 0, len(stat.GetTransfersAsString()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1710,6 +1710,8 @@ func (s *httpdServer) setupWebAdminRoutes() {
|
|||
Delete(webGroupPath+"/{name}", deleteGroup)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminViewConnections), s.refreshCookie).
|
||||
Get(webConnectionsPath, s.handleWebGetConnections)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminViewConnections), s.refreshCookie).
|
||||
Get(webConnectionsPath+jsonAPISuffix, getActiveConnections)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie).
|
||||
Get(webFoldersPath, s.handleWebGetFolders)
|
||||
router.With(s.checkPerm(dataprovider.PermAdminManageFolders), compressor.Handler, s.refreshCookie).
|
||||
|
|
|
@ -99,7 +99,6 @@ const (
|
|||
templateMFA = "mfa.html"
|
||||
templateSetup = "adminsetup.html"
|
||||
pageAdminsTitle = "Admins"
|
||||
pageConnectionsTitle = "Connections"
|
||||
pageStatusTitle = "Status"
|
||||
pageEventRulesTitle = "Event rules"
|
||||
pageEventActionsTitle = "Event actions"
|
||||
|
@ -185,11 +184,6 @@ type eventActionsPage struct {
|
|||
Actions []dataprovider.BaseEventAction
|
||||
}
|
||||
|
||||
type connectionsPage struct {
|
||||
basePage
|
||||
Connections []common.ConnectionStatus
|
||||
}
|
||||
|
||||
type statusPage struct {
|
||||
basePage
|
||||
Status *ServicesStatus
|
||||
|
@ -412,7 +406,7 @@ func loadAdminTemplates(templatesPath string) {
|
|||
filepath.Join(templatesPath, templateCommonDir, templateChangePwd),
|
||||
}
|
||||
connectionsPaths := []string{
|
||||
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
|
||||
filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateBase),
|
||||
filepath.Join(templatesPath, templateAdminDir, templateConnections),
|
||||
}
|
||||
|
@ -3336,12 +3330,8 @@ func (s *httpdServer) handleWebGetConnections(w http.ResponseWriter, r *http.Req
|
|||
s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
|
||||
return
|
||||
}
|
||||
connectionStats := common.Connections.GetStats(claims.Role)
|
||||
connectionStats = append(connectionStats, getNodesConnections(claims.Username, claims.Role)...)
|
||||
data := connectionsPage{
|
||||
basePage: s.getBasePageData(pageConnectionsTitle, webConnectionsPath, r),
|
||||
Connections: connectionStats,
|
||||
}
|
||||
|
||||
data := s.getBasePageData(util.I18nSessionsTitle, webConnectionsPath, r)
|
||||
renderAdminTemplate(w, templateConnections, data)
|
||||
}
|
||||
|
||||
|
|
|
@ -1077,19 +1077,6 @@ func TestCommandGetFsError(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGetConnectionInfo(t *testing.T) {
|
||||
c := common.ConnectionStatus{
|
||||
Username: "test_user",
|
||||
ConnectionID: "123",
|
||||
ClientVersion: "client",
|
||||
RemoteAddress: "127.0.0.1:1234",
|
||||
Protocol: common.ProtocolSSH,
|
||||
Command: "sha1sum /test_file_ftp.dat",
|
||||
}
|
||||
info := c.GetConnectionInfo()
|
||||
assert.Contains(t, info, "sha1sum /test_file_ftp.dat")
|
||||
}
|
||||
|
||||
func TestSCPFileMode(t *testing.T) {
|
||||
mode := getFileModeAsString(0, true)
|
||||
assert.Equal(t, "0755", mode)
|
||||
|
@ -1832,42 +1819,6 @@ func TestTransferFailingReader(t *testing.T) {
|
|||
assert.Len(t, connection.GetTransfers(), 0)
|
||||
}
|
||||
|
||||
func TestConnectionStatusStruct(t *testing.T) {
|
||||
var transfers []common.ConnectionTransfer
|
||||
transferUL := common.ConnectionTransfer{
|
||||
OperationType: "upload",
|
||||
StartTime: util.GetTimeAsMsSinceEpoch(time.Now()),
|
||||
Size: 123,
|
||||
VirtualPath: "/test.upload",
|
||||
}
|
||||
transferDL := common.ConnectionTransfer{
|
||||
OperationType: "download",
|
||||
StartTime: util.GetTimeAsMsSinceEpoch(time.Now()),
|
||||
Size: 123,
|
||||
VirtualPath: "/test.download",
|
||||
}
|
||||
transfers = append(transfers, transferUL)
|
||||
transfers = append(transfers, transferDL)
|
||||
c := common.ConnectionStatus{
|
||||
Username: "test",
|
||||
ConnectionID: "123",
|
||||
ClientVersion: "fakeClient-1.0.0",
|
||||
RemoteAddress: "127.0.0.1:1234",
|
||||
ConnectionTime: util.GetTimeAsMsSinceEpoch(time.Now()),
|
||||
LastActivity: util.GetTimeAsMsSinceEpoch(time.Now()),
|
||||
Protocol: "SFTP",
|
||||
Transfers: transfers,
|
||||
}
|
||||
durationString := c.GetConnectionDuration()
|
||||
assert.NotEqual(t, 0, len(durationString))
|
||||
|
||||
transfersString := c.GetTransfersAsString()
|
||||
assert.NotEqual(t, 0, len(transfersString))
|
||||
|
||||
connInfo := c.GetConnectionInfo()
|
||||
assert.NotEqual(t, 0, len(connInfo))
|
||||
}
|
||||
|
||||
func TestConfigsFromProvider(t *testing.T) {
|
||||
err := dataprovider.UpdateConfigs(nil, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -58,6 +58,7 @@ const (
|
|||
I18nConfigsTitle = "title.configs"
|
||||
I18nOAuth2Title = "title.oauth2_success"
|
||||
I18nOAuth2ErrorTitle = "title.oauth2_error"
|
||||
I18nSessionsTitle = "title.connections"
|
||||
I18nErrorSetupInstallCode = "setup.install_code_mismatch"
|
||||
I18nInvalidAuth = "general.invalid_auth_request"
|
||||
I18nError429Message = "general.error429"
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
"users": "Users",
|
||||
"groups": "Groups",
|
||||
"folders": "Virtual folders",
|
||||
"connections": "Active sessions",
|
||||
"connections": "Active connections",
|
||||
"event_manager": "Event Manager",
|
||||
"event_rules": "Rules",
|
||||
"event_actions": "Actions",
|
||||
|
@ -219,7 +219,9 @@
|
|||
"duplicated_name": "The specified name already exists",
|
||||
"permissions_required": "Permissions are required",
|
||||
"backup_ok": "Backup successfully restored",
|
||||
"configs_saved": "Configurations has been successfully updated"
|
||||
"configs_saved": "Configurations has been successfully updated",
|
||||
"protocol": "Protocol",
|
||||
"refresh": "Refresh"
|
||||
},
|
||||
"fs": {
|
||||
"view_file": "View file \"{{- path}}\"",
|
||||
|
@ -670,5 +672,19 @@
|
|||
},
|
||||
"admin": {
|
||||
"role_permissions": "A role admin cannot have the following permissions: {{val}}"
|
||||
},
|
||||
"connections": {
|
||||
"view_manage": "View and manage connections",
|
||||
"started": "Started",
|
||||
"remote_address": "Remote address",
|
||||
"last_activity": "Last activity",
|
||||
"disconnect_confirm_btn": "Yes, disconnect",
|
||||
"disconnect_confirm": "Do you want to disconnect the selected connection? This action is irreversible",
|
||||
"disconnect_ko": "Unable to disconnect the selected connection",
|
||||
"upload": "UL: \"{{- path}}\"",
|
||||
"download": "DL: \"{{- path}}\"",
|
||||
"upload_info": "$t(connections.upload). Size: {{- size}}. Speed: {{- speed}}",
|
||||
"download_info": "$t(connections.download). Size: {{- size}}. Speed: {{- speed}}",
|
||||
"client": "Client: {{- val}}"
|
||||
}
|
||||
}
|
|
@ -31,7 +31,7 @@
|
|||
"users": "Utenti",
|
||||
"groups": "Gruppi",
|
||||
"folders": "Cartelle virtuali",
|
||||
"connections": "Sessioni attive",
|
||||
"connections": "Connessioni attive",
|
||||
"event_manager": "Gestione eventi",
|
||||
"event_rules": "Regole",
|
||||
"event_actions": "Azioni",
|
||||
|
@ -219,7 +219,9 @@
|
|||
"duplicated_name": "Il nome specificato esiste già",
|
||||
"permissions_required": "I permessi sono obbligatori",
|
||||
"backup_ok": "Backup ripristinato correttamente",
|
||||
"configs_saved": "Configurazioni aggiornate"
|
||||
"configs_saved": "Configurazioni aggiornate",
|
||||
"protocol": "Protocollo",
|
||||
"refresh": "Aggiorna"
|
||||
},
|
||||
"fs": {
|
||||
"view_file": "Visualizza file \"{{- path}}\"",
|
||||
|
@ -670,5 +672,19 @@
|
|||
},
|
||||
"admin": {
|
||||
"role_permissions": "Un amministratore di ruolo non può avere le seguenti autorizzazioni: {{val}}"
|
||||
},
|
||||
"connections": {
|
||||
"view_manage": "Visualizza e gestisci connessioni attive",
|
||||
"started": "Iniziata",
|
||||
"remote_address": "Indirizzo remoto",
|
||||
"last_activity": "Ultima attività",
|
||||
"disconnect_confirm_btn": "Si, disconnetti",
|
||||
"disconnect_confirm": "Vuoi disconnettere la connessione selezionata? Questa azione è irreversibile",
|
||||
"disconnect_ko": "Impossibile disconnettere la connessione selezionata",
|
||||
"upload": "UL: \"{{- path}}\"",
|
||||
"download": "DL: \"{{- path}}\"",
|
||||
"upload_info": "$t(connections.upload). Dimensione: {{- size}}. Velocità: {{- speed}}",
|
||||
"download_info": "$t(connections.download). Dimensione: {{- size}}. Velocità: {{- speed}}",
|
||||
"client": "Client: {{- val}}"
|
||||
}
|
||||
}
|
|
@ -81,6 +81,11 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
+' '+(e?'KMGTPEZY'[--e]+'iB':'Bytes')
|
||||
}
|
||||
|
||||
function humanizeSpeed(a,b,c,d,e){
|
||||
return (b=Math,c=b.log,d=1024,e=c(a)/c(d)|0,a/b.pow(d,e)).toFixed(1)
|
||||
+' '+(e?'KMGTPEZY'[--e]+'B/s':'Bytes/s')
|
||||
}
|
||||
|
||||
function initRepeaterItems() {
|
||||
let repeaterDeleteButtons = document.querySelectorAll('[data-repeater-delete]');
|
||||
let repeaterCreateButtons = document.querySelectorAll('[data-repeater-create]');
|
||||
|
|
|
@ -1,221 +1,376 @@
|
|||
<!--
|
||||
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 class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">View and manage connections</h6>
|
||||
{{- define "page_body"}}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h3 data-i18n="connections.view_manage" class="card-title section-title">View and manage connections</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">
|
||||
<a href="{{.ConnectionsURL}}" class="btn btn-primary">
|
||||
<i class="ki-solid ki-arrows-circle fs-2"></i>
|
||||
<span data-i18n="general.refresh">Refresh</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5">
|
||||
<thead>
|
||||
<tr>
|
||||
<tr class="text-start text-muted fw-bold fs-6 gs-0">
|
||||
<th>ID</th>
|
||||
<th>Node</th>
|
||||
<th>Username</th>
|
||||
<th>Time</th>
|
||||
<th>Info</th>
|
||||
<th>Transfers</th>
|
||||
<th data-i18n="login.username">Username</th>
|
||||
<th data-i18n="connections.started">Started</th>
|
||||
<th data-i18n="connections.remote_address">Remote address</th>
|
||||
<th data-i18n="general.protocol">Protocol</th>
|
||||
<th data-i18n="connections.last_activity">Last activity</th>
|
||||
<th data-i18n="general.info">Info</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Connections}}
|
||||
<tr>
|
||||
<td>{{.ConnectionID}}</td>
|
||||
<td>{{.Node}}</td>
|
||||
<td>{{.Username}}</td>
|
||||
<td>{{.GetConnectionDuration}}</td>
|
||||
<td>{{.GetConnectionInfo}}</td>
|
||||
<td>{{.GetTransfersAsString}}</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="disconnectModal" tabindex="-1" role="dialog" aria-labelledby="disconnectModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="disconnectModalLabel">
|
||||
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 close the selected connection?</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" type="button" data-dismiss="modal">
|
||||
Cancel
|
||||
</button>
|
||||
<a class="btn btn-warning" href="#" onclick="disconnectAction()">
|
||||
Disconnect
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{- 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}}>
|
||||
|
||||
{{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 type="text/javascript">
|
||||
|
||||
function disconnectAction() {
|
||||
let table = $('#dataTable').DataTable();
|
||||
table.button('disconnect:name').enable(false);
|
||||
let selectedData = table.row({ selected: true }).data()
|
||||
let connectionID = selectedData[0];
|
||||
let nodeID = selectedData[1];
|
||||
let path = '{{.ConnectionsURL}}' + "/" + fixedEncodeURIComponent(connectionID)+"?node="+encodeURIComponent(nodeID);
|
||||
$('#disconnectModal').modal('hide');
|
||||
$('#errorMsg').hide();
|
||||
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: 'DELETE',
|
||||
dataType: 'json',
|
||||
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
|
||||
timeout: 15000,
|
||||
success: function (result) {
|
||||
window.location.href = '{{.ConnectionsURL}}';
|
||||
},
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
var txt = "Failed to close the selected connection";
|
||||
if ($xhr) {
|
||||
var json = $xhr.responseJSON;
|
||||
if (json) {
|
||||
if (json.message){
|
||||
txt += ": " + json.message;
|
||||
} else {
|
||||
txt += ": " + json.error;
|
||||
}
|
||||
}
|
||||
function disconnectAction(connectionID, node) {
|
||||
ModalAlert.fire({
|
||||
text: $.t('connections.disconnect_confirm'),
|
||||
icon: "warning",
|
||||
confirmButtonText: $.t('connections.disconnect_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 = '{{.ConnectionsURL}}' + "/" + encodeURIComponent(connectionID);
|
||||
if (node) {
|
||||
path+="?node="+ encodeURIComponent(node);
|
||||
}
|
||||
$('#errorTxt').text(txt);
|
||||
$('#errorMsg').show();
|
||||
|
||||
axios.delete(path, {
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{.CSRFToken}}'
|
||||
},
|
||||
validateStatus: function (status) {
|
||||
return status == 200;
|
||||
}
|
||||
}).then(function(response){
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
},250);
|
||||
}).catch(function(error){
|
||||
KTApp.hidePageLoading();
|
||||
ModalAlert.fire({
|
||||
text: $.t('connections.disconnect_ko'),
|
||||
icon: "warning",
|
||||
confirmButtonText: $.t('general.ok'),
|
||||
customClass: {
|
||||
confirmButton: "btn btn-primary"
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$.fn.dataTable.ext.buttons.disconnect = {
|
||||
text: 'Disconnect',
|
||||
name: 'disconnect',
|
||||
action: function (e, dt, node, config) {
|
||||
$('#disconnectModal').modal('show');
|
||||
},
|
||||
enabled: false
|
||||
};
|
||||
var datatable = function(){
|
||||
var dt;
|
||||
|
||||
$.fn.dataTable.ext.buttons.refresh = {
|
||||
text: '<i class="fas fa-sync-alt"></i>',
|
||||
name: 'refresh',
|
||||
titleAttr: "Refresh",
|
||||
action: function (e, dt, node, config) {
|
||||
location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
var table = $('#dataTable').DataTable({
|
||||
"select": {
|
||||
"style": "single",
|
||||
"blurable": true
|
||||
},
|
||||
"buttons": [
|
||||
{
|
||||
"text": "Column visibility",
|
||||
"extend": "colvis",
|
||||
"columns": ":not(.noVis)"
|
||||
}
|
||||
],
|
||||
"lengthChange": true,
|
||||
"columnDefs": [
|
||||
{
|
||||
"targets": [0, 1],
|
||||
"visible": false,
|
||||
"searchable": false,
|
||||
"className": "noVis"
|
||||
var initDatatable = function () {
|
||||
$('#errorMsg').addClass("d-none");
|
||||
dt = $('#dataTable').DataTable({
|
||||
ajax: {
|
||||
url: "{{.ConnectionsURL}}/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");
|
||||
}
|
||||
},
|
||||
{
|
||||
"targets": [2],
|
||||
"className": "noVis"
|
||||
columns: [
|
||||
{
|
||||
data: "connection_id",
|
||||
visible: false,
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
return escapeHTML(data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "node",
|
||||
visible: false,
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
defaultContent: "",
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
return escapeHTML(data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "username",
|
||||
defaultContent: "",
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
return escapeHTML(data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "connection_time",
|
||||
searchable: false,
|
||||
defaultContent: 0,
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
if (data > 0){
|
||||
return $.t('general.datetime', {
|
||||
val: parseInt(data, 10),
|
||||
formatParams: {
|
||||
val: { year: '2-digit', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' },
|
||||
}
|
||||
});
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "remote_address",
|
||||
defaultContent: "",
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
return escapeHTML(data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "protocol",
|
||||
defaultContent: "",
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
return escapeHTML(data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "last_activity",
|
||||
searchable: false,
|
||||
defaultContent: 0,
|
||||
render: function(data, type, row) {
|
||||
if (type === 'display') {
|
||||
if (data > 0){
|
||||
return $.t('general.datetime', {
|
||||
val: parseInt(data, 10),
|
||||
formatParams: {
|
||||
val: { year: '2-digit', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric' },
|
||||
}
|
||||
});
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "active_transfers",
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
render: function (data, type, row) {
|
||||
if (type === 'display') {
|
||||
let result = "";
|
||||
if (row.active_transfers && row.active_transfers.length > 0){
|
||||
let transfer = row.active_transfers[0];
|
||||
let path = escapeHTML(transfer.path);
|
||||
let elapsed = row.current_time - transfer.start_time;
|
||||
if (elapsed > 0 && transfer.size > 0){
|
||||
let speed = (transfer.size*1.0) / (elapsed/1000.0);
|
||||
if (transfer.operation_type === 'upload'){
|
||||
result = $.t('connections.upload_info', {path: path, size: fileSizeIEC(transfer.size), speed: humanizeSpeed(speed)});
|
||||
} else {
|
||||
result = $.t('connections.download_info', {path: path, size: fileSizeIEC(transfer.size), speed: humanizeSpeed(speed)});
|
||||
}
|
||||
} else {
|
||||
if (transfer.operation_type === 'upload'){
|
||||
result = $.t('connections.upload', {path: path});
|
||||
} else {
|
||||
result = $.t('connections.download', {path: path});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (row.client_version){
|
||||
if (result){
|
||||
result+= ". ";
|
||||
}
|
||||
result+= $.t('connections.client', {val: escapeHTML(row.client_version)});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "",
|
||||
searchable: false,
|
||||
orderable: false,
|
||||
className: 'text-end',
|
||||
render: function (data, type, row) {
|
||||
if (type === 'display') {
|
||||
//{{- if .LoggedUser.HasPermission "close_conns"}}
|
||||
return `<div class="d-flex justify-content-end">
|
||||
<div class="ms-2">
|
||||
<a href="#" class="btn btn-sm btn-icon btn-light-danger" data-table-action="close_conn">
|
||||
<i class="ki-solid ki-cross fs-1"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>`;
|
||||
//{{- end}}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
},
|
||||
],
|
||||
deferRender: true,
|
||||
stateSave: true,
|
||||
stateDuration: 0,
|
||||
colReorder: {
|
||||
enable: true
|
||||
},
|
||||
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();
|
||||
}
|
||||
],
|
||||
"scrollX": false,
|
||||
"scrollY": false,
|
||||
"responsive": true,
|
||||
"language": {
|
||||
"emptyTable": "No user connected"
|
||||
},
|
||||
"order": [[2, 'asc']]
|
||||
});
|
||||
});
|
||||
|
||||
new $.fn.dataTable.FixedHeader( table );
|
||||
dt.on('draw', drawAction);
|
||||
dt.on('column-reorder', function(e, settings, details){
|
||||
drawAction();
|
||||
});
|
||||
}
|
||||
|
||||
table.button().add(0, 'refresh');
|
||||
//table.button().add(0,'pageLength');
|
||||
function drawAction() {
|
||||
KTMenu.createInstances();
|
||||
handleRowActions();
|
||||
$('#table_body').localize();
|
||||
}
|
||||
|
||||
{{if .LoggedAdmin.HasPermission "close_conns"}}
|
||||
table.button().add(0,'disconnect');
|
||||
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('disconnect:name').enable(selectedRows == 1);
|
||||
});
|
||||
{{end}}
|
||||
table.buttons().container().appendTo('.col-md-6:eq(0)', table.table().container());
|
||||
function handleRowActions() {
|
||||
const closeButtons = document.querySelectorAll('[data-table-action="close_conn"]');
|
||||
closeButtons.forEach(d => {
|
||||
let el = $(d);
|
||||
el.off("click");
|
||||
el.on("click", function(e){
|
||||
e.preventDefault();
|
||||
const parent = e.target.closest('tr');
|
||||
let data = dt.row(parent).data();
|
||||
disconnectAction(data.connection_id, data.node);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
init: function () {
|
||||
initDatatable();
|
||||
handleDatatableActions();
|
||||
}
|
||||
}
|
||||
}();
|
||||
|
||||
$(document).on("i18nshow", function(){
|
||||
datatable.init();
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
{{- end}}
|
|
@ -77,7 +77,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
{{- end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5">
|
||||
<thead>
|
||||
|
@ -284,7 +283,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
<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">`;
|
||||
|
||||
//{{- if .LoggedUser.HasPermission "manage_folders"}}
|
||||
numActions++;
|
||||
actions+=`<div class="menu-item px-3">
|
||||
|
|
Loading…
Reference in a new issue