Browse Source

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>
pull/18081/head^2
Vijay 1 year ago committed by GitHub
parent
commit
2f20c77e4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .changelog/17754.txt
  2. 1
      agent/agent.go
  3. 3
      agent/agent_endpoint_test.go
  4. 10
      agent/consul/leader.go
  5. 7
      agent/consul/state/catalog.go
  6. 25
      agent/consul/state/catalog_test.go
  7. 31
      agent/consul/state/state_store_test.go
  8. 12
      agent/local/state_test.go
  9. 3
      agent/structs/structs.go
  10. 96
      agent/ui_endpoint.go
  11. 6
      agent/ui_endpoint_test.go
  12. 1
      api/catalog_test.go
  13. 5
      api/txn_test.go
  14. 2
      sdk/testutil/server.go
  15. 12
      ui/packages/consul-ui/app/components/consul/node/list/index.hbs
  16. 41
      ui/packages/consul-ui/app/components/consul/node/search-bar/index.hbs
  17. 8
      ui/packages/consul-ui/app/filter/predicates/node.js
  18. 5
      ui/packages/consul-ui/app/models/node.js
  19. 45
      ui/packages/consul-ui/app/serializers/application.js
  20. 37
      ui/packages/consul-ui/app/sort/comparators/node.js
  21. 8
      ui/packages/consul-ui/app/templates/dc/nodes/index.hbs
  22. 1
      ui/packages/consul-ui/mock-api/v1/internal/ui/node/_
  23. 1
      ui/packages/consul-ui/mock-api/v1/internal/ui/nodes
  24. 45
      ui/packages/consul-ui/tests/unit/sort/comparators/node-test.js
  25. 4
      ui/packages/consul-ui/translations/common/en-us.yaml
  26. 1
      ui/packages/consul-ui/vendor/consul-ui/routes.js

3
.changelog/17754.txt

