mirror of https://github.com/status-im/consul.git
Displays Consul version of each nodes in UI nodes section (#17754)
* update UINodes and UINodeInfo response with consul-version info added as NodeMeta, fetched from serf members * update test cases TestUINodes, TestUINodeInfo * added nil check for map * add consul-version in local agent node metadata * get consul version from serf member and add this as node meta in catalog register request * updated ui mock response to include consul versions as node meta * updated ui trans and added version as query param to node list route * updates in ui templates to display consul version with filter and sorts * updates in ui - model class, serializers,comparators,predicates for consul version feature * added change log for Consul Version Feature * updated to get version from consul service, if for some reason not available from serf * updated changelog text * updated dependent testcases * multiselection version filter * Update agent/consul/state/catalog.go comments updated Co-authored-by: Jared Kirschner <85913323+jkirschner-hashicorp@users.noreply.github.com> --------- Co-authored-by: Jared Kirschner <85913323+jkirschner-hashicorp@users.noreply.github.com>
This commit is contained in:
parent
f51a9d29ae
commit
2f20c77e4d
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:feature
|
||||||
|
ui: Display the Consul agent version in the nodes list, and allow filtering and sorting of nodes based on versions.
|
||||||
|
```
|
|
@ -3999,6 +3999,7 @@ func (a *Agent) loadMetadata(conf *config.RuntimeConfig) error {
|
||||||
meta[k] = v
|
meta[k] = v
|
||||||
}
|
}
|
||||||
meta[structs.MetaSegmentKey] = conf.SegmentName
|
meta[structs.MetaSegmentKey] = conf.SegmentName
|
||||||
|
meta[structs.MetaConsulVersion] = conf.Version
|
||||||
return a.State.LoadMetadata(meta)
|
return a.State.LoadMetadata(meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1506,7 +1506,8 @@ func TestAgent_Self(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, cs[a.config.SegmentName], val.Coord)
|
require.Equal(t, cs[a.config.SegmentName], val.Coord)
|
||||||
|
|
||||||
delete(val.Meta, structs.MetaSegmentKey) // Added later, not in config.
|
delete(val.Meta, structs.MetaSegmentKey) // Added later, not in config.
|
||||||
|
delete(val.Meta, structs.MetaConsulVersion) // Added later, not in config.
|
||||||
require.Equal(t, a.config.NodeMeta, val.Meta)
|
require.Equal(t, a.config.NodeMeta, val.Meta)
|
||||||
|
|
||||||
if tc.expectXDS {
|
if tc.expectXDS {
|
||||||
|
|
|
@ -1087,6 +1087,13 @@ AFTER_CHECK:
|
||||||
"partition", getSerfMemberEnterpriseMeta(member).PartitionOrDefault(),
|
"partition", getSerfMemberEnterpriseMeta(member).PartitionOrDefault(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Get consul version from serf member
|
||||||
|
// add this as node meta in catalog register request
|
||||||
|
buildVersion, err := metadata.Build(&member)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Register with the catalog.
|
// Register with the catalog.
|
||||||
req := structs.RegisterRequest{
|
req := structs.RegisterRequest{
|
||||||
Datacenter: s.config.Datacenter,
|
Datacenter: s.config.Datacenter,
|
||||||
|
@ -1102,6 +1109,9 @@ AFTER_CHECK:
|
||||||
Output: structs.SerfCheckAliveOutput,
|
Output: structs.SerfCheckAliveOutput,
|
||||||
},
|
},
|
||||||
EnterpriseMeta: *nodeEntMeta,
|
EnterpriseMeta: *nodeEntMeta,
|
||||||
|
NodeMeta: map[string]string{
|
||||||
|
structs.MetaConsulVersion: buildVersion.String(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
if node != nil {
|
if node != nil {
|
||||||
req.TaggedAddresses = node.TaggedAddresses
|
req.TaggedAddresses = node.TaggedAddresses
|
||||||
|
|
|
@ -3450,6 +3450,13 @@ func parseNodes(tx ReadTxn, ws memdb.WatchSet, idx uint64,
|
||||||
ws.AddWithLimit(watchLimit, services.WatchCh(), allServicesCh)
|
ws.AddWithLimit(watchLimit, services.WatchCh(), allServicesCh)
|
||||||
for service := services.Next(); service != nil; service = services.Next() {
|
for service := services.Next(); service != nil; service = services.Next() {
|
||||||
ns := service.(*structs.ServiceNode).ToNodeService()
|
ns := service.(*structs.ServiceNode).ToNodeService()
|
||||||
|
// If version isn't defined in node meta, set it from the Consul service meta
|
||||||
|
if _, ok := dump.Meta[structs.MetaConsulVersion]; !ok && ns.ID == "consul" && ns.Meta["version"] != "" {
|
||||||
|
if dump.Meta == nil {
|
||||||
|
dump.Meta = make(map[string]string)
|
||||||
|
}
|
||||||
|
dump.Meta[structs.MetaConsulVersion] = ns.Meta["version"]
|
||||||
|
}
|
||||||
dump.Services = append(dump.Services, ns)
|
dump.Services = append(dump.Services, ns)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4837,6 +4837,9 @@ func TestStateStore_NodeInfo_NodeDump(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register some nodes
|
// Register some nodes
|
||||||
|
// node1 is registered withOut any nodemeta, and a consul service with id
|
||||||
|
// 'consul' is added later with meta 'version'. The expected node must have
|
||||||
|
// meta 'consul-version' with same value
|
||||||
testRegisterNode(t, s, 0, "node1")
|
testRegisterNode(t, s, 0, "node1")
|
||||||
testRegisterNode(t, s, 1, "node2")
|
testRegisterNode(t, s, 1, "node2")
|
||||||
|
|
||||||
|
@ -4845,6 +4848,8 @@ func TestStateStore_NodeInfo_NodeDump(t *testing.T) {
|
||||||
testRegisterService(t, s, 3, "node1", "service2")
|
testRegisterService(t, s, 3, "node1", "service2")
|
||||||
testRegisterService(t, s, 4, "node2", "service1")
|
testRegisterService(t, s, 4, "node2", "service1")
|
||||||
testRegisterService(t, s, 5, "node2", "service2")
|
testRegisterService(t, s, 5, "node2", "service2")
|
||||||
|
// Register consul service with meta 'version' for node1
|
||||||
|
testRegisterServiceWithMeta(t, s, 10, "node1", "consul", map[string]string{"version": "1.17.0"})
|
||||||
|
|
||||||
// Register service-level checks
|
// Register service-level checks
|
||||||
testRegisterCheck(t, s, 6, "node1", "service1", "check1", api.HealthPassing)
|
testRegisterCheck(t, s, 6, "node1", "service1", "check1", api.HealthPassing)
|
||||||
|
@ -4894,6 +4899,19 @@ func TestStateStore_NodeInfo_NodeDump(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Services: []*structs.NodeService{
|
Services: []*structs.NodeService{
|
||||||
|
{
|
||||||
|
ID: "consul",
|
||||||
|
Service: "consul",
|
||||||
|
Address: "1.1.1.1",
|
||||||
|
Meta: map[string]string{"version": "1.17.0"},
|
||||||
|
Port: 1111,
|
||||||
|
Weights: &structs.Weights{Passing: 1, Warning: 1},
|
||||||
|
RaftIndex: structs.RaftIndex{
|
||||||
|
CreateIndex: 10,
|
||||||
|
ModifyIndex: 10,
|
||||||
|
},
|
||||||
|
EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ID: "service1",
|
ID: "service1",
|
||||||
Service: "service1",
|
Service: "service1",
|
||||||
|
@ -4921,6 +4939,7 @@ func TestStateStore_NodeInfo_NodeDump(t *testing.T) {
|
||||||
EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(),
|
EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Meta: map[string]string{"consul-version": "1.17.0"},
|
||||||
},
|
},
|
||||||
&structs.NodeInfo{
|
&structs.NodeInfo{
|
||||||
Node: "node2",
|
Node: "node2",
|
||||||
|
@ -4988,7 +5007,7 @@ func TestStateStore_NodeInfo_NodeDump(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
if idx != 9 {
|
if idx != 10 {
|
||||||
t.Fatalf("bad index: %d", idx)
|
t.Fatalf("bad index: %d", idx)
|
||||||
}
|
}
|
||||||
require.Len(t, dump, 1)
|
require.Len(t, dump, 1)
|
||||||
|
@ -4999,8 +5018,8 @@ func TestStateStore_NodeInfo_NodeDump(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
if idx != 9 {
|
if idx != 10 {
|
||||||
t.Fatalf("bad index: %d", 9)
|
t.Fatalf("bad index: %d", idx)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(dump, expect) {
|
if !reflect.DeepEqual(dump, expect) {
|
||||||
t.Fatalf("bad: %#v", dump[0].Services[0])
|
t.Fatalf("bad: %#v", dump[0].Services[0])
|
||||||
|
|
|
@ -189,6 +189,37 @@ func testRegisterServiceWithChangeOpts(t *testing.T, s *Store, idx uint64, nodeI
|
||||||
return svc
|
return svc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testRegisterServiceWithMeta registers service with Meta passed as arg.
|
||||||
|
func testRegisterServiceWithMeta(t *testing.T, s *Store, idx uint64, nodeID, serviceID string, meta map[string]string, opts ...func(service *structs.NodeService)) *structs.NodeService {
|
||||||
|
svc := &structs.NodeService{
|
||||||
|
ID: serviceID,
|
||||||
|
Service: serviceID,
|
||||||
|
Address: "1.1.1.1",
|
||||||
|
Port: 1111,
|
||||||
|
Meta: meta,
|
||||||
|
}
|
||||||
|
for _, o := range opts {
|
||||||
|
o(svc)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.EnsureService(idx, nodeID, svc); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := s.db.Txn(false)
|
||||||
|
defer tx.Abort()
|
||||||
|
service, err := tx.First(tableServices, indexID, NodeServiceQuery{Node: nodeID, Service: serviceID, PeerName: svc.PeerName})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
if result, ok := service.(*structs.ServiceNode); !ok ||
|
||||||
|
result.Node != nodeID ||
|
||||||
|
result.ServiceID != serviceID {
|
||||||
|
t.Fatalf("bad service: %#v", result)
|
||||||
|
}
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
// testRegisterService register a service with given transaction idx
|
// testRegisterService register a service with given transaction idx
|
||||||
// If the service already exists, transaction number might not be increased
|
// If the service already exists, transaction number might not be increased
|
||||||
// Use `testRegisterServiceWithChange()` if you want perform a registration that
|
// Use `testRegisterServiceWithChange()` if you want perform a registration that
|
||||||
|
|
|
@ -189,7 +189,8 @@ func TestAgentAntiEntropy_Services(t *testing.T) {
|
||||||
id := services.NodeServices.Node.ID
|
id := services.NodeServices.Node.ID
|
||||||
addrs := services.NodeServices.Node.TaggedAddresses
|
addrs := services.NodeServices.Node.TaggedAddresses
|
||||||
meta := services.NodeServices.Node.Meta
|
meta := services.NodeServices.Node.Meta
|
||||||
delete(meta, structs.MetaSegmentKey) // Added later, not in config.
|
delete(meta, structs.MetaSegmentKey) // Added later, not in config.
|
||||||
|
delete(meta, structs.MetaConsulVersion) // Added later, not in config.
|
||||||
assert.Equal(t, a.Config.NodeID, id)
|
assert.Equal(t, a.Config.NodeID, id)
|
||||||
assert.Equal(t, a.Config.TaggedAddresses, addrs)
|
assert.Equal(t, a.Config.TaggedAddresses, addrs)
|
||||||
assert.Equal(t, unNilMap(a.Config.NodeMeta), meta)
|
assert.Equal(t, unNilMap(a.Config.NodeMeta), meta)
|
||||||
|
@ -1355,7 +1356,8 @@ func TestAgentAntiEntropy_Checks(t *testing.T) {
|
||||||
id := services.NodeServices.Node.ID
|
id := services.NodeServices.Node.ID
|
||||||
addrs := services.NodeServices.Node.TaggedAddresses
|
addrs := services.NodeServices.Node.TaggedAddresses
|
||||||
meta := services.NodeServices.Node.Meta
|
meta := services.NodeServices.Node.Meta
|
||||||
delete(meta, structs.MetaSegmentKey) // Added later, not in config.
|
delete(meta, structs.MetaSegmentKey) // Added later, not in config.
|
||||||
|
delete(meta, structs.MetaConsulVersion) // Added later, not in config.
|
||||||
assert.Equal(r, a.Config.NodeID, id)
|
assert.Equal(r, a.Config.NodeID, id)
|
||||||
assert.Equal(r, a.Config.TaggedAddresses, addrs)
|
assert.Equal(r, a.Config.TaggedAddresses, addrs)
|
||||||
assert.Equal(r, unNilMap(a.Config.NodeMeta), meta)
|
assert.Equal(r, unNilMap(a.Config.NodeMeta), meta)
|
||||||
|
@ -2016,7 +2018,8 @@ func TestAgentAntiEntropy_NodeInfo(t *testing.T) {
|
||||||
addrs := services.NodeServices.Node.TaggedAddresses
|
addrs := services.NodeServices.Node.TaggedAddresses
|
||||||
meta := services.NodeServices.Node.Meta
|
meta := services.NodeServices.Node.Meta
|
||||||
nodeLocality := services.NodeServices.Node.Locality
|
nodeLocality := services.NodeServices.Node.Locality
|
||||||
delete(meta, structs.MetaSegmentKey) // Added later, not in config.
|
delete(meta, structs.MetaSegmentKey) // Added later, not in config.
|
||||||
|
delete(meta, structs.MetaConsulVersion) // Added later, not in config.
|
||||||
require.Equal(t, a.Config.NodeID, id)
|
require.Equal(t, a.Config.NodeID, id)
|
||||||
require.Equal(t, a.Config.TaggedAddresses, addrs)
|
require.Equal(t, a.Config.TaggedAddresses, addrs)
|
||||||
require.Equal(t, a.Config.StructLocality(), nodeLocality)
|
require.Equal(t, a.Config.StructLocality(), nodeLocality)
|
||||||
|
@ -2041,7 +2044,8 @@ func TestAgentAntiEntropy_NodeInfo(t *testing.T) {
|
||||||
addrs := services.NodeServices.Node.TaggedAddresses
|
addrs := services.NodeServices.Node.TaggedAddresses
|
||||||
meta := services.NodeServices.Node.Meta
|
meta := services.NodeServices.Node.Meta
|
||||||
nodeLocality := services.NodeServices.Node.Locality
|
nodeLocality := services.NodeServices.Node.Locality
|
||||||
delete(meta, structs.MetaSegmentKey) // Added later, not in config.
|
delete(meta, structs.MetaSegmentKey) // Added later, not in config.
|
||||||
|
delete(meta, structs.MetaConsulVersion) // Added later, not in config.
|
||||||
require.Equal(t, nodeID, id)
|
require.Equal(t, nodeID, id)
|
||||||
require.Equal(t, a.Config.TaggedAddresses, addrs)
|
require.Equal(t, a.Config.TaggedAddresses, addrs)
|
||||||
require.Equal(t, a.Config.StructLocality(), nodeLocality)
|
require.Equal(t, a.Config.StructLocality(), nodeLocality)
|
||||||
|
|
|
@ -220,6 +220,9 @@ const (
|
||||||
// WildcardSpecifier is the string which should be used for specifying a wildcard
|
// WildcardSpecifier is the string which should be used for specifying a wildcard
|
||||||
// The exact semantics of the wildcard is left up to the code where its used.
|
// The exact semantics of the wildcard is left up to the code where its used.
|
||||||
WildcardSpecifier = "*"
|
WildcardSpecifier = "*"
|
||||||
|
|
||||||
|
// MetaConsulVersion is the node metadata key used to store the node's consul version
|
||||||
|
MetaConsulVersion = "consul-version"
|
||||||
)
|
)
|
||||||
|
|
||||||
var allowedConsulMetaKeysForMeshGateway = map[string]struct{}{MetaWANFederationKey: {}}
|
var allowedConsulMetaKeysForMeshGateway = map[string]struct{}{MetaWANFederationKey: {}}
|
||||||
|
|
|
@ -13,9 +13,12 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/go-hclog"
|
"github.com/hashicorp/go-hclog"
|
||||||
|
"github.com/hashicorp/serf/serf"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/acl"
|
"github.com/hashicorp/consul/acl"
|
||||||
"github.com/hashicorp/consul/agent/config"
|
"github.com/hashicorp/consul/agent/config"
|
||||||
|
"github.com/hashicorp/consul/agent/consul"
|
||||||
|
"github.com/hashicorp/consul/agent/metadata"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/hashicorp/consul/api"
|
"github.com/hashicorp/consul/api"
|
||||||
"github.com/hashicorp/consul/logging"
|
"github.com/hashicorp/consul/logging"
|
||||||
|
@ -110,7 +113,18 @@ RPC:
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get version info for all serf members into a map of key-address,value-version.
|
||||||
|
// This logic of calling 'AgentMembersMapAddrVer()' and inserting version info in this func
|
||||||
|
// can be discarded in future releases ( may be after 3 or 4 minor releases),
|
||||||
|
// when all the nodes are registered with consul-version in nodemeta.
|
||||||
|
var err error
|
||||||
|
mapAddrVer, err := AgentMembersMapAddrVer(s, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Use empty list instead of nil
|
// Use empty list instead of nil
|
||||||
|
// Also check if consul-version exists in Meta, else add it
|
||||||
for _, info := range out.Dump {
|
for _, info := range out.Dump {
|
||||||
if info.Services == nil {
|
if info.Services == nil {
|
||||||
info.Services = make([]*structs.NodeService, 0)
|
info.Services = make([]*structs.NodeService, 0)
|
||||||
|
@ -118,12 +132,24 @@ RPC:
|
||||||
if info.Checks == nil {
|
if info.Checks == nil {
|
||||||
info.Checks = make([]*structs.HealthCheck, 0)
|
info.Checks = make([]*structs.HealthCheck, 0)
|
||||||
}
|
}
|
||||||
|
// Check if Node Meta - 'consul-version' already exists by virtue of adding
|
||||||
|
// 'consul-version' during node registration itself.
|
||||||
|
// If not, get it from mapAddrVer.
|
||||||
|
if _, ok := info.Meta[structs.MetaConsulVersion]; !ok {
|
||||||
|
if _, okver := mapAddrVer[info.Address]; okver {
|
||||||
|
if info.Meta == nil {
|
||||||
|
info.Meta = make(map[string]string)
|
||||||
|
}
|
||||||
|
info.Meta[structs.MetaConsulVersion] = mapAddrVer[info.Address]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if out.Dump == nil {
|
if out.Dump == nil {
|
||||||
out.Dump = make(structs.NodeDump, 0)
|
out.Dump = make(structs.NodeDump, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use empty list instead of nil
|
// Use empty list instead of nil
|
||||||
|
// Also check if consul-version exists in Meta, else add it
|
||||||
for _, info := range out.ImportedDump {
|
for _, info := range out.ImportedDump {
|
||||||
if info.Services == nil {
|
if info.Services == nil {
|
||||||
info.Services = make([]*structs.NodeService, 0)
|
info.Services = make([]*structs.NodeService, 0)
|
||||||
|
@ -131,11 +157,60 @@ RPC:
|
||||||
if info.Checks == nil {
|
if info.Checks == nil {
|
||||||
info.Checks = make([]*structs.HealthCheck, 0)
|
info.Checks = make([]*structs.HealthCheck, 0)
|
||||||
}
|
}
|
||||||
|
// Check if Node Meta - 'consul-version' already exists by virtue of adding
|
||||||
|
// 'consul-version' during node registration itself.
|
||||||
|
// If not, get it from mapAddrVer.
|
||||||
|
if _, ok := info.Meta[structs.MetaConsulVersion]; !ok {
|
||||||
|
if _, okver := mapAddrVer[info.Address]; okver {
|
||||||
|
if info.Meta == nil {
|
||||||
|
info.Meta = make(map[string]string)
|
||||||
|
}
|
||||||
|
info.Meta[structs.MetaConsulVersion] = mapAddrVer[info.Address]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return append(out.Dump, out.ImportedDump...), nil
|
return append(out.Dump, out.ImportedDump...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AgentMembersMapAddrVer is used to get version info from all serf members into a
|
||||||
|
// map of key-address,value-version.
|
||||||
|
func AgentMembersMapAddrVer(s *HTTPHandlers, req *http.Request) (map[string]string, error) {
|
||||||
|
var members []serf.Member
|
||||||
|
|
||||||
|
//Get WAN Members
|
||||||
|
wanMembers := s.agent.WANMembers()
|
||||||
|
|
||||||
|
//Get LAN Members
|
||||||
|
//Get the request partition and default to that of the agent.
|
||||||
|
entMeta := s.agent.AgentEnterpriseMeta()
|
||||||
|
if err := s.parseEntMetaPartition(req, entMeta); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
filter := consul.LANMemberFilter{
|
||||||
|
Partition: entMeta.PartitionOrDefault(),
|
||||||
|
}
|
||||||
|
filter.AllSegments = true
|
||||||
|
lanMembers, err := s.agent.delegate.LANMembers(filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//aggregate members
|
||||||
|
members = append(wanMembers, lanMembers...)
|
||||||
|
|
||||||
|
//create a map with key as IPv4 address and value as consul-version
|
||||||
|
mapAddrVer := make(map[string]string, len(members))
|
||||||
|
for i := range members {
|
||||||
|
buildVersion, err := metadata.Build(&members[i])
|
||||||
|
if err == nil {
|
||||||
|
mapAddrVer[members[i].Addr.String()] = buildVersion.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapAddrVer, nil
|
||||||
|
}
|
||||||
|
|
||||||
// UINodeInfo is used to get info on a single node in a given datacenter. We return a
|
// UINodeInfo is used to get info on a single node in a given datacenter. We return a
|
||||||
// NodeInfo which provides overview information for the node
|
// NodeInfo which provides overview information for the node
|
||||||
func (s *HTTPHandlers) UINodeInfo(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
func (s *HTTPHandlers) UINodeInfo(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||||
|
@ -172,6 +247,16 @@ RPC:
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get version info for all serf members into a map of key-address,value-version.
|
||||||
|
// This logic of calling 'AgentMembersMapAddrVer()' and inserting version info in this func
|
||||||
|
// can be discarded in future releases ( may be after 3 or 4 minor releases),
|
||||||
|
// when all the nodes are registered with consul-version in nodemeta.
|
||||||
|
var err error
|
||||||
|
mapAddrVer, err := AgentMembersMapAddrVer(s, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Return only the first entry
|
// Return only the first entry
|
||||||
if len(out.Dump) > 0 {
|
if len(out.Dump) > 0 {
|
||||||
info := out.Dump[0]
|
info := out.Dump[0]
|
||||||
|
@ -181,6 +266,17 @@ RPC:
|
||||||
if info.Checks == nil {
|
if info.Checks == nil {
|
||||||
info.Checks = make([]*structs.HealthCheck, 0)
|
info.Checks = make([]*structs.HealthCheck, 0)
|
||||||
}
|
}
|
||||||
|
// Check if Node Meta - 'consul-version' already exists by virtue of adding
|
||||||
|
// 'consul-version' during node registration itself.
|
||||||
|
// If not, get it from mapAddrVer.
|
||||||
|
if _, ok := info.Meta[structs.MetaConsulVersion]; !ok {
|
||||||
|
if _, okver := mapAddrVer[info.Address]; okver {
|
||||||
|
if info.Meta == nil {
|
||||||
|
info.Meta = make(map[string]string)
|
||||||
|
}
|
||||||
|
info.Meta[structs.MetaConsulVersion] = mapAddrVer[info.Address]
|
||||||
|
}
|
||||||
|
}
|
||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -162,6 +162,9 @@ func TestUINodes(t *testing.T) {
|
||||||
require.Len(t, nodes[2].Services, 0)
|
require.Len(t, nodes[2].Services, 0)
|
||||||
require.NotNil(t, nodes[1].Checks)
|
require.NotNil(t, nodes[1].Checks)
|
||||||
require.Len(t, nodes[2].Services, 0)
|
require.Len(t, nodes[2].Services, 0)
|
||||||
|
|
||||||
|
// check for consul-version in node meta
|
||||||
|
require.Equal(t, nodes[0].Meta[structs.MetaConsulVersion], a.Config.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUINodes_Filter(t *testing.T) {
|
func TestUINodes_Filter(t *testing.T) {
|
||||||
|
@ -260,6 +263,9 @@ func TestUINodeInfo(t *testing.T) {
|
||||||
node.Checks == nil || len(node.Checks) != 0 {
|
node.Checks == nil || len(node.Checks) != 0 {
|
||||||
t.Fatalf("bad: %v", node)
|
t.Fatalf("bad: %v", node)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check for consul-version in node meta
|
||||||
|
require.Equal(t, node.Meta[structs.MetaConsulVersion], a.Config.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUIServices(t *testing.T) {
|
func TestUIServices(t *testing.T) {
|
||||||
|
|
|
@ -65,6 +65,7 @@ func TestAPI_CatalogNodes(t *testing.T) {
|
||||||
},
|
},
|
||||||
Meta: map[string]string{
|
Meta: map[string]string{
|
||||||
"consul-network-segment": "",
|
"consul-network-segment": "",
|
||||||
|
"consul-version": s.Config.Version,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
require.Equal(r, want, got)
|
require.Equal(r, want, got)
|
||||||
|
|
|
@ -361,7 +361,10 @@ func TestAPI_ClientTxn(t *testing.T) {
|
||||||
"wan": s.Config.Bind,
|
"wan": s.Config.Bind,
|
||||||
"wan_ipv4": s.Config.Bind,
|
"wan_ipv4": s.Config.Bind,
|
||||||
},
|
},
|
||||||
Meta: map[string]string{"consul-network-segment": ""},
|
Meta: map[string]string{
|
||||||
|
"consul-network-segment": "",
|
||||||
|
"consul-version": s.Config.Version,
|
||||||
|
},
|
||||||
CreateIndex: ret.Results[1].Node.CreateIndex,
|
CreateIndex: ret.Results[1].Node.CreateIndex,
|
||||||
ModifyIndex: ret.Results[1].Node.ModifyIndex,
|
ModifyIndex: ret.Results[1].Node.ModifyIndex,
|
||||||
},
|
},
|
||||||
|
|
|
@ -130,6 +130,7 @@ type TestServerConfig struct {
|
||||||
Args []string `json:"-"`
|
Args []string `json:"-"`
|
||||||
ReturnPorts func() `json:"-"`
|
ReturnPorts func() `json:"-"`
|
||||||
Audit *TestAuditConfig `json:"audit,omitempty"`
|
Audit *TestAuditConfig `json:"audit,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TestACLs struct {
|
type TestACLs struct {
|
||||||
|
@ -212,6 +213,7 @@ func defaultServerConfig(t TestingTB, consulVersion *version.Version) *TestServe
|
||||||
Stdout: logBuffer,
|
Stdout: logBuffer,
|
||||||
Stderr: logBuffer,
|
Stderr: logBuffer,
|
||||||
Peering: &TestPeeringConfig{Enabled: true},
|
Peering: &TestPeeringConfig{Enabled: true},
|
||||||
|
Version: consulVersion.String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add version-specific tweaks
|
// Add version-specific tweaks
|
||||||
|
|
|
@ -50,5 +50,17 @@ as |item index|>
|
||||||
{{item.Address}}
|
{{item.Address}}
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
<dl>
|
||||||
|
<dt>
|
||||||
|
<span>ConsulVersion</span>
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{!-- Displaying consul version from node meta data --}}
|
||||||
|
{{#if item.Meta.consul-version}}
|
||||||
|
<FlightIcon class='w-4 h-4' @size='24' @name='consul-color' @stretched={{true}} />
|
||||||
|
<span>v{{item.Meta.consul-version}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
</ListCollection>
|
</ListCollection>
|
||||||
|
|
|
@ -19,12 +19,14 @@
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
(t (concat "components.consul.node.search-bar." search.status.value)
|
(if search.status.value
|
||||||
default=(array
|
search.status.value
|
||||||
(concat "common.search." search.status.value)
|
(t (concat "components.consul.node.search-bar." search.status.value)
|
||||||
(concat "common.consul." search.status.value)
|
default=(array
|
||||||
(concat "common.brand." search.status.value)
|
(concat "common.search." search.status.value)
|
||||||
)
|
(concat "common.consul." search.status.value)
|
||||||
|
(concat "common.brand." search.status.value)
|
||||||
|
))
|
||||||
)
|
)
|
||||||
|
|
||||||
as |key value|}}
|
as |key value|}}
|
||||||
|
@ -95,6 +97,27 @@ as |key value|}}
|
||||||
{{/let}}
|
{{/let}}
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
</search.Select>
|
</search.Select>
|
||||||
|
<search.Select
|
||||||
|
class="type-version"
|
||||||
|
@position="left"
|
||||||
|
@onchange={{action @filter.version.change}}
|
||||||
|
@multiple={{true}}
|
||||||
|
as |components|>
|
||||||
|
<BlockSlot @name="selected">
|
||||||
|
<span>
|
||||||
|
{{t "common.consul.version"}}
|
||||||
|
</span>
|
||||||
|
</BlockSlot>
|
||||||
|
<BlockSlot @name="options">
|
||||||
|
{{#let components.Optgroup components.Option as |Optgroup Option|}}
|
||||||
|
{{#each @versions as |version|}}
|
||||||
|
<Option @value={{version}} @selected={{includes version @filter.version.value}}>
|
||||||
|
{{concat version ".x" }}
|
||||||
|
</Option>
|
||||||
|
{{/each}}
|
||||||
|
{{/let}}
|
||||||
|
</BlockSlot>
|
||||||
|
</search.Select>
|
||||||
</:filter>
|
</:filter>
|
||||||
<:sort as |search|>
|
<:sort as |search|>
|
||||||
<search.Select
|
<search.Select
|
||||||
|
@ -112,6 +135,8 @@ as |key value|}}
|
||||||
(array "Node:desc" (t "common.sort.alpha.desc"))
|
(array "Node:desc" (t "common.sort.alpha.desc"))
|
||||||
(array "Status:asc" (t "common.sort.status.asc"))
|
(array "Status:asc" (t "common.sort.status.asc"))
|
||||||
(array "Status:desc" (t "common.sort.status.desc"))
|
(array "Status:desc" (t "common.sort.status.desc"))
|
||||||
|
(array "Version:asc" (t "common.sort.version.asc"))
|
||||||
|
(array "Version:desc" (t "common.sort.version.desc"))
|
||||||
))
|
))
|
||||||
as |selectable|
|
as |selectable|
|
||||||
}}
|
}}
|
||||||
|
@ -129,6 +154,10 @@ as |key value|}}
|
||||||
<Option @value="Node:asc" @selected={{eq "Node:asc" @sort.value}}>{{t "common.sort.alpha.asc"}}</Option>
|
<Option @value="Node:asc" @selected={{eq "Node:asc" @sort.value}}>{{t "common.sort.alpha.asc"}}</Option>
|
||||||
<Option @value="Node:desc" @selected={{eq "Node:desc" @sort.value}}>{{t "common.sort.alpha.desc"}}</Option>
|
<Option @value="Node:desc" @selected={{eq "Node:desc" @sort.value}}>{{t "common.sort.alpha.desc"}}</Option>
|
||||||
</Optgroup>
|
</Optgroup>
|
||||||
|
<Optgroup @label={{t "common.consul.version"}}>
|
||||||
|
<Option @value="Version:asc" @selected={{eq "Version:asc" @sort.value}}>{{t "common.sort.version.asc"}}</Option>
|
||||||
|
<Option @value="Version:desc" @selected={{eq "Version:desc" @sort.value}}>{{t "common.sort.version.desc"}}</Option>
|
||||||
|
</Optgroup>
|
||||||
{{/let}}
|
{{/let}}
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
</search.Select>
|
</search.Select>
|
||||||
|
|
|
@ -9,4 +9,12 @@ export default {
|
||||||
warning: (item, value) => item.Status === value,
|
warning: (item, value) => item.Status === value,
|
||||||
critical: (item, value) => item.Status === value,
|
critical: (item, value) => item.Status === value,
|
||||||
},
|
},
|
||||||
|
version: (item, value) => {
|
||||||
|
for (const element of value) {
|
||||||
|
if (item.Version.includes(element + '.')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -67,4 +67,9 @@ export default class Node extends Model {
|
||||||
get ChecksWarning() {
|
get ChecksWarning() {
|
||||||
return this.NodeChecks.filter((item) => item.Status === 'warning').length;
|
return this.NodeChecks.filter((item) => item.Status === 'warning').length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed('Meta')
|
||||||
|
get Version() {
|
||||||
|
return this.Meta?.['consul-version'] ?? '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -156,6 +156,10 @@ export default class ApplicationSerializer extends Serializer {
|
||||||
// ember-data methods so we have the opportunity to do this on a per-model
|
// ember-data methods so we have the opportunity to do this on a per-model
|
||||||
// level
|
// level
|
||||||
const meta = this.normalizeMeta(store, modelClass, normalizedPayload, id, requestType);
|
const meta = this.normalizeMeta(store, modelClass, normalizedPayload, id, requestType);
|
||||||
|
// get distinct consul versions from list and add it as meta
|
||||||
|
if (modelClass.modelName === 'node' && requestType === 'query') {
|
||||||
|
meta.versions = this.getDistinctConsulVersions(normalizedPayload);
|
||||||
|
}
|
||||||
if (requestType !== 'query') {
|
if (requestType !== 'query') {
|
||||||
normalizedPayload.meta = meta;
|
normalizedPayload.meta = meta;
|
||||||
}
|
}
|
||||||
|
@ -215,4 +219,45 @@ export default class ApplicationSerializer extends Serializer {
|
||||||
normalizePayload(payload, id, requestType) {
|
normalizePayload(payload, id, requestType) {
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getDistinctConsulVersions will be called only for nodes and query request type
|
||||||
|
// the list of versions is to be added as meta to resp, without changing original response structure
|
||||||
|
// hence this function is added in application.js
|
||||||
|
getDistinctConsulVersions(payload) {
|
||||||
|
// create a Set and add version with only major.minor : ex-1.24.6 as 1.24
|
||||||
|
let versionSet = new Set();
|
||||||
|
payload.forEach(function (item) {
|
||||||
|
if (item.Meta && item.Meta['consul-version'] !== '') {
|
||||||
|
const split = item.Meta['consul-version'].split('.');
|
||||||
|
versionSet.add(split[0] + '.' + split[1]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const versionArray = Array.from(versionSet);
|
||||||
|
|
||||||
|
// Sort the array in descending order using a custom comparison function
|
||||||
|
versionArray.sort((a, b) => {
|
||||||
|
// Split the versions into arrays of numbers
|
||||||
|
const versionA = a.split('.').map((part) => {
|
||||||
|
const number = Number(part);
|
||||||
|
return isNaN(number) ? 0 : number;
|
||||||
|
});
|
||||||
|
const versionB = b.split('.').map((part) => {
|
||||||
|
const number = Number(part);
|
||||||
|
return isNaN(number) ? 0 : number;
|
||||||
|
});
|
||||||
|
|
||||||
|
const minLength = Math.min(versionA.length, versionB.length);
|
||||||
|
|
||||||
|
// start with comparing major version num, if equal then compare minor
|
||||||
|
for (let i = 0; i < minLength; i++) {
|
||||||
|
if (versionA[i] !== versionB[i]) {
|
||||||
|
return versionB[i] - versionA[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return versionB.length - versionA.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
return versionArray; //sorted array
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,43 @@ export default ({ properties }) =>
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
} else if (key.startsWith('Version:')) {
|
||||||
|
return function (itemA, itemB) {
|
||||||
|
const [, dir] = key.split(':');
|
||||||
|
let a, b;
|
||||||
|
if (dir === 'asc') {
|
||||||
|
a = itemA;
|
||||||
|
b = itemB;
|
||||||
|
} else {
|
||||||
|
b = itemA;
|
||||||
|
a = itemB;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the versions into arrays of numbers
|
||||||
|
const versionA = a.Version.split('.').map((part) => {
|
||||||
|
const number = Number(part);
|
||||||
|
return isNaN(number) ? 0 : number;
|
||||||
|
});
|
||||||
|
const versionB = b.Version.split('.').map((part) => {
|
||||||
|
const number = Number(part);
|
||||||
|
return isNaN(number) ? 0 : number;
|
||||||
|
});
|
||||||
|
|
||||||
|
const minLength = Math.min(versionA.length, versionB.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < minLength; i++) {
|
||||||
|
const diff = versionA[i] - versionB[i];
|
||||||
|
switch (true) {
|
||||||
|
case diff > 0:
|
||||||
|
return 1;
|
||||||
|
case diff < 0:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return versionA.length - versionB.length;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return properties(['Node'])(key);
|
return properties(['Node'])(key);
|
||||||
};
|
};
|
||||||
|
|
|
@ -40,10 +40,15 @@
|
||||||
change=(action (mut searchproperty) value='target.selectedItems')
|
change=(action (mut searchproperty) value='target.selectedItems')
|
||||||
default=this._searchProperties
|
default=this._searchProperties
|
||||||
)
|
)
|
||||||
|
version=(hash
|
||||||
|
value=(if this.version (split this.version ',') undefined)
|
||||||
|
change=(action (mut this.version) value='target.selectedItems')
|
||||||
|
)
|
||||||
)
|
)
|
||||||
api.data
|
api.data
|
||||||
leader.data
|
leader.data
|
||||||
as |sort filters items leader|
|
api.data.meta.versions
|
||||||
|
as |sort filters items leader versions|
|
||||||
}}
|
}}
|
||||||
{{#let (reject-by 'Meta.synthetic-node' items) as |filtered|}}
|
{{#let (reject-by 'Meta.synthetic-node' items) as |filtered|}}
|
||||||
<AppView>
|
<AppView>
|
||||||
|
@ -61,6 +66,7 @@
|
||||||
@onsearch={{action (mut search) value='target.value'}}
|
@onsearch={{action (mut search) value='target.value'}}
|
||||||
@sort={{sort}}
|
@sort={{sort}}
|
||||||
@filter={{filters}}
|
@filter={{filters}}
|
||||||
|
@versions={{versions}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
|
|
|
@ -30,6 +30,7 @@ return `
|
||||||
"TaggedAddresses":{"lan":"${ip}","wan":"${ip}"},
|
"TaggedAddresses":{"lan":"${ip}","wan":"${ip}"},
|
||||||
"Meta":{
|
"Meta":{
|
||||||
"consul-network-segment":"",
|
"consul-network-segment":"",
|
||||||
|
"consul-version": "${env('CONSUL_VERSION') ? fake.helpers.randomize([env('CONSUL_VERSION'),"1.10.4","1.15.2", "1.17.8","1.7.2","1.12.4", "1.17.2","1.0.9","2.0.2"]) : fake.helpers.randomize(["1.10.4","1.15.2", "1.17.8","1.7.2","1.12.4", "1.17.2","1.0.9","2.0.2"]) }",
|
||||||
"consul-dashboard-url": "${fake.internet.protocol()}://${fake.internet.domainName()}/?id={{Node}}"
|
"consul-dashboard-url": "${fake.internet.protocol()}://${fake.internet.domainName()}/?id={{Node}}"
|
||||||
},
|
},
|
||||||
"Services":[
|
"Services":[
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
},
|
},
|
||||||
"Meta": {
|
"Meta": {
|
||||||
"consul-network-segment":"",
|
"consul-network-segment":"",
|
||||||
|
"consul-version": "${env('CONSUL_VERSION') ? fake.helpers.randomize([env('CONSUL_VERSION'),"1.10.4","1.15.2", "1.17.8","1.7.2","1.12.4", "1.17.2","1.0.9","2.0.2"]) : fake.helpers.randomize(["1.10.4","1.15.2", "1.17.8","1.7.2","1.12.4", "1.17.2","1.0.9","2.0.2"]) }",
|
||||||
"synthetic-node": ${env('CONSUL_AGENTLESS_ENABLED') ? fake.helpers.randomize([true, false, false, false]) : false}
|
"synthetic-node": ${env('CONSUL_AGENTLESS_ENABLED') ? fake.helpers.randomize([true, false, false, false]) : false}
|
||||||
},
|
},
|
||||||
"Services":[
|
"Services":[
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) HashiCorp, Inc.
|
||||||
|
* SPDX-License-Identifier: MPL-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import comparators from 'consul-ui/sort/comparators/node';
|
||||||
|
import { properties } from 'consul-ui/services/sort';
|
||||||
|
import { module, test } from 'qunit';
|
||||||
|
|
||||||
|
module('Unit | Sort | Comparator | node', function () {
|
||||||
|
const comparator = comparators({ properties });
|
||||||
|
test('items are sorted by a fake Version', function (assert) {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
Version: '2.24.1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: '1.12.6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: '2.09.3',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const comp = comparator('Version:asc');
|
||||||
|
assert.equal(typeof comp, 'function');
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
Version: '1.12.6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: '2.09.3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Version: '2.24.1',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let actual = items.sort(comp);
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
|
||||||
|
expected.reverse();
|
||||||
|
actual = items.sort(comparator('Version:desc'));
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
});
|
|
@ -53,6 +53,7 @@ consul:
|
||||||
redundancyzone: Redundancy zone
|
redundancyzone: Redundancy zone
|
||||||
peername: Peer
|
peername: Peer
|
||||||
partition: Admin Partitions
|
partition: Admin Partitions
|
||||||
|
version: Version
|
||||||
search:
|
search:
|
||||||
search: Search
|
search: Search
|
||||||
searchproperty: Search Across
|
searchproperty: Search Across
|
||||||
|
@ -77,6 +78,9 @@ sort:
|
||||||
status:
|
status:
|
||||||
asc: Unhealthy to Healthy
|
asc: Unhealthy to Healthy
|
||||||
desc: Healthy to Unhealthy
|
desc: Healthy to Unhealthy
|
||||||
|
version:
|
||||||
|
asc: Oldest to Latest
|
||||||
|
desc: Latest to Oldest
|
||||||
validations:
|
validations:
|
||||||
dns-hostname:
|
dns-hostname:
|
||||||
help: |
|
help: |
|
||||||
|
|
|
@ -218,6 +218,7 @@
|
||||||
queryParams: {
|
queryParams: {
|
||||||
sortBy: 'sort',
|
sortBy: 'sort',
|
||||||
status: 'status',
|
status: 'status',
|
||||||
|
version: 'version',
|
||||||
searchproperty: {
|
searchproperty: {
|
||||||
as: 'searchproperty',
|
as: 'searchproperty',
|
||||||
empty: [['Node', 'Address', 'Meta', 'PeerName']],
|
empty: [['Node', 'Address', 'Meta', 'PeerName']],
|
||||||
|
|
Loading…
Reference in New Issue