Compare commits
109 Commits
fix/ee-350
...
fix/EE-368
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56ba5f896e | ||
|
|
790eb8c39e | ||
|
|
f2773e5aa6 | ||
|
|
b2214d4238 | ||
|
|
bbc152c2c2 | ||
|
|
2552eb5e25 | ||
|
|
ddaf9dc885 | ||
|
|
11c778cfeb | ||
|
|
11dffdee9a | ||
|
|
d4d80ed8f7 | ||
|
|
0ba10b44ec | ||
|
|
0f617f7f87 | ||
|
|
423dd5e394 | ||
|
|
44737029a9 | ||
|
|
ce22544c60 | ||
|
|
9106e74e61 | ||
|
|
6c57ddb563 | ||
|
|
a2e1570162 | ||
|
|
ea60740d48 | ||
|
|
762c664948 | ||
|
|
d574a71cb1 | ||
|
|
bb066cd58c | ||
|
|
e779939ae1 | ||
|
|
aa830a0e58 | ||
|
|
52ac54f15c | ||
|
|
cc0ab75aca | ||
|
|
7e3347da2b | ||
|
|
87e9d7f8d4 | ||
|
|
6d3a33635d | ||
|
|
090268d7b6 | ||
|
|
698a91596e | ||
|
|
bb447bb02a | ||
|
|
5ffcbe8677 | ||
|
|
ac6296b86d | ||
|
|
3239a61bda | ||
|
|
2a43285593 | ||
|
|
36071837cb | ||
|
|
1ef713d80b | ||
|
|
82b848af0c | ||
|
|
b059641c80 | ||
|
|
728e885b9d | ||
|
|
3acefba069 | ||
|
|
9205f67791 | ||
|
|
6d95643a68 | ||
|
|
149c414d08 | ||
|
|
f8b4663e0a | ||
|
|
7b774c702d | ||
|
|
8045a15a50 | ||
|
|
9a18dd8162 | ||
|
|
70a7eefa22 | ||
|
|
3356d1abe2 | ||
|
|
7ee8dac832 | ||
|
|
5b3f099f4e | ||
|
|
5f5cb36df1 | ||
|
|
3645ff7459 | ||
|
|
9a92b97b7e | ||
|
|
005c48b1ad | ||
|
|
4fb1880ddc | ||
|
|
54145ce949 | ||
|
|
b040aa1e78 | ||
|
|
985eef6987 | ||
|
|
a5c3116b0c | ||
|
|
df381b6a33 | ||
|
|
9223c0226a | ||
|
|
314fdc850e | ||
|
|
43bbeed141 | ||
|
|
e07253bcef | ||
|
|
23b9baa059 | ||
|
|
05357ecce5 | ||
|
|
1a8fe82821 | ||
|
|
95f4db4f48 | ||
|
|
43600083a7 | ||
|
|
e6477b0b97 | ||
|
|
6aa7fdb4f2 | ||
|
|
4997e9c7be | ||
|
|
f0456cbf5f | ||
|
|
a0d349e0b3 | ||
|
|
f5e774c89d | ||
|
|
552d3f8a3e | ||
|
|
39f9173956 | ||
|
|
e4fc41fc94 | ||
|
|
ce7d234cba | ||
|
|
35701f5899 | ||
|
|
3d4d2b50ae | ||
|
|
0da4e3ae63 | ||
|
|
ad7055ee01 | ||
|
|
8076455423 | ||
|
|
23eca3ce80 | ||
|
|
4cc672f902 | ||
|
|
82fb5f7ac1 | ||
|
|
de59ea030a | ||
|
|
d9be6d1724 | ||
|
|
958a8e97e9 | ||
|
|
5fd202d629 | ||
|
|
768f1aa663 | ||
|
|
69caa1179f | ||
|
|
9a2cdc4a93 | ||
|
|
14a8b1d897 | ||
|
|
712207e69f | ||
|
|
8d46692d66 | ||
|
|
3241738775 | ||
|
|
ce840997bf | ||
|
|
88c4a43a19 | ||
|
|
b4acbfc9e1 | ||
|
|
8bf1c91bc9 | ||
|
|
a66fd78dc1 | ||
|
|
b004b33935 | ||
|
|
d32793e84e | ||
|
|
fd4b515350 |
9
api/build/variables.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package build
|
||||
|
||||
// Variables to be set during the build time
|
||||
var BuildNumber string
|
||||
var ImageTag string
|
||||
var NodejsVersion string
|
||||
var YarnVersion string
|
||||
var WebpackVersion string
|
||||
var GoVersion string
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/portainer/libhelm"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/apikey"
|
||||
"github.com/portainer/portainer/api/build"
|
||||
"github.com/portainer/portainer/api/chisel"
|
||||
"github.com/portainer/portainer/api/cli"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
@@ -743,7 +744,15 @@ func main() {
|
||||
|
||||
for {
|
||||
server := buildServer(flags)
|
||||
logrus.Printf("[INFO] [cmd,main] Starting Portainer version %s\n", portainer.APIVersion)
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"Version": portainer.APIVersion,
|
||||
"BuildNumber": build.BuildNumber,
|
||||
"ImageTag": build.ImageTag,
|
||||
"NodejsVersion": build.NodejsVersion,
|
||||
"YarnVersion": build.YarnVersion,
|
||||
"WebpackVersion": build.WebpackVersion,
|
||||
"GoVersion": build.GoVersion},
|
||||
).Print("[INFO] [cmd,main] Starting Portainer")
|
||||
err := server.Start()
|
||||
logrus.Printf("[INFO] [cmd,main] Http server exited: %v\n", err)
|
||||
}
|
||||
|
||||
@@ -103,8 +103,26 @@ func (store *Store) backupWithOptions(options *BackupOptions) (string, error) {
|
||||
store.createBackupFolders()
|
||||
|
||||
options = store.setupOptions(options)
|
||||
dbPath := store.databasePath()
|
||||
|
||||
return options.BackupPath, store.copyDBFile(store.databasePath(), options.BackupPath)
|
||||
if err := store.Close(); err != nil {
|
||||
return options.BackupPath, fmt.Errorf(
|
||||
"error closing datastore before creating backup: %v",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
if err := store.copyDBFile(dbPath, options.BackupPath); err != nil {
|
||||
return options.BackupPath, err
|
||||
}
|
||||
|
||||
if _, err := store.Open(); err != nil {
|
||||
return options.BackupPath, fmt.Errorf(
|
||||
"error opening datastore after creating backup: %v",
|
||||
err,
|
||||
)
|
||||
}
|
||||
return options.BackupPath, nil
|
||||
}
|
||||
|
||||
// RestoreWithOptions previously saved backup for the current Edition with options
|
||||
|
||||
@@ -103,6 +103,9 @@ func (m *Migrator) Migrate() error {
|
||||
|
||||
// Portainer 2.14
|
||||
newMigration(50, m.migrateDBVersionToDB50),
|
||||
|
||||
// Portainer 2.15
|
||||
newMigration(60, m.migrateDBVersionToDB60),
|
||||
}
|
||||
|
||||
var lastDbVersion int
|
||||
|
||||
30
api/datastore/migrator/migrate_dbversion60.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package migrator
|
||||
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func (m *Migrator) migrateDBVersionToDB60() error {
|
||||
if err := m.addGpuInputFieldDB60(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) addGpuInputFieldDB60() error {
|
||||
migrateLog.Info("- add gpu input field")
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
endpoint.Gpus = []portainer.Pair{}
|
||||
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -43,6 +43,7 @@
|
||||
},
|
||||
"EdgeCheckinInterval": 0,
|
||||
"EdgeKey": "",
|
||||
"Gpus": [],
|
||||
"GroupId": 1,
|
||||
"Id": 1,
|
||||
"IsEdgeDevice": false,
|
||||
@@ -175,6 +176,8 @@
|
||||
}
|
||||
},
|
||||
"DockerVersion": "20.10.13",
|
||||
"GpuUseAll": false,
|
||||
"GpuUseList": null,
|
||||
"HealthyContainerCount": 0,
|
||||
"ImageCount": 9,
|
||||
"NodeCount": 0,
|
||||
|
||||
@@ -7,9 +7,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
_container "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// Snapshotter represents a service used to create environment(endpoint) snapshots
|
||||
@@ -154,11 +155,35 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
|
||||
healthyContainers := 0
|
||||
unhealthyContainers := 0
|
||||
stacks := make(map[string]struct{})
|
||||
gpuUseSet := make(map[string]struct{})
|
||||
gpuUseAll := false
|
||||
for _, container := range containers {
|
||||
if container.State == "exited" {
|
||||
stoppedContainers++
|
||||
} else if container.State == "running" {
|
||||
runningContainers++
|
||||
|
||||
// snapshot GPUs
|
||||
response, err := cli.ContainerInspect(context.Background(), container.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var gpuOptions *_container.DeviceRequest = nil
|
||||
for _, deviceRequest := range response.HostConfig.Resources.DeviceRequests {
|
||||
if deviceRequest.Driver == "nvidia" || deviceRequest.Capabilities[0][0] == "gpu" {
|
||||
gpuOptions = &deviceRequest
|
||||
}
|
||||
}
|
||||
|
||||
if gpuOptions != nil {
|
||||
if gpuOptions.Count == -1 {
|
||||
gpuUseAll = true
|
||||
}
|
||||
for _, id := range gpuOptions.DeviceIDs {
|
||||
gpuUseSet[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(container.Status, "(healthy)") {
|
||||
@@ -174,6 +199,14 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
|
||||
}
|
||||
}
|
||||
|
||||
gpuUseList := make([]string, 0, len(gpuUseSet))
|
||||
for gpuUse := range gpuUseSet {
|
||||
gpuUseList = append(gpuUseList, gpuUse)
|
||||
}
|
||||
|
||||
snapshot.GpuUseAll = gpuUseAll
|
||||
snapshot.GpuUseList = gpuUseList
|
||||
|
||||
snapshot.RunningContainerCount = runningContainers
|
||||
snapshot.StoppedContainerCount = stoppedContainers
|
||||
snapshot.HealthyContainerCount = healthyContainers
|
||||
|
||||
16
api/go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/portainer/portainer/api
|
||||
|
||||
go 1.17
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.5.1
|
||||
@@ -20,7 +20,7 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.10.1
|
||||
github.com/gofrs/uuid v4.0.0+incompatible
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/google/go-cmp v0.5.6
|
||||
github.com/google/go-cmp v0.5.8
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
@@ -32,8 +32,8 @@ require (
|
||||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
|
||||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220703222411-e3cf664b39c6
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021
|
||||
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a
|
||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
|
||||
github.com/portainer/libhttp v0.0.0-20211208103139-07a5f798eb3f
|
||||
github.com/rkl-/digest v0.0.0-20180419075440-8316caa4a777
|
||||
@@ -43,6 +43,7 @@ require (
|
||||
github.com/viney-shih/go-lock v1.1.1
|
||||
go.etcd.io/bbolt v1.3.6
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
|
||||
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d
|
||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
@@ -61,7 +62,6 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.1 // indirect
|
||||
github.com/aws/smithy-go v1.9.0 // indirect
|
||||
github.com/containerd/containerd v1.6.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/distribution v2.8.0+incompatible // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
@@ -95,6 +95,9 @@ require (
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/onsi/ginkgo v1.16.4 // indirect
|
||||
github.com/onsi/gomega v1.15.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
@@ -112,12 +115,11 @@ require (
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
|
||||
google.golang.org/grpc v1.43.0 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gotest.tools/v3 v3.0.3 // indirect
|
||||
k8s.io/klog/v2 v2.30.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c // indirect
|
||||
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect
|
||||
|
||||
838
api/go.sum
@@ -25,6 +25,7 @@ type endpointCreatePayload struct {
|
||||
URL string
|
||||
EndpointCreationType endpointCreationEnum
|
||||
PublicURL string
|
||||
Gpus []portainer.Pair
|
||||
GroupID int
|
||||
TLS bool
|
||||
TLSSkipVerify bool
|
||||
@@ -142,6 +143,13 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
payload.PublicURL = publicURL
|
||||
}
|
||||
|
||||
gpus := make([]portainer.Pair, 0)
|
||||
err = request.RetrieveMultiPartFormJSONValue(r, "Gpus", &gpus, true)
|
||||
if err != nil {
|
||||
return errors.New("Invalid Gpus parameter")
|
||||
}
|
||||
payload.Gpus = gpus
|
||||
|
||||
checkinInterval, _ := request.RetrieveNumericMultiPartFormValue(r, "CheckinInterval", true)
|
||||
payload.EdgeCheckinInterval = checkinInterval
|
||||
|
||||
@@ -290,6 +298,7 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
|
||||
Type: portainer.AzureEnvironment,
|
||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||
PublicURL: payload.PublicURL,
|
||||
Gpus: payload.Gpus,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
AzureCredentials: credentials,
|
||||
@@ -323,6 +332,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
||||
URL: portainerHost,
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||
Gpus: payload.Gpus,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
},
|
||||
@@ -378,6 +388,7 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload)
|
||||
Type: endpointType,
|
||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||
PublicURL: payload.PublicURL,
|
||||
Gpus: payload.Gpus,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
},
|
||||
@@ -412,6 +423,7 @@ func (handler *Handler) createKubernetesEndpoint(payload *endpointCreatePayload)
|
||||
Type: portainer.KubernetesLocalEnvironment,
|
||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||
PublicURL: payload.PublicURL,
|
||||
Gpus: payload.Gpus,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: payload.TLS,
|
||||
TLSSkipVerify: payload.TLSSkipVerify,
|
||||
@@ -441,6 +453,7 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload,
|
||||
Type: endpointType,
|
||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||
PublicURL: payload.PublicURL,
|
||||
Gpus: payload.Gpus,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: payload.TLS,
|
||||
TLSSkipVerify: payload.TLSSkipVerify,
|
||||
|
||||
@@ -4,24 +4,14 @@ import (
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/portainer/portainer/api/internal/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
EdgeDeviceFilterAll = "all"
|
||||
EdgeDeviceFilterTrusted = "trusted"
|
||||
EdgeDeviceFilterUntrusted = "untrusted"
|
||||
EdgeDeviceFilterNone = "none"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -42,14 +32,19 @@ var endpointGroupNames map[portainer.EndpointGroupID]string
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param start query int false "Start searching from"
|
||||
// @param search query string false "Search query"
|
||||
// @param groupId query int false "List environments(endpoints) of this group"
|
||||
// @param limit query int false "Limit results to this value"
|
||||
// @param sort query int false "Sort results by this value"
|
||||
// @param order query int false "Order sorted results by desc/asc" Enum("asc", "desc")
|
||||
// @param search query string false "Search query"
|
||||
// @param groupIds query []int false "List environments(endpoints) of these groups"
|
||||
// @param status query []int false "List environments(endpoints) by this status"
|
||||
// @param types query []int false "List environments(endpoints) of this type"
|
||||
// @param tagIds query []int false "search environments(endpoints) with these tags (depends on tagsPartialMatch)"
|
||||
// @param tagsPartialMatch query bool false "If true, will return environment(endpoint) which has one of tagIds, if false (or missing) will return only environments(endpoints) that has all the tags"
|
||||
// @param endpointIds query []int false "will return only these environments(endpoints)"
|
||||
// @param edgeDeviceFilter query string false "will return only these edge environments, none will return only regular edge environments" Enum("all", "trusted", "untrusted", "none")
|
||||
// @param provisioned query bool false "If true, will return environment(endpoint) that were provisioned"
|
||||
// @param edgeDevice query bool false "if exists true show only edge devices, false show only regular edge endpoints. if missing, will show both types (relevant only for edge endpoints)"
|
||||
// @param edgeDeviceUntrusted query bool false "if true, show only untrusted endpoints, if false show only trusted (relevant only for edge devices, and if edgeDevice is true)"
|
||||
// @param name query string false "will return only environments(endpoints) with this name"
|
||||
// @success 200 {array} portainer.Endpoint "Endpoints"
|
||||
// @failure 500 "Server error"
|
||||
@@ -60,103 +55,42 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
start--
|
||||
}
|
||||
|
||||
search, _ := request.RetrieveQueryParameter(r, "search", true)
|
||||
if search != "" {
|
||||
search = strings.ToLower(search)
|
||||
}
|
||||
|
||||
groupID, _ := request.RetrieveNumericQueryParameter(r, "groupId", true)
|
||||
limit, _ := request.RetrieveNumericQueryParameter(r, "limit", true)
|
||||
sortField, _ := request.RetrieveQueryParameter(r, "sort", true)
|
||||
sortOrder, _ := request.RetrieveQueryParameter(r, "order", true)
|
||||
|
||||
var endpointTypes []int
|
||||
request.RetrieveJSONQueryParameter(r, "types", &endpointTypes, true)
|
||||
|
||||
var tagIDs []portainer.TagID
|
||||
request.RetrieveJSONQueryParameter(r, "tagIds", &tagIDs, true)
|
||||
|
||||
tagsPartialMatch, _ := request.RetrieveBooleanQueryParameter(r, "tagsPartialMatch", true)
|
||||
|
||||
var endpointIDs []portainer.EndpointID
|
||||
request.RetrieveJSONQueryParameter(r, "endpointIds", &endpointIDs, true)
|
||||
|
||||
var statuses []int
|
||||
request.RetrieveJSONQueryParameter(r, "status", &statuses, true)
|
||||
|
||||
var groupIDs []int
|
||||
request.RetrieveJSONQueryParameter(r, "groupIds", &groupIDs, true)
|
||||
|
||||
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from the database", err}
|
||||
return httperror.InternalServerError("Unable to retrieve environment groups from the database", err)
|
||||
}
|
||||
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err}
|
||||
return httperror.InternalServerError("Unable to retrieve environments from the database", err)
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
return httperror.InternalServerError("Unable to retrieve settings from the database", err)
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||
}
|
||||
|
||||
query, err := parseQuery(r)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid query parameters", err)
|
||||
}
|
||||
|
||||
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
|
||||
totalAvailableEndpoints := len(filteredEndpoints)
|
||||
|
||||
if groupID != 0 {
|
||||
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, []int{groupID})
|
||||
filteredEndpoints, totalAvailableEndpoints, err := handler.filterEndpointsByQuery(filteredEndpoints, query, endpointGroups, settings)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to filter endpoints", err)
|
||||
}
|
||||
|
||||
if endpointIDs != nil {
|
||||
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, endpointIDs)
|
||||
}
|
||||
|
||||
if len(groupIDs) > 0 {
|
||||
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, groupIDs)
|
||||
}
|
||||
|
||||
name, _ := request.RetrieveQueryParameter(r, "name", true)
|
||||
if name != "" {
|
||||
filteredEndpoints = filterEndpointsByName(filteredEndpoints, name)
|
||||
}
|
||||
|
||||
edgeDeviceFilter, _ := request.RetrieveQueryParameter(r, "edgeDeviceFilter", false)
|
||||
if edgeDeviceFilter != "" {
|
||||
filteredEndpoints = filterEndpointsByEdgeDevice(filteredEndpoints, edgeDeviceFilter)
|
||||
}
|
||||
|
||||
if len(statuses) > 0 {
|
||||
filteredEndpoints = filterEndpointsByStatuses(filteredEndpoints, statuses, settings)
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
tags, err := handler.DataStore.Tag().Tags()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve tags from the database", err}
|
||||
}
|
||||
tagsMap := make(map[portainer.TagID]string)
|
||||
for _, tag := range tags {
|
||||
tagsMap[tag.ID] = tag.Name
|
||||
}
|
||||
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, endpointGroups, tagsMap, search)
|
||||
}
|
||||
|
||||
if endpointTypes != nil {
|
||||
filteredEndpoints = filterEndpointsByTypes(filteredEndpoints, endpointTypes)
|
||||
}
|
||||
|
||||
if tagIDs != nil {
|
||||
filteredEndpoints = filteredEndpointsByTags(filteredEndpoints, tagIDs, endpointGroups, tagsPartialMatch)
|
||||
}
|
||||
|
||||
// Sort endpoints by field
|
||||
sortEndpointsByField(filteredEndpoints, endpointGroups, sortField, sortOrder == "desc")
|
||||
|
||||
filteredEndpointCount := len(filteredEndpoints)
|
||||
@@ -196,64 +130,6 @@ func paginateEndpoints(endpoints []portainer.Endpoint, start, limit int) []porta
|
||||
return endpoints[start:end]
|
||||
}
|
||||
|
||||
func filterEndpointsByGroupIDs(endpoints []portainer.Endpoint, endpointGroupIDs []int) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if utils.Contains(endpointGroupIDs, int(endpoint.GroupID)) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
|
||||
if endpointMatchSearchCriteria(&endpoint, endpointTags, searchCriteria) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
continue
|
||||
}
|
||||
|
||||
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, tagsMap, searchCriteria) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func filterEndpointsByStatuses(endpoints []portainer.Endpoint, statuses []int, settings *portainer.Settings) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
status := endpoint.Status
|
||||
if endpointutils.IsEdgeEndpoint(&endpoint) {
|
||||
isCheckValid := false
|
||||
edgeCheckinInterval := endpoint.EdgeCheckinInterval
|
||||
if endpoint.EdgeCheckinInterval == 0 {
|
||||
edgeCheckinInterval = settings.EdgeAgentCheckinInterval
|
||||
}
|
||||
if edgeCheckinInterval != 0 && endpoint.LastCheckInDate != 0 {
|
||||
isCheckValid = time.Now().Unix()-endpoint.LastCheckInDate <= int64(edgeCheckinInterval*EdgeDeviceIntervalMultiplier+EdgeDeviceIntervalAdd)
|
||||
}
|
||||
status = portainer.EndpointStatusDown // Offline
|
||||
if isCheckValid {
|
||||
status = portainer.EndpointStatusUp // Online
|
||||
}
|
||||
}
|
||||
|
||||
if utils.Contains(statuses, int(status)) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func sortEndpointsByField(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, sortField string, isSortDesc bool) {
|
||||
|
||||
switch sortField {
|
||||
@@ -294,123 +170,6 @@ func sortEndpointsByField(endpoints []portainer.Endpoint, endpointGroups []porta
|
||||
}
|
||||
}
|
||||
|
||||
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, searchCriteria string) bool {
|
||||
if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.Contains(strings.ToLower(endpoint.URL), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
|
||||
if endpoint.Status == portainer.EndpointStatusUp && searchCriteria == "up" {
|
||||
return true
|
||||
} else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" {
|
||||
return true
|
||||
}
|
||||
for _, tag := range tags {
|
||||
if strings.Contains(strings.ToLower(tag), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) bool {
|
||||
for _, group := range endpointGroups {
|
||||
if group.ID == endpoint.GroupID {
|
||||
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
tags := convertTagIDsToTags(tagsMap, group.TagIDs)
|
||||
for _, tag := range tags {
|
||||
if strings.Contains(strings.ToLower(tag), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []int) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
typeSet := map[portainer.EndpointType]bool{}
|
||||
for _, endpointType := range endpointTypes {
|
||||
typeSet[portainer.EndpointType(endpointType)] = true
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if typeSet[endpoint.Type] {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func filterEndpointsByEdgeDevice(endpoints []portainer.Endpoint, edgeDeviceFilter string) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if shouldReturnEdgeDevice(endpoint, edgeDeviceFilter) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func shouldReturnEdgeDevice(endpoint portainer.Endpoint, edgeDeviceFilter string) bool {
|
||||
// none - return all endpoints that are not edge devices
|
||||
if edgeDeviceFilter == EdgeDeviceFilterNone && !endpoint.IsEdgeDevice {
|
||||
return true
|
||||
}
|
||||
|
||||
if !endpointutils.IsEdgeEndpoint(&endpoint) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch edgeDeviceFilter {
|
||||
case EdgeDeviceFilterAll:
|
||||
return true
|
||||
case EdgeDeviceFilterTrusted:
|
||||
return endpoint.UserTrusted
|
||||
case EdgeDeviceFilterUntrusted:
|
||||
return !endpoint.UserTrusted
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string {
|
||||
tags := make([]string, 0)
|
||||
for _, tagID := range tagIDs {
|
||||
tags = append(tags, tagsMap[tagID])
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
func filteredEndpointsByTags(endpoints []portainer.Endpoint, tagIDs []portainer.TagID, endpointGroups []portainer.EndpointGroup, partialMatch bool) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
endpointGroup := getEndpointGroup(endpoint.GroupID, endpointGroups)
|
||||
endpointMatched := false
|
||||
if partialMatch {
|
||||
endpointMatched = endpointPartialMatchTags(endpoint, endpointGroup, tagIDs)
|
||||
} else {
|
||||
endpointMatched = endpointFullMatchTags(endpoint, endpointGroup, tagIDs)
|
||||
}
|
||||
|
||||
if endpointMatched {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func getEndpointGroup(groupID portainer.EndpointGroupID, groups []portainer.EndpointGroup) portainer.EndpointGroup {
|
||||
var endpointGroup portainer.EndpointGroup
|
||||
for _, group := range groups {
|
||||
@@ -421,72 +180,3 @@ func getEndpointGroup(groupID portainer.EndpointGroupID, groups []portainer.Endp
|
||||
}
|
||||
return endpointGroup
|
||||
}
|
||||
|
||||
func endpointPartialMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
|
||||
tagSet := make(map[portainer.TagID]bool)
|
||||
for _, tagID := range tagIDs {
|
||||
tagSet[tagID] = true
|
||||
}
|
||||
for _, tagID := range endpoint.TagIDs {
|
||||
if tagSet[tagID] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, tagID := range endpointGroup.TagIDs {
|
||||
if tagSet[tagID] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func endpointFullMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
|
||||
missingTags := make(map[portainer.TagID]bool)
|
||||
for _, tagID := range tagIDs {
|
||||
missingTags[tagID] = true
|
||||
}
|
||||
for _, tagID := range endpoint.TagIDs {
|
||||
if missingTags[tagID] {
|
||||
delete(missingTags, tagID)
|
||||
}
|
||||
}
|
||||
for _, tagID := range endpointGroup.TagIDs {
|
||||
if missingTags[tagID] {
|
||||
delete(missingTags, tagID)
|
||||
}
|
||||
}
|
||||
return len(missingTags) == 0
|
||||
}
|
||||
|
||||
func filteredEndpointsByIds(endpoints []portainer.Endpoint, ids []portainer.EndpointID) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
idsSet := make(map[portainer.EndpointID]bool)
|
||||
for _, id := range ids {
|
||||
idsSet[id] = true
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if idsSet[endpoint.ID] {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
|
||||
}
|
||||
|
||||
func filterEndpointsByName(endpoints []portainer.Endpoint, name string) []portainer.Endpoint {
|
||||
if name == "" {
|
||||
return endpoints
|
||||
}
|
||||
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Name == name {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
@@ -16,66 +16,64 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type endpointListEdgeDeviceTest struct {
|
||||
type endpointListTest struct {
|
||||
title string
|
||||
expected []portainer.EndpointID
|
||||
filter string
|
||||
}
|
||||
|
||||
func Test_endpointList(t *testing.T) {
|
||||
var err error
|
||||
is := assert.New(t)
|
||||
func Test_endpointList_edgeDeviceFilter(t *testing.T) {
|
||||
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
defer teardown()
|
||||
|
||||
trustedEndpoint := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
untrustedEndpoint := portainer.Endpoint{ID: 2, UserTrusted: false, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
trustedEdgeDevice := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
untrustedEdgeDevice := portainer.Endpoint{ID: 2, UserTrusted: false, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
regularUntrustedEdgeEndpoint := portainer.Endpoint{ID: 3, UserTrusted: false, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
regularTrustedEdgeEndpoint := portainer.Endpoint{ID: 4, UserTrusted: true, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
regularEndpoint := portainer.Endpoint{ID: 5, UserTrusted: false, IsEdgeDevice: false, GroupID: 1, Type: portainer.DockerEnvironment}
|
||||
|
||||
endpoints := []portainer.Endpoint{
|
||||
trustedEndpoint,
|
||||
untrustedEndpoint,
|
||||
handler, teardown := setup(t, []portainer.Endpoint{
|
||||
trustedEdgeDevice,
|
||||
untrustedEdgeDevice,
|
||||
regularUntrustedEdgeEndpoint,
|
||||
regularTrustedEdgeEndpoint,
|
||||
regularEndpoint,
|
||||
})
|
||||
|
||||
defer teardown()
|
||||
|
||||
type endpointListEdgeDeviceTest struct {
|
||||
endpointListTest
|
||||
edgeDevice *bool
|
||||
edgeDeviceUntrusted bool
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
err = store.Endpoint().Create(&endpoint)
|
||||
is.NoError(err, "error creating environment")
|
||||
}
|
||||
|
||||
err = store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||
is.NoError(err, "error creating a user")
|
||||
|
||||
bouncer := helper.NewTestRequestBouncer()
|
||||
h := NewHandler(bouncer, nil)
|
||||
h.DataStore = store
|
||||
h.ComposeStackManager = testhelpers.NewComposeStackManager()
|
||||
|
||||
tests := []endpointListEdgeDeviceTest{
|
||||
{
|
||||
"should show all edge endpoints",
|
||||
[]portainer.EndpointID{trustedEndpoint.ID, untrustedEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
|
||||
EdgeDeviceFilterAll,
|
||||
endpointListTest: endpointListTest{
|
||||
"should show all endpoints expect of the untrusted devices",
|
||||
[]portainer.EndpointID{trustedEdgeDevice.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID, regularEndpoint.ID},
|
||||
},
|
||||
edgeDevice: nil,
|
||||
},
|
||||
{
|
||||
"should show only trusted edge devices",
|
||||
[]portainer.EndpointID{trustedEndpoint.ID, regularTrustedEdgeEndpoint.ID},
|
||||
EdgeDeviceFilterTrusted,
|
||||
endpointListTest: endpointListTest{
|
||||
"should show only trusted edge devices and regular endpoints",
|
||||
[]portainer.EndpointID{trustedEdgeDevice.ID, regularEndpoint.ID},
|
||||
},
|
||||
edgeDevice: BoolAddr(true),
|
||||
},
|
||||
{
|
||||
"should show only untrusted edge devices",
|
||||
[]portainer.EndpointID{untrustedEndpoint.ID, regularUntrustedEdgeEndpoint.ID},
|
||||
EdgeDeviceFilterUntrusted,
|
||||
endpointListTest: endpointListTest{
|
||||
"should show only untrusted edge devices and regular endpoints",
|
||||
[]portainer.EndpointID{untrustedEdgeDevice.ID, regularEndpoint.ID},
|
||||
},
|
||||
edgeDevice: BoolAddr(true),
|
||||
edgeDeviceUntrusted: true,
|
||||
},
|
||||
{
|
||||
"should show no edge devices",
|
||||
[]portainer.EndpointID{regularEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
|
||||
EdgeDeviceFilterNone,
|
||||
endpointListTest: endpointListTest{
|
||||
"should show no edge devices",
|
||||
[]portainer.EndpointID{regularEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
|
||||
},
|
||||
edgeDevice: BoolAddr(false),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -83,8 +81,13 @@ func Test_endpointList(t *testing.T) {
|
||||
t.Run(test.title, func(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
req := buildEndpointListRequest(test.filter)
|
||||
resp, err := doEndpointListRequest(req, h, is)
|
||||
query := fmt.Sprintf("edgeDeviceUntrusted=%v&", test.edgeDeviceUntrusted)
|
||||
if test.edgeDevice != nil {
|
||||
query += fmt.Sprintf("edgeDevice=%v&", *test.edgeDevice)
|
||||
}
|
||||
|
||||
req := buildEndpointListRequest(query)
|
||||
resp, err := doEndpointListRequest(req, handler, is)
|
||||
is.NoError(err)
|
||||
|
||||
is.Equal(len(test.expected), len(resp))
|
||||
@@ -100,8 +103,28 @@ func Test_endpointList(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func buildEndpointListRequest(filter string) *http.Request {
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/endpoints?edgeDeviceFilter=%s", filter), nil)
|
||||
func setup(t *testing.T, endpoints []portainer.Endpoint) (handler *Handler, teardown func()) {
|
||||
is := assert.New(t)
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
err := store.Endpoint().Create(&endpoint)
|
||||
is.NoError(err, "error creating environment")
|
||||
}
|
||||
|
||||
err := store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||
is.NoError(err, "error creating a user")
|
||||
|
||||
bouncer := helper.NewTestRequestBouncer()
|
||||
handler = NewHandler(bouncer, nil)
|
||||
handler.DataStore = store
|
||||
handler.ComposeStackManager = testhelpers.NewComposeStackManager()
|
||||
|
||||
return handler, teardown
|
||||
}
|
||||
|
||||
func buildEndpointListRequest(query string) *http.Request {
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/endpoints?%s", query), nil)
|
||||
|
||||
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
@@ -22,6 +22,8 @@ type endpointUpdatePayload struct {
|
||||
// URL or IP address where exposed containers will be reachable.\
|
||||
// Defaults to URL if not specified
|
||||
PublicURL *string `example:"docker.mydomain.tld:2375"`
|
||||
// GPUs information
|
||||
Gpus []portainer.Pair
|
||||
// Group identifier
|
||||
GroupID *int `example:"1"`
|
||||
// Require TLS to connect against this environment(endpoint)
|
||||
@@ -110,6 +112,10 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
endpoint.PublicURL = *payload.PublicURL
|
||||
}
|
||||
|
||||
if payload.Gpus != nil {
|
||||
endpoint.Gpus = payload.Gpus
|
||||
}
|
||||
|
||||
if payload.EdgeCheckinInterval != nil {
|
||||
endpoint.EdgeCheckinInterval = *payload.EdgeCheckinInterval
|
||||
}
|
||||
|
||||
415
api/http/handler/endpoints/filter.go
Normal file
@@ -0,0 +1,415 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type EnvironmentsQuery struct {
|
||||
search string
|
||||
types []portainer.EndpointType
|
||||
tagIds []portainer.TagID
|
||||
endpointIds []portainer.EndpointID
|
||||
tagsPartialMatch bool
|
||||
groupIds []portainer.EndpointGroupID
|
||||
status []portainer.EndpointStatus
|
||||
edgeDevice *bool
|
||||
edgeDeviceUntrusted bool
|
||||
name string
|
||||
}
|
||||
|
||||
func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
|
||||
search, _ := request.RetrieveQueryParameter(r, "search", true)
|
||||
if search != "" {
|
||||
search = strings.ToLower(search)
|
||||
}
|
||||
|
||||
status, err := getNumberArrayQueryParameter[portainer.EndpointStatus](r, "status")
|
||||
if err != nil {
|
||||
return EnvironmentsQuery{}, err
|
||||
}
|
||||
|
||||
groupIDs, err := getNumberArrayQueryParameter[portainer.EndpointGroupID](r, "groupIds")
|
||||
if err != nil {
|
||||
return EnvironmentsQuery{}, err
|
||||
}
|
||||
|
||||
endpointTypes, err := getNumberArrayQueryParameter[portainer.EndpointType](r, "types")
|
||||
if err != nil {
|
||||
return EnvironmentsQuery{}, err
|
||||
}
|
||||
|
||||
tagIDs, err := getNumberArrayQueryParameter[portainer.TagID](r, "tagIds")
|
||||
if err != nil {
|
||||
return EnvironmentsQuery{}, err
|
||||
}
|
||||
|
||||
tagsPartialMatch, _ := request.RetrieveBooleanQueryParameter(r, "tagsPartialMatch", true)
|
||||
|
||||
endpointIDs, err := getNumberArrayQueryParameter[portainer.EndpointID](r, "endpointIds")
|
||||
if err != nil {
|
||||
return EnvironmentsQuery{}, err
|
||||
}
|
||||
|
||||
name, _ := request.RetrieveQueryParameter(r, "name", true)
|
||||
|
||||
edgeDeviceParam, _ := request.RetrieveQueryParameter(r, "edgeDevice", true)
|
||||
|
||||
var edgeDevice *bool
|
||||
if edgeDeviceParam != "" {
|
||||
edgeDevice = BoolAddr(edgeDeviceParam == "true")
|
||||
}
|
||||
|
||||
edgeDeviceUntrusted, _ := request.RetrieveBooleanQueryParameter(r, "edgeDeviceUntrusted", true)
|
||||
|
||||
return EnvironmentsQuery{
|
||||
search: search,
|
||||
types: endpointTypes,
|
||||
tagIds: tagIDs,
|
||||
endpointIds: endpointIDs,
|
||||
tagsPartialMatch: tagsPartialMatch,
|
||||
groupIds: groupIDs,
|
||||
status: status,
|
||||
edgeDevice: edgeDevice,
|
||||
edgeDeviceUntrusted: edgeDeviceUntrusted,
|
||||
name: name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) filterEndpointsByQuery(filteredEndpoints []portainer.Endpoint, query EnvironmentsQuery, groups []portainer.EndpointGroup, settings *portainer.Settings) ([]portainer.Endpoint, int, error) {
|
||||
totalAvailableEndpoints := len(filteredEndpoints)
|
||||
|
||||
if len(query.endpointIds) > 0 {
|
||||
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, query.endpointIds)
|
||||
}
|
||||
|
||||
if len(query.groupIds) > 0 {
|
||||
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, query.groupIds)
|
||||
}
|
||||
|
||||
if query.name != "" {
|
||||
filteredEndpoints = filterEndpointsByName(filteredEndpoints, query.name)
|
||||
}
|
||||
|
||||
if query.edgeDevice != nil {
|
||||
filteredEndpoints = filterEndpointsByEdgeDevice(filteredEndpoints, *query.edgeDevice, query.edgeDeviceUntrusted)
|
||||
} else {
|
||||
// If the edgeDevice parameter is not set, we need to filter out the untrusted edge devices
|
||||
filteredEndpoints = filter(filteredEndpoints, func(endpoint portainer.Endpoint) bool {
|
||||
return !endpoint.IsEdgeDevice || endpoint.UserTrusted
|
||||
})
|
||||
}
|
||||
|
||||
if len(query.status) > 0 {
|
||||
filteredEndpoints = filterEndpointsByStatuses(filteredEndpoints, query.status, settings)
|
||||
}
|
||||
|
||||
if query.search != "" {
|
||||
tags, err := handler.DataStore.Tag().Tags()
|
||||
if err != nil {
|
||||
return nil, 0, errors.WithMessage(err, "Unable to retrieve tags from the database")
|
||||
}
|
||||
|
||||
tagsMap := make(map[portainer.TagID]string)
|
||||
for _, tag := range tags {
|
||||
tagsMap[tag.ID] = tag.Name
|
||||
}
|
||||
|
||||
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, groups, tagsMap, query.search)
|
||||
}
|
||||
|
||||
if len(query.types) > 0 {
|
||||
filteredEndpoints = filterEndpointsByTypes(filteredEndpoints, query.types)
|
||||
}
|
||||
|
||||
if len(query.tagIds) > 0 {
|
||||
filteredEndpoints = filteredEndpointsByTags(filteredEndpoints, query.tagIds, groups, query.tagsPartialMatch)
|
||||
}
|
||||
|
||||
return filteredEndpoints, totalAvailableEndpoints, nil
|
||||
}
|
||||
|
||||
func filterEndpointsByGroupIDs(endpoints []portainer.Endpoint, endpointGroupIDs []portainer.EndpointGroupID) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if slices.Contains(endpointGroupIDs, endpoint.GroupID) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
|
||||
if endpointMatchSearchCriteria(&endpoint, endpointTags, searchCriteria) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
continue
|
||||
}
|
||||
|
||||
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, tagsMap, searchCriteria) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func filterEndpointsByStatuses(endpoints []portainer.Endpoint, statuses []portainer.EndpointStatus, settings *portainer.Settings) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
status := endpoint.Status
|
||||
if endpointutils.IsEdgeEndpoint(&endpoint) {
|
||||
isCheckValid := false
|
||||
edgeCheckinInterval := endpoint.EdgeCheckinInterval
|
||||
if endpoint.EdgeCheckinInterval == 0 {
|
||||
edgeCheckinInterval = settings.EdgeAgentCheckinInterval
|
||||
}
|
||||
|
||||
if edgeCheckinInterval != 0 && endpoint.LastCheckInDate != 0 {
|
||||
isCheckValid = time.Now().Unix()-endpoint.LastCheckInDate <= int64(edgeCheckinInterval*EdgeDeviceIntervalMultiplier+EdgeDeviceIntervalAdd)
|
||||
}
|
||||
|
||||
status = portainer.EndpointStatusDown // Offline
|
||||
if isCheckValid {
|
||||
status = portainer.EndpointStatusUp // Online
|
||||
}
|
||||
}
|
||||
|
||||
if slices.Contains(statuses, status) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, searchCriteria string) bool {
|
||||
if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.Contains(strings.ToLower(endpoint.URL), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
|
||||
if endpoint.Status == portainer.EndpointStatusUp && searchCriteria == "up" {
|
||||
return true
|
||||
} else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" {
|
||||
return true
|
||||
}
|
||||
for _, tag := range tags {
|
||||
if strings.Contains(strings.ToLower(tag), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) bool {
|
||||
for _, group := range endpointGroups {
|
||||
if group.ID == endpoint.GroupID {
|
||||
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
tags := convertTagIDsToTags(tagsMap, group.TagIDs)
|
||||
for _, tag := range tags {
|
||||
if strings.Contains(strings.ToLower(tag), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []portainer.EndpointType) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
typeSet := map[portainer.EndpointType]bool{}
|
||||
for _, endpointType := range endpointTypes {
|
||||
typeSet[portainer.EndpointType(endpointType)] = true
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if typeSet[endpoint.Type] {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func filterEndpointsByEdgeDevice(endpoints []portainer.Endpoint, edgeDevice bool, untrusted bool) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if shouldReturnEdgeDevice(endpoint, edgeDevice, untrusted) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func shouldReturnEdgeDevice(endpoint portainer.Endpoint, edgeDeviceParam bool, untrustedParam bool) bool {
|
||||
if !endpointutils.IsEdgeEndpoint(&endpoint) {
|
||||
return true
|
||||
}
|
||||
|
||||
if !edgeDeviceParam {
|
||||
return !endpoint.IsEdgeDevice
|
||||
}
|
||||
|
||||
return endpoint.IsEdgeDevice && endpoint.UserTrusted == !untrustedParam
|
||||
}
|
||||
|
||||
func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string {
|
||||
tags := make([]string, 0)
|
||||
for _, tagID := range tagIDs {
|
||||
tags = append(tags, tagsMap[tagID])
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
func filteredEndpointsByTags(endpoints []portainer.Endpoint, tagIDs []portainer.TagID, endpointGroups []portainer.EndpointGroup, partialMatch bool) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
endpointGroup := getEndpointGroup(endpoint.GroupID, endpointGroups)
|
||||
endpointMatched := false
|
||||
if partialMatch {
|
||||
endpointMatched = endpointPartialMatchTags(endpoint, endpointGroup, tagIDs)
|
||||
} else {
|
||||
endpointMatched = endpointFullMatchTags(endpoint, endpointGroup, tagIDs)
|
||||
}
|
||||
|
||||
if endpointMatched {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func endpointPartialMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
|
||||
tagSet := make(map[portainer.TagID]bool)
|
||||
for _, tagID := range tagIDs {
|
||||
tagSet[tagID] = true
|
||||
}
|
||||
for _, tagID := range endpoint.TagIDs {
|
||||
if tagSet[tagID] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, tagID := range endpointGroup.TagIDs {
|
||||
if tagSet[tagID] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func endpointFullMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
|
||||
missingTags := make(map[portainer.TagID]bool)
|
||||
for _, tagID := range tagIDs {
|
||||
missingTags[tagID] = true
|
||||
}
|
||||
for _, tagID := range endpoint.TagIDs {
|
||||
if missingTags[tagID] {
|
||||
delete(missingTags, tagID)
|
||||
}
|
||||
}
|
||||
for _, tagID := range endpointGroup.TagIDs {
|
||||
if missingTags[tagID] {
|
||||
delete(missingTags, tagID)
|
||||
}
|
||||
}
|
||||
return len(missingTags) == 0
|
||||
}
|
||||
|
||||
func filteredEndpointsByIds(endpoints []portainer.Endpoint, ids []portainer.EndpointID) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
idsSet := make(map[portainer.EndpointID]bool)
|
||||
for _, id := range ids {
|
||||
idsSet[id] = true
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if idsSet[endpoint.ID] {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
|
||||
}
|
||||
|
||||
func filterEndpointsByName(endpoints []portainer.Endpoint, name string) []portainer.Endpoint {
|
||||
if name == "" {
|
||||
return endpoints
|
||||
}
|
||||
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Name == name {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func filter(endpoints []portainer.Endpoint, predicate func(endpoint portainer.Endpoint) bool) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if predicate(endpoint) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func getArrayQueryParameter(r *http.Request, parameter string) []string {
|
||||
list, exists := r.Form[fmt.Sprintf("%s[]", parameter)]
|
||||
if !exists {
|
||||
list = []string{}
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
func getNumberArrayQueryParameter[T ~int](r *http.Request, parameter string) ([]T, error) {
|
||||
list := getArrayQueryParameter(r, parameter)
|
||||
if list == nil {
|
||||
return []T{}, nil
|
||||
}
|
||||
|
||||
var result []T
|
||||
for _, item := range list {
|
||||
number, err := strconv.Atoi(item)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Unable to parse parameter %s", parameter)
|
||||
|
||||
}
|
||||
|
||||
result = append(result, T(number))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
119
api/http/handler/endpoints/filter_test.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
helper "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type filterTest struct {
|
||||
title string
|
||||
expected []portainer.EndpointID
|
||||
query EnvironmentsQuery
|
||||
}
|
||||
|
||||
func Test_Filter_edgeDeviceFilter(t *testing.T) {
|
||||
|
||||
trustedEdgeDevice := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
untrustedEdgeDevice := portainer.Endpoint{ID: 2, UserTrusted: false, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
regularUntrustedEdgeEndpoint := portainer.Endpoint{ID: 3, UserTrusted: false, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
regularTrustedEdgeEndpoint := portainer.Endpoint{ID: 4, UserTrusted: true, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
|
||||
regularEndpoint := portainer.Endpoint{ID: 5, GroupID: 1, Type: portainer.DockerEnvironment}
|
||||
|
||||
endpoints := []portainer.Endpoint{
|
||||
trustedEdgeDevice,
|
||||
untrustedEdgeDevice,
|
||||
regularUntrustedEdgeEndpoint,
|
||||
regularTrustedEdgeEndpoint,
|
||||
regularEndpoint,
|
||||
}
|
||||
|
||||
handler, teardown := setupFilterTest(t, endpoints)
|
||||
|
||||
defer teardown()
|
||||
|
||||
tests := []filterTest{
|
||||
{
|
||||
"should show all edge endpoints except of the untrusted devices",
|
||||
[]portainer.EndpointID{trustedEdgeDevice.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
|
||||
EnvironmentsQuery{
|
||||
types: []portainer.EndpointType{portainer.EdgeAgentOnDockerEnvironment, portainer.EdgeAgentOnKubernetesEnvironment},
|
||||
},
|
||||
},
|
||||
{
|
||||
"should show only trusted edge devices and other regular endpoints",
|
||||
[]portainer.EndpointID{trustedEdgeDevice.ID, regularEndpoint.ID},
|
||||
EnvironmentsQuery{
|
||||
edgeDevice: BoolAddr(true),
|
||||
},
|
||||
},
|
||||
{
|
||||
"should show only untrusted edge devices and other regular endpoints",
|
||||
[]portainer.EndpointID{untrustedEdgeDevice.ID, regularEndpoint.ID},
|
||||
EnvironmentsQuery{
|
||||
edgeDevice: BoolAddr(true),
|
||||
edgeDeviceUntrusted: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"should show no edge devices",
|
||||
[]portainer.EndpointID{regularEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
|
||||
EnvironmentsQuery{
|
||||
edgeDevice: BoolAddr(false),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runTests(tests, t, handler, endpoints)
|
||||
}
|
||||
|
||||
func runTests(tests []filterTest, t *testing.T, handler *Handler, endpoints []portainer.Endpoint) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.title, func(t *testing.T) {
|
||||
runTest(t, test, handler, endpoints)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runTest(t *testing.T, test filterTest, handler *Handler, endpoints []portainer.Endpoint) {
|
||||
is := assert.New(t)
|
||||
|
||||
filteredEndpoints, _, err := handler.filterEndpointsByQuery(endpoints, test.query, []portainer.EndpointGroup{}, &portainer.Settings{})
|
||||
|
||||
is.NoError(err)
|
||||
|
||||
is.Equal(len(test.expected), len(filteredEndpoints))
|
||||
|
||||
respIds := []portainer.EndpointID{}
|
||||
|
||||
for _, endpoint := range filteredEndpoints {
|
||||
respIds = append(respIds, endpoint.ID)
|
||||
}
|
||||
|
||||
is.ElementsMatch(test.expected, respIds)
|
||||
|
||||
}
|
||||
|
||||
func setupFilterTest(t *testing.T, endpoints []portainer.Endpoint) (handler *Handler, teardown func()) {
|
||||
is := assert.New(t)
|
||||
_, store, teardown := datastore.MustNewTestStore(true, true)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
err := store.Endpoint().Create(&endpoint)
|
||||
is.NoError(err, "error creating environment")
|
||||
}
|
||||
|
||||
err := store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
|
||||
is.NoError(err, "error creating a user")
|
||||
|
||||
bouncer := helper.NewTestRequestBouncer()
|
||||
handler = NewHandler(bouncer, nil)
|
||||
handler.DataStore = store
|
||||
handler.ComposeStackManager = testhelpers.NewComposeStackManager()
|
||||
|
||||
return handler, teardown
|
||||
}
|
||||
6
api/http/handler/endpoints/utils.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package endpoints
|
||||
|
||||
func BoolAddr(b bool) *bool {
|
||||
boolVar := b
|
||||
return &boolVar
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package helm
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/libhelm"
|
||||
@@ -108,7 +107,7 @@ func (handler *Handler) getHelmClusterAccess(r *http.Request) (*options.Kubernet
|
||||
|
||||
hostURL := "localhost"
|
||||
if !sslSettings.SelfSigned {
|
||||
hostURL = strings.Split(r.Host, ":")[0]
|
||||
hostURL = r.Host
|
||||
}
|
||||
|
||||
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
@@ -145,8 +144,7 @@ func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenD
|
||||
}
|
||||
|
||||
func (handler *Handler) buildCluster(r *http.Request, endpoint portainer.Endpoint) clientV1.NamedCluster {
|
||||
hostURL := strings.Split(r.Host, ":")[0]
|
||||
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)
|
||||
kubeConfigInternal := handler.kubeClusterAccessService.GetData(r.Host, endpoint.ID)
|
||||
return clientV1.NamedCluster{
|
||||
Name: buildClusterName(endpoint.Name),
|
||||
Cluster: clientV1.Cluster{
|
||||
|
||||
@@ -95,8 +95,10 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp
|
||||
}
|
||||
}
|
||||
//if LDAP authentication is on, compose the related fields from application settings
|
||||
if publicSettings.AuthenticationMethod == portainer.AuthenticationLDAP {
|
||||
publicSettings.TeamSync = len(appSettings.LDAPSettings.GroupSearchSettings) > 0
|
||||
if publicSettings.AuthenticationMethod == portainer.AuthenticationLDAP && appSettings.LDAPSettings.GroupSearchSettings != nil {
|
||||
if len(appSettings.LDAPSettings.GroupSearchSettings) > 0 {
|
||||
publicSettings.TeamSync = len(appSettings.LDAPSettings.GroupSearchSettings[0].GroupBaseDN) > 0
|
||||
}
|
||||
}
|
||||
return publicSettings
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/pkg/errors"
|
||||
@@ -133,6 +135,20 @@ func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedR
|
||||
return handler.userIsAdminOrEndpointAdmin(user, endpointID)
|
||||
}
|
||||
|
||||
// if stack management is disabled for non admins and the user isn't an admin, then return false. Otherwise return true
|
||||
func (handler *Handler) userCanManageStacks(securityContext *security.RestrictedRequestContext, endpoint *portainer.Endpoint) (bool, error) {
|
||||
if endpointutils.IsDockerEndpoint(endpoint) && !endpoint.SecuritySettings.AllowStackManagementForRegularUsers {
|
||||
canCreate, err := handler.userCanCreateStack(securityContext, portainer.EndpointID(endpoint.ID))
|
||||
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("Failed to get user from the database: %w", err)
|
||||
}
|
||||
|
||||
return canCreate, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) checkUniqueStackName(endpoint *portainer.Endpoint, name string, stackID portainer.StackID) (bool, error) {
|
||||
stacks, err := handler.DataStore.Stack().Stacks()
|
||||
if err != nil {
|
||||
|
||||
@@ -82,6 +82,22 @@ func (handler *Handler) stackAssociate(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an environment with the specified identifier inside the database", Err: err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an environment with the specified identifier inside the database", Err: err}
|
||||
}
|
||||
|
||||
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
|
||||
}
|
||||
if !canManage {
|
||||
errMsg := "Stack management is disabled for non-admin users"
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: fmt.Errorf(errMsg)}
|
||||
}
|
||||
|
||||
stack.EndpointID = portainer.EndpointID(endpointID)
|
||||
stack.SwarmID = swarmId
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
|
||||
@@ -76,22 +75,18 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if endpointutils.IsDockerEndpoint(endpoint) && !endpoint.SecuritySettings.AllowStackManagementForRegularUsers {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
|
||||
}
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user info from request context", Err: err}
|
||||
}
|
||||
|
||||
canCreate, err := handler.userCanCreateStack(securityContext, portainer.EndpointID(endpointID))
|
||||
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack creation", err}
|
||||
}
|
||||
|
||||
if !canCreate {
|
||||
errMsg := "Stack creation is disabled for non-admin users"
|
||||
return &httperror.HandlerError{http.StatusForbidden, errMsg, errors.New(errMsg)}
|
||||
}
|
||||
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
|
||||
}
|
||||
if !canManage {
|
||||
errMsg := "Stack creation is disabled for non-admin users"
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
|
||||
@@ -103,6 +103,15 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
||||
}
|
||||
}
|
||||
|
||||
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
|
||||
}
|
||||
if !canManage {
|
||||
errMsg := "Stack deletion is disabled for non-admin users"
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: fmt.Errorf(errMsg)}
|
||||
}
|
||||
|
||||
// stop scheduler updates of the stack before removal
|
||||
if stack.AutoUpdate != nil {
|
||||
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
|
||||
|
||||
@@ -3,11 +3,12 @@ package stacks
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
@@ -59,6 +60,15 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
|
||||
}
|
||||
if !canManage {
|
||||
errMsg := "Stack management is disabled for non-admin users"
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
|
||||
}
|
||||
|
||||
if endpoint != nil {
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
@@ -76,7 +86,7 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ package stacks
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/http/errors"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/stackutils"
|
||||
)
|
||||
@@ -55,6 +55,15 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
|
||||
}
|
||||
if !canManage {
|
||||
errMsg := "Stack management is disabled for non-admin users"
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
|
||||
}
|
||||
|
||||
if endpoint != nil {
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
@@ -72,7 +81,7 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
if resourceControl != nil {
|
||||
|
||||
@@ -87,6 +87,15 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
|
||||
}
|
||||
|
||||
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
|
||||
}
|
||||
if !canManage {
|
||||
errMsg := "Stack migration is disabled for non-admin users"
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
|
||||
|
||||
@@ -64,6 +64,15 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
|
||||
}
|
||||
|
||||
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
|
||||
}
|
||||
if !canManage {
|
||||
errMsg := "Stack management is disabled for non-admin users"
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
|
||||
}
|
||||
|
||||
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, stack.Name, stack.ID, stack.SwarmID != "")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
|
||||
|
||||
@@ -75,6 +75,15 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
|
||||
}
|
||||
if !canManage {
|
||||
errMsg := "Stack management is disabled for non-admin users"
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
|
||||
}
|
||||
|
||||
if stack.Status == portainer.StackStatusInactive {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Stack is already inactive", errors.New("Stack is already inactive")}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,15 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
|
||||
}
|
||||
}
|
||||
|
||||
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
|
||||
}
|
||||
if !canManage {
|
||||
errMsg := "Stack editing is disabled for non-admin users"
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
|
||||
}
|
||||
|
||||
updateError := handler.updateAndDeployStack(r, stack, endpoint)
|
||||
if updateError != nil {
|
||||
return updateError
|
||||
|
||||
@@ -120,6 +120,15 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
}
|
||||
|
||||
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
|
||||
}
|
||||
if !canManage {
|
||||
errMsg := "Stack editing is disabled for non-admin users"
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
|
||||
}
|
||||
|
||||
//stop the autoupdate job if there is any
|
||||
if stack.AutoUpdate != nil {
|
||||
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
|
||||
|
||||
@@ -111,6 +111,15 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
|
||||
}
|
||||
if !canManage {
|
||||
errMsg := "Stack management is disabled for non-admin users"
|
||||
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
|
||||
}
|
||||
|
||||
var payload stackGitRedployPayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
|
||||
@@ -27,7 +27,7 @@ func NewHandler(bouncer *security.RequestBouncer, status *portainer.Status, demo
|
||||
h.Handle("/status",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.statusInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/status/version",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.statusInspectVersion))).Methods(http.MethodGet)
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.version))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
type inspectVersionResponse struct {
|
||||
// Whether portainer has an update available
|
||||
UpdateAvailable bool `json:"UpdateAvailable" example:"false"`
|
||||
// The latest version available
|
||||
LatestVersion string `json:"LatestVersion" example:"2.0.0"`
|
||||
}
|
||||
|
||||
type githubData struct {
|
||||
TagName string `json:"tag_name"`
|
||||
}
|
||||
|
||||
// @id StatusInspectVersion
|
||||
// @summary Check for portainer updates
|
||||
// @description Check if portainer has an update available
|
||||
// @description **Access policy**: authenticated
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @tags status
|
||||
// @produce json
|
||||
// @success 200 {object} inspectVersionResponse "Success"
|
||||
// @router /status/version [get]
|
||||
func (handler *Handler) statusInspectVersion(w http.ResponseWriter, r *http.Request) {
|
||||
motd, err := client.Get(portainer.VersionCheckURL, 5)
|
||||
if err != nil {
|
||||
response.JSON(w, &inspectVersionResponse{UpdateAvailable: false})
|
||||
return
|
||||
}
|
||||
|
||||
var data githubData
|
||||
err = json.Unmarshal(motd, &data)
|
||||
if err != nil {
|
||||
response.JSON(w, &inspectVersionResponse{UpdateAvailable: false})
|
||||
return
|
||||
}
|
||||
|
||||
resp := inspectVersionResponse{
|
||||
UpdateAvailable: false,
|
||||
}
|
||||
|
||||
currentVersion := semver.New(portainer.APIVersion)
|
||||
latestVersion := semver.New(data.TagName)
|
||||
if currentVersion.LessThan(*latestVersion) {
|
||||
resp.UpdateAvailable = true
|
||||
resp.LatestVersion = data.TagName
|
||||
}
|
||||
|
||||
response.JSON(w, &resp)
|
||||
}
|
||||
105
api/http/handler/status/version.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/build"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
|
||||
"github.com/portainer/libhttp/response"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type versionResponse struct {
|
||||
// Whether portainer has an update available
|
||||
UpdateAvailable bool `json:"UpdateAvailable" example:"false"`
|
||||
// The latest version available
|
||||
LatestVersion string `json:"LatestVersion" example:"2.0.0"`
|
||||
|
||||
ServerVersion string
|
||||
DatabaseVersion string
|
||||
Build BuildInfo
|
||||
}
|
||||
|
||||
type BuildInfo struct {
|
||||
BuildNumber string
|
||||
ImageTag string
|
||||
NodejsVersion string
|
||||
YarnVersion string
|
||||
WebpackVersion string
|
||||
GoVersion string
|
||||
}
|
||||
|
||||
// @id Version
|
||||
// @summary Check for portainer updates
|
||||
// @description Check if portainer has an update available
|
||||
// @description **Access policy**: authenticated
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @tags status
|
||||
// @produce json
|
||||
// @success 200 {object} versionResponse "Success"
|
||||
// @router /status/version [get]
|
||||
func (handler *Handler) version(w http.ResponseWriter, r *http.Request) {
|
||||
result := &versionResponse{
|
||||
ServerVersion: portainer.APIVersion,
|
||||
DatabaseVersion: strconv.Itoa(portainer.DBVersion),
|
||||
Build: BuildInfo{
|
||||
BuildNumber: build.BuildNumber,
|
||||
ImageTag: build.ImageTag,
|
||||
NodejsVersion: build.NodejsVersion,
|
||||
YarnVersion: build.YarnVersion,
|
||||
WebpackVersion: build.WebpackVersion,
|
||||
GoVersion: build.GoVersion,
|
||||
},
|
||||
}
|
||||
|
||||
latestVersion := getLatestVersion()
|
||||
if hasNewerVersion(portainer.APIVersion, latestVersion) {
|
||||
result.UpdateAvailable = true
|
||||
result.LatestVersion = latestVersion
|
||||
}
|
||||
|
||||
response.JSON(w, &result)
|
||||
}
|
||||
|
||||
func getLatestVersion() string {
|
||||
motd, err := client.Get(portainer.VersionCheckURL, 5)
|
||||
if err != nil {
|
||||
log.WithError(err).Debug("couldn't fetch latest Portainer release version")
|
||||
return ""
|
||||
}
|
||||
|
||||
var data struct {
|
||||
TagName string `json:"tag_name"`
|
||||
}
|
||||
|
||||
err = json.Unmarshal(motd, &data)
|
||||
if err != nil {
|
||||
log.WithError(err).Debug("couldn't parse latest Portainer version")
|
||||
return ""
|
||||
}
|
||||
|
||||
return data.TagName
|
||||
}
|
||||
|
||||
func hasNewerVersion(currentVersion, latestVersion string) bool {
|
||||
currentVersionSemver, err := semver.NewVersion(currentVersion)
|
||||
if err != nil {
|
||||
log.WithField("version", currentVersion).Debug("current Portainer version isn't a semver")
|
||||
return false
|
||||
}
|
||||
|
||||
latestVersionSemver, err := semver.NewVersion(latestVersion)
|
||||
if err != nil {
|
||||
log.WithField("version", latestVersion).Debug("latest Portainer version isn't a semver")
|
||||
return false
|
||||
}
|
||||
|
||||
return currentVersionSemver.LessThan(*latestVersionSemver)
|
||||
}
|
||||
@@ -81,11 +81,11 @@ func FilterRegistries(registries []portainer.Registry, user *portainer.User, tea
|
||||
}
|
||||
|
||||
// FilterEndpoints filters environments(endpoints) based on user role and team memberships.
|
||||
// Non administrator and non-team-leader only have access to authorized environments(endpoints) (can be inherited via endpoint groups).
|
||||
// Non administrator only have access to authorized environments(endpoints) (can be inherited via endpoint groups).
|
||||
func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.Endpoint {
|
||||
filteredEndpoints := endpoints
|
||||
|
||||
if !context.IsAdmin && !context.IsTeamLeader {
|
||||
if !context.IsAdmin {
|
||||
filteredEndpoints = make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
@@ -101,11 +101,11 @@ func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.Endpoint
|
||||
}
|
||||
|
||||
// FilterEndpointGroups filters environment(endpoint) groups based on user role and team memberships.
|
||||
// Non administrator users and Non-team-leaders only have access to authorized environment(endpoint) groups.
|
||||
// Non administrator users only have access to authorized environment(endpoint) groups.
|
||||
func FilterEndpointGroups(endpointGroups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.EndpointGroup {
|
||||
filteredEndpointGroups := endpointGroups
|
||||
|
||||
if !context.IsAdmin && !context.IsTeamLeader {
|
||||
if !context.IsAdmin {
|
||||
filteredEndpointGroups = make([]portainer.EndpointGroup, 0)
|
||||
|
||||
for _, group := range endpointGroups {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// KubeClusterAccessService represents a service that is responsible for centralizing kube cluster access data
|
||||
@@ -94,11 +95,20 @@ func (service *kubeClusterAccessService) IsSecure() bool {
|
||||
// - pass down params to binaries
|
||||
func (service *kubeClusterAccessService) GetData(hostURL string, endpointID portainer.EndpointID) kubernetesClusterAccessData {
|
||||
baseURL := service.baseURL
|
||||
|
||||
// When the api call is internal, the baseURL should not be used.
|
||||
if hostURL == "localhost" {
|
||||
hostURL = hostURL + service.httpsBindAddr
|
||||
baseURL = "/"
|
||||
}
|
||||
|
||||
if baseURL != "/" {
|
||||
baseURL = fmt.Sprintf("/%s/", strings.Trim(baseURL, "/"))
|
||||
}
|
||||
|
||||
clusterURL := hostURL + service.httpsBindAddr + baseURL
|
||||
logrus.Infof("[kubeconfig] [hostURL: %s, httpsBindAddr: %s, baseURL: %s]", hostURL, service.httpsBindAddr, baseURL)
|
||||
|
||||
clusterURL := hostURL + baseURL
|
||||
|
||||
clusterServerURL := fmt.Sprintf("https://%sapi/endpoints/%d/kubernetes", clusterURL, endpointID)
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ func TestKubeClusterAccessService_GetKubeConfigInternal(t *testing.T) {
|
||||
clusterAccessDetails := kcs.GetData("mysite.com", 1)
|
||||
|
||||
wantClusterAccessDetails := kubernetesClusterAccessData{
|
||||
ClusterServerURL: "https://mysite.com:9443/api/endpoints/1/kubernetes",
|
||||
ClusterServerURL: "https://mysite.com/api/endpoints/1/kubernetes",
|
||||
CertificateAuthorityFile: "",
|
||||
CertificateAuthorityData: "",
|
||||
}
|
||||
|
||||
@@ -3,16 +3,18 @@ package oauth
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Service represents a service used to authenticate users against an authorization server
|
||||
@@ -29,17 +31,39 @@ func NewService() *Service {
|
||||
func (*Service) Authenticate(code string, configuration *portainer.OAuthSettings) (string, error) {
|
||||
token, err := getOAuthToken(code, configuration)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] - Failed retrieving access token: %v", err)
|
||||
log.Debugf("[internal,oauth] [message: failed retrieving oauth token: %v]", err)
|
||||
return "", err
|
||||
}
|
||||
username, err := getUsername(token.AccessToken, configuration)
|
||||
|
||||
idToken, err := getIdToken(token)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] - Failed retrieving oauth user name: %v", err)
|
||||
log.Debugf("[internal,oauth] [message: failed parsing id_token: %v]", err)
|
||||
}
|
||||
|
||||
resource, err := getResource(token.AccessToken, configuration)
|
||||
if err != nil {
|
||||
log.Debugf("[internal,oauth] [message: failed retrieving resource: %v]", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
resource = mergeSecondIntoFirst(idToken, resource)
|
||||
|
||||
username, err := getUsername(resource, configuration)
|
||||
if err != nil {
|
||||
log.Debugf("[internal,oauth] [message: failed retrieving username: %v]", err)
|
||||
return "", err
|
||||
}
|
||||
return username, nil
|
||||
}
|
||||
|
||||
// mergeSecondIntoFirst merges the overlap map into the base overwriting any existing values.
|
||||
func mergeSecondIntoFirst(base map[string]interface{}, overlap map[string]interface{}) map[string]interface{} {
|
||||
for k, v := range overlap {
|
||||
base[k] = v
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func getOAuthToken(code string, configuration *portainer.OAuthSettings) (*oauth2.Token, error) {
|
||||
unescapedCode, err := url.QueryUnescape(code)
|
||||
if err != nil {
|
||||
@@ -55,27 +79,55 @@ func getOAuthToken(code string, configuration *portainer.OAuthSettings) (*oauth2
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func getUsername(token string, configuration *portainer.OAuthSettings) (string, error) {
|
||||
// getIdToken retrieves parsed id_token from the OAuth token response.
|
||||
// This is necessary for OAuth providers like Azure
|
||||
// that do not provide information about user groups on the user resource endpoint.
|
||||
func getIdToken(token *oauth2.Token) (map[string]interface{}, error) {
|
||||
tokenData := make(map[string]interface{})
|
||||
|
||||
idToken := token.Extra("id_token")
|
||||
if idToken == nil {
|
||||
return tokenData, nil
|
||||
}
|
||||
|
||||
jwtParser := jwt.Parser{
|
||||
SkipClaimsValidation: true,
|
||||
}
|
||||
|
||||
t, _, err := jwtParser.ParseUnverified(idToken.(string), jwt.MapClaims{})
|
||||
if err != nil {
|
||||
return tokenData, errors.Wrap(err, "failed to parse id_token")
|
||||
}
|
||||
|
||||
if claims, ok := t.Claims.(jwt.MapClaims); ok {
|
||||
for k, v := range claims {
|
||||
tokenData[k] = v
|
||||
}
|
||||
}
|
||||
return tokenData, nil
|
||||
}
|
||||
|
||||
func getResource(token string, configuration *portainer.OAuthSettings) (map[string]interface{}, error) {
|
||||
req, err := http.NewRequest("GET", configuration.ResourceURI, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", &oauth2.RetrieveError{
|
||||
return nil, &oauth2.RetrieveError{
|
||||
Response: resp,
|
||||
Body: body,
|
||||
}
|
||||
@@ -83,47 +135,32 @@ func getUsername(token string, configuration *portainer.OAuthSettings) (string,
|
||||
|
||||
content, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if content == "application/x-www-form-urlencoded" || content == "text/plain" {
|
||||
values, err := url.ParseQuery(string(body))
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
username := values.Get(configuration.UserIdentifier)
|
||||
if username == "" {
|
||||
return username, &oauth2.RetrieveError{
|
||||
Response: resp,
|
||||
Body: body,
|
||||
datamap := make(map[string]interface{})
|
||||
for k, v := range values {
|
||||
if len(v) == 0 {
|
||||
datamap[k] = ""
|
||||
} else {
|
||||
datamap[k] = v[0]
|
||||
}
|
||||
}
|
||||
|
||||
return username, nil
|
||||
return datamap, nil
|
||||
}
|
||||
|
||||
var datamap map[string]interface{}
|
||||
if err = json.Unmarshal(body, &datamap); err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
username, ok := datamap[configuration.UserIdentifier].(string)
|
||||
if ok && username != "" {
|
||||
return username, nil
|
||||
}
|
||||
|
||||
if !ok {
|
||||
username, ok := datamap[configuration.UserIdentifier].(float64)
|
||||
if ok && username != 0 {
|
||||
return fmt.Sprint(int(username)), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", &oauth2.RetrieveError{
|
||||
Response: resp,
|
||||
Body: body,
|
||||
}
|
||||
return datamap, nil
|
||||
}
|
||||
|
||||
func buildConfig(configuration *portainer.OAuthSettings) *oauth2.Config {
|
||||
@@ -137,6 +174,6 @@ func buildConfig(configuration *portainer.OAuthSettings) *oauth2.Config {
|
||||
ClientSecret: configuration.ClientSecret,
|
||||
Endpoint: endpoint,
|
||||
RedirectURL: configuration.RedirectURI,
|
||||
Scopes: []string{configuration.Scopes},
|
||||
Scopes: strings.Split(configuration.Scopes, ","),
|
||||
}
|
||||
}
|
||||
|
||||
24
api/oauth/oauth_resource.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func getUsername(datamap map[string]interface{}, configuration *portainer.OAuthSettings) (string, error) {
|
||||
username, ok := datamap[configuration.UserIdentifier].(string)
|
||||
if ok && username != "" {
|
||||
return username, nil
|
||||
}
|
||||
|
||||
if !ok {
|
||||
username, ok := datamap[configuration.UserIdentifier].(float64)
|
||||
if ok && username != 0 {
|
||||
return fmt.Sprint(int(username)), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("failed to extract username from oauth resource")
|
||||
}
|
||||
80
api/oauth/oauth_resource_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portaineree "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func Test_getUsername(t *testing.T) {
|
||||
t.Run("fails for non-matching user identifier", func(t *testing.T) {
|
||||
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
|
||||
datamap := map[string]interface{}{"name": "john"}
|
||||
|
||||
_, err := getUsername(datamap, oauthSettings)
|
||||
if err == nil {
|
||||
t.Errorf("getUsername should fail if user identifier doesn't exist as key in oauth userinfo object")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fails if username is empty string", func(t *testing.T) {
|
||||
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
|
||||
datamap := map[string]interface{}{"username": ""}
|
||||
|
||||
_, err := getUsername(datamap, oauthSettings)
|
||||
if err == nil {
|
||||
t.Errorf("getUsername should fail if username from oauth userinfo object is empty string")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fails if username is 0 int", func(t *testing.T) {
|
||||
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
|
||||
datamap := map[string]interface{}{"username": 0}
|
||||
|
||||
_, err := getUsername(datamap, oauthSettings)
|
||||
if err == nil {
|
||||
t.Errorf("getUsername should fail if username from oauth userinfo object is 0 val int")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fails if username is negative int", func(t *testing.T) {
|
||||
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
|
||||
datamap := map[string]interface{}{"username": -1}
|
||||
|
||||
_, err := getUsername(datamap, oauthSettings)
|
||||
if err == nil {
|
||||
t.Errorf("getUsername should fail if username from oauth userinfo object is -1 (negative) int")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("succeeds if username is matched and is not empty", func(t *testing.T) {
|
||||
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
|
||||
datamap := map[string]interface{}{"username": "john"}
|
||||
|
||||
_, err := getUsername(datamap, oauthSettings)
|
||||
if err != nil {
|
||||
t.Errorf("getUsername should succeed if username from oauth userinfo object matched and non-empty")
|
||||
}
|
||||
})
|
||||
|
||||
// looks like a bug!?
|
||||
t.Run("fails if username is matched and is positive int", func(t *testing.T) {
|
||||
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
|
||||
datamap := map[string]interface{}{"username": 1}
|
||||
|
||||
_, err := getUsername(datamap, oauthSettings)
|
||||
if err == nil {
|
||||
t.Errorf("getUsername should fail if username from oauth userinfo object matched is positive int")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("succeeds if username is matched and is non-zero (or negative) float", func(t *testing.T) {
|
||||
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
|
||||
datamap := map[string]interface{}{"username": 1.1}
|
||||
|
||||
_, err := getUsername(datamap, oauthSettings)
|
||||
if err != nil {
|
||||
t.Errorf("getUsername should succeed if username from oauth userinfo object matched and non-zero (or negative)")
|
||||
}
|
||||
})
|
||||
}
|
||||
145
api/oauth/oauth_test.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/oauth/oauthtest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func Test_getOAuthToken(t *testing.T) {
|
||||
validCode := "valid-code"
|
||||
srv, config := oauthtest.RunOAuthServer(validCode, &portainer.OAuthSettings{})
|
||||
defer srv.Close()
|
||||
|
||||
t.Run("getOAuthToken fails upon invalid code", func(t *testing.T) {
|
||||
code := ""
|
||||
_, err := getOAuthToken(code, config)
|
||||
if err == nil {
|
||||
t.Errorf("getOAuthToken should fail upon providing invalid code; code=%v", code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("getOAuthToken succeeds upon providing valid code", func(t *testing.T) {
|
||||
code := validCode
|
||||
token, err := getOAuthToken(code, config)
|
||||
|
||||
if token == nil || err != nil {
|
||||
t.Errorf("getOAuthToken should successfully return access token upon providing valid code")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_getIdToken(t *testing.T) {
|
||||
verifiedToken := `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2NTM1NDA3MjksImV4cCI6MTY4NTA3NjcyOSwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2huIiwiU3VybmFtZSI6IkRvZSIsIkdyb3VwcyI6WyJGaXJzdCIsIlNlY29uZCJdfQ.GeU8XCV4Y4p5Vm-i63Aj7UP5zpb_0Zxb7-DjM2_z-s8`
|
||||
nonVerifiedToken := `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2NTM1NDA3MjksImV4cCI6MTY4NTA3NjcyOSwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2huIiwiU3VybmFtZSI6IkRvZSIsIkdyb3VwcyI6WyJGaXJzdCIsIlNlY29uZCJdfQ.`
|
||||
claims := map[string]interface{}{
|
||||
"iss": "Online JWT Builder",
|
||||
"iat": float64(1653540729),
|
||||
"exp": float64(1685076729),
|
||||
"aud": "www.example.com",
|
||||
"sub": "john.doe@example.com",
|
||||
"GivenName": "John",
|
||||
"Surname": "Doe",
|
||||
"Groups": []interface{}{"First", "Second"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
testName string
|
||||
idToken string
|
||||
expectedResult map[string]interface{}
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
testName: "should return claims if token exists and is verified",
|
||||
idToken: verifiedToken,
|
||||
expectedResult: claims,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should return claims if token exists but is not verified",
|
||||
idToken: nonVerifiedToken,
|
||||
expectedResult: claims,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
testName: "should return empty map if token does not exist",
|
||||
idToken: "",
|
||||
expectedResult: make(map[string]interface{}),
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
token := &oauth2.Token{}
|
||||
if tc.idToken != "" {
|
||||
token = token.WithExtra(map[string]interface{}{"id_token": tc.idToken})
|
||||
}
|
||||
|
||||
result, err := getIdToken(token)
|
||||
assert.Equal(t, err, tc.expectedError)
|
||||
assert.Equal(t, result, tc.expectedResult)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getResource(t *testing.T) {
|
||||
srv, config := oauthtest.RunOAuthServer("", &portainer.OAuthSettings{})
|
||||
defer srv.Close()
|
||||
|
||||
t.Run("should fail upon missing Authorization Bearer header", func(t *testing.T) {
|
||||
_, err := getResource("", config)
|
||||
if err == nil {
|
||||
t.Errorf("getResource should fail if access token is not provided in auth bearer header")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should fail upon providing incorrect Authorization Bearer header", func(t *testing.T) {
|
||||
_, err := getResource("incorrect-token", config)
|
||||
if err == nil {
|
||||
t.Errorf("getResource should fail if incorrect access token provided in auth bearer header")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should succeed upon providing correct Authorization Bearer header", func(t *testing.T) {
|
||||
_, err := getResource(oauthtest.AccessToken, config)
|
||||
if err != nil {
|
||||
t.Errorf("getResource should succeed if correct access token provided in auth bearer header")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_Authenticate(t *testing.T) {
|
||||
code := "valid-code"
|
||||
authService := NewService()
|
||||
|
||||
t.Run("should fail if user identifier does not get matched in resource", func(t *testing.T) {
|
||||
srv, config := oauthtest.RunOAuthServer(code, &portainer.OAuthSettings{})
|
||||
defer srv.Close()
|
||||
|
||||
_, err := authService.Authenticate(code, config)
|
||||
if err == nil {
|
||||
t.Error("Authenticate should fail to extract username from resource if incorrect UserIdentifier provided")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should succeed if user identifier does get matched in resource", func(t *testing.T) {
|
||||
config := &portainer.OAuthSettings{UserIdentifier: "username"}
|
||||
srv, config := oauthtest.RunOAuthServer(code, config)
|
||||
defer srv.Close()
|
||||
|
||||
username, err := authService.Authenticate(code, config)
|
||||
if err != nil {
|
||||
t.Errorf("Authenticate should succeed to extract username from resource if correct UserIdentifier provided; UserIdentifier=%s", config.UserIdentifier)
|
||||
}
|
||||
|
||||
want := "test-oauth-user"
|
||||
if username != want {
|
||||
t.Errorf("Authenticate should return correct username; got=%s, want=%s", username, want)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
96
api/oauth/oauthtest/oauth_server.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package oauthtest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
const (
|
||||
AccessToken = "test-token"
|
||||
)
|
||||
|
||||
// OAuthRoutes is an OAuth 2.0 compliant handler
|
||||
func OAuthRoutes(code string, config *portainer.OAuthSettings) http.Handler {
|
||||
router := mux.NewRouter()
|
||||
|
||||
router.HandleFunc(
|
||||
"/authorize",
|
||||
func(w http.ResponseWriter, req *http.Request) {
|
||||
location := fmt.Sprintf("%s?code=%s&state=%s", config.RedirectURI, code, "anything")
|
||||
// w.Header().Set("Location", location)
|
||||
// w.WriteHeader(http.StatusFound)
|
||||
http.Redirect(w, req, location, http.StatusFound)
|
||||
},
|
||||
).Methods(http.MethodGet)
|
||||
|
||||
router.HandleFunc(
|
||||
"/access_token",
|
||||
func(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := req.ParseForm(); err != nil {
|
||||
fmt.Fprintf(w, "ParseForm() err: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
reqCode := req.FormValue("code")
|
||||
if reqCode != code {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 86400,
|
||||
"access_token": AccessToken,
|
||||
"scope": "groups",
|
||||
})
|
||||
},
|
||||
).Methods(http.MethodPost)
|
||||
|
||||
router.HandleFunc(
|
||||
"/user",
|
||||
func(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
authHeader := req.Header.Get("Authorization")
|
||||
splitToken := strings.Split(authHeader, "Bearer ")
|
||||
if len(splitToken) < 2 || splitToken[1] != AccessToken {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"username": "test-oauth-user",
|
||||
"groups": "testing",
|
||||
})
|
||||
},
|
||||
).Methods(http.MethodGet)
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
// RunOAuthServer is a barebones OAuth 2.0 compliant test server which can be used to test OAuth 2 functionality
|
||||
func RunOAuthServer(code string, config *portainer.OAuthSettings) (*httptest.Server, *portainer.OAuthSettings) {
|
||||
srv := httptest.NewUnstartedServer(http.DefaultServeMux)
|
||||
|
||||
addr := srv.Listener.Addr()
|
||||
|
||||
config.AuthorizationURI = fmt.Sprintf("http://%s/authorize", addr)
|
||||
config.AccessTokenURI = fmt.Sprintf("http://%s/access_token", addr)
|
||||
config.ResourceURI = fmt.Sprintf("http://%s/user", addr)
|
||||
config.RedirectURI = fmt.Sprintf("http://%s/", addr)
|
||||
|
||||
srv.Config.Handler = OAuthRoutes(code, config)
|
||||
srv.Start()
|
||||
|
||||
return srv, config
|
||||
}
|
||||
@@ -199,6 +199,8 @@ type (
|
||||
StackCount int `json:"StackCount"`
|
||||
SnapshotRaw DockerSnapshotRaw `json:"DockerSnapshotRaw"`
|
||||
NodeCount int `json:"NodeCount"`
|
||||
GpuUseAll bool `json:"GpuUseAll"`
|
||||
GpuUseList []string `json:"GpuUseList"`
|
||||
}
|
||||
|
||||
// DockerSnapshotRaw represents all the information related to a snapshot as returned by the Docker API
|
||||
@@ -310,6 +312,7 @@ type (
|
||||
GroupID EndpointGroupID `json:"GroupId" example:"1"`
|
||||
// URL or IP address where exposed containers will be reachable
|
||||
PublicURL string `json:"PublicURL" example:"docker.mydomain.tld:2375"`
|
||||
Gpus []Pair `json:"Gpus"`
|
||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty" example:""`
|
||||
// List of tag identifiers to which this environment(endpoint) is associated
|
||||
|
||||
@@ -693,6 +693,12 @@ definitions:
|
||||
$ref: '#/definitions/portainer.DockerSnapshotRaw'
|
||||
DockerVersion:
|
||||
type: string
|
||||
GpuUseAll:
|
||||
type: boolean
|
||||
GpuUseList:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
HealthyContainerCount:
|
||||
type: integer
|
||||
ImageCount:
|
||||
@@ -849,6 +855,11 @@ definitions:
|
||||
EdgeKey:
|
||||
description: The key which is used to map the agent to Portainer
|
||||
type: string
|
||||
Gpus:
|
||||
description: Endpoint Gpus information
|
||||
items:
|
||||
$ref: '#/definitions/portainer.Pair'
|
||||
type: array
|
||||
GroupId:
|
||||
description: Endpoint group identifier
|
||||
example: 1
|
||||
|
||||
2
app/__mocks__/svg.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export default 'SvgrURL';
|
||||
export const ReactComponent = 'div';
|
||||
@@ -1,5 +1,4 @@
|
||||
import $ from 'jquery';
|
||||
import feather from 'feather-icons';
|
||||
import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
|
||||
|
||||
/* @ngInject */
|
||||
@@ -29,10 +28,6 @@ export function onStartupAngular($rootScope, $state, $interval, LocalStorage, En
|
||||
HttpRequestHelper.resetAgentHeaders();
|
||||
});
|
||||
|
||||
$transitions.onSuccess({}, () => {
|
||||
feather.replace();
|
||||
});
|
||||
|
||||
// Keep-alive Edge endpoints by sending a ping request every minute
|
||||
$interval(() => {
|
||||
ping(EndpointProvider, SystemService);
|
||||
|
||||
@@ -87,7 +87,7 @@ body,
|
||||
|
||||
.form-section-title {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 10px;
|
||||
color: var(--text-form-section-title-color);
|
||||
padding-left: 0;
|
||||
font-weight: 500;
|
||||
@@ -245,7 +245,6 @@ a[ng-click] {
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border-blocklist);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-box-color);
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@@ -283,7 +282,6 @@ a[ng-click] {
|
||||
.blocklist-item-logo {
|
||||
width: 100%;
|
||||
max-width: 60px;
|
||||
height: 100%;
|
||||
max-height: 60px;
|
||||
}
|
||||
|
||||
@@ -401,7 +399,7 @@ a[ng-click] {
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 30px 25px;
|
||||
padding: 20px 25px;
|
||||
background-color: var(--white-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
@@ -868,3 +866,18 @@ json-tree .branch-preview {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.web-editor {
|
||||
background-color: var(--bg-webeditor-color);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.web-editor a {
|
||||
color: var(--text-link-color);
|
||||
}
|
||||
|
||||
.web-editor a:hover {
|
||||
color: var(--text-link-hover-color);
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
141
app/assets/css/bootstrap-override.css
vendored
@@ -1,4 +1,15 @@
|
||||
/* Label, Section Title */
|
||||
.label {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.label-success {
|
||||
background-color: var(--ui-success-7);
|
||||
}
|
||||
|
||||
.label-danger {
|
||||
background-color: var(--ui-error-6);
|
||||
}
|
||||
|
||||
.control-label {
|
||||
color: var(--ui-gray-7);
|
||||
@@ -16,6 +27,16 @@
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-left {
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.blue {
|
||||
background: var(--bg-dashboard-item) !important;
|
||||
}
|
||||
@@ -58,6 +79,12 @@
|
||||
background-color: var(--ui-gray-3);
|
||||
}
|
||||
|
||||
.switch-values {
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
/* Toggle */
|
||||
|
||||
.slider {
|
||||
@@ -153,78 +180,6 @@ input:checked + .slider:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Datatable */
|
||||
|
||||
.datatable .searchBar {
|
||||
border: 1px solid var(--border-searchbar);
|
||||
padding: 5px;
|
||||
background: var(--bg-searchbar) !important;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.datatable .searchBar input[type='text'] {
|
||||
border: 0px !important;
|
||||
}
|
||||
|
||||
.datatable .toolBar {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.datatable .footer {
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.datatable .toolBar {
|
||||
padding-top: 20px !important;
|
||||
padding-bottom: 20px !important;
|
||||
}
|
||||
|
||||
.toolBar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.toolBar > .toolBarTitle {
|
||||
flex: auto;
|
||||
display: inline-flex;
|
||||
flex-wrap: nowarp;
|
||||
}
|
||||
|
||||
.toolBar > .searchBar {
|
||||
flex: right;
|
||||
margin-right: 10px;
|
||||
width: 500px;
|
||||
height: 30px;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.datatable .searchBar {
|
||||
padding: 4px 10px !important;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.toolBar > .actionBar {
|
||||
margin-right: 10px;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.datatable .actionBar {
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
.toolBar > .settings {
|
||||
width: 60px;
|
||||
text-align: right;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.datatable .toolBar .settings {
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
|
||||
/* Widget */
|
||||
|
||||
.widget .widget-icon i {
|
||||
@@ -309,19 +264,31 @@ input:checked + .slider:before {
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 55px 20px 20px 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.background-error {
|
||||
padding-top: 55px;
|
||||
background-image: url(../images/icon-error.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: top left;
|
||||
}
|
||||
|
||||
.background-warning {
|
||||
padding-top: 55px;
|
||||
background-image: url(../images/icon-warning.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: top 10px left 10px;
|
||||
background-position: top left;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 10px 0px 10px 0px;
|
||||
margin-bottom: 10px;
|
||||
padding: 0px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.modal-header .close {
|
||||
margin-top: -40px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.modal-header .modal-title {
|
||||
@@ -384,6 +351,10 @@ input:checked + .slider:before {
|
||||
background-color: var(--ui-error-8);
|
||||
}
|
||||
|
||||
.table .label .label-warn {
|
||||
background-color: var(--ui-warning-8);
|
||||
}
|
||||
|
||||
.table .label .label-success {
|
||||
background-color: var(--ui-success-7);
|
||||
}
|
||||
@@ -398,3 +369,21 @@ input:checked + .slider:before {
|
||||
.control-label {
|
||||
@apply inline-flex items-center;
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
width: 50%;
|
||||
display: inline-block;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.progress .progress-bar {
|
||||
background-color: var(--ui-blue-8);
|
||||
}
|
||||
|
||||
.progress + span {
|
||||
display: inline-block;
|
||||
font-size: 85%;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
border-radius: 5px;
|
||||
display: inline-flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
"3": "#f5f5f4",
|
||||
"4": "#e7e5e4",
|
||||
"5": "#d7d3d0",
|
||||
"6": "#d7d3d0",
|
||||
"6": "#a9a29d",
|
||||
"7": "#79716b",
|
||||
"8": "#57534e",
|
||||
"9": "#44403c",
|
||||
|
||||
@@ -6,37 +6,38 @@
|
||||
}
|
||||
|
||||
pr-icon {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: currentColor;
|
||||
margin: 0;
|
||||
|
||||
font-size: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
width: var(--icon-size);
|
||||
|
||||
--icon-size: 1em;
|
||||
}
|
||||
|
||||
.icon-xs {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
--icon-size: 10px;
|
||||
}
|
||||
|
||||
.icon-sm {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
--icon-size: 14px;
|
||||
}
|
||||
|
||||
.icon-md {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
--icon-size: 16px;
|
||||
}
|
||||
|
||||
.icon-lg {
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
--icon-size: 22px;
|
||||
}
|
||||
|
||||
.icon-xl {
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
--icon-size: 26px;
|
||||
}
|
||||
|
||||
.icon.icon-alt {
|
||||
@@ -89,6 +90,14 @@ pr-icon {
|
||||
stroke: var(--white-color);
|
||||
}
|
||||
|
||||
.icon-badge {
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5%;
|
||||
}
|
||||
|
||||
.icon-nested-gray {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
@@ -113,3 +122,11 @@ pr-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-only-icon {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.btn-only-icon pr-icon {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -111,10 +111,10 @@ div.input-mask {
|
||||
background: var(--bg-widget-table-color);
|
||||
}
|
||||
.widget .widget-body table thead * {
|
||||
font-size: 14px !important;
|
||||
font-size: 14px;
|
||||
}
|
||||
.widget .widget-body table tbody * {
|
||||
font-size: 13px !important;
|
||||
font-size: 13px;
|
||||
}
|
||||
.widget .widget-body .error {
|
||||
color: #ff0000;
|
||||
|
||||
@@ -147,7 +147,6 @@
|
||||
--bg-daterangepicker-hover: var(--grey-16);
|
||||
--bg-daterangepicker-in-range: var(--grey-58);
|
||||
--bg-daterangepicker-active: var(--blue-14);
|
||||
--bg-tooltip-color: var(--white-color);
|
||||
--bg-input-autofill-color: var(--white-color);
|
||||
--bg-btn-default-hover-color: var(--grey-59);
|
||||
--bg-btn-focus: var(--grey-59);
|
||||
@@ -162,6 +161,7 @@
|
||||
--bg-searchbar: var(--ui-gray-2);
|
||||
--bg-inputbox: var(--ui-gray-2);
|
||||
--bg-dropdown-hover: var(--ui-gray-3);
|
||||
--bg-webeditor-color: var(--ui-gray-3);
|
||||
|
||||
--text-main-color: var(--grey-7);
|
||||
--text-body-color: var(--grey-6);
|
||||
@@ -265,6 +265,9 @@
|
||||
--bg-multiselect-helpercontainer: var(--white-color);
|
||||
--text-input-textarea: var(--white-color);
|
||||
|
||||
--sort-icon-muted: var(--ui-gray-5);
|
||||
--sort-icon-hover: var(--ui-gray-6);
|
||||
--sort-icon: var(--ui-gray-9);
|
||||
--border-checkbox: var(--ui-gray-5);
|
||||
--bg-checkbox: var(--white-color);
|
||||
--border-searchbar: var(--ui-gray-5);
|
||||
@@ -296,8 +299,9 @@
|
||||
--bg-navtabs-color: var(--grey-3);
|
||||
--bg-navtabs-hover-color: var(--grey-3);
|
||||
--bg-table-selected-color: var(--grey-3);
|
||||
--bg-codemirror-color: var(--grey-2);
|
||||
--bg-codemirror-gutters-color: var(--grey-2);
|
||||
--bg-codemirror-color: var(--ui-gray-warm-11);
|
||||
--bg-codemirror-gutters-color: var(--ui-gray-warm-8);
|
||||
--bg-codemirror-selected-color: var(--ui-gray-warm-7);
|
||||
--bg-dropdown-menu-color: var(--grey-1);
|
||||
--bg-log-viewer-color: var(--grey-2);
|
||||
--bg-log-line-selected-color: var(--grey-3);
|
||||
@@ -317,7 +321,6 @@
|
||||
--bg-multiselect-checkbox-color: var(--grey-3);
|
||||
--bg-panel-body-color: var(--grey-1);
|
||||
--bg-boxselector-wrapper-disabled-color: var(--grey-39);
|
||||
--bg-codemirror-selected-color: var(--grey-3);
|
||||
--bg-sidebar-header-color: var(--grey-1);
|
||||
--bg-multiselect-color: var(--grey-1);
|
||||
--bg-daterangepicker-color: var(--grey-3);
|
||||
@@ -342,6 +345,7 @@
|
||||
--bg-searchbar: var(--grey-1);
|
||||
--bg-inputbox: var(--grey-2);
|
||||
--bg-dropdown-hover: var(--grey-3);
|
||||
--bg-webeditor-color: var(--ui-gray-warm-9);
|
||||
|
||||
--text-main-color: var(--white-color);
|
||||
--text-body-color: var(--white-color);
|
||||
@@ -444,6 +448,9 @@
|
||||
--bg-multiselect-helpercontainer: var(--grey-1);
|
||||
--text-input-textarea: var(--grey-1);
|
||||
|
||||
--sort-icon-muted: var(--ui-gray-7);
|
||||
--sort-icon-hover: var(--ui-gray-6);
|
||||
--sort-icon: var(--ui-gray-3);
|
||||
--border-checkbox: var(--ui-gray-5);
|
||||
--bg-checkbox: var(--white-color);
|
||||
--border-searchbar: var(--ui-gray-5);
|
||||
@@ -473,7 +480,7 @@
|
||||
--bg-blocklist-item-selected-color: var(--black-color);
|
||||
--bg-input-group-addon-color: var(--grey-3);
|
||||
--bg-table-color: var(--black-color);
|
||||
--bg-codemirror-gutters-color: var(--black-color);
|
||||
--bg-codemirror-gutters-color: var(--ui-gray-warm-11);
|
||||
--bg-codemirror-color: var(--black-color);
|
||||
--bg-codemirror-selected-color: var(--grey-3);
|
||||
--bg-log-viewer-color: var(--black-color);
|
||||
@@ -521,6 +528,7 @@
|
||||
--bg-inputbox: var(--black-color);
|
||||
--bg-searchbar: var(--black-color);
|
||||
--bg-dropdown-hover: var(--black-color);
|
||||
--bg-webeditor-color: var(--ui-gray-warm-9);
|
||||
|
||||
--text-main-color: var(--white-color);
|
||||
--text-body-color: var(--white-color);
|
||||
@@ -615,6 +623,9 @@
|
||||
--text-cm-string-color: var(--red-7);
|
||||
--text-progress-bar-color: var(--black-color);
|
||||
|
||||
--sort-icon-muted: var(--ui-gray-7);
|
||||
--sort-icon-hover: var(--ui-gray-6);
|
||||
--sort-icon: var(--ui-gray-3);
|
||||
--border-checkbox: var(--ui-gray-5);
|
||||
--bg-checkbox: var(--white-color);
|
||||
--border-searchbar: var(--ui-gray-5);
|
||||
|
||||
@@ -32,12 +32,14 @@
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text-link-color);
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:focus {
|
||||
color: var(--text-link-hover-color);
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.input-group-addon {
|
||||
@@ -161,12 +163,22 @@ code {
|
||||
|
||||
.CodeMirror-gutters {
|
||||
background: var(--bg-codemirror-gutters-color);
|
||||
border-right: 1px solid var(--border-codemirror-gutters-color);
|
||||
border-right: 0px;
|
||||
}
|
||||
|
||||
.CodeMirror-linenumber {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.CodeMirror pre.CodeMirror-line,
|
||||
.CodeMirror pre.CodeMirror-line-like {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
background: var(--bg-codemirror-color);
|
||||
color: var(--text-codemirror-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.CodeMirror-selected {
|
||||
@@ -276,6 +288,14 @@ json-tree .branch-preview {
|
||||
.rzslider .rz-bubble.rz-limit {
|
||||
color: var(--text-rzslider-limit-color);
|
||||
}
|
||||
|
||||
.rz-bubble.rz-limit.rz-ceil {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: auto !important;
|
||||
top: -26px;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
@@ -396,3 +416,7 @@ fieldset[disabled] .btn {
|
||||
border-right: 0px;
|
||||
border-bottom: 3px solid red;
|
||||
}
|
||||
|
||||
.label-default {
|
||||
line-height: 11px;
|
||||
}
|
||||
|
||||
3
app/assets/ico/arrow-right-long.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.39563 50H95.8332M95.8332 50L66.6667 20.8334M95.8332 50L66.6667 79.1667" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 274 B |
3
app/assets/ico/arrows-updown.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M49.9999 87.5001L49.9999 12.5004M49.9999 87.5001L67.6776 69.8224M49.9999 87.5001L32.3222 69.8224M49.9999 12.5004L32.3223 30.178M49.9999 12.5004L67.6776 30.1781" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 360 B |
3
app/assets/ico/asterisk.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M49.9999 8.3335V91.6668M79.4627 20.5374L20.5371 79.463M91.6666 50.0002H8.33325M79.4627 79.463L20.5371 20.5374" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 310 B |
5
app/assets/ico/bomb.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M39.5835 91.6665C56.8424 91.6665 70.8335 77.6754 70.8335 60.4165C70.8335 43.1576 56.8424 29.1665 39.5835 29.1665C22.3246 29.1665 8.3335 43.1576 8.3335 60.4165C8.3335 77.6754 22.3246 91.6665 39.5835 91.6665Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M20.8335 62.4998C20.8335 50.9939 30.1609 41.6665 41.6668 41.6665" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M71.6499 9.34864V4.1665M85.4165 14.7403L89.0808 11.076M85.2593 42.1688L88.9237 45.8332M57.8308 14.7403L54.1665 11.076M90.651 28.3498H95.8332M63.0022 36.9975L74.9998 24.9998" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 831 B |
3
app/assets/ico/circle-notch.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M37.1986 10.3367C20.4498 15.7383 8.33334 31.4541 8.33334 49.9999C8.33334 73.0118 26.9881 91.6666 50 91.6666C73.0119 91.6666 91.6667 73.0118 91.6667 49.9999C91.6667 31.4541 79.5502 15.7383 62.8014 10.3367" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 404 B |
3
app/assets/ico/clock-rewind.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M94.5831 56.25L86.2523 47.9167L77.9165 56.25M87.5 50C87.5 70.7107 70.7107 87.5 50 87.5C29.2893 87.5 12.5 70.7107 12.5 50C12.5 29.2893 29.2893 12.5 50 12.5C63.758 12.5 75.7855 19.9089 82.3104 30.9545M50 29.1667V50L62.5 58.3333" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 426 B |
3
app/assets/ico/compress.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M87.7688 12.2312L66.9354 33.0645M66.9354 33.0645H87.7688M66.9354 33.0645V12.2312M12.2964 12.2964L22.7131 22.7131L33.1298 33.1298M33.1298 33.1298V12.2964M33.1298 33.1298H12.2964M87.4042 87.4042L66.5709 66.5709M66.5709 66.5709L66.5709 87.4042M66.5709 66.5709L87.4042 66.5709M12.6353 87.3647L33.4686 66.5314M33.4686 66.5314H12.6353M33.4686 66.5314V87.3647" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 553 B |
4
app/assets/ico/custom.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3817 5.28009C22.1592 5.28009 28.4649 11.5864 28.4649 19.3646C28.4649 27.1428 22.1592 33.4491 14.3817 33.4491C6.60065 33.4526 0.294922 27.1428 0.294922 19.3646C0.294922 11.5864 6.60065 5.28009 14.3817 5.28009Z" fill="#E0F2FE"/>
|
||||
<path d="M18.059 25.043H10.414C9.9746 25.043 9.56654 24.8721 9.25613 24.5617C8.94573 24.2513 8.77832 23.8397 8.77832 23.4037V15.7615C8.77832 15.3255 8.94922 14.9139 9.25962 14.6035C9.57002 14.293 9.98157 14.1221 10.4175 14.1221H14.24C14.54 14.1221 14.7876 14.3663 14.7876 14.6697C14.7876 14.9732 14.5435 15.2174 14.24 15.2174H10.4175C10.271 15.2174 10.135 15.2732 10.0304 15.3778C9.92577 15.4824 9.86996 15.6185 9.86996 15.765V23.4072C9.86996 23.5537 9.92577 23.6897 10.0304 23.7943C10.135 23.899 10.271 23.9548 10.4175 23.9548H18.059C18.2055 23.9548 18.3415 23.899 18.4462 23.7943C18.5508 23.6897 18.6066 23.5537 18.6066 23.4072V19.5843C18.6066 19.2844 18.8507 19.0367 19.1542 19.0367C19.4576 19.0367 19.7017 19.2809 19.7017 19.5843V23.4072C19.7017 23.8432 19.5308 24.2547 19.2204 24.5652C18.91 24.8756 18.4985 25.0465 18.0625 25.0465L18.059 25.043ZM12.6008 21.7678C12.4578 21.7678 12.3183 21.712 12.2137 21.6074C12.0777 21.4713 12.0254 21.276 12.0707 21.0876L12.6148 18.9042C12.6392 18.8065 12.688 18.7193 12.7578 18.6495L17.9439 13.4629C18.5892 12.8176 19.7087 12.8176 20.3539 13.4629C20.6748 13.7838 20.8527 14.2128 20.8527 14.6663C20.8527 15.1197 20.6748 15.5487 20.3539 15.8696L15.1678 21.0563C15.098 21.126 15.0108 21.1748 14.9132 21.1993L12.7299 21.7469C12.6845 21.7573 12.6427 21.7643 12.5973 21.7643L12.6008 21.7678ZM13.6401 19.3157L13.3507 20.4703L14.5051 20.1808L19.5832 15.1023C19.6983 14.9872 19.761 14.8337 19.761 14.6697C19.761 14.5058 19.6983 14.3523 19.5832 14.2372C19.353 14.007 18.9484 14.007 18.7182 14.2372L13.6401 19.3157Z" fill="#0086C9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
3
app/assets/ico/dataflow-1.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M70.8333 83.3335H70C62.9993 83.3335 59.499 83.3335 56.8251 81.9711C54.4731 80.7727 52.5608 78.8604 51.3624 76.5084C50 73.8345 50 70.3342 50 63.3335V36.6668C50 29.6662 50 26.1658 51.3624 23.492C52.5608 21.1399 54.4731 19.2277 56.8251 18.0292C59.499 16.6668 62.9993 16.6668 70 16.6668H70.8333M70.8333 83.3335C70.8333 87.9359 74.5643 91.6668 79.1667 91.6668C83.769 91.6668 87.5 87.9359 87.5 83.3335C87.5 78.7311 83.769 75.0002 79.1667 75.0002C74.5643 75.0002 70.8333 78.7311 70.8333 83.3335ZM70.8333 16.6668C70.8333 21.2692 74.5643 25.0002 79.1667 25.0002C83.769 25.0002 87.5 21.2692 87.5 16.6668C87.5 12.0645 83.769 8.3335 79.1667 8.3335C74.5643 8.3335 70.8333 12.0645 70.8333 16.6668ZM29.1667 50.0002L70.8333 50.0002M29.1667 50.0002C29.1667 54.6025 25.4357 58.3335 20.8333 58.3335C16.231 58.3335 12.5 54.6025 12.5 50.0002C12.5 45.3978 16.231 41.6668 20.8333 41.6668C25.4357 41.6668 29.1667 45.3978 29.1667 50.0002ZM70.8333 50.0002C70.8333 54.6025 74.5643 58.3335 79.1667 58.3335C83.769 58.3335 87.5 54.6025 87.5 50.0002C87.5 45.3978 83.769 41.6668 79.1667 41.6668C74.5643 41.6668 70.8333 45.3978 70.8333 50.0002Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
3
app/assets/ico/dataflow-2.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.6667 75.0002V74.1668C16.6667 67.1662 16.6667 63.6658 18.0291 60.992C19.2275 58.6399 21.1398 56.7277 23.4918 55.5293C26.1657 54.1668 29.666 54.1668 36.6667 54.1668H63.3333C70.334 54.1668 73.8343 54.1668 76.5082 55.5293C78.8602 56.7277 80.7725 58.6399 81.9709 60.992C83.3333 63.6658 83.3333 67.1662 83.3333 74.1668V75.0002M16.6667 75.0002C12.0643 75.0002 8.33333 78.7311 8.33333 83.3335C8.33333 87.9359 12.0643 91.6668 16.6667 91.6668C21.269 91.6668 25 87.9359 25 83.3335C25 78.7311 21.269 75.0002 16.6667 75.0002ZM83.3333 75.0002C78.731 75.0002 75 78.7311 75 83.3335C75 87.9359 78.731 91.6668 83.3333 91.6668C87.9357 91.6668 91.6667 87.9359 91.6667 83.3335C91.6667 78.7311 87.9357 75.0002 83.3333 75.0002ZM50 75.0002C45.3976 75.0002 41.6667 78.7311 41.6667 83.3335C41.6667 87.9359 45.3976 91.6668 50 91.6668C54.6024 91.6668 58.3333 87.9359 58.3333 83.3335C58.3333 78.7311 54.6024 75.0002 50 75.0002ZM50 75.0002V33.3335M25 33.3335H75C78.8828 33.3335 80.8243 33.3335 82.3557 32.6992C84.3976 31.8534 86.0199 30.2311 86.8657 28.1892C87.5 26.6578 87.5 24.7163 87.5 20.8335C87.5 16.9507 87.5 15.0092 86.8657 13.4778C86.0199 11.4359 84.3976 9.81362 82.3557 8.96783C80.8243 8.3335 78.8828 8.3335 75 8.3335H25C21.1171 8.3335 19.1757 8.3335 17.6443 8.96783C15.6024 9.81362 13.9801 11.4359 13.1343 13.4778C12.5 15.0092 12.5 16.9507 12.5 20.8335C12.5 24.7163 12.5 26.6578 13.1343 28.1892C13.9801 30.2311 15.6024 31.8534 17.6443 32.6992C19.1757 33.3335 21.1172 33.3335 25 33.3335Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
3
app/assets/ico/expand.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M66.6667 33.3333L87.5 12.5M87.5 12.5H66.6667M87.5 12.5V33.3333M33.3333 33.3333L12.5 12.5M12.5 12.5L12.5 33.3333M12.5 12.5L33.3333 12.5M33.3333 66.6667L12.5 87.5M12.5 87.5H33.3333M12.5 87.5L12.5 66.6667M66.6667 66.6667L87.5 87.5M87.5 87.5V66.6667M87.5 87.5H66.6667" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 464 B |
3
app/assets/ico/file-code.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M58.3333 9.45654V26.6671C58.3333 29.0007 58.3333 30.1675 58.7875 31.0588C59.1869 31.8428 59.8244 32.4802 60.6084 32.8797C61.4997 33.3338 62.6665 33.3338 65 33.3338H82.2106M58.3333 72.9168L68.75 62.5002L58.3333 52.0835M41.6667 52.0835L31.25 62.5002L41.6667 72.9168M83.3333 41.6178V71.6668C83.3333 78.6675 83.3333 82.1678 81.9709 84.8417C80.7725 87.1937 78.8602 89.106 76.5082 90.3044C73.8343 91.6668 70.334 91.6668 63.3333 91.6668H36.6667C29.666 91.6668 26.1657 91.6668 23.4918 90.3044C21.1398 89.106 19.2275 87.1937 18.0291 84.8417C16.6667 82.1678 16.6667 78.6675 16.6667 71.6668V28.3335C16.6667 21.3328 16.6667 17.8325 18.0291 15.1586C19.2275 12.8066 21.1398 10.8943 23.4918 9.69591C26.1657 8.3335 29.666 8.3335 36.6667 8.3335H50.0491C53.1064 8.3335 54.6351 8.3335 56.0737 8.67887C57.3492 8.98508 58.5685 9.49014 59.6869 10.1755C60.9484 10.9485 62.0293 12.0295 64.1912 14.1914L77.4755 27.4756C79.6374 29.6375 80.7183 30.7185 81.4913 31.9799C82.1767 33.0983 82.6818 34.3176 82.988 35.5931C83.3333 37.0317 83.3333 38.5604 83.3333 41.6178Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
app/assets/ico/file-signature.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M58.3333 9.45654V26.6671C58.3333 29.0007 58.3333 30.1675 58.7875 31.0588C59.1869 31.8428 59.8244 32.4802 60.6084 32.8797C61.4997 33.3338 62.6664 33.3338 65 33.3338H82.2106M37.5 66.6668L45.8333 75.0002L64.5833 56.2502M58.3333 8.3335H36.6667C29.666 8.3335 26.1657 8.3335 23.4918 9.69591C21.1397 10.8943 19.2275 12.8066 18.0291 15.1586C16.6667 17.8325 16.6667 21.3328 16.6667 28.3335V71.6668C16.6667 78.6675 16.6667 82.1678 18.0291 84.8417C19.2275 87.1937 21.1397 89.106 23.4918 90.3044C26.1657 91.6668 29.666 91.6668 36.6667 91.6668H63.3333C70.334 91.6668 73.8343 91.6668 76.5082 90.3044C78.8602 89.106 80.7725 87.1937 81.9709 84.8417C83.3333 82.1678 83.3333 78.6675 83.3333 71.6668V33.3335L58.3333 8.3335Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 905 B |
3
app/assets/ico/file-upload.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M58.3333 9.45654V26.6671C58.3333 29.0007 58.3333 30.1675 58.7875 31.0588C59.1869 31.8428 59.8244 32.4802 60.6084 32.8797C61.4997 33.3338 62.6664 33.3338 65 33.3338H82.2106M62.5 62.5L50 50M50 50L37.5 62.5M50 50L50 75M58.3333 8.3335H36.6667C29.666 8.3335 26.1657 8.3335 23.4918 9.69591C21.1397 10.8943 19.2275 12.8066 18.0291 15.1586C16.6667 17.8325 16.6667 21.3328 16.6667 28.3335V71.6668C16.6667 78.6675 16.6667 82.1678 18.0291 84.8417C19.2275 87.1937 21.1397 89.106 23.4918 90.3044C26.1657 91.6668 29.666 91.6668 36.6667 91.6668H63.3333C70.334 91.6668 73.8343 91.6668 76.5082 90.3044C78.8602 89.106 80.7725 87.1937 81.9709 84.8417C83.3333 82.1678 83.3333 78.6675 83.3333 71.6668V33.3335L58.3333 8.3335Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 904 B |
3
app/assets/ico/flask.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M37.5 25.0002V43.7552C37.5 46.0507 37.5 47.1985 37.2138 48.2621C36.9603 49.2044 36.5432 50.095 35.9816 50.893C35.3477 51.7938 34.4659 52.5286 32.7025 53.9981L17.2975 66.8356C15.5341 68.3051 14.6523 69.0399 14.0184 69.9406C13.4568 70.7387 13.0397 71.6292 12.7862 72.5716C12.5 73.6352 12.5 74.783 12.5 77.0785V78.3335C12.5 83.0006 12.5 85.3342 13.4083 87.1168C14.2072 88.6848 15.4821 89.9596 17.0501 90.7586C18.8327 91.6668 21.1662 91.6668 25.8333 91.6668H74.1667C78.8338 91.6668 81.1673 91.6668 82.9499 90.7586C84.5179 89.9596 85.7928 88.6848 86.5917 87.1168C87.5 85.3342 87.5 83.0006 87.5 78.3335V77.0785C87.5 74.783 87.5 73.6352 87.2138 72.5716C86.9603 71.6292 86.5432 70.7387 85.9816 69.9406C85.3477 69.0399 84.4659 68.3051 82.7025 66.8356L67.2975 53.9981C65.5341 52.5286 64.6523 51.7938 64.0184 50.893C63.4568 50.095 63.0397 49.2044 62.7861 48.2621C62.5 47.1985 62.5 46.0507 62.5 43.7552V25.0002M34.5833 25.0002H65.4167C66.5834 25.0002 67.1668 25.0002 67.6125 24.7731C68.0045 24.5734 68.3232 24.2546 68.5229 23.8626C68.75 23.417 68.75 22.8336 68.75 21.6668V11.6668C68.75 10.5001 68.75 9.91667 68.5229 9.47102C68.3232 9.07901 68.0045 8.7603 67.6125 8.56057C67.1668 8.3335 66.5834 8.3335 65.4167 8.3335H34.5833C33.4166 8.3335 32.8332 8.3335 32.3875 8.56057C31.9955 8.7603 31.6768 9.07901 31.4771 9.47102C31.25 9.91667 31.25 10.5001 31.25 11.6668V21.6668C31.25 22.8336 31.25 23.417 31.4771 23.8626C31.6768 24.2546 31.9955 24.5734 32.3875 24.7731C32.8332 25.0002 33.4166 25.0002 34.5833 25.0002ZM22.9167 70.8335H77.0833C79.0194 70.8335 79.9874 70.8335 80.7924 70.9936C84.0982 71.6512 86.6823 74.2353 87.3399 77.5411C87.5 78.3461 87.5 79.3141 87.5 81.2502C87.5 83.1862 87.5 84.1543 87.3399 84.9593C86.6823 88.265 84.0982 90.8492 80.7924 91.5067C79.9874 91.6668 79.0194 91.6668 77.0833 91.6668H22.9167C20.9806 91.6668 20.0126 91.6668 19.2076 91.5067C15.9018 90.8492 13.3177 88.265 12.6601 84.9593C12.5 84.1543 12.5 83.1862 12.5 81.2502C12.5 79.3141 12.5 78.3461 12.6601 77.5411C13.3177 74.2353 15.9018 71.6512 19.2076 70.9936C20.0126 70.8335 20.9806 70.8335 22.9167 70.8335Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
3
app/assets/ico/git.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M27.3629 17.9077L15.5368 5.65985C14.6877 4.78038 13.3115 4.78038 12.4624 5.65985L10.0975 8.10918L13.3098 11.4363C13.5655 11.327 13.8452 11.2663 14.1389 11.2663C15.3414 11.2663 16.3162 12.2758 16.3162 13.5213C16.3162 13.8255 16.2575 14.1151 16.152 14.38L19.227 17.5648C19.4827 17.4555 19.7624 17.3948 20.0561 17.3948C21.2586 17.3948 22.2334 18.4044 22.2334 19.6499C22.2334 20.8953 21.2586 21.9049 20.0561 21.9049C18.8536 21.9049 17.8788 20.8953 17.8788 19.6499C17.8788 19.3457 17.9374 19.056 18.0429 18.7912L14.9757 15.6145V23.639C15.7624 23.979 16.3156 24.7827 16.3156 25.7212C16.3156 26.9666 15.3408 27.9762 14.1383 27.9762C12.9358 27.9762 11.961 26.9666 11.961 25.7212C11.961 24.7833 12.5143 23.979 13.3009 23.639V15.6035C12.5143 15.2635 11.961 14.4598 11.961 13.5213C11.961 13.2172 12.0196 12.9275 12.1252 12.6627L8.91281 9.33559L0.636858 17.9077C-0.212286 18.7872 -0.212286 20.2125 0.636858 21.0919L12.4629 33.3404C13.3121 34.2198 14.6882 34.2198 15.5374 33.3404L27.3634 21.0919C28.2126 20.2125 28.2126 18.7872 27.3634 17.9077H27.3629Z" fill="#E15B39"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
5
app/assets/ico/hacker.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M43.542 76.0248C47.5986 74.1886 52.6393 74.1886 56.6959 76.0248M39.6892 69.9387C44.8263 73.9617 44.8263 80.4844 39.6892 84.5075C34.5521 88.5305 26.2232 88.5305 21.0861 84.5075C15.9491 80.4844 15.9491 73.9618 21.0861 69.9387C26.2232 65.9156 34.5521 65.9156 39.6892 69.9387ZM79.1522 69.9387C84.2892 73.9617 84.2892 80.4844 79.1522 84.5075C74.0151 88.5305 65.6862 88.5305 60.5491 84.5075C55.412 80.4844 55.412 73.9618 60.5491 69.9387C65.6862 65.9156 74.0151 65.9156 79.1522 69.9387Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M28.7138 40.7474C15.866 42.0672 8.27072 45.0645 8.27072 47.9823C8.27072 52.2584 26.9536 55.7248 50 55.7248C73.0465 55.7248 91.7294 52.2584 91.7294 47.9823C91.7294 45.4722 85.0332 42.5759 75.0544 41.1611" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M28.8541 40.2396L37.3725 14.6046L45.8429 20.592C45.8429 20.592 51.7915 18.0993 55.1599 16.6418C59.9267 14.5791 66.8807 12.4751 66.8807 12.4751L70.8711 26.3574L74.8616 40.2396" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
app/assets/ico/heartbeat.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M64.5833 47.9167H60.4167L54.1666 60.4167L45.8333 35.4167L39.5833 47.9167H35.4166M49.9714 21.3992C41.6408 11.66 27.7489 9.04018 17.3112 17.9584C6.8735 26.8766 5.40401 41.7874 13.6008 52.335C19.792 60.3018 37.3804 76.2956 45.6167 83.6454C47.1308 84.9964 47.8878 85.672 48.7743 85.9379C49.5439 86.1688 50.399 86.1688 51.1686 85.9379C52.0551 85.672 52.8121 84.9964 54.3262 83.6454C62.5625 76.2956 80.1509 60.3018 86.3421 52.335C94.5389 41.7874 93.2488 26.7828 82.6317 17.9584C72.0146 9.13399 58.3021 11.66 49.9714 21.3992Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 719 B |
4
app/assets/ico/laptop-code.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="101" height="101" viewBox="0 0 101 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M87.5885 67.2583V30.5916C87.5885 25.9245 87.5885 23.591 86.6803 21.8084C85.8813 20.2404 84.6065 18.9655 83.0385 18.1666C81.2559 17.2583 78.9223 17.2583 74.2552 17.2583H25.9219C21.2548 17.2583 18.9212 17.2583 17.1386 18.1666C15.5706 18.9655 14.2958 20.2404 13.4968 21.8084C12.5885 23.591 12.5885 25.9245 12.5885 30.5916V67.2583M19.533 83.925H80.6441C83.2274 83.925 84.519 83.925 85.5787 83.641C88.4545 82.8705 90.7007 80.6242 91.4713 77.7485C91.7552 76.6887 91.7552 75.3971 91.7552 72.8139C91.7552 71.5222 91.7552 70.8764 91.6132 70.3466C91.228 68.9087 90.1048 67.7856 88.667 67.4003C88.1371 67.2583 87.4913 67.2583 86.1997 67.2583H13.9774C12.6858 67.2583 12.04 67.2583 11.5101 67.4003C10.0722 67.7856 8.94913 68.9087 8.56385 70.3466C8.42188 70.8764 8.42188 71.5222 8.42188 72.8139C8.42188 75.3971 8.42188 76.6887 8.70583 77.7485C9.47639 80.6242 11.7226 82.8705 14.5984 83.641C15.6581 83.925 16.9497 83.925 19.533 83.925Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M58.422 54.7181L68.8386 44.3014C64.7707 40.2335 62.4899 37.9527 58.422 33.8848M41.7553 33.8848C37.6873 37.9527 35.4066 40.2335 31.3386 44.3014C35.4066 48.3694 37.6873 50.6501 41.7553 54.7181" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
3
app/assets/ico/laptop.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="101" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M87.4999 67.2583V30.5916C87.4999 25.9245 87.4999 23.591 86.5916 21.8084C85.7927 20.2404 84.5179 18.9655 82.9498 18.1666C81.1672 17.2583 78.8337 17.2583 74.1666 17.2583H25.8333C21.1661 17.2583 18.8326 17.2583 17.05 18.1666C15.482 18.9655 14.2071 20.2404 13.4082 21.8084C12.4999 23.591 12.4999 25.9245 12.4999 30.5916V67.2583M19.4444 83.925H80.5555C83.1387 83.925 84.4304 83.925 85.4901 83.641C88.3658 82.8705 90.6121 80.6242 91.3826 77.7485C91.6666 76.6887 91.6666 75.3971 91.6666 72.8139C91.6666 71.5222 91.6666 70.8764 91.5246 70.3466C91.1393 68.9087 90.0162 67.7856 88.5783 67.4003C88.0485 67.2583 87.4027 67.2583 86.111 67.2583H13.8888C12.5972 67.2583 11.9514 67.2583 11.4215 67.4003C9.98362 67.7856 8.86051 68.9087 8.47523 70.3466C8.33325 70.8764 8.33325 71.5222 8.33325 72.8139C8.33325 75.3971 8.33325 76.6887 8.6172 77.7485C9.38776 80.6242 11.634 82.8705 14.5098 83.641C15.5695 83.925 16.8611 83.925 19.4444 83.925Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
3
app/assets/ico/magic-wand.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M54.1668 58.3335L41.6668 45.8335M62.5432 14.5835V8.3335M78.9571 21.0862L83.3766 16.6668M78.9571 54.1668L83.3766 58.5862M45.8766 21.0862L41.4572 16.6668M85.4599 37.5002H91.7099M25.5475 86.9528L64.0361 48.4642C65.6861 46.8141 66.5112 45.9891 66.8203 45.0377C67.0922 44.2009 67.0922 43.2994 66.8203 42.4626C66.5112 41.5112 65.6861 40.6862 64.0361 39.0361L60.9641 35.9642C59.3141 34.3141 58.489 33.4891 57.5377 33.18C56.7008 32.9081 55.7994 32.9081 54.9625 33.18C54.0112 33.4891 53.1861 34.3141 51.5361 35.9642L13.0475 74.4528C11.3974 76.1029 10.5724 76.9279 10.2633 77.8793C9.99135 78.7161 9.99135 79.6176 10.2633 80.4544C10.5724 81.4058 11.3974 82.2308 13.0475 83.8809L16.1194 86.9528C17.7695 88.6029 18.5945 89.4279 19.5459 89.737C20.3827 90.0089 21.2842 90.0089 22.121 89.737C23.0724 89.4279 23.8974 88.6028 25.5475 86.9528Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
app/assets/ico/memory.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="auto" height="auto" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M14.5 18.3V20.8799M17 11.0399V8.03992M5.5 18.3V20.8799M10 18.3V20.8799M10 18.3H15.2C16.8802 18.3 19.8583 18.3 20.5 17.973C21.0645 17.6854 21.5234 17.2265 21.811 16.662C22.054 16.1852 22.1164 15.4789 22.1325 14.5199M10 18.3H8.8C7.11984 18.3 4.27976 18.3 3.63803 17.973C3.07354 17.6854 2.6146 17.2265 2.32698 16.662C2.08404 16.1852 2.0216 15.4789 2.00555 14.5199M19 18.3V20.8799M12.12 11.0399V8.03992M7.08 11.0399V8.03992M2.00555 14.5199C2 14.1882 2 13.9318 2 13.5V8.8C2 7.11984 2 6.27976 2.32698 5.63803C2.6146 5.07354 3.07354 4.6146 3.63803 4.32698C4.27976 4 7.11984 4 8.8 4H15.2C16.8802 4 19.8583 4 20.5 4.32698C21.0645 4.6146 21.5234 5.07354 21.811 5.63803C22.138 6.27976 22.138 7.11984 22.138 8.8V13.5C22.138 13.9318 22.138 14.1882 22.1325 14.5199M2.00555 14.5199H22.1325" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg>
|
||||
|
After Width: | Height: | Size: 979 B |
@@ -1,6 +0,0 @@
|
||||
<svg width="53" height="53" viewBox="0 0 53 53" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24.6796 0.74147H0V25.1903H24.6796V0.74147Z" fill="#F35325"/>
|
||||
<path d="M52.0165 0.741547H27.3369V25.1904H52.0165V0.741547Z" fill="#81BC06"/>
|
||||
<path d="M24.6796 27.8229H0V52.2718H24.6796V27.8229Z" fill="#05A6F0"/>
|
||||
<path d="M52.0165 27.8229H27.3369V52.2718H52.0165V27.8229Z" fill="#FFBA08"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 401 B |
1
app/assets/ico/object-group.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="auto" height="auto" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M19 7V17M5 7V17M17 5L7 5M17 19H7M4.6 7H5.4C5.96005 7 6.24008 7 6.45399 6.89101C6.64215 6.79513 6.79513 6.64215 6.89101 6.45399C7 6.24008 7 5.96005 7 5.4V4.6C7 4.03995 7 3.75992 6.89101 3.54601C6.79513 3.35785 6.64215 3.20487 6.45399 3.10899C6.24008 3 5.96005 3 5.4 3H4.6C4.03995 3 3.75992 3 3.54601 3.10899C3.35785 3.20487 3.20487 3.35785 3.10899 3.54601C3 3.75992 3 4.03995 3 4.6V5.4C3 5.96005 3 6.24008 3.10899 6.45399C3.20487 6.64215 3.35785 6.79513 3.54601 6.89101C3.75992 7 4.03995 7 4.6 7ZM4.6 21H5.4C5.96005 21 6.24008 21 6.45399 20.891C6.64215 20.7951 6.79513 20.6422 6.89101 20.454C7 20.2401 7 19.9601 7 19.4V18.6C7 18.0399 7 17.7599 6.89101 17.546C6.79513 17.3578 6.64215 17.2049 6.45399 17.109C6.24008 17 5.96005 17 5.4 17H4.6C4.03995 17 3.75992 17 3.54601 17.109C3.35785 17.2049 3.20487 17.3578 3.10899 17.546C3 17.7599 3 18.0399 3 18.6V19.4C3 19.9601 3 20.2401 3.10899 20.454C3.20487 20.6422 3.35785 20.7951 3.54601 20.891C3.75992 21 4.03995 21 4.6 21ZM18.6 7H19.4C19.9601 7 20.2401 7 20.454 6.89101C20.6422 6.79513 20.7951 6.64215 20.891 6.45399C21 6.24008 21 5.96005 21 5.4V4.6C21 4.03995 21 3.75992 20.891 3.54601C20.7951 3.35785 20.6422 3.20487 20.454 3.10899C20.2401 3 19.9601 3 19.4 3H18.6C18.0399 3 17.7599 3 17.546 3.10899C17.3578 3.20487 17.2049 3.35785 17.109 3.54601C17 3.75992 17 4.03995 17 4.6V5.4C17 5.96005 17 6.24008 17.109 6.45399C17.2049 6.64215 17.3578 6.79513 17.546 6.89101C17.7599 7 18.0399 7 18.6 7ZM18.6 21H19.4C19.9601 21 20.2401 21 20.454 20.891C20.6422 20.7951 20.7951 20.6422 20.891 20.454C21 20.2401 21 19.9601 21 19.4V18.6C21 18.0399 21 17.7599 20.891 17.546C20.7951 17.3578 20.6422 17.2049 20.454 17.109C20.2401 17 19.9601 17 19.4 17H18.6C18.0399 17 17.7599 17 17.546 17.109C17.3578 17.2049 17.2049 17.3578 17.109 17.546C17 17.7599 17 18.0399 17 18.6V19.4C17 19.9601 17 20.2401 17.109 20.454C17.2049 20.6422 17.3578 20.7951 17.546 20.891C17.7599 21 18.0399 21 18.6 21Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
6
app/assets/ico/palette.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.33325 50.0002C8.33325 73.012 26.9881 91.6668 49.9999 91.6668C56.9035 91.6668 62.4999 86.0704 62.4999 79.1668V77.0835C62.4999 75.1484 62.4999 74.1809 62.6069 73.3686C63.3453 67.7594 67.7592 63.3456 73.3683 62.6071C74.1806 62.5002 75.1482 62.5002 77.0833 62.5002H79.1666C86.0701 62.5002 91.6666 56.9037 91.6666 50.0002C91.6666 26.9883 73.0118 8.3335 49.9999 8.3335C26.9881 8.3335 8.33325 26.9883 8.33325 50.0002Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M29.1666 54.1668C31.4678 54.1668 33.3333 52.3014 33.3333 50.0002C33.3333 47.699 31.4678 45.8335 29.1666 45.8335C26.8654 45.8335 24.9999 47.699 24.9999 50.0002C24.9999 52.3014 26.8654 54.1668 29.1666 54.1668Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M66.6666 37.5002C68.9678 37.5002 70.8333 35.6347 70.8333 33.3335C70.8333 31.0323 68.9678 29.1668 66.6666 29.1668C64.3654 29.1668 62.4999 31.0323 62.4999 33.3335C62.4999 35.6347 64.3654 37.5002 66.6666 37.5002Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M41.6666 33.3335C43.9678 33.3335 45.8333 31.468 45.8333 29.1668C45.8333 26.8656 43.9678 25.0002 41.6666 25.0002C39.3654 25.0002 37.4999 26.8656 37.4999 29.1668C37.4999 31.468 39.3654 33.3335 41.6666 33.3335Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
5
app/assets/ico/placeholder.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="752pt" height="752pt" version="1.1" viewBox="0 0 752 752" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m552.21 372.31c-1.1094-1.8477-2.5859-2.957-4.4336-3.3242l-36.203-9.2344c-4.0625-1.1094-8.1289 1.4766-8.8672 5.543l-8.1289 32.879-31.395-7.7617-1.4766-0.37109c-1.8477-0.37109-4.0625 0-5.543 0.73828-1.8477 1.1094-2.957 2.5859-3.3242 4.4336l-8.1289 32.879-32.879-8.1289c-4.0625-1.1094-8.1289 1.4766-8.8672 5.543l-8.8633 32.508-32.879-8.1289c-1.8477-0.37109-4.0625 0-5.543 0.73828-1.8477 1.1094-2.957 2.5859-3.3242 4.4336l-0.73828 2.957-8.4961 34.723c-1.1094 4.0625 1.4766 8.1289 5.543 8.8672l143.33 35.832c1.8477 0.37109 3.3242 0.73828 5.1719 0.73828 9.2344 0 17.73-6.2812 19.949-15.516l36.203-144.81c0.36719-1.8477-0.003906-3.6914-1.1094-5.5391zm-137.05 63.535 32.137 8.1289c1.8477 0.73828 4.4336 0.37109 6.2812-0.73828s3.3242-3.3242 3.3242-5.543l6.2812-25.488 20.688 27.336-10.344 41.375-65.754-16.625zm87.547 83.117c-0.73828 3.3242-4.0625 5.1719-7.0195 4.4336l-136.31-33.984 5.1719-20.316 113.04 28.445c0.37109 0 1.1094 0.37109 1.4766 0.37109h0.37109c0.37109 0 0.73828 0 1.4766-0.37109h0.37109c0.73828 0 1.1094-0.37109 1.4766-0.73828 1.8477-1.1094 2.957-2.5859 3.3242-4.4336l12.93-52.086c0.37109-2.2148 0-4.4336-1.1094-6.2812l-18.102-23.641 17.73 4.4336c4.0625 1.1094 8.1289-1.4766 8.8672-5.543l8.1289-32.879 22.164 5.543z" fill="currentColor" stroke="currentColor"/>
|
||||
<path d="m351.62 434.37c4.0625 0 7.3867-3.3242 7.3867-7.3867v-33.984h33.617c4.0625 0 7.3867-3.3242 7.3867-7.3867v-33.617h33.617c4.0625 0 7.3867-3.3242 7.3867-7.3867v-33.984h30.293c4.0625 0 7.3867-3.3242 7.3867-7.3867v-65.391c0-11.453-9.2344-20.688-20.688-20.688h-235.31c-11.453 0-20.688 9.2344-20.688 20.688v215c0 11.453 9.2344 20.688 20.688 20.688h87.551c4.0625 0 7.3867-3.3242 7.3867-7.3867v-31.398zm33.617-56.148h-33.617c-4.0625 0-7.3867 3.3242-7.3867 7.3867v33.984h-91.242v-22.535l28.812-21.797 25.121 14.406c2.957 1.4766 6.2812 1.1094 8.8672-1.1094l55.043-53.195 14.406 11.453zm-82.379 80.16h-80.164c-3.3242 0-5.9102-2.5859-5.9102-5.9102v-214.62c0-3.3242 2.5859-5.9102 5.9102-5.9102h235.31c3.3242 0 5.9102 2.5859 5.9102 5.9102v58.367h-29.922c-4.0625 0-7.3867 3.3242-7.3867 7.3867v33.984h-29.551l-21.797-17.363c-2.957-2.2148-7.0195-2.2148-9.6055 0.37109l-56.148 53.566-24.383-14.039c-2.5859-1.4766-5.9102-1.4766-8.1289 0.37109l-35.832 26.965c-1.8477 1.4766-2.957 3.6953-2.957 5.9102v33.988c0 1.8477 0.73828 3.6953 2.2148 5.1719 1.4766 1.4766 3.3242 2.2148 5.1719 2.2148h57.258z" fill="currentColor" stroke="currentColor"/>
|
||||
<path d="m277 329.09c18.469 0 33.246-15.145 33.246-33.246 0-18.469-15.145-33.246-33.246-33.246-18.469 0-33.246 15.145-33.246 33.246-0.37109 18.102 14.777 33.246 33.246 33.246zm0-52.086c10.344 0 18.469 8.4961 18.469 18.469 0 10.344-8.4961 18.469-18.469 18.469-10.344 0-18.469-8.4961-18.469-18.469-0.37109-10.34 8.125-18.469 18.469-18.469z" fill="currentColor" stroke="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
3
app/assets/ico/plug.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M50.0004 74.401C68.1158 74.401 82.8013 59.7156 82.8013 41.6001L82.8013 24.5843L66.3959 24.5843M50.0004 74.401C31.885 74.401 17.1995 59.7156 17.1995 41.6001L17.1995 24.5843L66.3959 24.5843M50.0004 74.401C50.0004 84.5503 50.0004 95.7898 66.3959 95.7898L71.7592 95.7898M66.3959 24.5843L66.3959 4.21012M33.572 24.5843L33.572 4.21012" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 529 B |
3
app/assets/ico/restore-window.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M43.75 8.3449C40.9373 8.38302 39.2487 8.54586 37.8834 9.24153C36.3154 10.0405 35.0406 11.3153 34.2416 12.8833C33.5459 14.2487 33.3831 15.9372 33.345 18.7499M81.25 8.3449C84.0627 8.38302 85.7513 8.54586 87.1166 9.24153C88.6846 10.0405 89.9594 11.3153 90.7584 12.8833C91.4541 14.2487 91.6169 15.9372 91.655 18.7499M91.655 56.2499C91.6169 59.0626 91.4541 60.7512 90.7584 62.1165C89.9594 63.6845 88.6846 64.9594 87.1166 65.7583C85.7513 66.454 84.0627 66.6168 81.25 66.6549M91.6667 33.3332V41.6666M58.3335 8.33325H66.6665M21.6667 91.6666H53.3333C58.0004 91.6666 60.334 91.6666 62.1166 90.7583C63.6846 89.9594 64.9594 88.6845 65.7584 87.1165C66.6667 85.3339 66.6667 83.0004 66.6667 78.3333V46.6666C66.6667 41.9995 66.6667 39.6659 65.7584 37.8833C64.9594 36.3153 63.6846 35.0405 62.1166 34.2415C60.334 33.3333 58.0004 33.3333 53.3333 33.3333H21.6667C16.9996 33.3333 14.666 33.3333 12.8834 34.2415C11.3154 35.0405 10.0406 36.3153 9.24161 37.8833C8.33333 39.6659 8.33333 41.9995 8.33333 46.6666V78.3333C8.33333 83.0004 8.33333 85.3339 9.24161 87.1165C10.0406 88.6845 11.3154 89.9594 12.8834 90.7583C14.666 91.6666 16.9996 91.6666 21.6667 91.6666Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
3
app/assets/ico/restore.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M66.6667 22.6806V19.3473C66.6667 14.6801 66.6667 12.3466 65.7584 10.564C64.9594 8.99598 63.6846 7.72114 62.1166 6.9222C60.334 6.01392 58.0004 6.01392 53.3333 6.01392H46.6667C41.9996 6.01392 39.666 6.01392 37.8834 6.9222C36.3154 7.72114 35.0406 8.99598 34.2416 10.564C33.3333 12.3466 33.3333 14.6801 33.3333 19.3473V22.6806M12.5 22.6806H87.5M79.1667 22.6806V69.3473C79.1667 76.3479 79.1667 79.8482 77.8042 82.5221C76.6058 84.8742 74.6936 86.7864 72.3415 87.9848C69.6677 89.3473 66.1673 89.3473 59.1667 89.3473H40.8333C33.8327 89.3473 30.3323 89.3473 27.6585 87.9848C25.3064 86.7864 23.3942 84.8742 22.1958 82.5221C20.8333 79.8482 20.8333 76.3479 20.8333 69.3473V22.6806M50 68.7718L50 43.7718M50 43.7718L62.5 56.2718M50 43.7718L37.5 56.2718" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 939 B |
3
app/assets/ico/rocket.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M54.1652 45.8332L14.5818 85.4165M58.4086 14.7435C63.4837 18.111 68.3617 22.0811 72.92 26.6394C77.5176 31.237 81.5168 36.1599 84.9029 41.2822M38.5608 32.9004L26.5822 28.9076C25.2027 28.4477 23.6833 28.7404 22.5732 29.6796L10.6684 39.753C8.23116 41.8152 8.92357 45.7398 11.9193 46.8435L23.1993 50.9993M48.6695 76.4687L52.8253 87.7487C53.929 90.7445 57.8536 91.4369 59.9158 88.9997L69.9892 77.0949C70.9285 75.9848 71.2211 74.4653 70.7612 73.0858L66.7684 61.1072M80.6176 9.46122L60.174 12.8685C57.9665 13.2364 55.9417 14.3214 54.4128 15.9558L26.8583 45.4105C19.716 53.0454 19.9147 64.9686 27.3074 72.3614C34.7001 79.7541 46.6233 79.9527 54.2582 72.8104L83.713 45.256C85.3473 43.7271 86.4323 41.7023 86.8003 39.4948L90.2075 19.0512C91.1476 13.411 86.2578 8.52119 80.6176 9.46122Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 975 B |
1
app/assets/ico/route.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="auto" height="auto" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M11.5 5H11.9344C14.9816 5 16.5053 5 17.0836 5.54729C17.5836 6.02037 17.8051 6.71728 17.6702 7.39221C17.514 8.17302 16.2701 9.05285 13.7823 10.8125L9.71772 13.6875C7.2299 15.4471 5.98599 16.327 5.82984 17.1078C5.69486 17.7827 5.91642 18.4796 6.41636 18.9527C6.99474 19.5 8.51836 19.5 11.5656 19.5H12.5M8 5C8 6.65685 6.65685 8 5 8C3.34315 8 2 6.65685 2 5C2 3.34315 3.34315 2 5 2C6.65685 2 8 3.34315 8 5ZM22 19C22 20.6569 20.6569 22 19 22C17.3431 22 16 20.6569 16 19C16 17.3431 17.3431 16 19 16C20.6569 16 22 17.3431 22 19Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg>
|
||||
|
After Width: | Height: | Size: 725 B |
3
app/assets/ico/share.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M86.6307 52.5309C87.6478 51.659 88.1564 51.2231 88.3428 50.7043C88.5063 50.249 88.5063 49.751 88.3428 49.2957C88.1564 48.7769 87.6478 48.341 86.6307 47.4691L51.336 17.2165C49.585 15.7157 48.7095 14.9653 47.9683 14.9469C47.3242 14.931 46.7088 15.214 46.3018 15.7134C45.8333 16.2882 45.8333 17.4413 45.8333 19.7474V37.6443C36.9388 39.2004 28.7983 43.7073 22.7488 50.4744C16.1534 57.8521 12.5051 67.3998 12.5 77.2957V79.8457C16.8723 74.5786 22.3313 70.3188 28.5032 67.358C33.9446 64.7476 39.8267 63.2013 45.8333 62.7939V80.2526C45.8333 82.5587 45.8333 83.7118 46.3018 84.2866C46.7088 84.786 47.3242 85.069 47.9683 85.0531C48.7095 85.0347 49.585 84.2843 51.336 82.7835L86.6307 52.5309Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 882 B |
3
app/assets/ico/sort.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M70.8333 16.6667V83.3334M70.8333 83.3334L54.1667 66.6667M70.8333 83.3334L87.5 66.6667M29.1667 83.3334V16.6667M29.1667 16.6667L12.5 33.3334M29.1667 16.6667L45.8333 33.3334" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 371 B |
3
app/assets/ico/tachometer.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="auto" height="auto" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.7453 16C18.5362 14.8661 19 13.4872 19 12C19 11.4851 18.9444 10.9832 18.8389 10.5M6.25469 16C5.46381 14.8662 5 13.4872 5 12C5 8.13401 8.13401 5 12 5C12.4221 5 12.8355 5.03737 13.2371 5.10897M16.4999 7.5L11.9999 12M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12ZM13 12C13 12.5523 12.5523 13 12 13C11.4477 13 11 12.5523 11 12C11 11.4477 11.4477 11 12 11C12.5523 11 13 11.4477 13 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 655 B |
3
app/assets/ico/tag-2.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M33.3333 33.3335H33.375M8.33332 21.6668L8.33331 40.3106C8.33331 42.3489 8.33331 43.368 8.56356 44.3271C8.7677 45.1774 9.10441 45.9903 9.56131 46.7359C10.0767 47.5768 10.7973 48.2975 12.2386 49.7387L44.1912 81.6913C49.1414 86.6416 51.6165 89.1167 54.4706 90.044C56.9811 90.8597 59.6855 90.8597 62.196 90.044C65.0501 89.1167 67.5252 86.6416 72.4755 81.6913L81.6912 72.4756C86.6414 67.5254 89.1165 65.0503 90.0438 62.1962C90.8596 59.6857 90.8596 56.9813 90.0438 54.4708C89.1165 51.6167 86.6414 49.1416 81.6912 44.1914L49.7385 12.2387C48.2973 10.7975 47.5767 10.0768 46.7357 9.56149C45.9901 9.10459 45.1772 8.76789 44.3269 8.56375C43.3678 8.3335 42.3487 8.3335 40.3105 8.3335L21.6666 8.3335C16.9995 8.3335 14.666 8.3335 12.8834 9.24178C11.3154 10.0407 10.0405 11.3156 9.2416 12.8836C8.33332 14.6662 8.33332 16.9997 8.33332 21.6668Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
3
app/assets/ico/tags.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M87.5 45.8335L55.8579 14.1914C53.696 12.0295 52.615 10.9485 51.3536 10.1755C50.2352 9.49014 49.0158 8.98508 47.7404 8.67887C46.3018 8.3335 44.7731 8.3335 41.7157 8.3335L25 8.3335M33.0342 41.072H33.0758M12.5 36.2502L12.5 44.4773C12.5 46.5156 12.5 47.5347 12.7303 48.4938C12.9344 49.3441 13.2711 50.1569 13.728 50.9025C14.2433 51.7435 14.964 52.4641 16.4052 53.9054L48.9052 86.4054C52.2054 89.7056 53.8555 91.3556 55.7582 91.9739C57.4319 92.5177 59.2348 92.5177 60.9085 91.9739C62.8112 91.3556 64.4613 89.7056 67.7614 86.4054L78.0719 76.0949C81.372 72.7948 83.0221 71.1447 83.6404 69.242C84.1842 67.5683 84.1842 65.7654 83.6404 64.0917C83.0221 62.189 81.3721 60.5389 78.0719 57.2387L47.6552 26.8221C46.214 25.3808 45.4933 24.6602 44.6524 24.1448C43.9068 23.6879 43.0939 23.3512 42.2436 23.1471C41.2845 22.9168 40.2654 22.9168 38.2272 22.9168H25.8333C21.1662 22.9168 18.8327 22.9168 17.0501 23.8251C15.4821 24.6241 14.2072 25.8989 13.4083 27.4669C12.5 29.2495 12.5 31.5831 12.5 36.2502Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
4
app/assets/ico/template.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3817 5.28009C22.1592 5.28009 28.4649 11.5864 28.4649 19.3646C28.4649 27.1428 22.1592 33.4491 14.3817 33.4491C6.60065 33.4526 0.294922 27.1428 0.294922 19.3646C0.294922 11.5864 6.60065 5.28009 14.3817 5.28009Z" fill="#E0F2FE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.3533 15.103C19.4222 14.6894 19.0636 14.3308 18.65 14.3997L16.5794 14.7448C17.02 15.0919 17.4471 15.4705 17.8568 15.8803C18.2726 16.296 18.6563 16.7296 19.0076 17.1771L19.3533 15.103ZM18.5628 18.5987C18.1171 17.947 17.5985 17.319 17.0083 16.7288C16.4237 16.1442 15.8021 15.6299 15.1571 15.1871L12.8847 17.6163C12.8799 17.6216 12.875 17.6269 12.87 17.632L11.1762 19.4426C10.4928 20.1731 10.397 21.2469 10.8789 22.0734L14.2825 18.6698C14.5168 18.4355 14.8967 18.4355 15.131 18.6698C15.3654 18.9041 15.3654 19.284 15.131 19.5184L11.7409 22.9085C12.5586 23.3509 13.5978 23.2434 14.3104 22.5767L16.1186 20.8852C16.1253 20.8786 16.1322 20.8722 16.1392 20.8659L18.5628 18.5987ZM17.2294 21.4893L19.4111 19.4484C19.7449 19.1361 19.9666 18.7225 20.0417 18.2715L20.5369 15.3003C20.7412 14.0745 19.6785 13.0117 18.4527 13.2161L15.4814 13.7113C15.0305 13.7864 14.6169 14.0081 14.3046 14.3419L12.2636 16.5236L10.8877 16.065C10.4885 15.9319 10.0489 16.0166 9.72772 16.2884L7.99751 17.7524C7.29234 18.3491 7.49268 19.4846 8.35946 19.8039L9.46051 20.2096C9.2529 21.1392 9.43813 22.1371 10.0118 22.9405L8.52958 24.4228C8.29527 24.6571 8.29527 25.037 8.52958 25.2713C8.7639 25.5056 9.1438 25.5056 9.37811 25.2713L10.8688 23.7806C11.6623 24.3233 12.6355 24.4952 13.5434 24.2925L13.949 25.3934C14.2684 26.2602 15.4039 26.4605 16.0006 25.7554L17.4646 24.0251C17.7364 23.704 17.8211 23.2643 17.688 22.8652L17.2294 21.4893ZM16.2652 22.3913L15.1302 23.4531C14.9793 23.5942 14.8189 23.7192 14.6511 23.8278L15.0751 24.9786C15.0755 24.9797 15.0759 24.9806 15.0762 24.9812C15.0769 24.9815 15.0779 24.9819 15.0793 24.9821M15.0826 24.9824C15.0831 24.9819 15.0837 24.9812 15.0845 24.9802L16.5486 23.25C16.5498 23.2485 16.5502 23.2465 16.5496 23.2447L16.2652 22.3913M9.92511 19.1019C10.0338 18.9341 10.1587 18.7737 10.2999 18.6228L11.3616 17.4878L10.5082 17.2034C10.5064 17.2028 10.5043 17.2032 10.5029 17.2044L8.77264 18.6684C8.77168 18.6693 8.77098 18.6699 8.77049 18.6704" fill="#0086C9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
4
app/assets/ico/theme/auto.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3817 5.28003C22.1592 5.28003 28.4649 11.5865 28.4649 19.365C28.4649 27.1435 22.1592 33.45 14.3817 33.45C6.60065 33.4535 0.294922 27.1435 0.294922 19.365C0.294922 11.5865 6.60065 5.28003 14.3817 5.28003Z" fill="#E0F2FE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.5297 15.5683C14.8574 15.3761 14.1468 15.3553 13.4642 15.508C12.7816 15.6606 12.1493 15.9817 11.6262 16.4413C11.1031 16.9008 10.7063 17.4839 10.4728 18.136C10.3554 18.464 9.99118 18.6358 9.65933 18.5198C9.32747 18.4037 9.15365 18.0437 9.27108 17.7157C9.57638 16.8629 10.0953 16.1004 10.7793 15.4995C11.4634 14.8985 12.2903 14.4786 13.1829 14.279C14.0755 14.0794 15.0048 14.1065 15.8839 14.3579C16.7598 14.6083 17.5575 15.0731 18.2032 15.7092L19.5868 16.9943V15.3008C19.5868 14.9528 19.8722 14.6707 20.2242 14.6707C20.5762 14.6707 20.8616 14.9528 20.8616 15.3008V18.4509C20.8616 18.7988 20.5762 19.0809 20.2242 19.0809H17.0373C16.6852 19.0809 16.3999 18.7988 16.3999 18.4509C16.3999 18.1029 16.6852 17.8208 17.0373 17.8208H18.6151L17.3232 16.6209L17.3088 16.6072C16.8141 16.1179 16.202 15.7605 15.5297 15.5683ZM7.90137 20.5509C7.90137 20.203 8.18674 19.9209 8.53875 19.9209H11.7257C12.0777 19.9209 12.3631 20.203 12.3631 20.5509C12.3631 20.8989 12.0777 21.1809 11.7257 21.1809H10.1479L11.4398 22.3809L11.4541 22.3946C11.9489 22.8839 12.561 23.2413 13.2332 23.4335C13.9055 23.6257 14.6161 23.6465 15.2987 23.4938C15.9813 23.3411 16.6137 23.0201 17.1368 22.5605C17.6599 22.1009 18.0566 21.5179 18.2901 20.8658C18.4075 20.5377 18.7718 20.3659 19.1036 20.482C19.4355 20.5981 19.6093 20.9581 19.4919 21.2861C19.1866 22.1389 18.6677 22.9013 17.9836 23.5023C17.2996 24.1033 16.4727 24.5231 15.58 24.7228C14.6874 24.9224 13.7582 24.8953 12.879 24.6439C12.0032 24.3935 11.2055 23.9287 10.5598 23.2926L9.17614 22.0074V23.701C9.17614 24.049 8.89077 24.331 8.53875 24.331C8.18674 24.331 7.90137 24.049 7.90137 23.701V20.5509Z" fill="#0086C9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
4
app/assets/ico/theme/darkmode.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3817 5.28003C22.1592 5.28003 28.4649 11.5865 28.4649 19.365C28.4649 27.1435 22.1592 33.45 14.3817 33.45C6.60065 33.4535 0.294922 27.1435 0.294922 19.365C0.294922 11.5865 6.60065 5.28003 14.3817 5.28003Z" fill="#E0F2FE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4242 13.7577C14.5475 13.974 14.5308 14.2418 14.3815 14.4415C13.8988 15.087 13.6665 15.8823 13.7269 16.6828C13.7873 17.4832 14.1363 18.2357 14.7105 18.8033C15.2848 19.3709 16.046 19.7159 16.8559 19.7756C17.6657 19.8353 18.4703 19.6057 19.1234 19.1286C19.3254 18.981 19.5963 18.9644 19.8152 19.0863C20.0341 19.2083 20.1601 19.4459 20.1369 19.6932C20.0353 20.7805 19.6224 21.8167 18.9467 22.6806C18.271 23.5444 17.3604 24.2002 16.3213 24.5712C15.2823 24.9421 14.1579 25.0129 13.0797 24.7753C12.0014 24.5376 11.014 24.0014 10.2328 23.2293C9.45166 22.4571 8.90913 21.4811 8.66871 20.4153C8.42828 19.3495 8.49991 18.2381 8.87521 17.2111C9.25051 16.1841 9.91396 15.284 10.7879 14.6161C11.6619 13.9482 12.7102 13.5401 13.8103 13.4396C14.0604 13.4168 14.3008 13.5413 14.4242 13.7577ZM12.6775 14.989C12.2816 15.1435 11.9077 15.353 11.5677 15.6129C10.8852 16.1344 10.3672 16.8373 10.0742 17.6392C9.78113 18.4411 9.7252 19.3089 9.91293 20.1411C10.1007 20.9733 10.5243 21.7354 11.1342 22.3383C11.7442 22.9412 12.5152 23.3599 13.3571 23.5454C14.199 23.731 15.077 23.6757 15.8883 23.3861C16.6996 23.0964 17.4106 22.5844 17.9382 21.9098C18.2012 21.5737 18.4131 21.2041 18.5695 20.8128C17.9916 21.0012 17.3774 21.0776 16.7611 21.0321C15.6468 20.95 14.5993 20.4753 13.8091 19.6943C13.019 18.9133 12.5387 17.8779 12.4556 16.7765C12.4097 16.1672 12.4869 15.5602 12.6775 14.989Z" fill="#0086C9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
4
app/assets/ico/theme/highcontrastmode.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3817 5.28003C22.1592 5.28003 28.4649 11.5865 28.4649 19.365C28.4649 27.1435 22.1592 33.45 14.3817 33.45C6.60065 33.4535 0.294922 27.1435 0.294922 19.365C0.294922 11.5865 6.60065 5.28003 14.3817 5.28003Z" fill="#E0F2FE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.72192 19.1859C8.78197 19.2875 8.85875 19.4126 8.95184 19.555C9.21567 19.9588 9.60629 20.4957 10.1132 21.0301C11.1385 22.1111 12.5641 23.106 14.3234 23.106C16.0828 23.106 17.5084 22.1111 18.5337 21.0301C19.0406 20.4957 19.4312 19.9588 19.695 19.555C19.7881 19.4126 19.8649 19.2875 19.925 19.1859C19.8649 19.0843 19.7881 18.9592 19.695 18.8167C19.4312 18.4129 19.0406 17.8761 18.5337 17.3417C17.5084 16.2606 16.0828 15.2658 14.3234 15.2658C12.5641 15.2658 11.1385 16.2606 10.1132 17.3417C9.60629 17.8761 9.21567 18.4129 8.95184 18.8167C8.85875 18.9592 8.78197 19.0843 8.72192 19.1859ZM20.6531 19.1859C21.2231 18.9041 21.223 18.9039 21.2229 18.9037L21.2219 18.9018L21.2199 18.8977L21.2132 18.8848C21.2076 18.874 21.1997 18.859 21.1896 18.8401C21.1693 18.8023 21.14 18.7487 21.1018 18.6815C21.0254 18.5473 20.9131 18.3584 20.7659 18.1331C20.4723 17.6838 20.0358 17.0831 19.4637 16.4799C18.3313 15.2859 16.5921 14.0057 14.3234 14.0057C12.0548 14.0057 10.3156 15.2859 9.18316 16.4799C8.61112 17.0831 8.17458 17.6838 7.88098 18.1331C7.73378 18.3584 7.62146 18.5473 7.54507 18.6815C7.50685 18.7487 7.47754 18.8023 7.45729 18.8401C7.44716 18.859 7.43929 18.874 7.4337 18.8848L7.42701 18.8977L7.42494 18.9018L7.42423 18.9032C7.42412 18.9034 7.42374 18.9041 7.99383 19.1859L7.42374 18.9041C7.33402 19.0815 7.33402 19.2903 7.42374 19.4676L7.99383 19.1859C7.42374 19.4676 7.42362 19.4674 7.42374 19.4676L7.42423 19.4686L7.42494 19.47L7.42701 19.4741L7.4337 19.487C7.43929 19.4978 7.44716 19.5127 7.45729 19.5317C7.47754 19.5695 7.50685 19.6231 7.54507 19.6903C7.62146 19.8245 7.73378 20.0134 7.88098 20.2386C8.17458 20.688 8.61112 21.2887 9.18316 21.8919C10.3156 23.0858 12.0548 24.366 14.3234 24.366C16.5921 24.366 18.3313 23.0858 19.4637 21.8919C20.0358 21.2887 20.4723 20.688 20.7659 20.2386C20.9131 20.0134 21.0254 19.8245 21.1018 19.6903C21.14 19.6231 21.1693 19.5695 21.1896 19.5317C21.1997 19.5127 21.2076 19.4978 21.2132 19.487L21.2199 19.4741L21.2219 19.47L21.2227 19.4686C21.2228 19.4684 21.2231 19.4676 20.6531 19.1859ZM20.6531 19.1859L21.2231 19.4676C21.3129 19.2903 21.3126 19.0811 21.2229 18.9037L20.6531 19.1859ZM14.3234 18.1096C13.7221 18.1096 13.2346 18.5915 13.2346 19.1859C13.2346 19.7803 13.7221 20.2622 14.3234 20.2622C14.9248 20.2622 15.4123 19.7803 15.4123 19.1859C15.4123 18.5915 14.9248 18.1096 14.3234 18.1096ZM11.9598 19.1859C11.9598 17.8956 13.018 16.8496 14.3234 16.8496C15.6288 16.8496 16.6871 17.8956 16.6871 19.1859C16.6871 20.4762 15.6288 21.5222 14.3234 21.5222C13.018 21.5222 11.9598 20.4762 11.9598 19.1859Z" fill="#0086C9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
4
app/assets/ico/theme/lightmode.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3817 5.28003C22.1592 5.28003 28.4649 11.5865 28.4649 19.365C28.4649 27.1435 22.1592 33.45 14.3817 33.45C6.60065 33.4535 0.294922 27.1435 0.294922 19.365C0.294922 11.5865 6.60065 5.28003 14.3817 5.28003Z" fill="#E0F2FE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.3225 11.6122C14.7096 11.6122 15.0235 11.9224 15.0235 12.3051V13.5561C15.0235 13.9388 14.7096 14.249 14.3225 14.249C13.9353 14.249 13.6215 13.9388 13.6215 13.5561V12.3051C13.6215 11.9224 13.9353 11.6122 14.3225 11.6122ZM8.90324 13.8293C9.17699 13.5587 9.62084 13.5587 9.8946 13.8293L10.7932 14.7176C11.067 14.9882 11.067 15.4269 10.7932 15.6975C10.5195 15.9681 10.0756 15.9681 9.80188 15.6975L8.90324 14.8092C8.62948 14.5386 8.62948 14.0999 8.90324 13.8293ZM19.7417 13.8293C20.0154 14.0999 20.0154 14.5386 19.7417 14.8092L18.843 15.6975C18.5693 15.9681 18.1254 15.9681 17.8517 15.6975C17.5779 15.4269 17.5779 14.9882 17.8517 14.7176L18.7503 13.8293C19.0241 13.5587 19.4679 13.5587 19.7417 13.8293ZM14.3225 16.7511C12.9621 16.7511 11.8592 17.8412 11.8592 19.1859C11.8592 20.5306 12.9621 21.6207 14.3225 21.6207C15.6829 21.6207 16.7857 20.5306 16.7857 19.1859C16.7857 17.8412 15.6829 16.7511 14.3225 16.7511ZM10.4572 19.1859C10.4572 17.0759 12.1878 15.3654 14.3225 15.3654C16.4572 15.3654 18.1877 17.0759 18.1877 19.1859C18.1877 21.2959 16.4572 23.0064 14.3225 23.0064C12.1878 23.0064 10.4572 21.2959 10.4572 19.1859ZM6.66016 19.1859C6.66016 18.8032 6.974 18.493 7.36115 18.493H8.62685C9.014 18.493 9.32784 18.8032 9.32784 19.1859C9.32784 19.5686 9.014 19.8788 8.62685 19.8788H7.36115C6.974 19.8788 6.66016 19.5686 6.66016 19.1859ZM19.3171 19.1859C19.3171 18.8032 19.6309 18.493 20.0181 18.493H21.2838C21.6709 18.493 21.9848 18.8032 21.9848 19.1859C21.9848 19.5686 21.6709 19.8788 21.2838 19.8788H20.0181C19.6309 19.8788 19.3171 19.5686 19.3171 19.1859ZM10.7932 22.6743C11.067 22.9449 11.067 23.3836 10.7932 23.6542L9.8946 24.5425C9.62084 24.8131 9.17699 24.8131 8.90324 24.5425C8.62948 24.2719 8.62948 23.8332 8.90324 23.5626L9.80188 22.6743C10.0756 22.4037 10.5195 22.4037 10.7932 22.6743ZM17.8517 22.6743C18.1254 22.4037 18.5693 22.4037 18.843 22.6743L19.7417 23.5626C20.0154 23.8332 20.0154 24.2719 19.7417 24.5425C19.4679 24.8131 19.0241 24.8131 18.7503 24.5425L17.8517 23.6542C17.5779 23.3836 17.5779 22.9449 17.8517 22.6743ZM14.3225 24.1228C14.7096 24.1228 15.0235 24.433 15.0235 24.8157V26.0667C15.0235 26.4494 14.7096 26.7596 14.3225 26.7596C13.9353 26.7596 13.6215 26.4494 13.6215 26.0667V24.8157C13.6215 24.433 13.9353 24.1228 14.3225 24.1228Z" fill="#0086C9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
3
app/assets/ico/tools.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M25 25L43.75 43.75M25 25H12.5L8.33337 12.5L12.5 8.33337L25 12.5V25ZM80.2458 11.4209L69.2974 22.3693C67.6474 24.0194 66.8223 24.8444 66.5132 25.7958C66.2413 26.6326 66.2413 27.5341 66.5132 28.3709C66.8223 29.3223 67.6474 30.1473 69.2974 31.7974L70.286 32.786C71.9361 34.4361 72.7611 35.2611 73.7125 35.5702C74.5493 35.8421 75.4508 35.8421 76.2876 35.5702C77.239 35.2611 78.064 34.4361 79.7141 32.786L89.9554 22.5447C91.0584 25.2287 91.6667 28.1683 91.6667 31.25C91.6667 43.9066 81.4066 54.1667 68.75 54.1667C67.2242 54.1667 65.7331 54.0176 64.2907 53.7331C62.2651 53.3336 61.2523 53.1339 60.6383 53.195C59.9856 53.2601 59.6639 53.358 59.0855 53.6675C58.5415 53.9586 57.9958 54.5043 56.9043 55.5957L27.0834 85.4166C23.6316 88.8684 18.0352 88.8684 14.5834 85.4166C11.1316 81.9649 11.1316 76.3684 14.5834 72.9166L44.4043 43.0957C45.4958 42.0043 46.0415 41.4586 46.3326 40.9146C46.6421 40.3362 46.74 40.0145 46.805 39.3618C46.8662 38.7478 46.6665 37.735 46.267 35.7094C45.9825 34.2669 45.8334 32.7759 45.8334 31.25C45.8334 18.5935 56.0935 8.33337 68.75 8.33337C72.9396 8.33337 76.8666 9.45764 80.2458 11.4209ZM50.0003 62.4998L72.9167 85.4163C76.3685 88.868 81.9649 88.868 85.4167 85.4163C88.8684 81.9645 88.8684 76.368 85.4166 72.9162L66.5639 54.0638C65.2293 53.9375 63.928 53.6967 62.67 53.3514C61.0489 52.9065 59.2706 53.2294 58.082 54.4181L50.0003 62.4998Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
4
app/assets/ico/upload.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="37" height="40" viewBox="0 0 37 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.8817 5.28003C22.6592 5.28003 28.9649 11.5865 28.9649 19.365C28.9649 27.1435 22.6592 33.45 14.8817 33.45C7.10065 33.4535 0.794922 27.1435 0.794922 19.365C0.794922 11.5865 7.10065 5.28003 14.8817 5.28003Z" fill="#E0F2FE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.7962 13.2812C13.6421 13.25 14.484 13.411 15.2587 13.7521C16.0335 14.0932 16.7208 14.6054 17.269 15.2504C17.7155 15.7756 18.0603 16.3778 18.2874 17.0259H18.6307C19.4583 17.0264 20.2625 17.3025 20.9157 17.8107C21.569 18.3188 22.0344 19.03 22.2385 19.8321C22.4427 20.6341 22.3739 21.4812 22.0431 22.2398C21.7123 22.9984 21.1382 23.6253 20.4116 24.0214C20.1207 24.18 19.7562 24.0728 19.5976 23.7818C19.439 23.4909 19.5463 23.1264 19.8372 22.9678C20.3298 22.6993 20.7189 22.2744 20.9431 21.7602C21.1674 21.246 21.214 20.6717 21.0756 20.128C20.9372 19.5844 20.6217 19.1023 20.1789 18.7579C19.7362 18.4134 19.1913 18.2262 18.6303 18.2259H17.8432C17.5696 18.2259 17.3307 18.0409 17.2623 17.7761C17.0958 17.1321 16.7855 16.5343 16.3547 16.0276C15.924 15.5209 15.3839 15.1184 14.7752 14.8504C14.1665 14.5824 13.505 14.4559 12.8403 14.4804C12.1757 14.5049 11.5253 14.6797 10.9379 14.9918C10.3506 15.3038 9.84163 15.745 9.44932 16.282C9.05701 16.8191 8.79156 17.4381 8.67291 18.0925C8.55427 18.7469 8.58553 19.4197 8.76434 20.0603C8.94315 20.7009 9.26486 21.2927 9.70528 21.7911C9.92471 22.0394 9.9013 22.4185 9.65299 22.638C9.40468 22.8574 9.02551 22.834 8.80608 22.5857C8.24555 21.9514 7.8361 21.1983 7.60853 20.383C7.38095 19.5677 7.34116 18.7114 7.49216 17.8785C7.64316 17.0456 7.98101 16.2577 8.48031 15.5742C8.97961 14.8907 9.62738 14.3292 10.3749 13.9321C11.1224 13.5349 11.9503 13.3124 12.7962 13.2812ZM14.4564 19.0766C14.6907 18.8423 15.0706 18.8423 15.3049 19.0766L17.8049 21.5766C18.0393 21.8109 18.0393 22.1908 17.8049 22.4251C17.5706 22.6594 17.1907 22.6594 16.9564 22.4251L15.4807 20.9494V25.1259C15.4807 25.4572 15.212 25.7259 14.8807 25.7259C14.5493 25.7259 14.2807 25.4572 14.2807 25.1259V20.9494L12.8049 22.4251C12.5706 22.6594 12.1907 22.6594 11.9564 22.4251C11.7221 22.1908 11.7221 21.8109 11.9564 21.5766L14.4564 19.0766Z" fill="#0086C9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
4
app/assets/ico/url.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="36" height="40" viewBox="0 0 36 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3817 5.28009C22.1592 5.28009 28.4649 11.5864 28.4649 19.3646C28.4649 27.1428 22.1592 33.4491 14.3817 33.4491C6.60065 33.4526 0.294922 27.1428 0.294922 19.3646C0.294922 11.5864 6.60065 5.28009 14.3817 5.28009Z" fill="#E0F2FE"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.76176 18.7646H11.3097C11.4479 17.0118 12.0382 15.3298 13.0159 13.8805C10.7429 14.4441 9.01305 16.3839 8.76176 18.7646ZM14.3803 14.0432C13.3165 15.4037 12.6679 17.0435 12.5139 18.7646H16.2467C16.0926 17.0435 15.4441 15.4037 14.3803 14.0432ZM16.2467 19.9646C16.0926 21.6858 15.4441 23.3256 14.3803 24.6861C13.3165 23.3256 12.6679 21.6858 12.5139 19.9646H16.2467ZM11.3097 19.9646H8.76176C9.01305 22.3454 10.7429 24.2852 13.0159 24.8488C12.0382 23.3995 11.4479 21.7175 11.3097 19.9646ZM15.7446 24.8488C16.7223 23.3995 17.3127 21.7175 17.4509 19.9646H19.9988C19.7475 22.3454 18.0177 24.2852 15.7446 24.8488ZM19.9988 18.7646H17.4509C17.3127 17.0118 16.7223 15.3298 15.7446 13.8805C18.0177 14.4441 19.7475 16.3839 19.9988 18.7646ZM7.53027 19.3646C7.53027 15.5815 10.5971 12.5146 14.3803 12.5146C18.1634 12.5146 21.2303 15.5815 21.2303 19.3646C21.2303 23.1478 18.1634 26.2146 14.3803 26.2146C10.5971 26.2146 7.53027 23.1478 7.53027 19.3646Z" fill="#0086C9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
3
app/assets/ico/user-circle.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.1512 80.9934C24.6859 75.0217 30.6038 70.8334 37.5 70.8334H62.5C69.3961 70.8334 75.314 75.0217 77.8487 80.9934M66.6666 39.5834C66.6666 48.7881 59.2047 56.25 50 56.25C40.7952 56.25 33.3333 48.7881 33.3333 39.5834C33.3333 30.3786 40.7952 22.9167 50 22.9167C59.2047 22.9167 66.6666 30.3786 66.6666 39.5834ZM91.6667 50C91.6667 73.0119 73.0119 91.6667 50 91.6667C26.9881 91.6667 8.33331 73.0119 8.33331 50C8.33331 26.9882 26.9881 8.33337 50 8.33337C73.0119 8.33337 91.6667 26.9882 91.6667 50Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 691 B |
5
app/assets/ico/user-lock.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M70.8333 87.5V79.1667C70.8333 74.7464 69.0774 70.5072 65.9518 67.3816C62.8262 64.256 58.5869 62.5 54.1667 62.5H20.8333C16.413 62.5 12.1738 64.256 9.04821 67.3816C5.9226 70.5072 4.16666 74.7464 4.16666 79.1667V87.5" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M37.5 45.8333C46.7048 45.8333 54.1667 38.3714 54.1667 29.1667C54.1667 19.9619 46.7048 12.5 37.5 12.5C28.2953 12.5 20.8333 19.9619 20.8333 29.1667C20.8333 38.3714 28.2953 45.8333 37.5 45.8333Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M88.5417 35.4166V28.1249C88.5417 24.0978 85.2771 20.8333 81.25 20.8333C77.2229 20.8333 73.9583 24.0978 73.9583 28.1249V35.4166M73.3333 52.0833H89.1667C91.5002 52.0833 92.667 52.0832 93.5583 51.6291C94.3423 51.2296 94.9797 50.5922 95.3792 49.8082C95.8333 48.9169 95.8333 47.7501 95.8333 45.4166V42.0833C95.8333 39.7497 95.8333 38.5829 95.3792 37.6916C94.9797 36.9076 94.3423 36.2702 93.5583 35.8707C92.667 35.4166 91.5002 35.4166 89.1667 35.4166H73.3333C70.9998 35.4166 69.833 35.4166 68.9417 35.8707C68.1577 36.2702 67.5203 36.9076 67.1208 37.6916C66.6667 38.5829 66.6667 39.7497 66.6667 42.0833V45.4166C66.6667 47.7501 66.6667 48.9169 67.1208 49.8082C67.5203 50.5922 68.1577 51.2296 68.9417 51.6291C69.833 52.0832 70.9998 52.0833 73.3333 52.0833Z" stroke="black" stroke-width="8.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |