@oneuptime/common 11.6.0 → 11.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.ts +41 -24
- package/Models/AnalyticsModels/MetricBaselineHourly.ts +2 -0
- package/Models/AnalyticsModels/MetricItemAggMV1m.ts +5 -2
- package/Models/AnalyticsModels/MetricItemAggMV1mByContainer.ts +5 -2
- package/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.ts +5 -2
- package/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.ts +5 -2
- package/Models/AnalyticsModels/MetricItemAggMV1mByService.ts +5 -2
- package/Server/Services/AnalyticsDatabaseService.ts +13 -1
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +41 -4
- package/Tests/Models/AnalyticsModels/AnalyticsBaseModel.test.ts +122 -0
- package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +164 -3
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +99 -0
- package/Tests/Types/HashCode.test.ts +41 -0
- package/Tests/Types/Log/LogQueryParser.test.ts +186 -0
- package/Tests/Utils/Dashboard/VariableInterpolation.test.ts +197 -0
- package/Tests/Utils/ValueFormatter.test.ts +305 -0
- package/Types/AnalyticsDatabase/TableColumn.ts +23 -0
- package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js +25 -20
- package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js +2 -0
- package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js +5 -2
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js +5 -2
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js +5 -2
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js +5 -2
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js +5 -2
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +9 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +29 -4
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Types/AnalyticsDatabase/TableColumn.js +7 -0
- package/build/dist/Types/AnalyticsDatabase/TableColumn.js.map +1 -1
- package/package.json +1 -1
|
@@ -1102,6 +1102,105 @@ describe("StatementGenerator", () => {
|
|
|
1102
1102
|
);
|
|
1103
1103
|
});
|
|
1104
1104
|
|
|
1105
|
+
test("emits AggregateFunction and scalar SimpleAggregateFunction measures", () => {
|
|
1106
|
+
const statement: Statement = generator.toColumnsCreateStatement([
|
|
1107
|
+
new AnalyticsTableColumn({
|
|
1108
|
+
key: "retentionDate",
|
|
1109
|
+
title: "<title>",
|
|
1110
|
+
description: "<description>",
|
|
1111
|
+
required: false,
|
|
1112
|
+
type: TableColumnType.Date,
|
|
1113
|
+
simpleAggregateFunction: "max",
|
|
1114
|
+
}),
|
|
1115
|
+
new AnalyticsTableColumn({
|
|
1116
|
+
key: "valueState",
|
|
1117
|
+
title: "<title>",
|
|
1118
|
+
description: "<description>",
|
|
1119
|
+
required: true,
|
|
1120
|
+
type: TableColumnType.AggregateFunction,
|
|
1121
|
+
aggregateFunctionDefinition: "sum, Float64",
|
|
1122
|
+
}),
|
|
1123
|
+
]);
|
|
1124
|
+
|
|
1125
|
+
expectStatement(
|
|
1126
|
+
statement,
|
|
1127
|
+
SQL`retentionDate SimpleAggregateFunction(max, DateTime), valueState AggregateFunction(sum, Float64)`,
|
|
1128
|
+
);
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
test("supports SimpleAggregateFunction on non-DateTime scalar types and codecs", () => {
|
|
1132
|
+
const statement: Statement = generator.toColumnsCreateStatement([
|
|
1133
|
+
new AnalyticsTableColumn({
|
|
1134
|
+
key: "sampleCount",
|
|
1135
|
+
title: "<title>",
|
|
1136
|
+
description: "<description>",
|
|
1137
|
+
required: true,
|
|
1138
|
+
type: TableColumnType.UInt64,
|
|
1139
|
+
simpleAggregateFunction: "sum",
|
|
1140
|
+
codec: { codec: "ZSTD", level: 1 },
|
|
1141
|
+
}),
|
|
1142
|
+
]);
|
|
1143
|
+
|
|
1144
|
+
expectStatement(
|
|
1145
|
+
statement,
|
|
1146
|
+
SQL`sampleCount SimpleAggregateFunction(sum, UInt64) CODEC(ZSTD(1))`,
|
|
1147
|
+
);
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
test("rejects a column that declares both aggregate representations", () => {
|
|
1151
|
+
expect(() => {
|
|
1152
|
+
return generator.toColumnsCreateStatement([
|
|
1153
|
+
new AnalyticsTableColumn({
|
|
1154
|
+
key: "ambiguousState",
|
|
1155
|
+
title: "<title>",
|
|
1156
|
+
description: "<description>",
|
|
1157
|
+
required: true,
|
|
1158
|
+
type: TableColumnType.AggregateFunction,
|
|
1159
|
+
aggregateFunctionDefinition: "sum, Float64",
|
|
1160
|
+
simpleAggregateFunction: "sum",
|
|
1161
|
+
}),
|
|
1162
|
+
]);
|
|
1163
|
+
}).toThrow(
|
|
1164
|
+
"Column ambiguousState cannot declare both AggregateFunction and SimpleAggregateFunction",
|
|
1165
|
+
);
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
test.each(["", " max", "max ", "max)", "max, DateTime"])(
|
|
1169
|
+
"rejects unsafe SimpleAggregateFunction name %p",
|
|
1170
|
+
(functionName: string) => {
|
|
1171
|
+
expect(() => {
|
|
1172
|
+
return generator.toColumnsCreateStatement([
|
|
1173
|
+
new AnalyticsTableColumn({
|
|
1174
|
+
key: "invalidMeasure",
|
|
1175
|
+
title: "<title>",
|
|
1176
|
+
description: "<description>",
|
|
1177
|
+
required: true,
|
|
1178
|
+
type: TableColumnType.Date,
|
|
1179
|
+
simpleAggregateFunction: functionName,
|
|
1180
|
+
}),
|
|
1181
|
+
]);
|
|
1182
|
+
}).toThrow(
|
|
1183
|
+
`Column invalidMeasure has invalid simpleAggregateFunction "${functionName}"`,
|
|
1184
|
+
);
|
|
1185
|
+
},
|
|
1186
|
+
);
|
|
1187
|
+
|
|
1188
|
+
test("rejects AggregateFunction without its state definition", () => {
|
|
1189
|
+
expect(() => {
|
|
1190
|
+
return generator.toColumnsCreateStatement([
|
|
1191
|
+
new AnalyticsTableColumn({
|
|
1192
|
+
key: "missingStateDefinition",
|
|
1193
|
+
title: "<title>",
|
|
1194
|
+
description: "<description>",
|
|
1195
|
+
required: true,
|
|
1196
|
+
type: TableColumnType.AggregateFunction,
|
|
1197
|
+
}),
|
|
1198
|
+
]);
|
|
1199
|
+
}).toThrow(
|
|
1200
|
+
"Column missingStateDefinition is AggregateFunction but missing aggregateFunctionDefinition",
|
|
1201
|
+
);
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1105
1204
|
test("emits single and pipelined CODEC clauses", () => {
|
|
1106
1205
|
const statement: Statement = generator.toColumnsCreateStatement([
|
|
1107
1206
|
new AnalyticsTableColumn({
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import HashCode from "../../Types/HashCode";
|
|
2
|
+
|
|
3
|
+
describe("HashCode", () => {
|
|
4
|
+
describe("fromString", () => {
|
|
5
|
+
test("returns 0 for an empty string", () => {
|
|
6
|
+
expect(HashCode.fromString("")).toBe(0);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("is deterministic for the same input", () => {
|
|
10
|
+
expect(HashCode.fromString("oneuptime")).toBe(
|
|
11
|
+
HashCode.fromString("oneuptime"),
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("produces different hashes for different inputs", () => {
|
|
16
|
+
expect(HashCode.fromString("abc")).not.toBe(HashCode.fromString("abd"));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("is order-sensitive", () => {
|
|
20
|
+
expect(HashCode.fromString("ab")).not.toBe(HashCode.fromString("ba"));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("returns a 32-bit signed integer", () => {
|
|
24
|
+
const hash: number = HashCode.fromString(
|
|
25
|
+
"a fairly long string to exercise the shift/overflow path",
|
|
26
|
+
);
|
|
27
|
+
expect(Number.isInteger(hash)).toBe(true);
|
|
28
|
+
expect(hash).toBeGreaterThanOrEqual(-(2 ** 31));
|
|
29
|
+
expect(hash).toBeLessThanOrEqual(2 ** 31 - 1);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("matches the known Java-style hashCode value", () => {
|
|
33
|
+
// "hello".hashCode() in Java is 99162322.
|
|
34
|
+
expect(HashCode.fromString("hello")).toBe(99162322);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("single character equals its char code", () => {
|
|
38
|
+
expect(HashCode.fromString("A")).toBe("A".charCodeAt(0));
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import parseLogQuery, {
|
|
2
|
+
FilterOperator,
|
|
3
|
+
ParsedToken,
|
|
4
|
+
TokenType,
|
|
5
|
+
tokensToDisplayString,
|
|
6
|
+
} from "../../../Types/Log/LogQueryParser";
|
|
7
|
+
|
|
8
|
+
describe("LogQueryParser", () => {
|
|
9
|
+
describe("parseLogQuery - empty and free text", () => {
|
|
10
|
+
test("returns an empty array for empty / whitespace input", () => {
|
|
11
|
+
expect(parseLogQuery("")).toEqual([]);
|
|
12
|
+
expect(parseLogQuery(" ")).toEqual([]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("combines free text words into a single Contains token", () => {
|
|
16
|
+
const tokens: Array<ParsedToken> = parseLogQuery("connection refused");
|
|
17
|
+
expect(tokens).toHaveLength(1);
|
|
18
|
+
expect(tokens[0]).toMatchObject({
|
|
19
|
+
type: TokenType.FreeText,
|
|
20
|
+
operator: FilterOperator.Contains,
|
|
21
|
+
value: "connection refused",
|
|
22
|
+
negated: false,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("strips quotes from a quoted phrase", () => {
|
|
27
|
+
const tokens: Array<ParsedToken> = parseLogQuery('"connection refused"');
|
|
28
|
+
expect(tokens).toHaveLength(1);
|
|
29
|
+
expect(tokens[0]!.value).toBe("connection refused");
|
|
30
|
+
expect(tokens[0]!.type).toBe(TokenType.FreeText);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("parseLogQuery - field filters and aliases", () => {
|
|
35
|
+
test("resolves the severity alias to severityText", () => {
|
|
36
|
+
const tokens: Array<ParsedToken> = parseLogQuery("severity:error");
|
|
37
|
+
expect(tokens[0]).toMatchObject({
|
|
38
|
+
type: TokenType.FieldFilter,
|
|
39
|
+
field: "severityText",
|
|
40
|
+
operator: FilterOperator.Equals,
|
|
41
|
+
value: "error",
|
|
42
|
+
negated: false,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("resolves the service alias to primaryEntityId", () => {
|
|
47
|
+
expect(parseLogQuery("service:api")[0]!.field).toBe("primaryEntityId");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("resolves level, message, and trace aliases", () => {
|
|
51
|
+
expect(parseLogQuery("level:warn")[0]!.field).toBe("severityText");
|
|
52
|
+
expect(parseLogQuery("message:hello")[0]!.field).toBe("body");
|
|
53
|
+
expect(parseLogQuery("trace:abc")[0]!.field).toBe("traceId");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("keeps unknown field names as-is", () => {
|
|
57
|
+
expect(parseLogQuery("customfield:x")[0]!.field).toBe("customfield");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("parseLogQuery - attribute filters", () => {
|
|
62
|
+
test("parses @-prefixed attribute access", () => {
|
|
63
|
+
const tokens: Array<ParsedToken> = parseLogQuery("@http.status_code:500");
|
|
64
|
+
expect(tokens[0]).toMatchObject({
|
|
65
|
+
type: TokenType.AttributeFilter,
|
|
66
|
+
field: "http.status_code",
|
|
67
|
+
operator: FilterOperator.Equals,
|
|
68
|
+
value: "500",
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("does not run aliasing on attribute field names", () => {
|
|
73
|
+
// "service" is an alias for field filters but must stay literal as an attribute.
|
|
74
|
+
expect(parseLogQuery("@service:api")[0]!.field).toBe("service");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("parseLogQuery - negation", () => {
|
|
79
|
+
test("negated equals becomes NotEquals", () => {
|
|
80
|
+
const tokens: Array<ParsedToken> = parseLogQuery("-severity:debug");
|
|
81
|
+
expect(tokens[0]).toMatchObject({
|
|
82
|
+
field: "severityText",
|
|
83
|
+
operator: FilterOperator.NotEquals,
|
|
84
|
+
value: "debug",
|
|
85
|
+
negated: true,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("negated attribute filter", () => {
|
|
90
|
+
const tokens: Array<ParsedToken> = parseLogQuery("-@http.method:GET");
|
|
91
|
+
expect(tokens[0]).toMatchObject({
|
|
92
|
+
type: TokenType.AttributeFilter,
|
|
93
|
+
field: "http.method",
|
|
94
|
+
operator: FilterOperator.NotEquals,
|
|
95
|
+
negated: true,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("parseLogQuery - operators", () => {
|
|
101
|
+
test("detects wildcard values", () => {
|
|
102
|
+
const tokens: Array<ParsedToken> = parseLogQuery("service:api-*");
|
|
103
|
+
expect(tokens[0]).toMatchObject({
|
|
104
|
+
operator: FilterOperator.Wildcard,
|
|
105
|
+
value: "api-*",
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("detects numeric comparison operators", () => {
|
|
110
|
+
expect(parseLogQuery("@duration:>1000")[0]).toMatchObject({
|
|
111
|
+
operator: FilterOperator.GreaterThan,
|
|
112
|
+
value: "1000",
|
|
113
|
+
});
|
|
114
|
+
expect(parseLogQuery("@duration:>=1000")[0]).toMatchObject({
|
|
115
|
+
operator: FilterOperator.GreaterThanOrEqual,
|
|
116
|
+
value: "1000",
|
|
117
|
+
});
|
|
118
|
+
expect(parseLogQuery("@duration:<50")[0]).toMatchObject({
|
|
119
|
+
operator: FilterOperator.LessThan,
|
|
120
|
+
value: "50",
|
|
121
|
+
});
|
|
122
|
+
expect(parseLogQuery("@duration:<=50")[0]).toMatchObject({
|
|
123
|
+
operator: FilterOperator.LessThanOrEqual,
|
|
124
|
+
value: "50",
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("parseLogQuery - boolean keywords and mixed queries", () => {
|
|
130
|
+
test("skips bare AND/OR/NOT keywords", () => {
|
|
131
|
+
const tokens: Array<ParsedToken> = parseLogQuery(
|
|
132
|
+
"severity:error AND service:api",
|
|
133
|
+
);
|
|
134
|
+
expect(tokens).toHaveLength(2);
|
|
135
|
+
expect(tokens[0]!.field).toBe("severityText");
|
|
136
|
+
expect(tokens[1]!.field).toBe("primaryEntityId");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("splits free text around a field filter", () => {
|
|
140
|
+
const tokens: Array<ParsedToken> = parseLogQuery(
|
|
141
|
+
"connection severity:error refused",
|
|
142
|
+
);
|
|
143
|
+
expect(tokens).toHaveLength(3);
|
|
144
|
+
expect(tokens[0]).toMatchObject({
|
|
145
|
+
type: TokenType.FreeText,
|
|
146
|
+
value: "connection",
|
|
147
|
+
});
|
|
148
|
+
expect(tokens[1]).toMatchObject({
|
|
149
|
+
type: TokenType.FieldFilter,
|
|
150
|
+
field: "severityText",
|
|
151
|
+
});
|
|
152
|
+
expect(tokens[2]).toMatchObject({
|
|
153
|
+
type: TokenType.FreeText,
|
|
154
|
+
value: "refused",
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("does not split a quoted phrase containing spaces", () => {
|
|
159
|
+
const tokens: Array<ParsedToken> = parseLogQuery(
|
|
160
|
+
'"connection refused" severity:error',
|
|
161
|
+
);
|
|
162
|
+
expect(tokens).toHaveLength(2);
|
|
163
|
+
expect(tokens[0]!.value).toBe("connection refused");
|
|
164
|
+
expect(tokens[1]!.field).toBe("severityText");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("strips quotes from field values", () => {
|
|
168
|
+
const tokens: Array<ParsedToken> = parseLogQuery('message:"a b c"');
|
|
169
|
+
expect(tokens[0]!.value).toBe("a b c");
|
|
170
|
+
expect(tokens[0]!.field).toBe("body");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("tokensToDisplayString", () => {
|
|
175
|
+
test("rejoins tokens using their raw representation", () => {
|
|
176
|
+
const tokens: Array<ParsedToken> = parseLogQuery(
|
|
177
|
+
"severity:error service:api",
|
|
178
|
+
);
|
|
179
|
+
expect(tokensToDisplayString(tokens)).toBe("severity:error service:api");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("returns an empty string for no tokens", () => {
|
|
183
|
+
expect(tokensToDisplayString([])).toBe("");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import Includes from "../../../Types/BaseDatabase/Includes";
|
|
2
|
+
import DashboardVariable, {
|
|
3
|
+
DashboardVariableType,
|
|
4
|
+
} from "../../../Types/Dashboard/DashboardVariable";
|
|
5
|
+
import DashboardVariableInterpolation from "../../../Utils/Dashboard/VariableInterpolation";
|
|
6
|
+
|
|
7
|
+
function makeVariable(
|
|
8
|
+
overrides: Partial<DashboardVariable>,
|
|
9
|
+
): DashboardVariable {
|
|
10
|
+
return {
|
|
11
|
+
id: "var-1",
|
|
12
|
+
name: "cluster",
|
|
13
|
+
type: DashboardVariableType.TelemetryAttribute,
|
|
14
|
+
...overrides,
|
|
15
|
+
} as DashboardVariable;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("DashboardVariableInterpolation", () => {
|
|
19
|
+
describe("resolveValue", () => {
|
|
20
|
+
test("returns scalar for a single-select with selectedValue", () => {
|
|
21
|
+
const variable: DashboardVariable = makeVariable({
|
|
22
|
+
selectedValue: "prod",
|
|
23
|
+
});
|
|
24
|
+
expect(DashboardVariableInterpolation.resolveValue(variable)).toEqual({
|
|
25
|
+
scalar: "prod",
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("falls back to defaultValue when selectedValue is unset", () => {
|
|
30
|
+
const variable: DashboardVariable = makeVariable({
|
|
31
|
+
defaultValue: "staging",
|
|
32
|
+
});
|
|
33
|
+
expect(DashboardVariableInterpolation.resolveValue(variable)).toEqual({
|
|
34
|
+
scalar: "staging",
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("prefers selectedValue over defaultValue", () => {
|
|
39
|
+
const variable: DashboardVariable = makeVariable({
|
|
40
|
+
selectedValue: "prod",
|
|
41
|
+
defaultValue: "staging",
|
|
42
|
+
});
|
|
43
|
+
expect(DashboardVariableInterpolation.resolveValue(variable)).toEqual({
|
|
44
|
+
scalar: "prod",
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("returns undefined when nothing is selected", () => {
|
|
49
|
+
expect(
|
|
50
|
+
DashboardVariableInterpolation.resolveValue(makeVariable({})),
|
|
51
|
+
).toBeUndefined();
|
|
52
|
+
expect(
|
|
53
|
+
DashboardVariableInterpolation.resolveValue(
|
|
54
|
+
makeVariable({ selectedValue: "" }),
|
|
55
|
+
),
|
|
56
|
+
).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("returns multi for a multi-select with values", () => {
|
|
60
|
+
const variable: DashboardVariable = makeVariable({
|
|
61
|
+
isMultiSelect: true,
|
|
62
|
+
selectedValues: ["a", "b"],
|
|
63
|
+
});
|
|
64
|
+
expect(DashboardVariableInterpolation.resolveValue(variable)).toEqual({
|
|
65
|
+
multi: ["a", "b"],
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("filters out empty values in multi-select", () => {
|
|
70
|
+
const variable: DashboardVariable = makeVariable({
|
|
71
|
+
isMultiSelect: true,
|
|
72
|
+
selectedValues: ["a", "", "b"],
|
|
73
|
+
});
|
|
74
|
+
expect(DashboardVariableInterpolation.resolveValue(variable)).toEqual({
|
|
75
|
+
multi: ["a", "b"],
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("multi-select with no picks falls through to scalar default", () => {
|
|
80
|
+
const variable: DashboardVariable = makeVariable({
|
|
81
|
+
isMultiSelect: true,
|
|
82
|
+
selectedValues: [],
|
|
83
|
+
defaultValue: "d",
|
|
84
|
+
});
|
|
85
|
+
expect(DashboardVariableInterpolation.resolveValue(variable)).toEqual({
|
|
86
|
+
scalar: "d",
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("applyToAttributes", () => {
|
|
92
|
+
test("returns the same reference when there are no variables", () => {
|
|
93
|
+
const attrs: Record<string, unknown> = { foo: "bar" };
|
|
94
|
+
expect(
|
|
95
|
+
DashboardVariableInterpolation.applyToAttributes(attrs, undefined),
|
|
96
|
+
).toBe(attrs);
|
|
97
|
+
expect(DashboardVariableInterpolation.applyToAttributes(attrs, [])).toBe(
|
|
98
|
+
attrs,
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("injects a scalar attribute for a selected variable", () => {
|
|
103
|
+
const variable: DashboardVariable = makeVariable({
|
|
104
|
+
attributeKey: "k8s.cluster.name",
|
|
105
|
+
selectedValue: "prod",
|
|
106
|
+
});
|
|
107
|
+
const result: Record<string, unknown> =
|
|
108
|
+
DashboardVariableInterpolation.applyToAttributes({}, [variable]);
|
|
109
|
+
expect(result).toEqual({ "k8s.cluster.name": "prod" });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("emits an Includes operator for multi-select", () => {
|
|
113
|
+
const variable: DashboardVariable = makeVariable({
|
|
114
|
+
attributeKey: "k8s.cluster.name",
|
|
115
|
+
isMultiSelect: true,
|
|
116
|
+
selectedValues: ["a", "b"],
|
|
117
|
+
});
|
|
118
|
+
const result: Record<string, unknown> =
|
|
119
|
+
DashboardVariableInterpolation.applyToAttributes({}, [variable]);
|
|
120
|
+
const value: unknown = result["k8s.cluster.name"];
|
|
121
|
+
expect(value).toBeInstanceOf(Includes);
|
|
122
|
+
expect((value as Includes).values).toEqual(["a", "b"]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("removes a previously-set filter when selection is cleared (All)", () => {
|
|
126
|
+
const variable: DashboardVariable = makeVariable({
|
|
127
|
+
attributeKey: "k8s.cluster.name",
|
|
128
|
+
selectedValue: "",
|
|
129
|
+
});
|
|
130
|
+
const result: Record<string, unknown> =
|
|
131
|
+
DashboardVariableInterpolation.applyToAttributes(
|
|
132
|
+
{ "k8s.cluster.name": "prod", other: "keep" },
|
|
133
|
+
[variable],
|
|
134
|
+
);
|
|
135
|
+
expect(result).toEqual({ other: "keep" });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("ignores non-TelemetryAttribute variables", () => {
|
|
139
|
+
const attrs: Record<string, unknown> = { foo: "bar" };
|
|
140
|
+
const variable: DashboardVariable = makeVariable({
|
|
141
|
+
type: DashboardVariableType.TextInput,
|
|
142
|
+
attributeKey: "k8s.cluster.name",
|
|
143
|
+
selectedValue: "prod",
|
|
144
|
+
});
|
|
145
|
+
expect(
|
|
146
|
+
DashboardVariableInterpolation.applyToAttributes(attrs, [variable]),
|
|
147
|
+
).toBe(attrs);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("ignores TelemetryAttribute variables without an attributeKey", () => {
|
|
151
|
+
const attrs: Record<string, unknown> = { foo: "bar" };
|
|
152
|
+
const variable: DashboardVariable = makeVariable({
|
|
153
|
+
selectedValue: "prod",
|
|
154
|
+
});
|
|
155
|
+
expect(
|
|
156
|
+
DashboardVariableInterpolation.applyToAttributes(attrs, [variable]),
|
|
157
|
+
).toBe(attrs);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("returns same reference when scalar value is already set", () => {
|
|
161
|
+
const attrs: Record<string, unknown> = { "k8s.cluster.name": "prod" };
|
|
162
|
+
const variable: DashboardVariable = makeVariable({
|
|
163
|
+
attributeKey: "k8s.cluster.name",
|
|
164
|
+
selectedValue: "prod",
|
|
165
|
+
});
|
|
166
|
+
expect(
|
|
167
|
+
DashboardVariableInterpolation.applyToAttributes(attrs, [variable]),
|
|
168
|
+
).toBe(attrs);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("handles undefined attributes map", () => {
|
|
172
|
+
const variable: DashboardVariable = makeVariable({
|
|
173
|
+
attributeKey: "k8s.cluster.name",
|
|
174
|
+
selectedValue: "prod",
|
|
175
|
+
});
|
|
176
|
+
const result: Record<string, unknown> =
|
|
177
|
+
DashboardVariableInterpolation.applyToAttributes(undefined, [variable]);
|
|
178
|
+
expect(result).toEqual({ "k8s.cluster.name": "prod" });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("applies multiple variables at once", () => {
|
|
182
|
+
const v1: DashboardVariable = makeVariable({
|
|
183
|
+
id: "v1",
|
|
184
|
+
attributeKey: "cluster",
|
|
185
|
+
selectedValue: "prod",
|
|
186
|
+
});
|
|
187
|
+
const v2: DashboardVariable = makeVariable({
|
|
188
|
+
id: "v2",
|
|
189
|
+
attributeKey: "namespace",
|
|
190
|
+
selectedValue: "default",
|
|
191
|
+
});
|
|
192
|
+
const result: Record<string, unknown> =
|
|
193
|
+
DashboardVariableInterpolation.applyToAttributes({}, [v1, v2]);
|
|
194
|
+
expect(result).toEqual({ cluster: "prod", namespace: "default" });
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|