@ -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.
```

1
agent/agent.go

@ -3999,6 +3999,7 @@ func (a *Agent) loadMetadata(conf *config.RuntimeConfig) error {
meta[k] = v
}
meta[structs.MetaSegmentKey] = conf.SegmentName
meta[structs.MetaConsulVersion] = conf.Version
return a.State.LoadMetadata(meta)
}

3
agent/agent_endpoint_test.go

@ -1506,7 +1506,8 @@ func TestAgent_Self(t *testing.T) {
require.NoError(t, err)
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)
if tc.expectXDS {

10
agent/consul/leader.go

@ -1087,6 +1087,13 @@ AFTER_CHECK:
"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.
req := structs.RegisterRequest{
Datacenter: s.config.Datacenter,
@ -1102,6 +1109,9 @@ AFTER_CHECK:
Output: structs.SerfCheckAliveOutput,
},
EnterpriseMeta: *nodeEntMeta,
NodeMeta: map[string]string{
structs.MetaConsulVersion: buildVersion.String(),
},
}
if node != nil {
req.TaggedAddresses = node.TaggedAddresses

7
agent/consul/state/catalog.go

@ -3450,6 +3450,13 @@ func parseNodes(tx ReadTxn, ws memdb.WatchSet, idx uint64,
ws.AddWithLimit(watchLimit, services.WatchCh(), allServicesCh)
for service := services.Next(); service != nil; service = services.Next() {
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)
}

25
agent/consul/state/catalog_test.go

@ -4837,6 +4837,9 @@ func TestStateStore_NodeInfo_NodeDump(t *testing.T) {
}
// 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, 1, "node2")
@ -4845,6 +4848,8 @@ func TestStateStore_NodeInfo_NodeDump(t *testing.T) {
testRegisterService(t, s, 3, "node1", "service2")
testRegisterService(t, s, 4, "node2", "service1")
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
testRegisterCheck(t, s, 6, "node1", "service1", "check1", api.HealthPassing)
@ -4894,6 +4899,19 @@ func TestStateStore_NodeInfo_NodeDump(t *testing.T) {
},
},
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",
Service: "service1",
@ -4921,6 +4939,7 @@ func TestStateStore_NodeInfo_NodeDump(t *testing.T) {
EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(),
},
},
Meta: map[string]string{"consul-version": "1.17.0"},
},
&structs.NodeInfo{
Node: "node2",
@ -4988,7 +5007,7 @@ func TestStateStore_NodeInfo_NodeDump(t *testing.T) {
if err != nil {
t.Fatalf("err: %s", err)
}
if idx != 9 {
if idx != 10 {
t.Fatalf("bad index: %d", idx)
}
require.Len(t, dump, 1)
@ -4999,8 +5018,8 @@ func TestStateStore_NodeInfo_NodeDump(t *testing.T) {
if err != nil {
t.Fatalf("err: %s", err)
}
if idx != 9 {
t.Fatalf("bad index: %d", 9)
if idx != 10 {
t.Fatalf("bad index: %d", idx)
}
if !reflect.DeepEqual(dump, expect) {
t.Fatalf("bad: %#v", dump[0].Services[0])

31
agent/consul/state/state_store_test.go

@ -189,6 +189,37 @@ func testRegisterServiceWithChangeOpts(t *testing.T, s *Store, idx uint64, nodeI
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
// If the service already exists, transaction number might not be increased
// Use `testRegisterServiceWithChange()` if you want perform a registration that

12
agent/local/state_test.go

@ -189,7 +189,8 @@ func TestAgentAntiEntropy_Services(t *testing.T) {
id := services.NodeServices.Node.ID
addrs := services.NodeServices.Node.TaggedAddresses
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.TaggedAddresses, addrs)
assert.Equal(t, unNilMap(a.Config.NodeMeta), meta)
@ -1355,7 +1356,8 @@ func TestAgentAntiEntropy_Checks(t *testing.T) {
id := services.NodeServices.Node.ID
addrs := services.NodeServices.Node.TaggedAddresses
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.TaggedAddresses, addrs)
assert.Equal(r, unNilMap(a.Config.NodeMeta), meta)
@ -2016,7 +2018,8 @@ func TestAgentAntiEntropy_NodeInfo(t *testing.T) {
addrs := services.NodeServices.Node.TaggedAddresses
meta := services.NodeServices.Node.Meta
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.TaggedAddresses, addrs)
require.Equal(t, a.Config.StructLocality(), nodeLocality)
@ -2041,7 +2044,8 @@ func TestAgentAntiEntropy_NodeInfo(t *testing.T) {
addrs := services.NodeServices.Node.TaggedAddresses
meta := services.NodeServices.Node.Meta
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, a.Config.TaggedAddresses, addrs)
require.Equal(t, a.Config.StructLocality(), nodeLocality)

3
agent/structs/structs.go

@ -220,6 +220,9 @@ const (
// 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.
WildcardSpecifier = "*"
// MetaConsulVersion is the node metadata key used to store the node's consul version
MetaConsulVersion = "consul-version"
)
var allowedConsulMetaKeysForMeshGateway = map[string]struct{}{MetaWANFederationKey: {}}

96
agent/ui_endpoint.go

@ -13,9 +13,12 @@ import (
"strings"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/serf/serf"
"github.com/hashicorp/consul/acl"
"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/api"
"github.com/hashicorp/consul/logging"
@ -110,7 +113,18 @@ RPC:
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
// Also check if consul-version exists in Meta, else add it
for _, info := range out.Dump {
if info.Services == nil {
info.Services = make([]*structs.NodeService, 0)
@ -118,12 +132,24 @@ RPC:
if info.Checks == nil {
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 {
out.Dump = make(structs.NodeDump, 0)
}
// Use empty list instead of nil
// Also check if consul-version exists in Meta, else add it
for _, info := range out.ImportedDump {
if info.Services == nil {
info.Services = make([]*structs.NodeService, 0)
@ -131,11 +157,60 @@ RPC:
if info.Checks == nil {
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
}
// 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
// NodeInfo which provides overview information for the node
func (s *HTTPHandlers) UINodeInfo(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
@ -172,6 +247,16 @@ RPC:
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
if len(out.Dump) > 0 {
info := out.Dump[0]
@ -181,6 +266,17 @@ RPC:
if info.Checks == nil {
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
}

6
agent/ui_endpoint_test.go

@ -162,6 +162,9 @@ func TestUINodes(t *testing.T) {
require.Len(t, nodes[2].Services, 0)
require.NotNil(t, nodes[1].Checks)
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) {
@ -260,6 +263,9 @@ func TestUINodeInfo(t *testing.T) {
node.Checks == nil || len(node.Checks) != 0 {
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) {

1
api/catalog_test.go

@ -65,6 +65,7 @@ func TestAPI_CatalogNodes(t *testing.T) {
},
Meta: map[string]string{
"consul-network-segment": "",
"consul-version": s.Config.Version,
},
}
require.Equal(r, want, got)

5
api/txn_test.go

@ -361,7 +361,10 @@ func TestAPI_ClientTxn(t *testing.T) {
"wan": 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,
ModifyIndex: ret.Results[1].Node.ModifyIndex,
},

2
sdk/testutil/server.go

@ -130,6 +130,7 @@ type TestServerConfig struct {
Args []string `json:"-"`
ReturnPorts func() `json:"-"`
Audit *TestAuditConfig `json:"audit,omitempty"`
Version string `json:"version,omitempty"`
}
type TestACLs struct {
@ -212,6 +213,7 @@ func defaultServerConfig(t TestingTB, consulVersion *version.Version) *TestServe
Stdout: logBuffer,
Stderr: logBuffer,
Peering: &TestPeeringConfig{Enabled: true},
Version: consulVersion.String(),
}
// Add version-specific tweaks

12
ui/packages/consul-ui/app/components/consul/node/list/index.hbs

@ -50,5 +50,17 @@ as |item index|>
{{item.Address}}
</dd>
</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>
</ListCollection>

41
ui/packages/consul-ui/app/components/consul/node/search-bar/index.hbs

@ -19,12 +19,14 @@
)
)
(t (concat "components.consul.node.search-bar." search.status.value)
default=(array
(concat "common.search." search.status.value)
(concat "common.consul." search.status.value)
(concat "common.brand." search.status.value)
)
(if search.status.value
search.status.value
(t (concat "components.consul.node.search-bar." search.status.value)
default=(array
(concat "common.search." search.status.value)
(concat "common.consul." search.status.value)
(concat "common.brand." search.status.value)
))
)
as |key value|}}
@ -95,6 +97,27 @@ as |key value|}}
{{/let}}
</BlockSlot>
</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>
<:sort as |search|>
<search.Select
@ -112,6 +135,8 @@ as |key value|}}
(array "Node:desc" (t "common.sort.alpha.desc"))
(array "Status:asc" (t "common.sort.status.asc"))
(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|
}}
@ -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:desc" @selected={{eq "Node:desc" @sort.value}}>{{t "common.sort.alpha.desc"}}</Option>
</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}}
</BlockSlot>
</search.Select>

8
ui/packages/consul-ui/app/filter/predicates/node.js

@ -9,4 +9,12 @@ export default {
warning: (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;
},
};

5
ui/packages/consul-ui/app/models/node.js

@ -67,4 +67,9 @@ export default class Node extends Model {
get ChecksWarning() {
return this.NodeChecks.filter((item) => item.Status === 'warning').length;
}
@computed('Meta')
get Version() {
return this.Meta?.['consul-version'] ?? '';
}
}

45
ui/packages/consul-ui/app/serializers/application.js

@ -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
// level
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') {
normalizedPayload.meta = meta;
}
@ -215,4 +219,45 @@ export default class ApplicationSerializer extends Serializer {
normalizePayload(payload, id, requestType) {
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
}
}

37
ui/packages/consul-ui/app/sort/comparators/node.js

@ -38,6 +38,43 @@ export default ({ properties }) =>
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);
};

8
ui/packages/consul-ui/app/templates/dc/nodes/index.hbs

@ -40,10 +40,15 @@
change=(action (mut searchproperty) value='target.selectedItems')
default=this._searchProperties
)
version=(hash
value=(if this.version (split this.version ',') undefined)
change=(action (mut this.version) value='target.selectedItems')
)
)
api.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|}}
<AppView>
@ -61,6 +66,7 @@
@onsearch={{action (mut search) value='target.value'}}
@sort={{sort}}
@filter={{filters}}
@versions={{versions}}
/>
{{/if}}
</BlockSlot>

1
ui/packages/consul-ui/mock-api/v1/internal/ui/node/_

@ -30,6 +30,7 @@ return `
"TaggedAddresses":{"lan":"${ip}","wan":"${ip}"},
"Meta":{
"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}}"
},
"Services":[

1
ui/packages/consul-ui/mock-api/v1/internal/ui/nodes

@ -25,6 +25,7 @@
},
"Meta": {
"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}
},
"Services":[

45
ui/packages/consul-ui/tests/unit/sort/comparators/node-test.js

@ -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);
});
});

4
ui/packages/consul-ui/translations/common/en-us.yaml

@ -53,6 +53,7 @@ consul:
redundancyzone: Redundancy zone
peername: Peer
partition: Admin Partitions
version: Version
search:
search: Search
searchproperty: Search Across
@ -77,6 +78,9 @@ sort:
status:
asc: Unhealthy to Healthy
desc: Healthy to Unhealthy
version:
asc: Oldest to Latest
desc: Latest to Oldest
validations:
dns-hostname:
help: |

1
ui/packages/consul-ui/vendor/consul-ui/routes.js vendored

@ -218,6 +218,7 @@
queryParams: {
sortBy: 'sort',
status: 'status',
version: 'version',
searchproperty: {
as: 'searchproperty',
empty: [['Node', 'Address', 'Meta', 'PeerName']],

Loading…
Cancel
Save