mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 15:59:41 +02:00
feat(app/edge-stacks): summarize the edge stack statuses in the backend (#818)
This commit is contained in:
parent
363a62d885
commit
e1c480d3c3
21 changed files with 645 additions and 312 deletions
28
api/slicesx/filter.go
Normal file
28
api/slicesx/filter.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package slicesx
|
||||
|
||||
// Iterates over elements of collection, returning an array of all elements predicate returns truthy for.
|
||||
//
|
||||
// Note: Unlike `FilterInPlace`, this method returns a new array.
|
||||
func Filter[T any](input []T, predicate func(T) bool) []T {
|
||||
result := make([]T, 0)
|
||||
for i := range input {
|
||||
if predicate(input[i]) {
|
||||
result = append(result, input[i])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Filter in place all elements from input that predicate returns truthy for and returns an array of the removed elements.
|
||||
//
|
||||
// Note: Unlike `Filter`, this method mutates input.
|
||||
func FilterInPlace[T any](input []T, predicate func(T) bool) []T {
|
||||
n := 0
|
||||
for _, v := range input {
|
||||
if predicate(v) {
|
||||
input[n] = v
|
||||
n++
|
||||
}
|
||||
}
|
||||
return input[:n]
|
||||
}
|
96
api/slicesx/filter_test.go
Normal file
96
api/slicesx/filter_test.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
package slicesx_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/portainer/api/slicesx"
|
||||
)
|
||||
|
||||
func Test_Filter(t *testing.T) {
|
||||
test(t, slicesx.Filter, "Filter even numbers",
|
||||
[]int{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
[]int{2, 4, 6, 8},
|
||||
func(x int) bool { return x%2 == 0 },
|
||||
)
|
||||
test(t, slicesx.Filter, "Filter odd numbers",
|
||||
[]int{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
[]int{1, 3, 5, 7, 9},
|
||||
func(x int) bool { return x%2 == 1 },
|
||||
)
|
||||
test(t, slicesx.Filter, "Filter strings starting with 'A'",
|
||||
[]string{"Apple", "Banana", "Avocado", "Grapes", "Apricot"},
|
||||
[]string{"Apple", "Avocado", "Apricot"},
|
||||
func(s string) bool { return s[0] == 'A' },
|
||||
)
|
||||
test(t, slicesx.Filter, "Filter strings longer than 5 chars",
|
||||
[]string{"Apple", "Banana", "Avocado", "Grapes", "Apricot"},
|
||||
[]string{"Banana", "Avocado", "Grapes", "Apricot"},
|
||||
func(s string) bool { return len(s) > 5 },
|
||||
)
|
||||
}
|
||||
|
||||
func Test_Retain(t *testing.T) {
|
||||
test(t, slicesx.FilterInPlace, "Filter even numbers",
|
||||
[]int{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
[]int{2, 4, 6, 8},
|
||||
func(x int) bool { return x%2 == 0 },
|
||||
)
|
||||
test(t, slicesx.FilterInPlace, "Filter odd numbers",
|
||||
[]int{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
[]int{1, 3, 5, 7, 9},
|
||||
func(x int) bool { return x%2 == 1 },
|
||||
)
|
||||
test(t, slicesx.FilterInPlace, "Filter strings starting with 'A'",
|
||||
[]string{"Apple", "Banana", "Avocado", "Grapes", "Apricot"},
|
||||
[]string{"Apple", "Avocado", "Apricot"},
|
||||
func(s string) bool { return s[0] == 'A' },
|
||||
)
|
||||
test(t, slicesx.FilterInPlace, "Filter strings longer than 5 chars",
|
||||
[]string{"Apple", "Banana", "Avocado", "Grapes", "Apricot"},
|
||||
[]string{"Banana", "Avocado", "Grapes", "Apricot"},
|
||||
func(s string) bool { return len(s) > 5 },
|
||||
)
|
||||
}
|
||||
|
||||
func Benchmark_Filter(b *testing.B) {
|
||||
n := 100000
|
||||
|
||||
source := make([]int, n)
|
||||
for i := range source {
|
||||
source[i] = i
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
e := slicesx.Filter(source, func(x int) bool { return x%2 == 0 })
|
||||
if len(e) != n/2 {
|
||||
b.FailNow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_FilterInPlace(b *testing.B) {
|
||||
n := 100000
|
||||
|
||||
source := make([]int, n)
|
||||
for i := range source {
|
||||
source[i] = i
|
||||
}
|
||||
|
||||
// Preallocate all copies before timing
|
||||
// because FilterInPlace mutates the original slice
|
||||
copies := make([][]int, b.N)
|
||||
for i := range b.N {
|
||||
buf := make([]int, len(source))
|
||||
copy(buf, source)
|
||||
copies[i] = buf
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := range b.N {
|
||||
e := slicesx.FilterInPlace(copies[i], func(x int) bool { return x%2 == 0 })
|
||||
if len(e) != n/2 {
|
||||
b.FailNow()
|
||||
}
|
||||
}
|
||||
}
|
7
api/slicesx/flatten.go
Normal file
7
api/slicesx/flatten.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package slicesx
|
||||
|
||||
import "slices"
|
||||
|
||||
func Flatten[T any](input [][]T) []T {
|
||||
return slices.Concat(input...)
|
||||
}
|
19
api/slicesx/flatten_test.go
Normal file
19
api/slicesx/flatten_test.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package slicesx_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/portainer/api/slicesx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Flatten(t *testing.T) {
|
||||
t.Run("Flatten an array of arrays", func(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
source := [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}
|
||||
expected := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
is.ElementsMatch(slicesx.Flatten(source), expected)
|
||||
|
||||
})
|
||||
}
|
17
api/slicesx/includes.go
Normal file
17
api/slicesx/includes.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package slicesx
|
||||
|
||||
import "slices"
|
||||
|
||||
// Checks if predicate returns truthy for any element of input. Iteration is stopped once predicate returns truthy.
|
||||
func Some[T any](input []T, predicate func(T) bool) bool {
|
||||
return slices.ContainsFunc(input, predicate)
|
||||
}
|
||||
|
||||
// Checks if predicate returns truthy for all elements of input. Iteration is stopped once predicate returns falsey.
|
||||
//
|
||||
// Note: This method returns true for empty collections because everything is true of elements of empty collections.
|
||||
// https://en.wikipedia.org/wiki/Vacuous_truth
|
||||
func Every[T any](input []T, predicate func(T) bool) bool {
|
||||
// if the slice doesn't contain an inverted predicate then all items follow the predicate
|
||||
return !slices.ContainsFunc(input, func(t T) bool { return !predicate(t) })
|
||||
}
|
76
api/slicesx/includes_test.go
Normal file
76
api/slicesx/includes_test.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package slicesx_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/portainer/api/slicesx"
|
||||
)
|
||||
|
||||
func Test_Every(t *testing.T) {
|
||||
test(t, slicesx.Every, "All start with an A (ok)",
|
||||
[]string{"Apple", "Avocado", "Apricot"},
|
||||
true,
|
||||
func(s string) bool { return s[0] == 'A' },
|
||||
)
|
||||
test(t, slicesx.Every, "All start with an A (ko = some don't start with A)",
|
||||
[]string{"Apple", "Avocado", "Banana"},
|
||||
false,
|
||||
func(s string) bool { return s[0] == 'A' },
|
||||
)
|
||||
test(t, slicesx.Every, "All are under 5 (ok)",
|
||||
[]int{1, 2, 3},
|
||||
true,
|
||||
func(i int) bool { return i < 5 },
|
||||
)
|
||||
test(t, slicesx.Every, "All are under 5 (ko = some above 10)",
|
||||
[]int{1, 2, 10},
|
||||
false,
|
||||
func(i int) bool { return i < 5 },
|
||||
)
|
||||
test(t, slicesx.Every, "All are true (ok)",
|
||||
[]struct{ x bool }{{x: true}, {x: true}, {x: true}},
|
||||
true,
|
||||
func(s struct{ x bool }) bool { return s.x })
|
||||
test(t, slicesx.Every, "All are true (ko = some are false)",
|
||||
[]struct{ x bool }{{x: true}, {x: true}, {x: false}},
|
||||
false,
|
||||
func(s struct{ x bool }) bool { return s.x })
|
||||
test(t, slicesx.Every, "Must be true on empty slice",
|
||||
[]int{},
|
||||
true,
|
||||
func(i int) bool { return i%2 == 0 },
|
||||
)
|
||||
}
|
||||
|
||||
func Test_Some(t *testing.T) {
|
||||
test(t, slicesx.Some, "Some start with an A (ok)",
|
||||
[]string{"Apple", "Avocado", "Banana"},
|
||||
true,
|
||||
func(s string) bool { return s[0] == 'A' },
|
||||
)
|
||||
test(t, slicesx.Some, "Some start with an A (ko = all don't start with A)",
|
||||
[]string{"Banana", "Cherry", "Peach"},
|
||||
false,
|
||||
func(s string) bool { return s[0] == 'A' },
|
||||
)
|
||||
test(t, slicesx.Some, "Some are under 5 (ok)",
|
||||
[]int{1, 2, 30},
|
||||
true,
|
||||
func(i int) bool { return i < 5 },
|
||||
)
|
||||
test(t, slicesx.Some, "Some are under 5 (ko = all above 5)",
|
||||
[]int{10, 11, 12},
|
||||
false,
|
||||
func(i int) bool { return i < 5 },
|
||||
)
|
||||
test(t, slicesx.Some, "Some are true (ok)",
|
||||
[]struct{ x bool }{{x: true}, {x: true}, {x: false}},
|
||||
true,
|
||||
func(s struct{ x bool }) bool { return s.x },
|
||||
)
|
||||
test(t, slicesx.Some, "Some are true (ko = all are false)",
|
||||
[]struct{ x bool }{{x: false}, {x: false}, {x: false}},
|
||||
false,
|
||||
func(s struct{ x bool }) bool { return s.x },
|
||||
)
|
||||
}
|
15
api/slicesx/map.go
Normal file
15
api/slicesx/map.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package slicesx
|
||||
|
||||
// Map applies the given function to each element of the slice and returns a new slice with the results
|
||||
func Map[T, U any](s []T, f func(T) U) []U {
|
||||
result := make([]U, len(s))
|
||||
for i, v := range s {
|
||||
result[i] = f(v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FlatMap applies the given function to each element of the slice and returns a new slice with the flattened results
|
||||
func FlatMap[T, U any](s []T, f func(T) []U) []U {
|
||||
return Flatten(Map(s, f))
|
||||
}
|
43
api/slicesx/map_test.go
Normal file
43
api/slicesx/map_test.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package slicesx_test
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/portainer/api/slicesx"
|
||||
)
|
||||
|
||||
func Test_Map(t *testing.T) {
|
||||
test(t, slicesx.Map, "Map integers to strings",
|
||||
[]int{1, 2, 3, 4, 5},
|
||||
[]string{"1", "2", "3", "4", "5"},
|
||||
strconv.Itoa,
|
||||
)
|
||||
test(t, slicesx.Map, "Map strings to integers",
|
||||
[]string{"1", "2", "3", "4", "5"},
|
||||
[]int{1, 2, 3, 4, 5},
|
||||
func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func Test_FlatMap(t *testing.T) {
|
||||
test(t, slicesx.FlatMap, "Map integers to strings and flatten",
|
||||
[]int{1, 2, 3, 4, 5},
|
||||
[]string{"1", "1", "2", "2", "3", "3", "4", "4", "5", "5"},
|
||||
func(i int) []string {
|
||||
x := strconv.Itoa(i)
|
||||
return []string{x, x}
|
||||
},
|
||||
)
|
||||
test(t, slicesx.FlatMap, "Map strings to integers and flatten",
|
||||
[]string{"1", "2", "3", "4", "5"},
|
||||
[]int{1, 1, 2, 2, 3, 3, 4, 4, 5, 5},
|
||||
func(s string) []int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return []int{n, n}
|
||||
},
|
||||
)
|
||||
}
|
|
@ -1,127 +0,0 @@
|
|||
package slicesx
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type filterTestCase[T any] struct {
|
||||
name string
|
||||
input []T
|
||||
expected []T
|
||||
predicate func(T) bool
|
||||
}
|
||||
|
||||
func TestFilter(t *testing.T) {
|
||||
intTestCases := []filterTestCase[int]{
|
||||
{
|
||||
name: "Filter even numbers",
|
||||
input: []int{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
expected: []int{2, 4, 6, 8},
|
||||
|
||||
predicate: func(n int) bool {
|
||||
return n%2 == 0
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Filter odd numbers",
|
||||
input: []int{1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
expected: []int{1, 3, 5, 7, 9},
|
||||
|
||||
predicate: func(n int) bool {
|
||||
return n%2 != 0
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runTestCases(t, intTestCases)
|
||||
|
||||
stringTestCases := []filterTestCase[string]{
|
||||
{
|
||||
name: "Filter strings starting with 'A'",
|
||||
input: []string{"Apple", "Banana", "Avocado", "Grapes", "Apricot"},
|
||||
expected: []string{"Apple", "Avocado", "Apricot"},
|
||||
predicate: func(s string) bool {
|
||||
return s[0] == 'A'
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Filter strings longer than 5 characters",
|
||||
input: []string{"Apple", "Banana", "Avocado", "Grapes", "Apricot"},
|
||||
expected: []string{"Banana", "Avocado", "Grapes", "Apricot"},
|
||||
predicate: func(s string) bool {
|
||||
return len(s) > 5
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runTestCases(t, stringTestCases)
|
||||
}
|
||||
|
||||
func runTestCases[T any](t *testing.T, testCases []filterTestCase[T]) {
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
result := Filter(testCase.input, testCase.predicate)
|
||||
|
||||
is.Equal(len(testCase.expected), len(result))
|
||||
is.ElementsMatch(testCase.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
intTestCases := []struct {
|
||||
name string
|
||||
input []int
|
||||
expected []string
|
||||
mapper func(int) string
|
||||
}{
|
||||
{
|
||||
name: "Map integers to strings",
|
||||
input: []int{1, 2, 3, 4, 5},
|
||||
expected: []string{"1", "2", "3", "4", "5"},
|
||||
mapper: strconv.Itoa,
|
||||
},
|
||||
}
|
||||
|
||||
runMapTestCases(t, intTestCases)
|
||||
|
||||
stringTestCases := []struct {
|
||||
name string
|
||||
input []string
|
||||
expected []int
|
||||
mapper func(string) int
|
||||
}{
|
||||
{
|
||||
name: "Map strings to integers",
|
||||
input: []string{"1", "2", "3", "4", "5"},
|
||||
expected: []int{1, 2, 3, 4, 5},
|
||||
mapper: func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runMapTestCases(t, stringTestCases)
|
||||
}
|
||||
|
||||
func runMapTestCases[T, U any](t *testing.T, testCases []struct {
|
||||
name string
|
||||
input []T
|
||||
expected []U
|
||||
mapper func(T) U
|
||||
}) {
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
result := Map(testCase.input, testCase.mapper)
|
||||
|
||||
is.Equal(len(testCase.expected), len(result))
|
||||
is.ElementsMatch(testCase.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
29
api/slicesx/slicesx_test.go
Normal file
29
api/slicesx/slicesx_test.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package slicesx_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type libFunc[T, U, V any] func([]T, func(T) U) V
|
||||
type predicateFunc[T, U any] func(T) U
|
||||
|
||||
func test[T, U, V any](t *testing.T, libFn libFunc[T, U, V], name string, input []T, expected V, predicate predicateFunc[T, U]) {
|
||||
t.Helper()
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
|
||||
result := libFn(input, predicate)
|
||||
|
||||
switch reflect.TypeOf(result).Kind() {
|
||||
case reflect.Slice, reflect.Array:
|
||||
is.Equal(expected, result)
|
||||
is.ElementsMatch(expected, result)
|
||||
default:
|
||||
is.Equal(expected, result)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,27 +1,5 @@
|
|||
package slicesx
|
||||
|
||||
// Map applies the given function to each element of the slice and returns a new slice with the results
|
||||
func Map[T, U any](s []T, f func(T) U) []U {
|
||||
result := make([]U, len(s))
|
||||
for i, v := range s {
|
||||
result[i] = f(v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Filter returns a new slice containing only the elements of the slice for which the given predicate returns true
|
||||
func Filter[T any](s []T, predicate func(T) bool) []T {
|
||||
n := 0
|
||||
for _, v := range s {
|
||||
if predicate(v) {
|
||||
s[n] = v
|
||||
n++
|
||||
}
|
||||
}
|
||||
|
||||
return s[:n]
|
||||
}
|
||||
|
||||
func Unique[T comparable](items []T) []T {
|
||||
return UniqueBy(items, func(item T) T {
|
||||
return item
|
46
api/slicesx/unique_test.go
Normal file
46
api/slicesx/unique_test.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package slicesx_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/portainer/api/slicesx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Unique(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
t.Run("Should extract unique numbers", func(t *testing.T) {
|
||||
|
||||
source := []int{1, 1, 2, 3, 4, 4, 5, 4, 6, 7, 8, 9, 1}
|
||||
result := slicesx.Unique(source)
|
||||
expected := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
|
||||
is.ElementsMatch(result, expected)
|
||||
})
|
||||
|
||||
t.Run("Should return empty array", func(t *testing.T) {
|
||||
source := []int{}
|
||||
result := slicesx.Unique(source)
|
||||
expected := []int{}
|
||||
is.ElementsMatch(result, expected)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_UniqueBy(t *testing.T) {
|
||||
is := assert.New(t)
|
||||
t.Run("Should extract unique numbers by property", func(t *testing.T) {
|
||||
|
||||
source := []struct{ int }{{1}, {1}, {2}, {3}, {4}, {4}, {5}, {4}, {6}, {7}, {8}, {9}, {1}}
|
||||
result := slicesx.UniqueBy(source, func(item struct{ int }) int { return item.int })
|
||||
expected := []struct{ int }{{1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}}
|
||||
|
||||
is.ElementsMatch(result, expected)
|
||||
})
|
||||
|
||||
t.Run("Should return empty array", func(t *testing.T) {
|
||||
source := []int{}
|
||||
result := slicesx.UniqueBy(source, func(x int) int { return x })
|
||||
expected := []int{}
|
||||
is.ElementsMatch(result, expected)
|
||||
})
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue