WIP new WebAdmin: connections page

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2024-01-20 15:35:05 +01:00
parent 73b2573b14
commit 8648351fc7
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
13 changed files with 413 additions and 347 deletions

14
go.mod
View file

@ -10,16 +10,16 @@ require (
github.com/alexedwards/argon2id v1.0.0 github.com/alexedwards/argon2id v1.0.0
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 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 v1.24.1
github.com/aws/aws-sdk-go-v2/config v1.26.4 github.com/aws/aws-sdk-go-v2/config v1.26.5
github.com/aws/aws-sdk-go-v2/credentials v1.16.15 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/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/marketplacemetering v1.19.6
github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0 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/secretsmanager v1.26.2
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 github.com/aws/aws-sdk-go-v2/service/sts v1.26.7
github.com/bmatcuk/doublestar/v4 v4.6.1 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/coreos/go-oidc/v3 v3.9.0
github.com/drakkan/webdav v0.0.0-20230227175313-32996838bcd8 github.com/drakkan/webdav v0.0.0-20230227175313-32996838bcd8
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
@ -74,7 +74,7 @@ require (
golang.org/x/sys v0.16.0 golang.org/x/sys v0.16.0
golang.org/x/term v0.16.0 golang.org/x/term v0.16.0
golang.org/x/time v0.5.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 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/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/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/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/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect
github.com/aws/smithy-go v1.19.0 // indirect github.com/aws/smithy-go v1.19.0 // indirect
github.com/beorn7/perks v1.0.1 // 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/metric v1.22.0 // indirect
go.opentelemetry.io/otel/trace v1.22.0 // indirect go.opentelemetry.io/otel/trace v1.22.0 // indirect
go.uber.org/multierr v1.11.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/mod v0.14.0 // indirect
golang.org/x/sync v0.6.0 // indirect golang.org/x/sync v0.6.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect

28
go.sum
View file

@ -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 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 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/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.5 h1:lodGSevz7d+kkFJodfauThRxK9mdJbyutUxGq1NNhvw=
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/config v1.26.5/go.mod h1:DxHrz6diQJOc9EwDslVRh84VjjrE17g+pVZXUeSxaDU=
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.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8=
github.com/aws/aws-sdk-go-v2/credentials v1.16.15/go.mod h1:pgtMCf7Dx4GWw5EpHOTc2Sy17LIP0A0N2C9nQ83pQ/0= 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 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/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.13 h1:8Nt4LBUEKV0FxLBO2BmRzDKax3hp2LRMKySMBwL4vMc=
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/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 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/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= 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/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 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/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.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow=
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/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 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/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0= 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/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 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k=
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 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.6 h1:Wlv9TzkrG9V7i6u8dEtmXPrBzvfFp+CgJNs696rAajM=
github.com/cockroachdb/cockroach-go/v2 v2.3.5/go.mod h1:1wNJ45eSXW9AnOc3skntW9ZUZz6gxrQK3cOj3rK+BC8= 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 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= 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 h1:q5zoXux4xkOZP473e1EZbG8Gq9f0vlg1VNH5Du/ybus=
gocloud.dev v0.36.0/go.mod h1:bLxah6JQVKBaIxzsr5BQLYB4IYdWHkMZdzCXlo6F0gg= 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-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-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= 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-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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 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-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 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 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.157.0 h1:ORAeqmbrrozeyw5NjnMxh7peHO0UzV4wWYSwZeCUb20=
google.golang.org/api v0.156.0/go.mod h1:bUSmn4KFO0Q+69zo9CNIDp4Psi6BqM0np0CbzKRSiSY= 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.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.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=

View file

@ -475,24 +475,6 @@ type ConnectionTransfer struct {
DLSize int64 `json:"-"` 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 // MetadataConfig defines how to handle metadata for cloud storage backends
type MetadataConfig struct { type MetadataConfig struct {
// If not zero the metadata will be read before downloads and will be // 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(), RemoteAddress: c.GetRemoteAddress(),
ConnectionTime: util.GetTimeAsMsSinceEpoch(c.GetConnectionTime()), ConnectionTime: util.GetTimeAsMsSinceEpoch(c.GetConnectionTime()),
LastActivity: util.GetTimeAsMsSinceEpoch(c.GetLastActivity()), LastActivity: util.GetTimeAsMsSinceEpoch(c.GetLastActivity()),
CurrentTime: util.GetTimeAsMsSinceEpoch(time.Now()),
Protocol: c.GetProtocol(), Protocol: c.GetProtocol(),
Command: c.GetCommand(), Command: c.GetCommand(),
Transfers: c.GetTransfers(), Transfers: c.GetTransfers(),
@ -1279,6 +1262,8 @@ type ConnectionStatus struct {
ConnectionTime int64 `json:"connection_time"` ConnectionTime int64 `json:"connection_time"`
// Last activity as unix timestamp in milliseconds // Last activity as unix timestamp in milliseconds
LastActivity int64 `json:"last_activity"` LastActivity int64 `json:"last_activity"`
// Current time as unix timestamp in milliseconds
CurrentTime int64 `json:"current_time"`
// Protocol for this connection // Protocol for this connection
Protocol string `json:"protocol"` Protocol string `json:"protocol"`
// active uploads/downloads // active uploads/downloads
@ -1289,45 +1274,6 @@ type ConnectionStatus struct {
Node string `json:"node,omitempty"` 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 // ActiveQuotaScan defines an active quota scan for a user
type ActiveQuotaScan struct { type ActiveQuotaScan struct {
// Username to which the quota scan refers // Username to which the quota scan refers

View file

@ -23,7 +23,6 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings"
"sync" "sync"
"testing" "testing"
"time" "time"
@ -892,23 +891,10 @@ func TestConnectionStatus(t *testing.T) {
assert.Len(t, stats, 3) assert.Len(t, stats, 3)
for _, stat := range stats { for _, stat := range stats {
assert.Equal(t, stat.Username, username) 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" { if stat.ConnectionID == "SFTP_id1" {
assert.Len(t, stat.Transfers, 2) 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" { } else if stat.ConnectionID == "DAV_id3" {
assert.Len(t, stat.Transfers, 1) assert.Len(t, stat.Transfers, 1)
assert.Greater(t, len(stat.GetTransfersAsString()), 0)
} else {
assert.Equal(t, 0, len(stat.GetTransfersAsString()))
} }
} }

View file

@ -1710,6 +1710,8 @@ func (s *httpdServer) setupWebAdminRoutes() {
Delete(webGroupPath+"/{name}", deleteGroup) Delete(webGroupPath+"/{name}", deleteGroup)
router.With(s.checkPerm(dataprovider.PermAdminViewConnections), s.refreshCookie). router.With(s.checkPerm(dataprovider.PermAdminViewConnections), s.refreshCookie).
Get(webConnectionsPath, s.handleWebGetConnections) Get(webConnectionsPath, s.handleWebGetConnections)
router.With(s.checkPerm(dataprovider.PermAdminViewConnections), s.refreshCookie).
Get(webConnectionsPath+jsonAPISuffix, getActiveConnections)
router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie). router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie).
Get(webFoldersPath, s.handleWebGetFolders) Get(webFoldersPath, s.handleWebGetFolders)
router.With(s.checkPerm(dataprovider.PermAdminManageFolders), compressor.Handler, s.refreshCookie). router.With(s.checkPerm(dataprovider.PermAdminManageFolders), compressor.Handler, s.refreshCookie).

View file

@ -99,7 +99,6 @@ const (
templateMFA = "mfa.html" templateMFA = "mfa.html"
templateSetup = "adminsetup.html" templateSetup = "adminsetup.html"
pageAdminsTitle = "Admins" pageAdminsTitle = "Admins"
pageConnectionsTitle = "Connections"
pageStatusTitle = "Status" pageStatusTitle = "Status"
pageEventRulesTitle = "Event rules" pageEventRulesTitle = "Event rules"
pageEventActionsTitle = "Event actions" pageEventActionsTitle = "Event actions"
@ -185,11 +184,6 @@ type eventActionsPage struct {
Actions []dataprovider.BaseEventAction Actions []dataprovider.BaseEventAction
} }
type connectionsPage struct {
basePage
Connections []common.ConnectionStatus
}
type statusPage struct { type statusPage struct {
basePage basePage
Status *ServicesStatus Status *ServicesStatus
@ -412,7 +406,7 @@ func loadAdminTemplates(templatesPath string) {
filepath.Join(templatesPath, templateCommonDir, templateChangePwd), filepath.Join(templatesPath, templateCommonDir, templateChangePwd),
} }
connectionsPaths := []string{ connectionsPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS), filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
filepath.Join(templatesPath, templateAdminDir, templateBase), filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateConnections), 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)) s.renderForbiddenPage(w, r, util.NewI18nError(errInvalidTokenClaims, util.I18nErrorInvalidToken))
return return
} }
connectionStats := common.Connections.GetStats(claims.Role)
connectionStats = append(connectionStats, getNodesConnections(claims.Username, claims.Role)...) data := s.getBasePageData(util.I18nSessionsTitle, webConnectionsPath, r)
data := connectionsPage{
basePage: s.getBasePageData(pageConnectionsTitle, webConnectionsPath, r),
Connections: connectionStats,
}
renderAdminTemplate(w, templateConnections, data) renderAdminTemplate(w, templateConnections, data)
} }

View file

@ -1077,19 +1077,6 @@ func TestCommandGetFsError(t *testing.T) {
assert.Error(t, err) 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) { func TestSCPFileMode(t *testing.T) {
mode := getFileModeAsString(0, true) mode := getFileModeAsString(0, true)
assert.Equal(t, "0755", mode) assert.Equal(t, "0755", mode)
@ -1832,42 +1819,6 @@ func TestTransferFailingReader(t *testing.T) {
assert.Len(t, connection.GetTransfers(), 0) 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) { func TestConfigsFromProvider(t *testing.T) {
err := dataprovider.UpdateConfigs(nil, "", "", "") err := dataprovider.UpdateConfigs(nil, "", "", "")
assert.NoError(t, err) assert.NoError(t, err)

View file

@ -58,6 +58,7 @@ const (
I18nConfigsTitle = "title.configs" I18nConfigsTitle = "title.configs"
I18nOAuth2Title = "title.oauth2_success" I18nOAuth2Title = "title.oauth2_success"
I18nOAuth2ErrorTitle = "title.oauth2_error" I18nOAuth2ErrorTitle = "title.oauth2_error"
I18nSessionsTitle = "title.connections"
I18nErrorSetupInstallCode = "setup.install_code_mismatch" I18nErrorSetupInstallCode = "setup.install_code_mismatch"
I18nInvalidAuth = "general.invalid_auth_request" I18nInvalidAuth = "general.invalid_auth_request"
I18nError429Message = "general.error429" I18nError429Message = "general.error429"

View file

@ -31,7 +31,7 @@
"users": "Users", "users": "Users",
"groups": "Groups", "groups": "Groups",
"folders": "Virtual folders", "folders": "Virtual folders",
"connections": "Active sessions", "connections": "Active connections",
"event_manager": "Event Manager", "event_manager": "Event Manager",
"event_rules": "Rules", "event_rules": "Rules",
"event_actions": "Actions", "event_actions": "Actions",
@ -219,7 +219,9 @@
"duplicated_name": "The specified name already exists", "duplicated_name": "The specified name already exists",
"permissions_required": "Permissions are required", "permissions_required": "Permissions are required",
"backup_ok": "Backup successfully restored", "backup_ok": "Backup successfully restored",
"configs_saved": "Configurations has been successfully updated" "configs_saved": "Configurations has been successfully updated",
"protocol": "Protocol",
"refresh": "Refresh"
}, },
"fs": { "fs": {
"view_file": "View file \"{{- path}}\"", "view_file": "View file \"{{- path}}\"",
@ -670,5 +672,19 @@
}, },
"admin": { "admin": {
"role_permissions": "A role admin cannot have the following permissions: {{val}}" "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}}"
} }
} }

View file

@ -31,7 +31,7 @@
"users": "Utenti", "users": "Utenti",
"groups": "Gruppi", "groups": "Gruppi",
"folders": "Cartelle virtuali", "folders": "Cartelle virtuali",
"connections": "Sessioni attive", "connections": "Connessioni attive",
"event_manager": "Gestione eventi", "event_manager": "Gestione eventi",
"event_rules": "Regole", "event_rules": "Regole",
"event_actions": "Azioni", "event_actions": "Azioni",
@ -219,7 +219,9 @@
"duplicated_name": "Il nome specificato esiste già", "duplicated_name": "Il nome specificato esiste già",
"permissions_required": "I permessi sono obbligatori", "permissions_required": "I permessi sono obbligatori",
"backup_ok": "Backup ripristinato correttamente", "backup_ok": "Backup ripristinato correttamente",
"configs_saved": "Configurazioni aggiornate" "configs_saved": "Configurazioni aggiornate",
"protocol": "Protocollo",
"refresh": "Aggiorna"
}, },
"fs": { "fs": {
"view_file": "Visualizza file \"{{- path}}\"", "view_file": "Visualizza file \"{{- path}}\"",
@ -670,5 +672,19 @@
}, },
"admin": { "admin": {
"role_permissions": "Un amministratore di ruolo non può avere le seguenti autorizzazioni: {{val}}" "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}}"
} }
} }

