mirror of
https://github.com/weaveworks/scope.git
synced 2026-08-23 22:36:24 +00:00
Created generic table model on backend
Replaced MetadataRow with generic Row in Table model Sending through multicolumn tables from the backend
This commit is contained in:
@@ -6,7 +6,6 @@ import MatchedText from '../matched-text';
|
||||
import NodeDetailsControlButton from './node-details-control-button';
|
||||
import ShowMore from '../show-more';
|
||||
|
||||
|
||||
const Controls = controls => (
|
||||
<div className="node-details-labels-controls">
|
||||
{sortBy(controls, 'rank').map(control => <NodeDetailsControlButton
|
||||
@@ -52,11 +51,11 @@ export default class NodeDetailsLabels extends React.Component {
|
||||
<div className="node-details-labels-field" key={field.id}>
|
||||
<div
|
||||
className="node-details-labels-field-label truncate"
|
||||
title={field.label} key={field.id}>
|
||||
{field.label}
|
||||
title={field.entries.label} key={field.id}>
|
||||
{field.entries.label}
|
||||
</div>
|
||||
<div className="node-details-labels-field-value truncate" title={field.value}>
|
||||
<MatchedText text={field.value} match={matches.get(field.id)} />
|
||||
<div className="node-details-labels-field-value truncate" title={field.entries.value}>
|
||||
<MatchedText text={field.entries.value} match={matches.get(field.id)} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -30,9 +30,17 @@ describe('SearchUtils', () => {
|
||||
tables: [{
|
||||
id: 'metric1',
|
||||
rows: [{
|
||||
id: 'row1',
|
||||
label: 'Row 1',
|
||||
value: 'Row Value 1'
|
||||
entries: {
|
||||
id: 'row1',
|
||||
label: 'Row 1',
|
||||
value: 'Row Value 1'
|
||||
}
|
||||
}, {
|
||||
entries: {
|
||||
id: 'row2',
|
||||
label: 'Row 2',
|
||||
value: 'Row Value 2'
|
||||
}
|
||||
}]
|
||||
}],
|
||||
},
|
||||
|
||||
@@ -154,9 +154,10 @@ export function searchTopology(nodes, { prefix, query, metric, comp, value }) {
|
||||
tables.forEach((table) => {
|
||||
if (table.get('rows')) {
|
||||
table.get('rows').forEach((field) => {
|
||||
const entries = field.get('entries');
|
||||
const keyPath = [nodeId, 'tables', field.get('id')];
|
||||
nodeMatches = findNodeMatch(nodeMatches, keyPath, field.get('value'),
|
||||
query, prefix, field.get('label'));
|
||||
nodeMatches = findNodeMatch(nodeMatches, keyPath, entries.get('value'),
|
||||
query, prefix, entries.get('label'));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -379,8 +379,8 @@ func (c *container) getBaseNode() report.Node {
|
||||
}).WithParents(report.EmptySets.
|
||||
Add(report.ContainerImage, report.MakeStringSet(report.MakeContainerImageNodeID(c.Image()))),
|
||||
)
|
||||
result = result.AddPrefixTable(LabelPrefix, c.container.Config.Labels)
|
||||
result = result.AddPrefixTable(EnvPrefix, c.env())
|
||||
result = result.AddPrefixLabels(LabelPrefix, c.container.Config.Labels)
|
||||
result = result.AddPrefixLabels(EnvPrefix, c.env())
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -258,7 +258,7 @@ func (r *Reporter) containerImageTopology() report.Topology {
|
||||
}
|
||||
nodeID := report.MakeContainerImageNodeID(imageID)
|
||||
node := report.MakeNodeWith(nodeID, latests)
|
||||
node = node.AddPrefixTable(ImageLabelPrefix, image.Labels)
|
||||
node = node.AddPrefixLabels(ImageLabelPrefix, image.Labels)
|
||||
result.AddNode(node)
|
||||
})
|
||||
|
||||
|
||||
@@ -56,5 +56,5 @@ func (m meta) MetaNode(id string) report.Node {
|
||||
Name: m.Name(),
|
||||
Namespace: m.Namespace(),
|
||||
Created: m.Created(),
|
||||
}).AddPrefixTable(LabelPrefix, m.Labels())
|
||||
}).AddPrefixLabels(LabelPrefix, m.Labels())
|
||||
}
|
||||
|
||||
+63
-40
@@ -19,34 +19,35 @@ import (
|
||||
|
||||
// Keys for use in Node
|
||||
const (
|
||||
WeavePeerName = "weave_peer_name"
|
||||
WeavePeerNickName = "weave_peer_nick_name"
|
||||
WeaveDNSHostname = "weave_dns_hostname"
|
||||
WeaveMACAddress = "weave_mac_address"
|
||||
WeaveVersion = "weave_version"
|
||||
WeaveEncryption = "weave_encryption"
|
||||
WeaveProtocol = "weave_protocol"
|
||||
WeavePeerDiscovery = "weave_peer_discovery"
|
||||
WeaveTargetCount = "weave_target_count"
|
||||
WeaveConnectionCount = "weave_connection_count"
|
||||
WeavePeerCount = "weave_peer_count"
|
||||
WeaveTrustedSubnets = "weave_trusted_subnet_count"
|
||||
WeaveIPAMTableID = "weave_ipam_table"
|
||||
WeaveIPAMStatus = "weave_ipam_status"
|
||||
WeaveIPAMRange = "weave_ipam_range"
|
||||
WeaveIPAMDefaultSubnet = "weave_ipam_default_subnet"
|
||||
WeaveDNSTableID = "weave_dns_table"
|
||||
WeaveDNSDomain = "weave_dns_domain"
|
||||
WeaveDNSUpstream = "weave_dns_upstream"
|
||||
WeaveDNSTTL = "weave_dns_ttl"
|
||||
WeaveDNSEntryCount = "weave_dns_entry_count"
|
||||
WeaveProxyTableID = "weave_proxy_table"
|
||||
WeaveProxyStatus = "weave_proxy_status"
|
||||
WeaveProxyAddress = "weave_proxy_address"
|
||||
WeavePluginTableID = "weave_plugin_table"
|
||||
WeavePluginStatus = "weave_plugin_status"
|
||||
WeavePluginDriver = "weave_plugin_driver"
|
||||
WeaveConnectionsTablePrefix = "weave_connections_table_"
|
||||
WeavePeerName = "weave_peer_name"
|
||||
WeavePeerNickName = "weave_peer_nick_name"
|
||||
WeaveDNSHostname = "weave_dns_hostname"
|
||||
WeaveMACAddress = "weave_mac_address"
|
||||
WeaveVersion = "weave_version"
|
||||
WeaveEncryption = "weave_encryption"
|
||||
WeaveProtocol = "weave_protocol"
|
||||
WeavePeerDiscovery = "weave_peer_discovery"
|
||||
WeaveTargetCount = "weave_target_count"
|
||||
WeaveConnectionCount = "weave_connection_count"
|
||||
WeavePeerCount = "weave_peer_count"
|
||||
WeaveTrustedSubnets = "weave_trusted_subnet_count"
|
||||
WeaveIPAMTableID = "weave_ipam_table"
|
||||
WeaveIPAMStatus = "weave_ipam_status"
|
||||
WeaveIPAMRange = "weave_ipam_range"
|
||||
WeaveIPAMDefaultSubnet = "weave_ipam_default_subnet"
|
||||
WeaveDNSTableID = "weave_dns_table"
|
||||
WeaveDNSDomain = "weave_dns_domain"
|
||||
WeaveDNSUpstream = "weave_dns_upstream"
|
||||
WeaveDNSTTL = "weave_dns_ttl"
|
||||
WeaveDNSEntryCount = "weave_dns_entry_count"
|
||||
WeaveProxyTableID = "weave_proxy_table"
|
||||
WeaveProxyStatus = "weave_proxy_status"
|
||||
WeaveProxyAddress = "weave_proxy_address"
|
||||
WeavePluginTableID = "weave_plugin_table"
|
||||
WeavePluginStatus = "weave_plugin_status"
|
||||
WeavePluginDriver = "weave_plugin_driver"
|
||||
WeaveConnectionsTablePrefix = "weave_connections_table_"
|
||||
WeaveConnectionsMulticolumnTablePrefix = "weave_connections_multicolumn_table_"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -73,14 +74,20 @@ var (
|
||||
}
|
||||
|
||||
weaveTableTemplates = report.TableTemplates{
|
||||
WeaveIPAMTableID: {ID: WeaveIPAMTableID, Label: "IPAM",
|
||||
WeaveIPAMTableID: {
|
||||
ID: WeaveIPAMTableID,
|
||||
Label: "IPAM",
|
||||
Type: report.PropertyListType,
|
||||
FixedRows: map[string]string{
|
||||
WeaveIPAMStatus: "Status",
|
||||
WeaveIPAMRange: "Range",
|
||||
WeaveIPAMDefaultSubnet: "Default Subnet",
|
||||
},
|
||||
},
|
||||
WeaveDNSTableID: {ID: WeaveDNSTableID, Label: "DNS",
|
||||
WeaveDNSTableID: {
|
||||
ID: WeaveDNSTableID,
|
||||
Label: "DNS",
|
||||
Type: report.PropertyListType,
|
||||
FixedRows: map[string]string{
|
||||
WeaveDNSDomain: "Domain",
|
||||
WeaveDNSUpstream: "Upstream",
|
||||
@@ -88,13 +95,19 @@ var (
|
||||
WeaveDNSEntryCount: "Entries",
|
||||
},
|
||||
},
|
||||
WeaveProxyTableID: {ID: WeaveProxyTableID, Label: "Proxy",
|
||||
WeaveProxyTableID: {
|
||||
ID: WeaveProxyTableID,
|
||||
Label: "Proxy",
|
||||
Type: report.PropertyListType,
|
||||
FixedRows: map[string]string{
|
||||
WeaveProxyStatus: "Status",
|
||||
WeaveProxyAddress: "Address",
|
||||
},
|
||||
},
|
||||
WeavePluginTableID: {ID: WeavePluginTableID, Label: "Plugin",
|
||||
WeavePluginTableID: {
|
||||
ID: WeavePluginTableID,
|
||||
Label: "Plugin",
|
||||
Type: report.PropertyListType,
|
||||
FixedRows: map[string]string{
|
||||
WeavePluginStatus: "Status",
|
||||
WeavePluginDriver: "Driver Name",
|
||||
@@ -103,8 +116,15 @@ var (
|
||||
WeaveConnectionsTablePrefix: {
|
||||
ID: WeaveConnectionsTablePrefix,
|
||||
Label: "Connections",
|
||||
Type: report.PropertyListType,
|
||||
Prefix: WeaveConnectionsTablePrefix,
|
||||
},
|
||||
WeaveConnectionsMulticolumnTablePrefix: {
|
||||
ID: WeaveConnectionsMulticolumnTablePrefix,
|
||||
Label: "Connections",
|
||||
Type: report.MulticolumnTableType,
|
||||
Prefix: WeaveConnectionsMulticolumnTablePrefix,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -434,28 +454,31 @@ func (w *Weave) addCurrentPeerInfo(latests map[string]string, node report.Node)
|
||||
latests[WeavePluginStatus] = "running"
|
||||
latests[WeavePluginDriver] = "weave"
|
||||
}
|
||||
node = node.AddPrefixTable(WeaveConnectionsTablePrefix, getConnectionsTable(w.statusCache.Router))
|
||||
node = node.AddPrefixTable(WeaveConnectionsMulticolumnTablePrefix, getConnectionsTable(w.statusCache.Router))
|
||||
node = node.WithParents(report.EmptySets.Add(report.Host, report.MakeStringSet(w.hostID)))
|
||||
|
||||
return latests, node
|
||||
}
|
||||
|
||||
func getConnectionsTable(router weave.Router) map[string]string {
|
||||
func getConnectionsTable(router weave.Router) []report.Row {
|
||||
const (
|
||||
outboundArrow = "->"
|
||||
inboundArrow = "<-"
|
||||
)
|
||||
table := make(map[string]string, len(router.Connections))
|
||||
table := make([]report.Row, len(router.Connections))
|
||||
for _, conn := range router.Connections {
|
||||
arrow := inboundArrow
|
||||
if conn.Outbound {
|
||||
arrow = outboundArrow
|
||||
}
|
||||
// TODO: we should probably use a multicolumn table for this
|
||||
// but there is no mechanism to support it yet.
|
||||
key := fmt.Sprintf("%s %s", arrow, conn.Address)
|
||||
value := fmt.Sprintf("%s, %s", conn.State, conn.Info)
|
||||
table[key] = value
|
||||
table = append(table, report.Row{
|
||||
ID: conn.Address,
|
||||
Entries: map[string]string{
|
||||
"ip": fmt.Sprintf("%s %s", arrow, conn.Address),
|
||||
"state": conn.State,
|
||||
"info": conn.Info,
|
||||
},
|
||||
})
|
||||
}
|
||||
return table
|
||||
}
|
||||
|
||||
@@ -35,23 +35,25 @@ func TestNodeTables(t *testing.T) {
|
||||
{
|
||||
ID: docker.EnvPrefix,
|
||||
Label: "Environment Variables",
|
||||
Rows: []report.MetadataRow{},
|
||||
Rows: []report.Row{},
|
||||
},
|
||||
{
|
||||
ID: docker.LabelPrefix,
|
||||
Label: "Docker Labels",
|
||||
Rows: []report.MetadataRow{
|
||||
Rows: []report.Row{
|
||||
{
|
||||
ID: "label_label1",
|
||||
Label: "label1",
|
||||
Value: "label1value",
|
||||
Entries: map[string]string{
|
||||
"id": "label_label1",
|
||||
"label": "label1",
|
||||
"value": "label1value",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: docker.ImageTableID,
|
||||
Label: "Image",
|
||||
Rows: []report.MetadataRow{},
|
||||
Rows: []report.Row{},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+114
-47
@@ -15,10 +15,33 @@ import (
|
||||
const (
|
||||
MaxTableRows = 20
|
||||
TruncationCountPrefix = "table_truncation_count_"
|
||||
MulticolumnTableType = "multicolumn-table"
|
||||
PropertyListType = "property-list"
|
||||
)
|
||||
|
||||
// AddPrefixTable appends arbitrary key-value pairs to the Node, returning a new node.
|
||||
func (node Node) AddPrefixTable(prefix string, labels map[string]string) Node {
|
||||
// AddPrefixTable appends arbitrary rows to the Node, returning a new node.
|
||||
func (node Node) AddPrefixTable(prefix string, rows []Row) Node {
|
||||
count := 0
|
||||
for _, row := range rows {
|
||||
if count >= MaxTableRows {
|
||||
break
|
||||
}
|
||||
// TODO: Figure a more natural way of storing rows
|
||||
for column, value := range row.Entries {
|
||||
key := fmt.Sprintf("%s %s", row.ID, column)
|
||||
node = node.WithLatest(prefix+key, mtime.Now(), value)
|
||||
}
|
||||
count++
|
||||
}
|
||||
if len(rows) > MaxTableRows {
|
||||
truncationCount := fmt.Sprintf("%d", len(rows)-MaxTableRows)
|
||||
node = node.WithLatest(TruncationCountPrefix+prefix, mtime.Now(), truncationCount)
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
// AddPrefixLabels appends arbitrary key-value pairs to the Node, returning a new node.
|
||||
func (node Node) AddPrefixLabels(prefix string, labels map[string]string) Node {
|
||||
count := 0
|
||||
for key, value := range labels {
|
||||
if count >= MaxTableRows {
|
||||
@@ -34,33 +57,86 @@ func (node Node) AddPrefixTable(prefix string, labels map[string]string) Node {
|
||||
return node
|
||||
}
|
||||
|
||||
// ExtractTable returns the key-value pairs to build a table from this node
|
||||
func (node Node) ExtractTable(template TableTemplate) (rows map[string]string, truncationCount int) {
|
||||
rows = map[string]string{}
|
||||
// ExtractTable returns the rows to build a table from this node
|
||||
func (node Node) ExtractTable(template TableTemplate) (rows []Row, truncationCount int) {
|
||||
rows = []Row{}
|
||||
switch template.Type {
|
||||
case MulticolumnTableType:
|
||||
keyRows := map[string]Row{}
|
||||
node.Latest.ForEach(func(key string, _ time.Time, value string) {
|
||||
if len(template.Prefix) > 0 && strings.HasPrefix(key, template.Prefix) {
|
||||
rowID, column := "", ""
|
||||
fmt.Sscanf(key[len(template.Prefix):], "%s %s", &rowID, &column)
|
||||
if _, ok := keyRows[rowID]; !ok {
|
||||
keyRows[rowID] = Row{
|
||||
ID: rowID,
|
||||
Entries: map[string]string{},
|
||||
}
|
||||
}
|
||||
keyRows[rowID].Entries[column] = value
|
||||
}
|
||||
})
|
||||
for _, row := range keyRows {
|
||||
rows = append(rows, row)
|
||||
}
|
||||
// By default assume it's a property list (for backward compatibility)
|
||||
default:
|
||||
keyValues := map[string]string{}
|
||||
node.Latest.ForEach(func(key string, _ time.Time, value string) {
|
||||
if label, ok := template.FixedRows[key]; ok {
|
||||
keyValues[label] = value
|
||||
}
|
||||
if len(template.Prefix) > 0 && strings.HasPrefix(key, template.Prefix) {
|
||||
label := key[len(template.Prefix):]
|
||||
keyValues[label] = value
|
||||
}
|
||||
})
|
||||
labels := make([]string, 0, len(rows))
|
||||
for label := range keyValues {
|
||||
labels = append(labels, label)
|
||||
}
|
||||
sort.Strings(labels)
|
||||
for _, label := range labels {
|
||||
rows = append(rows, Row{
|
||||
ID: "label_" + label,
|
||||
Entries: map[string]string{
|
||||
"label": label,
|
||||
"value": keyValues[label],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
truncationCount = 0
|
||||
node.Latest.ForEach(func(key string, _ time.Time, value string) {
|
||||
if label, ok := template.FixedRows[key]; ok {
|
||||
rows[label] = value
|
||||
}
|
||||
if len(template.Prefix) > 0 && strings.HasPrefix(key, template.Prefix) {
|
||||
label := key[len(template.Prefix):]
|
||||
rows[label] = value
|
||||
}
|
||||
})
|
||||
if str, ok := node.Latest.Lookup(TruncationCountPrefix + template.Prefix); ok {
|
||||
if n, err := fmt.Sscanf(str, "%d", &truncationCount); n != 1 || err != nil {
|
||||
log.Warn("Unexpected truncation count format %q", str)
|
||||
}
|
||||
}
|
||||
|
||||
return rows, truncationCount
|
||||
}
|
||||
|
||||
type Column struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
DataType string `json:"dataType"`
|
||||
Alignment string `json:"alignment"`
|
||||
}
|
||||
|
||||
type Row struct {
|
||||
ID string `json:"id"`
|
||||
Entries map[string]string `json:"entries"`
|
||||
}
|
||||
|
||||
// Table is the type for a table in the UI.
|
||||
type Table struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Rows []MetadataRow `json:"rows"`
|
||||
TruncationCount int `json:"truncationCount,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Type string `json:"type"`
|
||||
Columns []Column `json:"columns"`
|
||||
Rows []Row `json:"rows"`
|
||||
TruncationCount int `json:"truncationCount,omitempty"`
|
||||
}
|
||||
|
||||
type tablesByID []Table
|
||||
@@ -72,9 +148,14 @@ func (t tablesByID) Less(i, j int) bool { return t[i].ID < t[j].ID }
|
||||
// Copy returns a copy of the Table.
|
||||
func (t Table) Copy() Table {
|
||||
result := Table{
|
||||
ID: t.ID,
|
||||
Label: t.Label,
|
||||
Rows: make([]MetadataRow, 0, len(t.Rows)),
|
||||
ID: t.ID,
|
||||
Label: t.Label,
|
||||
Type: t.Type,
|
||||
Columns: make([]Column, 0, len(t.Columns)),
|
||||
Rows: make([]Row, 0, len(t.Rows)),
|
||||
}
|
||||
for _, column := range t.Columns {
|
||||
result.Columns = append(result.Columns, column)
|
||||
}
|
||||
for _, row := range t.Rows {
|
||||
result.Rows = append(result.Rows, row)
|
||||
@@ -82,18 +163,13 @@ func (t Table) Copy() Table {
|
||||
return result
|
||||
}
|
||||
|
||||
// FixedRow describes a row which is part of a TableTemplate and whose value is extracted
|
||||
// from a predetermined key
|
||||
type FixedRow struct {
|
||||
Label string `json:"label"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// TableTemplate describes how to render a table for the UI.
|
||||
type TableTemplate struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Prefix string `json:"prefix"`
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Prefix string `json:"prefix"`
|
||||
Type string `json:"type"`
|
||||
Columns []Column `json:"columns"`
|
||||
// FixedRows indicates what predetermined rows to render each entry is
|
||||
// indexed by the key to extract the row value is mapped to the row
|
||||
// label
|
||||
@@ -126,10 +202,13 @@ func (t TableTemplate) Merge(other TableTemplate) TableTemplate {
|
||||
fixedRows = other.FixedRows
|
||||
}
|
||||
|
||||
// TODO: Refactor the merging logic, as mixing
|
||||
// the types now might result in invalid tables.
|
||||
return TableTemplate{
|
||||
ID: max(t.ID, other.ID),
|
||||
Label: max(t.Label, other.Label),
|
||||
Prefix: max(t.Prefix, other.Prefix),
|
||||
Type: max(t.Type, other.Type),
|
||||
FixedRows: fixedRows,
|
||||
}
|
||||
}
|
||||
@@ -142,25 +221,13 @@ func (t TableTemplates) Tables(node Node) []Table {
|
||||
var result []Table
|
||||
for _, template := range t {
|
||||
rows, truncationCount := node.ExtractTable(template)
|
||||
table := Table{
|
||||
result = append(result, Table{
|
||||
ID: template.ID,
|
||||
Label: template.Label,
|
||||
Rows: []MetadataRow{},
|
||||
Type: template.Type,
|
||||
Rows: rows,
|
||||
TruncationCount: truncationCount,
|
||||
}
|
||||
keys := make([]string, 0, len(rows))
|
||||
for k := range rows {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
table.Rows = append(table.Rows, MetadataRow{
|
||||
ID: "label_" + key,
|
||||
Label: key,
|
||||
Value: rows[key],
|
||||
})
|
||||
}
|
||||
result = append(result, table)
|
||||
})
|
||||
}
|
||||
sort.Sort(tablesByID(result))
|
||||
return result
|
||||
|
||||
@@ -16,7 +16,7 @@ func TestPrefixTables(t *testing.T) {
|
||||
}
|
||||
nmd := report.MakeNode("foo1")
|
||||
|
||||
nmd = nmd.AddPrefixTable("foo_", want)
|
||||
nmd = nmd.AddPrefixLabels("foo_", want)
|
||||
have, truncationCount := nmd.ExtractTable(report.TableTemplate{Prefix: "foo_"})
|
||||
|
||||
if truncationCount != 0 {
|
||||
@@ -62,7 +62,7 @@ func TestTruncation(t *testing.T) {
|
||||
|
||||
nmd := report.MakeNode("foo1")
|
||||
|
||||
nmd = nmd.AddPrefixTable("foo_", want)
|
||||
nmd = nmd.AddPrefixLabels("foo_", want)
|
||||
_, truncationCount := nmd.ExtractTable(report.TableTemplate{Prefix: "foo_"})
|
||||
|
||||
if truncationCount != wantTruncationCount {
|
||||
|
||||
Reference in New Issue
Block a user