View file

@ -81,6 +81,11 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
+' '+(e?'KMGTPEZY'[--e]+'iB':'Bytes') +' '+(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() { function initRepeaterItems() {
let repeaterDeleteButtons = document.querySelectorAll('[data-repeater-delete]'); let repeaterDeleteButtons = document.querySelectorAll('[data-repeater-delete]');
let repeaterCreateButtons = document.querySelectorAll('[data-repeater-create]'); let repeaterCreateButtons = document.querySelectorAll('[data-repeater-create]');

View file

@ -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 This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, https://keenthemes.com/products/templates-mega-bundle
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.
You should have received a copy of the GNU Affero General Public License KeenThemes HTML/CSS/JS components are allowed for use only within the
along with this program. If not, see <https://www.gnu.org/licenses/>. 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" .}} {{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"}} {{- define "page_body"}}
<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet"> <div class="card shadow-sm">
<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet"> <div class="card-header bg-light">
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet"> <h3 data-i18n="connections.view_manage" class="card-title section-title">View and manage connections</h3>
<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">&times;</span>
</button>
</div> </div>
<script type="text/javascript"> <div id="card_body" class="card-body">
function dismissErrorMsg(){ <div id="loader" class="align-items-center text-center my-10">
$('#errorMsg').hide(); <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>
</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>
</div> </div>
<div class="card-body"> <div id="card_content" class="d-none">
<div class="table-responsive"> <div class="d-flex flex-stack flex-wrap mb-5">
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0"> <div class="d-flex align-items-center position-relative my-2">
<thead> <i class="ki-solid ki-magnifier fs-1 position-absolute ms-6"></i>
<tr> <input name="search" data-i18n="[placeholder]general.search" type="text" data-table-filter="search"
<th>ID</th> class="form-control rounded-1 w-250px ps-15 me-5" placeholder="Search" />
<th>Node</th>
<th>Username</th>
<th>Time</th>
<th>Info</th>
<th>Transfers</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>
</table>
</div> </div>
</div> <div class="d-flex justify-content-end my-2" data-table-toolbar="base">
</div> <a href="{{.ConnectionsURL}}" class="btn btn-primary">
{{end}} <i class="ki-solid ki-arrows-circle fs-2"></i>
<span data-i18n="general.refresh">Refresh</span>
{{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">&times;</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> </a>
</div> </div>
</div> </div>
<table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5">
<thead>
<tr class="text-start text-muted fw-bold fs-6 gs-0">
<th>ID</th>
<th>Node</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 id="table_body" class="text-gray-800 fw-semibold"></tbody>
</table>
</div> </div>
</div> </div>
{{end}} </div>
{{- end}}
{{define "extra_js"}} {{- define "extra_js"}}
<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script> <script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script> <script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
<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() { function disconnectAction(connectionID, node) {
let table = $('#dataTable').DataTable(); ModalAlert.fire({
table.button('disconnect:name').enable(false); text: $.t('connections.disconnect_confirm'),
let selectedData = table.row({ selected: true }).data() icon: "warning",
let connectionID = selectedData[0]; confirmButtonText: $.t('connections.disconnect_confirm_btn'),
let nodeID = selectedData[1]; cancelButtonText: $.t('general.cancel'),
let path = '{{.ConnectionsURL}}' + "/" + fixedEncodeURIComponent(connectionID)+"?node="+encodeURIComponent(nodeID); customClass: {
$('#disconnectModal').modal('hide'); confirmButton: "btn btn-danger",
$('#errorMsg').hide(); 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);
}
$.ajax({ axios.delete(path, {
url: path,
type: 'DELETE',
dataType: 'json',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
timeout: 15000, timeout: 15000,
success: function (result) { headers: {
window.location.href = '{{.ConnectionsURL}}'; '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"
}
});
});
}
});
}
var datatable = function(){
var dt;
var initDatatable = function () {
$('#errorMsg').addClass("d-none");
dt = $('#dataTable').DataTable({
ajax: {
url: "{{.ConnectionsURL}}/json",
dataSrc: "",
error: function ($xhr, textStatus, errorThrown) { error: function ($xhr, textStatus, errorThrown) {
var txt = "Failed to close the selected connection"; $(".dataTables_processing").hide();
let txt = "";
if ($xhr) { if ($xhr) {
var json = $xhr.responseJSON; let json = $xhr.responseJSON;
if (json) { if (json) {
if (json.message){ if (json.message){
txt += ": " + json.message; txt = json.message;
}
}
}
if (!txt){
txt = "general.error500";
}
setI18NData($('#errorTxt'), txt);
$('#errorMsg').removeClass("d-none");
}
},
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 { } else {
txt += ": " + json.error; 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});
} }
} }
} }
$('#errorTxt').text(txt); if (row.client_version){
$('#errorMsg').show(); if (result){
result+= ". ";
} }
}); result+= $.t('connections.client', {val: escapeHTML(row.client_version)});
}
return result;
}
return "";
} }
$(document).ready(function () {
$.fn.dataTable.ext.buttons.disconnect = {
text: 'Disconnect',
name: 'disconnect',
action: function (e, dt, node, config) {
$('#disconnectModal').modal('show');
}, },
enabled: false
};
$.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", data: "",
"extend": "colvis", searchable: false,
"columns": ":not(.noVis)" 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 "";
}
},
], ],
"lengthChange": true, deferRender: true,
"columnDefs": [ stateSave: true,
{ stateDuration: 0,
"targets": [0, 1], colReorder: {
"visible": false, enable: true
"searchable": false,
"className": "noVis"
}, },
{ stateLoadParams: function (settings, data) {
"targets": [2], if (data.search.search){
"className": "noVis" const filterSearch = document.querySelector('[data-table-filter="search"]');
filterSearch.value = data.search.search;
} }
],
"scrollX": false,
"scrollY": false,
"responsive": true,
"language": {
"emptyTable": "No user connected"
}, },
"order": [[2, 'asc']] 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();
}
}); });
new $.fn.dataTable.FixedHeader( table ); dt.on('draw', drawAction);
dt.on('column-reorder', function(e, settings, details){
table.button().add(0, 'refresh'); drawAction();
//table.button().add(0,'pageLength');
{{if .LoggedAdmin.HasPermission "close_conns"}}
table.button().add(0,'disconnect');
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 drawAction() {
KTMenu.createInstances();
handleRowActions();
$('#table_body').localize();
}
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();
});
}
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> </script>
{{end}} {{- end}}

View file

@ -77,7 +77,6 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
{{- end}} {{- end}}
</div> </div>
</div> </div>
</div>
<table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5"> <table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5">
<thead> <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> <i class="ki-duotone ki-down fs-5 ms-1 rotate-180"></i>
</button> </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">`; <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"}} //{{- if .LoggedUser.HasPermission "manage_folders"}}
numActions++; numActions++;
actions+=`<div class="menu-item px-3"> actions+=`<div class="menu-item px-3">