From 51819d6337dccc5af88f5e5a707a1e598d9e370a Mon Sep 17 00:00:00 2001 From: Naman Verma Date: Sat, 25 Jul 2026 14:11:07 +0530 Subject: [PATCH] chore: add migration script from current to perses dashboard (#11712) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: add migration script from current to perses dashboard * fix: move WrapInV5Envelope to types package * fix: generate internal name from title * fix: match with lower case signal for variables * fix: sanitize tags instead of throwing error * fix: extract row and widget positions to build expanded sections * chore: add a catch all panic check to log migration error * chore: catch typecast errors * fix: remove nil nil return * test: fix ut to check for v2 internal name * chore: remove spec md file * chore: separate error type for migration * feat: note down all errors in migration * fix: allow zero value for threshold values * fix: ignore react placeholders in layout * fix: note error in query parsing * fix: read threshold value properly during migration * fix: adjust to schema changes in variables * test: fix unit tests * fix: catch error from decodeTelemetryFields * fix: make threshold value non-nullable * fix: handle list of widget IDs in panel maps * fix: add better error note down for failed conversions * fix: normalize having array from v4 schema * fix: remove metric aggregation from non metric queries (malformed json) * fix: normalize telemetry field keys in query * chore: add temporary doc for other malformation handlers needed * fix: add more malformed query handling * fix: add more malformation handling * chore: reuse transition package (adds dependency, need to see what to do) * fix: fix go lint issue * revert: remove md file * fix: make none the default fill mode during migration * fix: add nil check on limit value when normalizing page size * fix: validate dashboard spec after migration * fix: remove unused method * fix: match shape safe version to existing version * chore: improve variable names * chore: better comments and names in layout migrator * chore: add explanation comment for continuing on empty id widget * fix: ditch dynamic vars with no attr, rearrange overlapping layouts * fix: add default aggregation count() if none is present in logs/traces * fix: add empty list tolerance to v1 variables * fix: add fixes based on backup migration testing * chore: skip over unreferenced widgets * chore: remove widgets that have a single query that has no metric name * chore: assign missing formula names like ui does * chore: assign query names to queries if missing * chore: drop legacy filter fields that ui also ignores * chore: make migration drop widgets that have no aggregations for metrics * chore: normalize group by * chore: remove expressions from metric aggregations * chore: add more error notes * chore: replace em dash with hyphen in widget id * chore: add more panel id sanitization * chore: replace faulty order by keys with aggregation expression * chore: fill valid span gaps * chore: handle misformed threshold operators * chore: remove repeat occurences of widgets in layouts * chore: remove limits that cross max limit * chore: restore function args malformed by v4->v5 migration * chore: remove invalid functions * chore: skip variables in wrong type * chore: add wdiget id type check in layout migration * chore: add a.count to malformed order by keys list * chore: remove bad aggregations that accompany valid ones * chore: drop trace operators with no expression * chore: drop invalid formulas * chore: skip unknown panel kind * chore: drop widgets with >1 and all aggregations as malformed * chore: skip empty type variables * chore: pass threshold format raw without typecasting * chore: add handling for trace operators around wrapinv5env * chore: handle no string value for variable sort * test: fix span gaps test * chore: undo schema change in threshold value * chore: final set of error fixes/hacks * fix: fix build errors post merge conflicts * test: remove test for floating pt value * test: use appendf instead of sprintf * feat: add scaffolding to migrate dashboards * fix: fill in ref path when building grid layout * feat: handle v1 payloads in v2 create api * fix: return empty objects instead of null if no variables/panels/layouts are there * test: fix unit test for empty layouts * fix: add empty links array in migration * test: add unit test to show v5 takes precedence over v4 * fix: change EXISTS to exists so that migrateCommon handles it properly * test: add unit test for count with attributes * fix: run order by handling after default aggregation handling * fix: restore formula fields that wrapinv5envelope removes * fix: keep thresholds that do not have an optional label * fix: force allowAll to be false if allowMultiple is false * fix: migrate name column even if data column migration fails * fix: use slog for logging * refactor(dashboard): drop ConvertAllV1ToV2 module scaffolding from base Remove the temporary v1→v2 bulk-conversion module method (ConvertAllV1ToV2 + migrateOneV1ToV2), its result types (V1ToV2MigrationResult / V1ToV2MigrationItem), the Module interface entry, and the EE delegation. This scaffolding lives on the migration feature branches, not the base branch that merges to main. V1Name and store.UpdateName are kept (small helpers; UpdateName is on the shared Store interface). * fix: add image validation and base64 to asset conversion --------- Co-authored-by: Srikanth Chekuri --- pkg/modules/dashboard/impldashboard/store.go | 17 + .../dashboard/impldashboard/v2_handler.go | 36 +- pkg/transition/migrate_common.go | 116 +- pkg/transition/migrate_shape_safe.go | 353 +++ pkg/types/dashboardtypes/dashboard.go | 9 +- pkg/types/dashboardtypes/perses_v1_to_v2.go | 103 + .../dashboardtypes/perses_v1_to_v2_decoder.go | 214 ++ .../dashboardtypes/perses_v1_to_v2_icons.go | 52 + .../dashboardtypes/perses_v1_to_v2_layouts.go | 352 +++ .../dashboardtypes/perses_v1_to_v2_panels.go | 487 ++++ .../dashboardtypes/perses_v1_to_v2_queries.go | 448 ++++ .../perses_v1_to_v2_queries_malformed.go | 499 ++++ .../dashboardtypes/perses_v1_to_v2_tags.go | 122 + .../dashboardtypes/perses_v1_to_v2_test.go | 2153 +++++++++++++++++ .../perses_v1_to_v2_variables.go | 243 ++ pkg/types/dashboardtypes/store.go | 4 + .../testdata/v1_avocado_icon.txt | 1 + .../querybuildertypesv5/migrate_envelope.go | 127 + 18 files changed, 5217 insertions(+), 119 deletions(-) create mode 100644 pkg/transition/migrate_shape_safe.go create mode 100644 pkg/types/dashboardtypes/perses_v1_to_v2.go create mode 100644 pkg/types/dashboardtypes/perses_v1_to_v2_decoder.go create mode 100644 pkg/types/dashboardtypes/perses_v1_to_v2_icons.go create mode 100644 pkg/types/dashboardtypes/perses_v1_to_v2_layouts.go create mode 100644 pkg/types/dashboardtypes/perses_v1_to_v2_panels.go create mode 100644 pkg/types/dashboardtypes/perses_v1_to_v2_queries.go create mode 100644 pkg/types/dashboardtypes/perses_v1_to_v2_queries_malformed.go create mode 100644 pkg/types/dashboardtypes/perses_v1_to_v2_tags.go create mode 100644 pkg/types/dashboardtypes/perses_v1_to_v2_test.go create mode 100644 pkg/types/dashboardtypes/perses_v1_to_v2_variables.go create mode 100644 pkg/types/dashboardtypes/testdata/v1_avocado_icon.txt create mode 100644 pkg/types/querybuildertypes/querybuildertypesv5/migrate_envelope.go diff --git a/pkg/modules/dashboard/impldashboard/store.go b/pkg/modules/dashboard/impldashboard/store.go index 803082c08ff..5a7391fc6b6 100644 --- a/pkg/modules/dashboard/impldashboard/store.go +++ b/pkg/modules/dashboard/impldashboard/store.go @@ -351,6 +351,23 @@ func (store *store) Update(ctx context.Context, orgID valuer.UUID, storableDashb return nil } +func (store *store) UpdateName(ctx context.Context, orgID valuer.UUID, id valuer.UUID, name string) error { + _, err := store. + sqlstore. + BunDBCtx(ctx). + NewUpdate(). + Model((*dashboardtypes.StorableDashboard)(nil)). + Set("name = ?", name). + Where("id = ?", id). + Where("org_id = ?", orgID). + Exec(ctx) + if err != nil { + return store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "dashboard with id %s doesn't exist", id) + } + + return nil +} + func (store *store) UpdatePublic(ctx context.Context, storable *dashboardtypes.StorablePublicDashboard) error { _, err := store. sqlstore. diff --git a/pkg/modules/dashboard/impldashboard/v2_handler.go b/pkg/modules/dashboard/impldashboard/v2_handler.go index a1bf46f4c0b..5e6f5a5073b 100644 --- a/pkg/modules/dashboard/impldashboard/v2_handler.go +++ b/pkg/modules/dashboard/impldashboard/v2_handler.go @@ -1,16 +1,21 @@ package impldashboard import ( + "bytes" "context" + "encoding/json" + "io" "net/http" "time" "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/http/binding" "github.com/SigNoz/signoz/pkg/http/render" + "github.com/SigNoz/signoz/pkg/transition" "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/types/coretypes" "github.com/SigNoz/signoz/pkg/types/dashboardtypes" + "github.com/SigNoz/signoz/pkg/types/tagtypes" "github.com/SigNoz/signoz/pkg/valuer" "github.com/gorilla/mux" ) @@ -27,12 +32,39 @@ func (handler *handler) CreateV2(rw http.ResponseWriter, r *http.Request) { orgID := valuer.MustNewUUID(claims.OrgID) - var req dashboardtypes.PostableDashboardV2 - if err := binding.JSON.BindBody(r.Body, &req); err != nil { + // Read the body ourselves (rather than binding straight from r.Body) so the + // raw bytes survive a failed bind for the v1 fallback below. + body, err := io.ReadAll(r.Body) + if err != nil { render.Error(rw, err) return } + var req dashboardtypes.PostableDashboardV2 + if err := binding.JSON.BindBody(bytes.NewReader(body), &req); err != nil { + // Fallback: the body may be a legacy v1 dashboard. Migrate it to v2 (same + // v4→v5 + v1→v2 pass the bulk migration runs) and retry; if it isn't a + // convertible v1 payload either, surface the original v2 binding error. + var data map[string]any + if json.Unmarshal(body, &data) != nil { + render.Error(rw, err) + return + } + storable := dashboardtypes.StorableDashboard{Data: data, OrgID: orgID} + transition.NewDashboardMigrateV5(handler.providerSettings.Logger, nil, nil).Migrate(ctx, storable.Data) + v2, convErr := storable.ConvertV1ToV2() + if convErr != nil { + render.Error(rw, err) + return + } + req = dashboardtypes.PostableDashboardV2{ + DashboardV2MetadataBase: v2.DashboardV2MetadataBase, + Name: v2.Name, + Tags: tagtypes.NewPostableTagsFromTags(v2.Tags), + Spec: v2.Spec, + } + } + dashboard, err := handler.module.CreateV2(ctx, orgID, claims.Email, valuer.MustNewUUID(claims.IdentityID()), dashboardtypes.SourceUser, req) if err != nil { render.Error(rw, err) diff --git a/pkg/transition/migrate_common.go b/pkg/transition/migrate_common.go index a163a5ba714..1c2bcb0c4c2 100644 --- a/pkg/transition/migrate_common.go +++ b/pkg/transition/migrate_common.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/SigNoz/signoz/pkg/telemetrytraces" + "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" ) type migrateCommon struct { @@ -23,119 +24,10 @@ func NewMigrateCommon(logger *slog.Logger) *migrateCommon { } } +// WrapInV5Envelope delegates to querybuildertypesv5.WrapInV5Envelope; the +// transform is stateless and shared with the v1→v2 dashboard conversion. func (migration *migrateCommon) WrapInV5Envelope(name string, queryMap map[string]any, queryType string) map[string]any { - // Create a properly structured v5 query - v5Query := map[string]any{ - "name": name, - "disabled": queryMap["disabled"], - "legend": queryMap["legend"], - } - - if name != queryMap["expression"] { - // formula - queryType = "builder_formula" - v5Query["expression"] = queryMap["expression"] - if functions, ok := queryMap["functions"]; ok { - v5Query["functions"] = functions - } - return map[string]any{ - "type": queryType, - "spec": v5Query, - } - } - - // Add signal based on data source - if dataSource, ok := queryMap["dataSource"].(string); ok { - switch dataSource { - case "traces": - v5Query["signal"] = "traces" - case "logs": - v5Query["signal"] = "logs" - case "metrics": - v5Query["signal"] = "metrics" - } - } - - if stepInterval, ok := queryMap["stepInterval"]; ok { - v5Query["stepInterval"] = stepInterval - } - - if aggregations, ok := queryMap["aggregations"]; ok { - v5Query["aggregations"] = aggregations - } - - if filter, ok := queryMap["filter"]; ok { - v5Query["filter"] = filter - } - - // Copy groupBy with proper structure - if groupBy, ok := queryMap["groupBy"].([]any); ok { - v5GroupBy := make([]any, len(groupBy)) - for i, gb := range groupBy { - if gbMap, ok := gb.(map[string]any); ok { - v5GroupBy[i] = map[string]any{ - "name": gbMap["key"], - "fieldDataType": gbMap["dataType"], - "fieldContext": gbMap["type"], - } - } - } - v5Query["groupBy"] = v5GroupBy - } - - // Copy orderBy with proper structure - if orderBy, ok := queryMap["orderBy"].([]any); ok { - v5OrderBy := make([]any, len(orderBy)) - for i, ob := range orderBy { - if obMap, ok := ob.(map[string]any); ok { - v5OrderBy[i] = map[string]any{ - "key": map[string]any{ - "name": obMap["columnName"], - "fieldDataType": obMap["dataType"], - "fieldContext": obMap["type"], - }, - "direction": obMap["order"], - } - } - } - v5Query["order"] = v5OrderBy - } - - // Copy selectColumns as selectFields - if selectColumns, ok := queryMap["selectColumns"].([]any); ok { - v5SelectFields := make([]any, len(selectColumns)) - for i, col := range selectColumns { - if colMap, ok := col.(map[string]any); ok { - v5SelectFields[i] = map[string]any{ - "name": colMap["key"], - "fieldDataType": colMap["dataType"], - "fieldContext": colMap["type"], - } - } - } - v5Query["selectFields"] = v5SelectFields - } - - // Copy limit and offset - if limit, ok := queryMap["limit"]; ok { - v5Query["limit"] = limit - } - if offset, ok := queryMap["offset"]; ok { - v5Query["offset"] = offset - } - - if having, ok := queryMap["having"]; ok { - v5Query["having"] = having - } - - if functions, ok := queryMap["functions"]; ok { - v5Query["functions"] = functions - } - - return map[string]any{ - "type": queryType, - "spec": v5Query, - } + return querybuildertypesv5.WrapInV5Envelope(name, queryMap, queryType) } func (mc *migrateCommon) updateQueryData(ctx context.Context, queryData map[string]any, version, widgetType string) bool { diff --git a/pkg/transition/migrate_shape_safe.go b/pkg/transition/migrate_shape_safe.go new file mode 100644 index 00000000000..71de14378ce --- /dev/null +++ b/pkg/transition/migrate_shape_safe.go @@ -0,0 +1,353 @@ +// nolint +package transition + +import ( + "context" + "log/slog" +) + +// ══════════════════════════════════════════════ +// Shape-safe (idempotent) migration +// ══════════════════════════════════════════════ +// +// A copy of the Migrate → updateWidget → updateQueryData chain with the +// "uniformly v4 input" assumption removed, so it is safe on a dashboard whose +// `version` tag lies (a "v5"-labelled dashboard with un-upgraded, possibly mixed, +// bodies — the v1→v2 converter's case). Versus the original: no version gate, and +// each step acts only on the pre-v5 shape (leaving a v5 field alone), so it is +// idempotent. The original Migrate is left unchanged (battle-tested, no test net). +// The *ShapeSafe methods below each note the original they copy; the reused steps +// (createFilterExpression, fixGroupBy, buildAggregationExpression, orderByExpr) are +// already v5-safe. + +// MigrateQueryDataShapeSafe is the per-query entry point (the core of +// updateQueryDataShapeSafe) for callers that process queries one at a time (the +// v1→v2 converter). widgetType is the v1 panelTypes (metric reduceTo on tables); +// "" is safe. +func (m *dashboardMigrateV5) MigrateQueryDataShapeSafe(ctx context.Context, queryData map[string]any, widgetType string) bool { + return m.updateQueryDataShapeSafe(ctx, queryData, widgetType) +} + +// updateQueryDataShapeSafe copies updateQueryData, with each destructive step +// guarded to act only on the pre-v5 shape (see the file header). +func (mc *migrateCommon) updateQueryDataShapeSafe(ctx context.Context, queryData map[string]any, widgetType string) bool { + updated := false + + aggregateOp, _ := queryData["aggregateOperator"].(string) + hasAggregation := aggregateOp != "" && aggregateOp != "noop" + + if mc.createAggregationsShapeSafe(ctx, queryData, widgetType) { + updated = true + } + + // createFilterExpression only touches v4 `filters`; skip if a v5 `filter` exists. + if _, hasFilter := queryData["filter"]; !hasFilter { + if mc.createFilterExpression(ctx, queryData) { + updated = true + } + } + + if mc.fixGroupBy(queryData) { + updated = true + } + + if mc.createHavingExpressionShapeSafe(queryData) { + updated = true + } + + if hasAggregation { + if orderBy, ok := queryData["orderBy"].([]any); ok && orderByIsPreV5(orderBy) { + newOrderBy := make([]any, 0) + for _, order := range orderBy { + if orderMap, ok := order.(map[string]any); ok { + columnName, _ := orderMap["columnName"].(string) + // skip timestamp, id (logs, traces), samples(metrics) ordering for aggregation queries + if columnName != "timestamp" && columnName != "samples" && columnName != "id" { + if columnName == "#SIGNOZ_VALUE" { + if expr, has := mc.orderByExpr(queryData); has { + orderMap["columnName"] = expr + } + } else { + // if the order by key is not part of the group by keys, remove it + present := false + + groupBy, ok := queryData["groupBy"].([]any) + if !ok { + return false + } + + for idx := range groupBy { + item, ok := groupBy[idx].(map[string]any) + if !ok { + continue + } + key, ok := item["key"].(string) + if !ok { + continue + } + if key == columnName { + present = true + } + } + + if !present { + mc.logger.WarnContext(ctx, "found a order by without group by, skipping", slog.String("order_col_name", columnName)) + continue + } + } + newOrderBy = append(newOrderBy, orderMap) + } + } + } + queryData["orderBy"] = newOrderBy + updated = true + } + } else { + dataSource, _ := queryData["dataSource"].(string) + + if orderBy, ok := queryData["orderBy"].([]any); ok && orderByIsPreV5(orderBy) { + newOrderBy := make([]any, 0) + for _, order := range orderBy { + if orderMap, ok := order.(map[string]any); ok { + columnName, _ := orderMap["columnName"].(string) + // skip id and timestamp for (traces) + if (columnName == "id" || columnName == "timestamp") && dataSource == "traces" { + mc.logger.InfoContext(ctx, "skipping `id` order by for traces") + continue + } + + // skip id for (logs) + if (columnName == "id" || columnName == "timestamp") && dataSource == "logs" { + mc.logger.InfoContext(ctx, "skipping `id`/`timestamp` order by for logs") + continue + } + + newOrderBy = append(newOrderBy, orderMap) + } + } + queryData["orderBy"] = newOrderBy + updated = true + } + } + + // Only the `&& functionsArePreV5(functions)` guard differs from updateQueryData. + if functions, ok := queryData["functions"].([]any); ok && functionsArePreV5(functions) { + v5Functions := make([]any, len(functions)) + for i, fn := range functions { + if fnMap, ok := fn.(map[string]any); ok { + v5Function := map[string]any{ + "name": fnMap["name"], + } + + // Convert args from v4 format to v5 FunctionArg format + if args, ok := fnMap["args"].([]any); ok { + v5Args := make([]any, len(args)) + for j, arg := range args { + // In v4, args were just values. In v5, they are FunctionArg objects + v5Args[j] = map[string]any{ + "name": "", // v4 didn't have named args + "value": arg, + } + } + v5Function["args"] = v5Args + } + + // Handle namedArgs if present (some functions might have used this) + if namedArgs, ok := fnMap["namedArgs"].(map[string]any); ok { + // Convert named args to the new format + existingArgs, _ := v5Function["args"].([]any) + if existingArgs == nil { + existingArgs = []any{} + } + + for name, value := range namedArgs { + existingArgs = append(existingArgs, map[string]any{ + "name": name, + "value": value, + }) + } + v5Function["args"] = existingArgs + } + + v5Functions[i] = v5Function + } + } + queryData["functions"] = v5Functions + updated = true + } + + delete(queryData, "aggregateOperator") + delete(queryData, "aggregateAttribute") + delete(queryData, "temporality") + delete(queryData, "timeAggregation") + delete(queryData, "spaceAggregation") + delete(queryData, "reduceTo") + delete(queryData, "filters") + delete(queryData, "ShiftBy") + delete(queryData, "IsAnomaly") + delete(queryData, "QueriesUsedInFormula") + delete(queryData, "seriesAggregation") + + return updated +} + +// createHavingExpressionShapeSafe copies createHavingExpression but leaves an +// already-v5 having:{expression} alone instead of wiping it. +func (mc *migrateCommon) createHavingExpressionShapeSafe(queryData map[string]any) bool { + if _, ok := queryData["having"].(map[string]any); ok { + return false // already v5-shaped + } + having, ok := queryData["having"].([]any) + if !ok || len(having) == 0 { + queryData["having"] = map[string]any{"expression": ""} + return true + } + + dataSource, _ := queryData["dataSource"].(string) + + for idx := range having { + if havingItem, ok := having[idx].(map[string]any); ok { + havingCol, has := mc.orderByExpr(queryData) + if has { + havingItem["columnName"] = havingCol + havingItem["key"] = map[string]any{"key": havingCol} + } + having[idx] = havingItem + } + } + queryData["having"] = map[string]any{"expression": mc.buildExpression(context.Background(), having, "AND", dataSource)} + return true +} + +// createAggregationsShapeSafe copies createAggregations but skips a query that +// already has a v5 aggregations[], and picks the metric time/space aggregation +// from the body's shape (has timeAggregation/spaceAggregation?) rather than the +// version tag. +func (mc *migrateCommon) createAggregationsShapeSafe(ctx context.Context, queryData map[string]any, widgetType string) bool { + if aggs, ok := queryData["aggregations"].([]any); ok && len(aggs) > 0 { + return false // already v5-shaped + } + + aggregateOp, hasOp := queryData["aggregateOperator"].(string) + aggregateAttr, hasAttr := queryData["aggregateAttribute"].(map[string]any) + dataSource, _ := queryData["dataSource"].(string) + + if aggregateOp == "noop" && dataSource != "metrics" { + return false + } + if !hasOp || !hasAttr { + return false + } + + var aggregation map[string]any + + switch dataSource { + case "metrics": + _, hasTime := queryData["timeAggregation"] + _, hasSpace := queryData["spaceAggregation"] + if hasTime || hasSpace { // acts as a check for v4 shape: the body carries its own time/space aggregation. + if _, ok := queryData["spaceAggregation"]; !ok { + queryData["spaceAggregation"] = aggregateOp + } + aggregation = map[string]any{ + "metricName": aggregateAttr["key"], + "temporality": queryData["temporality"], + "timeAggregation": queryData["timeAggregation"], + "spaceAggregation": queryData["spaceAggregation"], + } + if reduceTo, ok := queryData["reduceTo"].(string); ok { + aggregation["reduceTo"] = reduceTo + } + } else { + // v3 shape: derive time/space from the compound operator. + var timeAgg, spaceAgg, reduceTo string + switch aggregateOp { + case "sum_rate", "rate_sum": + timeAgg, spaceAgg, reduceTo = "rate", "sum", "sum" + case "avg_rate", "rate_avg": + timeAgg, spaceAgg, reduceTo = "rate", "avg", "avg" + case "min_rate", "rate_min": + timeAgg, spaceAgg, reduceTo = "rate", "min", "min" + case "max_rate", "rate_max": + timeAgg, spaceAgg, reduceTo = "rate", "max", "max" + case "hist_quantile_50": + timeAgg, spaceAgg, reduceTo = "", "p50", "avg" + case "hist_quantile_75": + timeAgg, spaceAgg, reduceTo = "", "p75", "avg" + case "hist_quantile_90": + timeAgg, spaceAgg, reduceTo = "", "p90", "avg" + case "hist_quantile_95": + timeAgg, spaceAgg, reduceTo = "", "p95", "avg" + case "hist_quantile_99": + timeAgg, spaceAgg, reduceTo = "", "p99", "avg" + case "rate": + timeAgg, spaceAgg, reduceTo = "rate", "sum", "sum" + case "p99", "p90", "p75", "p50", "p25", "p20", "p10", "p05": + mc.logger.InfoContext(ctx, "found invalid config") + timeAgg, spaceAgg, reduceTo = "avg", "avg", "avg" + case "min": + timeAgg, spaceAgg, reduceTo = "min", "min", "min" + case "max": + timeAgg, spaceAgg, reduceTo = "max", "max", "max" + case "avg": + timeAgg, spaceAgg, reduceTo = "avg", "avg", "avg" + case "sum": + timeAgg, spaceAgg, reduceTo = "sum", "sum", "sum" + case "count": + timeAgg, spaceAgg, reduceTo = "count", "sum", "sum" + case "count_distinct": + timeAgg, spaceAgg, reduceTo = "count_distinct", "sum", "sum" + case "noop": + mc.logger.WarnContext(ctx, "noop found in the aggregation data") + timeAgg, spaceAgg, reduceTo = "max", "max", "max" + } + aggregation = map[string]any{ + "metricName": aggregateAttr["key"], + "temporality": queryData["temporality"], + "timeAggregation": timeAgg, + "spaceAggregation": spaceAgg, + } + if widgetType == "table" { + aggregation["reduceTo"] = reduceTo + } else if reduceTo, ok := queryData["reduceTo"].(string); ok { + aggregation["reduceTo"] = reduceTo + } + } + case "logs", "traces": + aggregation = map[string]any{"expression": mc.buildAggregationExpression(aggregateOp, aggregateAttr)} + default: + return false + } + + queryData["aggregations"] = []any{aggregation} + return true +} + +// orderByIsPreV5 reports whether an orderBy slice is still in the v4 shape (an +// entry carries "columnName"); a v5 orderBy uses {key:{name}, direction}. +func orderByIsPreV5(orderBy []any) bool { + for _, o := range orderBy { + if m, ok := o.(map[string]any); ok { + if _, has := m["columnName"]; has { + return true + } + } + } + return false +} + +// functionsArePreV5 reports whether a functions slice is still in the v4 shape +// (args are raw values); a v5 function's args are {name,value} objects. +func functionsArePreV5(functions []any) bool { + for _, f := range functions { + if m, ok := f.(map[string]any); ok { + args, ok := m["args"].([]any) + if !ok || len(args) == 0 { + continue + } + _, argIsObject := args[0].(map[string]any) + return !argIsObject + } + } + return false +} diff --git a/pkg/types/dashboardtypes/dashboard.go b/pkg/types/dashboardtypes/dashboard.go index 8cbe96a407f..14085eb74da 100644 --- a/pkg/types/dashboardtypes/dashboard.go +++ b/pkg/types/dashboardtypes/dashboard.go @@ -7,7 +7,6 @@ import ( "time" "github.com/SigNoz/signoz/pkg/errors" - "github.com/SigNoz/signoz/pkg/transition" "github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" "github.com/SigNoz/signoz/pkg/valuer" @@ -22,6 +21,7 @@ var ( ErrCodeDashboardInvalidSource = errors.MustNewCode("dashboard_invalid_source") ErrCodeDashboardImmutable = errors.MustNewCode("dashboard_immutable") ErrCodeDashboardInvalidPatch = errors.MustNewCode("dashboard_invalid_patch") + ErrCodeDashboardMigrationFailed = errors.MustNewCode("dashboard_migration_failed") ) type StorableDashboard struct { @@ -413,27 +413,26 @@ func (dashboard *Dashboard) GetWidgetQuery(startTime, endTime, widgetIndex uint6 widgetData := data.Widgets[widgetIndex] switch widgetData.Query.QueryType { case "builder": - migrate := transition.NewMigrateCommon(logger) for _, query := range widgetData.Query.Builder.QueryData { queryName, ok := query["queryName"].(string) if !ok { return nil, errors.New(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "cannot type cast query name as string") } - compositeQueries = append(compositeQueries, migrate.WrapInV5Envelope(queryName, query, "builder_query")) + compositeQueries = append(compositeQueries, querybuildertypesv5.WrapInV5Envelope(queryName, query, "builder_query")) } for _, query := range widgetData.Query.Builder.QueryFormulas { queryName, ok := query["queryName"].(string) if !ok { return nil, errors.New(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "cannot type cast query name as string") } - compositeQueries = append(compositeQueries, migrate.WrapInV5Envelope(queryName, query, "builder_formula")) + compositeQueries = append(compositeQueries, querybuildertypesv5.WrapInV5Envelope(queryName, query, "builder_formula")) } for _, query := range widgetData.Query.Builder.QueryTraceOperator { queryName, ok := query["queryName"].(string) if !ok { return nil, errors.New(errors.TypeInvalidInput, ErrCodeDashboardInvalidWidgetQuery, "cannot type cast query name as string") } - compositeQueries = append(compositeQueries, migrate.WrapInV5Envelope(queryName, query, "builder_trace_operator")) + compositeQueries = append(compositeQueries, querybuildertypesv5.WrapInV5Envelope(queryName, query, "builder_trace_operator")) } case "clickhouse_sql": for _, query := range widgetData.Query.ClickhouseSQL { diff --git a/pkg/types/dashboardtypes/perses_v1_to_v2.go b/pkg/types/dashboardtypes/perses_v1_to_v2.go new file mode 100644 index 00000000000..118a3a899aa --- /dev/null +++ b/pkg/types/dashboardtypes/perses_v1_to_v2.go @@ -0,0 +1,103 @@ +package dashboardtypes + +import ( + "encoding/json" + + "github.com/SigNoz/signoz/pkg/errors" +) + +// V1 → V2 migration. The v1 storable shape is the frontend's `DashboardData` +// (see frontend/src/types/api/dashboard/getAll.ts); v2 is DashboardV2 / +// DashboardSpec. +// +// Assumes the v1 widget query data has already been migrated to v5 shape +// (transition.dashboardMigrateV5). Pre-v5 builder queries will produce +// invalid v2 envelopes — run the v4→v5 migration first. +// +// The conversion is split across sibling files by concern: +// - perses_v1_to_v2_tags.go tags +// - perses_v1_to_v2_panels.go widgets → panels (+ panel field mappers) +// - perses_v1_to_v2_queries.go widget queries +// - perses_v1_to_v2_layouts.go grid layouts and sections +// - perses_v1_to_v2_variables.go variables +// - perses_v1_to_v2_decoder.go v1Decoder: typed field reads + malformed-field detection + +// ══════════════════════════════════════════════ +// Entry point +// ══════════════════════════════════════════════ + +func (storable StorableDashboard) IsV2() bool { + metadata, _ := storable.Data["metadata"].(map[string]any) + if metadata == nil { + return false + } + version, _ := metadata["schemaVersion"].(string) + return version == SchemaVersion +} + +func (storable StorableDashboard) ConvertV1ToV2() (result *DashboardV2, err error) { + // Legacy v1 data can be arbitrarily malformed. The accessors degrade + // gracefully, but recover from any unforeseen panic so one bad dashboard + // surfaces as an error (to be logged and skipped) rather than crashing the run. + defer func() { + if r := recover(); r != nil { + result, err = nil, errors.Newf(errors.TypeInternal, ErrCodeDashboardMigrationFailed, "panic converting dashboard %s: %v", storable.ID, r) + } + }() + + if storable.IsV2() { + return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardMigrationFailed, "dashboard %s is already in %s schema", storable.ID, SchemaVersion) + } + + d := &v1Decoder{} + title := d.readString(storable.Data, "title") + description := d.readString(storable.Data, "description") + image := resolveV1Image(d.readString(storable.Data, "image")) + + sanitizeWidgetIDs(storable.Data) + panels := d.convertV1Panels(retainPlacedWidgets(storable.Data)) + spec := DashboardSpec{ + Display: Display{Name: clipName(title, MaxDisplayNameLen), Description: description}, + Variables: d.convertV1Variables(storable.Data["variables"]), + Panels: panels, + Layouts: d.convertV1Layouts(storable.Data, panels), + // v1 has no dashboard links; emit [] (not nil) so the required field round-trips. + Links: []Link{}, + } + + // marshal and unmarshal cycle to confirm full validation + raw, marshalErr := json.Marshal(spec) + if marshalErr != nil { + return nil, errors.WrapInternalf(marshalErr, errors.CodeInternal, "marshal converted dashboard %s", storable.ID) + } + if err := json.Unmarshal(raw, new(DashboardSpec)); err != nil { + return nil, errors.WrapInvalidInputf(err, ErrCodeDashboardMigrationFailed, "converted dashboard %s is invalid", storable.ID) + } + tags := d.convertV1TagsForOrg(storable.OrgID, storable.Data["tags"]) + + if err := d.errIfHasMalformedFields(); err != nil { + return nil, err + } + + return &DashboardV2{ + Identifiable: storable.Identifiable, + TimeAuditable: storable.TimeAuditable, + UserAuditable: storable.UserAuditable, + OrgID: storable.OrgID, + Locked: storable.Locked, + Source: storable.Source, + DashboardV2MetadataBase: DashboardV2MetadataBase{ + SchemaVersion: SchemaVersion, + Image: image, + }, + Name: generateDashboardName(title), + Tags: tags, + Spec: spec, + }, nil +} + +// V1Name derives the v2 name column from a v1 dashboard's title. Used to backfill the +// name of a dashboard whose data fails to migrate, so it stays findable by name. +func (storable StorableDashboard) V1Name() string { + return generateDashboardName(storable.Data.readString("title")) +} diff --git a/pkg/types/dashboardtypes/perses_v1_to_v2_decoder.go b/pkg/types/dashboardtypes/perses_v1_to_v2_decoder.go new file mode 100644 index 00000000000..7fc14900e46 --- /dev/null +++ b/pkg/types/dashboardtypes/perses_v1_to_v2_decoder.go @@ -0,0 +1,214 @@ +package dashboardtypes + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/SigNoz/signoz/pkg/errors" +) + +// ══════════════════════════════════════════════ +// v1 decoder +// ══════════════════════════════════════════════ + +// v1Decoder reads fields out of the untyped v1 dashboard blob. Every read* +// method follows the same contract: a field that is absent or null yields the +// zero value; a field present with the wrong type yields zero AND records a +// malformed-field error. Conversion proceeds (so one bad field doesn't abort +// the rest) and ConvertV1ToV2 returns d.malformedFieldsErr() at the end so the +// dashboard is logged and skipped. +// +// Polymorphic v1 fields (spanGaps bool|number, selectedValue string|array, …) +// are read with a type switch on the already-extracted value, never through +// these accessors, so they stay lenient by construction. +type v1Decoder struct { + bad []string + seen map[string]struct{} +} + +// note records a decoding problem (malformed field, unknown value, swallowed +// sub-parse error), deduping identical messages. ConvertV1ToV2 surfaces these +// via errIfHasMalformedFields. +func (d *v1Decoder) note(format string, args ...any) { + msg := fmt.Sprintf(format, args...) + if _, dup := d.seen[msg]; dup { + return + } + if d.seen == nil { + d.seen = make(map[string]struct{}) + } + d.seen[msg] = struct{}{} + d.bad = append(d.bad, msg) +} + +// noteMalformedField records a v1 field present with the wrong Go type. +func (d *v1Decoder) noteMalformedField(field string, raw any) { + d.note("%q has unexpected type %T", field, raw) +} + +// detailErr renders an error for a diagnostic note, unfolding the structured +// detail our JSON binding attaches via WithAdditional. A plain %v on these +// errors prints only the innermost message ("request body contains invalid +// field value") and drops the field/type context that says which field was +// wrong — the part that actually tells you what to fix. +func detailErr(err error) string { + if err == nil { + return "" + } + j := errors.AsJSON(err) + if len(j.Errors) == 0 { + return err.Error() + } + details := make([]string, 0, len(j.Errors)) + for _, e := range j.Errors { + details = append(details, e.Message) + } + return j.Message + ": " + strings.Join(details, "; ") +} + +func (d *v1Decoder) errIfHasMalformedFields() error { + if len(d.bad) == 0 { + return nil + } + // One field per line: these lists run long (a bad widget query is reported + // once per widget), and a single "; "-joined line is an unscannable wall. + return errors.Newf(errors.TypeInvalidInput, ErrCodeDashboardInvalidData, "malformed v1 dashboard fields:\n %s", strings.Join(d.bad, "\n ")) +} + +func readField[T any](d *v1Decoder, m map[string]any, key string) T { + var zero T + v, present := m[key] + if !present || v == nil { + return zero + } + t, ok := v.(T) + if !ok { + d.noteMalformedField(key, v) + return zero + } + return t +} + +func (d *v1Decoder) readString(m map[string]any, key string) string { + return readField[string](d, m, key) +} +func (d *v1Decoder) readFloat(m map[string]any, key string) float64 { + v, present := m[key] + if !present || v == nil { + return 0 + } + f, ok := coerceFloat(v) + if !ok { + d.noteMalformedField(key, v) + return 0 + } + return f +} + +// coerceFloat accepts a JSON number or a numeric string (v1 sometimes stores +// numbers like softMin as quoted strings). A blank string is "unset", not a +// number, so it fails to coerce. +func coerceFloat(v any) (float64, bool) { + switch n := v.(type) { + case float64: + return n, true + case string: + f, err := strconv.ParseFloat(strings.TrimSpace(n), 64) + if err != nil { + return 0, false + } + return f, true + } + return 0, false +} +func (d *v1Decoder) readBool(m map[string]any, key string) bool { return readField[bool](d, m, key) } +func (d *v1Decoder) readArray(m map[string]any, key string) []any { return readField[[]any](d, m, key) } +func (d *v1Decoder) readObject(m map[string]any, key string) map[string]any { + return readField[map[string]any](d, m, key) +} + +// readInt narrows a numeric field to int (JSON numbers decode as float64). +func (d *v1Decoder) readInt(m map[string]any, key string) int { return int(d.readFloat(m, key)) } + +func (d *v1Decoder) readFloatPtr(m map[string]any, key string) *float64 { + v, present := m[key] + if !present || v == nil { + return nil + } + // A blank string means "unset" (v1's empty softMin/softMax), not malformed. + if s, ok := v.(string); ok && strings.TrimSpace(s) == "" { + return nil + } + f, ok := coerceFloat(v) + if !ok { + d.noteMalformedField(key, v) + return nil + } + return &f +} + +// clipName truncates s to at most limit runes so a v1 name over a v2 length bound +// (MaxDisplayNameLen / MaxLayoutTitleLen) is shortened rather than failing migration. +func clipName(s string, limit int) string { + r := []rune(s) + if len(r) <= limit { + return s + } + return string(r[:limit]) +} + +func (d *v1Decoder) readStringMap(m map[string]any, key string) map[string]string { + // An empty list is a stand-in for an empty map here; tolerate it silently + // rather than flagging the wrong-type as malformed. + if s, ok := m[key].([]any); ok && len(s) == 0 { + return nil + } + raw := d.readObject(m, key) + if len(raw) == 0 { + return nil + } + out := make(map[string]string, len(raw)) + for k, v := range raw { + s, ok := v.(string) + if !ok { + d.noteMalformedField(key+"."+k, v) + continue + } + out[k] = s + } + return out +} + +func (d *v1Decoder) readObjects(m map[string]any, key string) []map[string]any { + raw := d.readArray(m, key) + if len(raw) == 0 { + return nil + } + out := make([]map[string]any, 0, len(raw)) + for i, item := range raw { + obj, ok := item.(map[string]any) + if !ok { + d.noteMalformedField(fmt.Sprintf("%s[%d]", key, i), item) + continue + } + out = append(out, obj) + } + return out +} + +// decodeMapInto converts an untyped map[string]any into a typed T by +// round-tripping through JSON, letting encoding/json (struct tags, custom +// UnmarshalJSON) do the field mapping instead of hand-copying out of the map. +func decodeMapInto[T any](src map[string]any) (T, error) { + var dst T + bytes, err := json.Marshal(src) + if err != nil { + return dst, err + } + if err := json.Unmarshal(bytes, &dst); err != nil { + return dst, err + } + return dst, nil +} diff --git a/pkg/types/dashboardtypes/perses_v1_to_v2_icons.go b/pkg/types/dashboardtypes/perses_v1_to_v2_icons.go new file mode 100644 index 00000000000..3aaf5963bee --- /dev/null +++ b/pkg/types/dashboardtypes/perses_v1_to_v2_icons.go @@ -0,0 +1,52 @@ +package dashboardtypes + +// v1 dashboards stored their `image` as one of a fixed set of base64 icon data +// URIs. v2 references the same icons by a stable asset path (dashboardIconPathPrefix +// + name). This maps each preset v1 base64 blob to its v2 icon path so a migrated +// dashboard keeps its icon without embedding the base64. +// +// Keys are copied verbatim from the frontend picker (Base64Icons in +// frontend/src/container/DashboardContainer/DashboardSettings/General/utils.tsx); +// names/order match the v2 picker (SYSTEM_ICON_NAMES in +// frontend/src/pages/DashboardPageV2/DashboardContainer/dashboardIcons.ts). +var v1Base64IconToAssetPath = map[string]string{ + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0Ljk0OTQgMTMuOTQyQzE2LjIzMTggMTIuNDI1OCAxNy4zMjY4IDkuNzAyMiAxNi4xOTU2IDYuNTc0ODdDMTUuNjQ0MyA1LjA1MjQ1IDE1LjAyMTkgNC4yMDI0OSAxNC4yOTY5IDMuNjYyNTJDMTMuODU1NyAzLjMzMzc5IDEyLjA5MzMgMi41MDYzMyA5Ljc1OTY1IDIuODY3NTZDOC4wNTM0OSAzLjEzMjU1IDUuNzc0ODcgNC4yMDg3NCA0LjI5MzY5IDUuOTU5OUMyLjg1NzUyIDcuNjYxMDYgMS43NDg4MyA5LjAwNDc0IDEuNjk3NTggMTAuMzA5N0MxLjYzMTMzIDExLjk4ODMgMi44OTYyNyAxMy40MzA4IDMuMDUwMDEgMTMuNjY0NUMzLjMyMzc0IDE0LjA3OTUgNS4xOTExNSAxNi40NTE4IDguNjk5NzEgMTYuNTczMUMxMS43OTcgMTYuNjc5MyAxMy44MTQ0IDE1LjI4NDQgMTQuOTQ5NCAxMy45NDJaIiBmaWxsPSIjNDAzRDNFIi8+CjxwYXRoIGQ9Ik00LjU1MzYzIDIuNzM3NDdDMi45Mzc0NiAzLjg5MTE2IDEuMTIxMzEgNi4yNTEwMyAxLjQ0NzU0IDkuNTYwODZDMS42MDYyOCAxMS4xNzIgMi4wMDI1MSAxMi4xNDk1IDIuNTcxMjMgMTIuODUwN0MyLjkxNzQ2IDEzLjI3ODIgNC40MTk4OCAxNC41NDkzIDYuNzczNTEgMTQuNzM2OEM5LjE0NTg4IDE0LjkyNTYgMTAuOTQ5NSAxNC4zOTQ0IDEyLjgzMzIgMTMuMDg0NEMxNi42NjE3IDEwLjQyMDggMTYuMDk4IDYuMzkzNTMgMTUuOTM0MyA1LjkyNDhDMTUuNzcwNSA1LjQ1NjA3IDE0LjU0NDQgMi42OTYyMiAxMS4xNzMzIDEuNzE1MDJDOC4xOTg0NCAwLjg1MDA2OCA1Ljk4MzU1IDEuNzE1MDIgNC41NTM2MyAyLjczNzQ3WiIgZmlsbD0iIzVFNjM2NyIvPgo8cGF0aCBkPSJNNy4zOTM1MyAyLjk2MTA5QzUuNjE3MzcgMi44OTczNCAzLjkxOTk2IDQuMjg4NTIgMy43NTYyMiA2LjAwNTkzQzMuNTkyNDggNy43MjIwOSA0LjY1NDkyIDkuMDI5NTIgNi4zMDk4MyA5LjI5NTc2QzcuOTY0NzUgOS41NjA3NCA5Ljg3ODM5IDguNTU1OCAxMC4yNjM0IDYuNDUwOTFDMTAuNjYwOSA0LjI4MjI3IDkuMDg5NjkgMy4wMjIzNCA3LjM5MzUzIDIuOTYxMDlaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNy45NDIxNyA1LjkwMTE1QzcuOTQyMTcgNS45MDExNSA4LjM2OTY1IDUuODEyNCA4LjQ1NDY1IDUuMTgyNDRDOC41MzgzOSA0LjU2MjQ3IDguMjMwOTEgNC4wMzM3NSA3LjUxMzQ1IDMuODQzNzZDNi43MzM0OSAzLjYzNzUyIDYuMjA0NzcgNC4wNjYyNSA2LjA2NzI3IDQuNTE3NDdDNS44NzYwMyA1LjE0NDk0IDYuMTU4NTIgNS40NDM2NyA2LjE1ODUyIDUuNDQzNjdDNi4xNTg1MiA1LjQ0MzY3IDUuMzkzNTYgNS42Mjc0MSA1LjMzMjMxIDYuNTI5ODdDNS4yNzQ4MSA3LjM4MTA3IDUuODU2MDMgNy44Mzg1NSA2LjQzOTc1IDcuOTc4NTRDNy4xNjA5NiA4LjE1MjI4IDcuOTc4NDIgNy45NTQ3OSA4LjE3ODQxIDcuMDM0ODRDOC4zNDQ2NSA2LjI3NzM4IDcuOTQyMTcgNS45MDExNSA3Ljk0MjE3IDUuOTAxMTVaIiBmaWxsPSIjMzAzMDMwIi8+CjxwYXRoIGQ9Ik02LjczOTgzIDQuNzUzNjJDNi42NzEwOSA1LjAxMjM1IDYuODA4NTggNS4yNjIzNCA3LjA3ODU3IDUuMzMxMDlDNy4zNjk4IDUuNDA0ODMgNy42MzQ3OSA1LjMwODU5IDcuNzA2MDMgNS4wMTExQzcuNzY4NTMgNC43NDczNyA3LjY0MzU0IDQuNTE0ODggNy4zMzYwNSA0LjQzOTg4QzcuMDgzNTcgNC4zNzczOSA2LjgxNDgzIDQuNDcxMTMgNi43Mzk4MyA0Ljc1MzYyWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTYuOTU5NzggNi4wMzk3NEM2LjYzMjMgNS45Mzg0OSA2LjE5OTgyIDYuMDY0NzMgNi4xMzEwNyA2LjUwNDcxQzYuMDYyMzMgNi45NDQ2OSA2LjMyNjA2IDcuMTY5NjggNi42NzEwNCA3LjIzMjE3QzcuMDE2MDMgNy4yOTQ2NyA3LjM0MjI2IDcuMTEzNDMgNy40MDYwMSA2Ljc2MDk1QzcuNDY4NSA2LjQwOTcyIDcuMjg2MDEgNi4xMzk3MyA2Ljk1OTc4IDYuMDM5NzRaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K": dashboardIconPathPrefix + "eight-ball", + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1Ljc5OTEgMTUuMDM1NUMxNS43MzE3IDE0LjY3NTUgMTUuNDcxNyAxMi42ODQzIDE1LjQ3MTcgMTIuNjg0M0gyLjUyNzM1QzIuNTI3MzUgMTIuNjg0MyAyLjI2NzM2IDE0LjY3NTUgMi4xOTk4NyAxNS4wMzU1QzIuMTMyMzcgMTUuMzk1NCAyLjkwMzU4IDE2LjQ5OTEgOC45OTk1MSAxNi40OTkxQzE1LjA5NTQgMTYuNDk5MSAxNS44NjY2IDE1LjM5NTQgMTUuNzk5MSAxNS4wMzU1WiIgZmlsbD0iIzgyQUVDMCIvPgo8cGF0aCBkPSJNMi41MjM2OCAxMi43NDQ1QzIuNTIzNjggMTIuMDEwNyA1LjQyMzUzIDExLjQxNTggOC45OTk1OSAxMS40MTU4QzEyLjU3NTcgMTEuNDE1OCAxNS40NzU1IDEyLjAxMDcgMTUuNDc1NSAxMi43NDQ1QzE1LjQ3NTUgMTMuNDc4MiAxMi41NzU3IDE0LjE5NDQgOC45OTk1OSAxNC4xOTQ0QzUuNDIzNTMgMTQuMTk0NCAyLjUyMzY4IDEzLjQ3ODIgMi41MjM2OCAxMi43NDQ1WiIgZmlsbD0iI0UwRTBFMCIvPgo8cGF0aCBkPSJNMTMuMjIxNyAzLjAzOTkyQzEyLjg2NjcgMS45MDg3MyAxMS42ODQzIDEuNSA4Ljk5ODE2IDEuNUM2LjMxMjA1IDEuNSA1LjEzMDg3IDEuOTA4NzMgNC43NzU4OSAzLjAzOTkyQzQuMDYwOTIgNS4zMTg1NSAzLjQxODQ2IDEyLjUwMzIgMy40MTg0NiAxMi41MDMyQzMuNDE4NDYgMTIuNTAzMiAzLjk3NzE4IDEzLjQzOTQgOC45OTk0MSAxMy40Mzk0QzE0LjAyMTYgMTMuNDM5NCAxNC41ODE2IDEyLjUwNDQgMTQuNTgxNiAxMi41MDQ0QzE0LjU4MTYgMTIuNTA0NCAxMy45MzY3IDUuMzE4NTUgMTMuMjIxNyAzLjAzOTkyWiIgZmlsbD0iI0Y0NDMzNiIvPgo8cGF0aCBvcGFjaXR5PSIwLjkiIGQ9Ik0zLjQxNzI0IDEyLjUxN0MzLjQxNzI0IDExLjk0MjEgNS45MTU4NSAxMS40NzU4IDguOTk5NDQgMTEuNDc1OEMxMi4wODMgMTEuNDc1OCAxNC41ODE2IDExLjk0MjEgMTQuNTgxNiAxMi41MTdDMTQuNTgxNiAxMy4wOTIgMTIuMDgzIDEzLjY1MzIgOC45OTk0NCAxMy42NTMyQzUuOTE1ODUgMTMuNjUzMiAzLjQxNzI0IDEzLjA5MiAzLjQxNzI0IDEyLjUxN1oiIGZpbGw9IiNDNjI4MjgiLz4KPHBhdGggZD0iTTEwLjA3MzEgNS40NjU5NkwxMi44MDE3IDIuNjQ5ODZDMTIuODU0MiAyLjYwMzYxIDEyLjkzMTcgMi42NjIzNiAxMi45MDE3IDIuNzI0ODZMMTAuODcxOCA1Ljk4MjE4QzEwLjcyMDYgNi4yOTcxNyAxMC44NTQzIDYuNTAwOTEgMTEuMjA0MyA2LjQ5MjE2TDEzLjc4NTQgNi40NjU5MUMxMy44NTU0IDYuNDY0NjYgMTMuODc2NyA2LjU1OTY1IDEzLjgxMjkgNi41ODg0TDExLjQ1ODEgNy40OTU4NUMxMS4xMzkzIDcuNjM5NiAxMS4wNzE4IDcuOTgwODMgMTEuMzQ0MyA4LjE5ODMyTDE0LjI4MDQgMTAuMzE1N0MxNC4zMzU0IDEwLjM1OTUgMTQuMjkwNCAxMC40NDU3IDE0LjIyMjkgMTAuNDI3TDEwLjg4MTggOS4wNDMyN0MxMC41NDU2IDguOTQ4MjggMTAuMzkxOSA5LjEzNDUyIDEwLjQ2MDYgOS40NzdMMTEuNTM4MSAxMy4wODgxQzExLjU1MTggMTMuMTU2OCAxMS40NjE4IDEzLjE5NDMgMTEuNDIzMSAxMy4xMzY4TDkuNTg0NDEgOS44ODA3M0M5LjM4ODE3IDkuNTkxOTkgOS4xNTA2OCA5LjUzNyA4Ljk4NDQ0IDkuODQzMjNMNy4yMjA3OSAxMy4xNzgxQzcuMTg3MDQgMTMuMjM5MyA3LjA5NDU0IDEzLjIxMDYgNy4xMDA3OSAxMy4xNDA2TDguMDExOTkgOS41NDJDOC4wNDY5OSA5LjE5NDUyIDcuODg3IDkuMTIyMDIgNy41NjIwMiA5LjI0OTUxTDMuNzgwOTcgMTEuMTg4MkMzLjcxNTk3IDExLjIxMzIgMy42NjM0NyAxMS4xMzE5IDMuNzEzNDcgMTEuMDgzMkw2Ljg4NTggOC40NDk1NUM3LjEzNTc5IDguMjA1ODIgNy4xMjQ1NCA3LjkzOTU4IDYuNzkzMzEgNy44MjgzNEw0LjE0NTk1IDYuOTI1ODhDNC4wNzk3IDYuOTAzMzkgNC4wOTIyIDYuODA3MTQgNC4xNjA5NSA2LjgwMjE0TDYuNzY1ODEgNi44NTIxNEM3LjExNDU0IDYuODI1ODkgNy4yOTA3OCA2LjQ3ODQxIDcuMTA4MjkgNi4xODA5Mkw1LjAxNDY1IDMuMTYyMzNDNC45Nzg0IDMuMTAyMzQgNS4wNDk2NSAzLjAzNjA5IDUuMTA1OSAzLjA3NzM0TDcuODk4MjUgNS41MDIyMUM4LjE4MTk5IDUuNzA1OTUgOC40MzA3MiA1LjY4NzIgOC40ODE5NyA1LjM0MjIyTDguOTM1NyAyLjA5MjM5QzguOTQ1NyAyLjAyMzY0IDkuMDQzMTkgMi4wMTg2NCA5LjA2MDY5IDIuMDg2MTRMOS40NTU2NyA1LjI1ODQ3QzkuNTQzMTYgNS41OTcyIDkuODEwNjUgNS42OTcyIDEwLjA3MzEgNS40NjU5NloiIGZpbGw9InVybCgjcGFpbnQwX3JhZGlhbF83MTZfNTM1KSIvPgo8cGF0aCBkPSJNOS4yMzA2MyA1LjI1NzI0TDEwLjQwMDYgMy4xNDYxQzEwLjQzNTYgMy4wODYxMSAxMC41MjgxIDMuMTE3MzUgMTAuNTE5MyAzLjE4NjFMMTAuMTU0MyA1LjQ5MjIzQzEwLjExMTggNS44Mzg0NiAxMC4zMDQzIDUuOTg5NyAxMC42MzE4IDUuODY5NzFMMTIuOTIyOSA1LjA5OTc1QzEyLjk4NzkgNS4wNzYgMTMuMDM5MiA1LjE1ODUgMTIuOTg3OSA1LjIwNTk5TDExLjE5MyA2LjczODQxQzEwLjkzNjggNi45NzU5IDEwLjk4MTggNy4zMjA4OCAxMS4zMTA1IDcuNDM5NjNMMTMuNjAwNCA4LjIxNTg0QzEzLjY2NTQgOC4yMzk1OCAxMy42NTE2IDguMzM1ODMgMTMuNTgxNiA4LjMzOTU4TDExLjE0MTggOC4zODgzM0MxMC43OTMgOC40MDU4MyAxMC43MDY4IDguNjMyMDYgMTAuODgxOCA4LjkzNDU1TDEyLjExNDIgMTEuMDM5NEMxMi4xNDkyIDExLjA5OTQgMTIuMDc1NSAxMS4xNjQ0IDEyLjAyMDUgMTEuMTIxOUwxMC4xNzkzIDkuNTk3MDFDOS45MDA1OSA5LjM4NzAyIDkuNjU5MzYgOS40MDk1MiA5LjU5OTM2IDkuNzUzMjVMOS4xNjE4OCAxMi4yNDU2QzkuMTQ5MzggMTIuMzE0NCA5LjA1MTg5IDEyLjMxNjkgOS4wMzY4OSAxMi4yNDk0TDguNTgxOTEgOS43NzgyNUM4LjUwNDQyIDkuNDM4MjcgOC4zMjk0MyA5LjQyMDc3IDguMDYxOTQgOS42NDQ1MUw2LjEyNDU0IDExLjI3MzJDNi4wNzA4IDExLjMxODIgNS45OTQ1NSAxMS4yNTY5IDYuMDI3MDUgMTEuMTk1N0w3LjE2NTc0IDkuMTAzMjlDNy4zMjQ0OCA4Ljc5MjA2IDcuMjI5NDggOC41NDQ1NyA2Ljg3OTUgOC41NDQ1N0w0LjQyMjEzIDguNTc1ODJDNC4zNTIxNCA4LjU3NTgyIDQuMzMzMzkgOC40ODA4MiA0LjM5NzEzIDguNDUzMzJMNi41NDIwMiA3LjYyNzEyQzYuODYzMjUgNy40OTA4NyA2LjkxOTUgNy4xMDU4OSA2LjY1MjAxIDYuODgwOTFMNC44NTIxMSA1LjQxNDczQzQuNzk4MzYgNS4zNjk3NCA0Ljg0NTg2IDUuMjg0NzQgNC45MTIxMSA1LjMwNDc0TDcuMTgzMjQgNS45ODQ3QzcuNTE2OTcgNi4wODcyIDcuNzQ2OTYgNS45ODk3IDcuNjg1NzEgNS42NDU5N0w3LjIxNDQ4IDMuMjY5ODVDNy4yMDE5OSAzLjIwMTEgNy4yOTMyMyAzLjE2NjEgNy4zMzA3MyAzLjIyMzZMOC41ODE5MSA1LjI1NTk5QzguNzcwNjUgNS41NDk3MyA5LjA1Njg5IDUuNTU5NzMgOS4yMzA2MyA1LjI1NzI0WiIgZmlsbD0iI0ZGRDVDQSIvPgo8ZGVmcz4KPHJhZGlhbEdyYWRpZW50IGlkPSJwYWludDBfcmFkaWFsXzcxNl81MzUiIGN4PSIwIiBjeT0iMCIgcj0iMSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGdyYWRpZW50VHJhbnNmb3JtPSJ0cmFuc2xhdGUoOC45OTk2MiA3LjYyNDY3KSBzY2FsZSg1LjQ0ODU0KSI+CjxzdG9wIG9mZnNldD0iMC41NzE4IiBzdG9wLWNvbG9yPSIjRkY2RTQwIi8+CjxzdG9wIG9mZnNldD0iMC43NjgyIiBzdG9wLWNvbG9yPSIjRkY3MDQ2IiBzdG9wLW9wYWNpdHk9IjAuNTQxNCIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNGRjc1NTUiIHN0b3Atb3BhY2l0eT0iMCIvPgo8L3JhZGlhbEdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo=": dashboardIconPathPrefix + "siren", + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTcuOTg0NCA1LjA1MTFDNy45ODQ0IDUuMDUxMSAzLjE3NDY2IDYuMzg4NTMgMi43OTk2OCA2LjkwNDc2QzIuNDI0NyA3LjQyMDk4IDEuNzc4NDggOC4zNzcxOCAxLjcxOTc0IDEwLjQyNDZDMS42NzIyNCAxMi4wNjcgMi41ODg0NCAxNi4yOTA1IDcuMDkxOTUgMTYuNDMwNUMxMS42OTA1IDE2LjU3NDIgMTMuNDAyOSAxMy41NDQ0IDE0LjIyNDEgMTEuNjY4M0MxNC45NjQgOS45NzU4NCAxNC45NTE1IDguMDMyMiAxNS40MjAzIDcuMTM5NzRDMTUuODg5IDYuMjQ3MjkgMTYuMzExNSA1LjQ3MzU4IDE2LjI4OSA0LjQ2NDg4QzE2LjI2NjUgMy40NTYxOSAxNS44OTAyIDIuNTg3NDggMTUuMjMyOCAyLjE2NTAxQzE0LjU3NTMgMS43NDI1MyAxNC4wMTI4IDEuODEyNTIgMTMuNzMxNiAxLjc0MjUzQzEzLjQ1MDQgMS42NzI1MyA3Ljk4NDQgNS4wNTExIDcuOTg0NCA1LjA1MTFaIiBmaWxsPSIjNzU3RTQwIi8+CjxwYXRoIGQ9Ik04LjgyODE3IDMuMzYyNDFDNi44MzMyOCA0LjE4MzYxIDQuMTgyMTcgNC45NTczMiAyLjkxNTk5IDYuNzQwOThDMS43NzIzIDguMzUwOSAxLjM5MTA3IDEwLjM1MzMgMi45Mzk3MyAxMi42OTk0QzQuMzUzNDEgMTQuODQxOCA4LjEyMTk2IDE1Ljc1NTUgMTAuOTQwNiAxMy43MzE5QzEzLjUyMTcgMTEuODc4MiAxMy43NTU0IDkuMTU3MSAxNC41MzA0IDcuMzQ5N0MxNS4zMDUzIDUuNTQyMjkgMTYuOTM1MiAzLjE2NzQyIDE0Ljk3NjYgMS45OTk5OEMxMi44ODggMC43NTc1NDUgMTAuNDA0MyAyLjcxMzY5IDguODI4MTcgMy4zNjI0MVoiIGZpbGw9IiNBRkI0MkEiLz4KPHBhdGggZD0iTTE0LjUzMDQgMi41MTczM0MxMy41ODE3IDEuOTY5ODYgMTIuMzcxNyAyLjIxMjM0IDExLjAzNDMgMi44Njk4MUM5LjY5Njg4IDMuNTI3MjggOC42NjQ0NCA0LjAxOTc1IDguMTk1NzEgNC4xODM0OUM3LjcyNTc0IDQuMzQ3MjMgMy43MzM0NSA1LjgwOTY2IDIuOTYzNDkgNy43NDk1NUMyLjMyOTc3IDkuMzQ0NDcgMi40OTQ3NiAxMS40MzMxIDQuMTU5NjcgMTIuOTU4QzUuODI0NTkgMTQuNDgyOSAxMC4wMjU2IDE0LjM4OTIgMTEuNjQ0MyAxMi4wODkzQzEzLjI2MjkgOS43ODk0NSAxMy4zMzI5IDcuOTM3MDQgMTMuNjg1NCA3LjExNTg0QzE0LjAzNzkgNi4yOTQ2MyAxNC44MDU0IDUuMDkyMTkgMTUuMDIyOSA0LjQ0MDk4QzE1LjI4MDMgMy42NjcyNyAxNS4xNDAzIDIuODY5ODEgMTQuNTMwNCAyLjUxNzMzWiIgZmlsbD0iI0ZGRjY5RCIvPgo8cGF0aCBkPSJNOS45MzA2NCA2Ljk3NDc1QzkuMTc5NDMgNi4wODM1NSA2LjMxNzA4IDYuMzY0NzggNS4zMzIxNCA3Ljc3MjIxQzQuMzQ3MTkgOS4xNzk2MyA0Ljc1NzE3IDEwLjMyMzMgNS4yMzgzOSAxMC45Mzk1QzUuODI0NjEgMTEuNjkwOCA3LjQyMDc4IDEyLjM3MDcgOC44NzQ0NSAxMS4zNjJDMTAuMzI4MSAxMC4zNTMzIDEwLjc1NDMgNy45NTIyIDkuOTMwNjQgNi45NzQ3NVoiIGZpbGw9IiM4NTVDNTIiLz4KPHBhdGggZD0iTTYuOTE0NDkgOC41NzcyM0M2LjU0NDUxIDkuMTE5NyA2LjcyOTUgOS45OTQ2NSA2LjA5MzI4IDEwLjAzNTlDNS40NTcwNyAxMC4wNzcyIDUuMDg3MDggOS4wNTU5NSA1LjUzODMxIDguMjAxQzYuMDI1NzkgNy4yNzg1NSA3LjE4MDcyIDYuODY3MzIgNy41MDk0NiA3LjMwNDhDNy44Mzk0NCA3Ljc0MzUyIDcuMjY1NzIgOC4wNjIyNiA2LjkxNDQ5IDguNTc3MjNaIiBmaWxsPSIjRDY3NjU5Ii8+Cjwvc3ZnPgo=": dashboardIconPathPrefix + "avocado", + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyLjc3ODEgOS41NjgzNUMxMi43NzgxIDkuNTY4MzUgMTYuMjU0MiA4Ljg4NTg4IDE2LjQxOTIgMTAuMTk4M0MxNi41MTU0IDEwLjk3MiAxNi4yNzI5IDEyLjM5NDQgMTQuOTYzIDEzLjU2MzFDMTMuODc1NiAxNC41MzMxIDEyLjA1ODIgMTUuMzQ5MyA5LjA1OTU3IDE1LjI5NDNDNi42MTU5NSAxNS4yNTA1IDQuOTUzNTQgMTQuNjY0MyAzLjgyNDg1IDE0LjAxNTZDMS45MzQ5NSAxMi45Mjk0IDEuNTYxMjIgMTEuNDU3IDEuNjU5OTYgMTAuNTMzM0MxLjgwMTIxIDkuMjA4MzYgMy45MjQ4NCA4LjYwMDkgNC4wMjEwOSA4LjU0NTlMMTIuNzc4MSA5LjU2ODM1WiIgZmlsbD0iI0YxQjM0RiIvPgo8cGF0aCBkPSJNMS45MTUwNCAxMC44ODY5QzEuOTE1MDQgMTAuODg2OSAzLjkxOTkzIDE0LjIxNDIgOS4xMzcxNiAxNC4yMzE3QzE0Ljc4NTYgMTQuMjUxNyAxNi41MTE4IDEwLjg3ODEgMTYuNDE4IDEwLjE5NjlDMTYuMjQwNSA4Ljg5MDc1IDEzLjIzMDcgOC42MjQ1MSAxMy4yMzA3IDguNjI0NTFMNi4wNDg1NyAxMC4zMTY5TDEuOTE1MDQgOS44NjMyVjEwLjg4NjlaIiBmaWxsPSIjRkFERkIxIi8+CjxwYXRoIGQ9Ik0xLjkzNjE3IDkuNjgzNDNDMS45MzYxNyA5LjY4MzQzIDEuNjExMTkgMTAuMDczNCAxLjY2ODY4IDEwLjgxNzFDMS43MTk5MyAxMS40NzcxIDIuMzM5OSAxMi40NDk1IDMuNDIzNTkgMTIuODAwOEM0LjM4MjI5IDEzLjExMiA1LjA4NiAxMi43NTQ1IDYuMDc4NDUgMTMuMDc1N0M3LjA3MDkgMTMuMzk3IDcuOTg1ODUgMTQuMzcxOSA5LjI1MjAzIDE0LjI5NTdDMTAuNTE4MiAxNC4yMTk0IDExLjE5MDcgMTMuMzE0NSAxMi4xMDQ0IDEzLjE4MkMxMy40NzY4IDEyLjk4MzMgMTQuMTMzIDEzLjM1MiAxNS4xNzA1IDEyLjMyODNDMTYuMzE0MiAxMS4xOTk2IDE2LjMyMDQgMTAuMTQ4NCAxNi4zMTQyIDkuODg3MTZDMTYuMzAxNyA5LjM1OTY5IDE1LjkyNzkgOS4wNzQ3MSAxNS45Mjc5IDkuMDc0NzFMMy4zMzExIDkuNTM1OTNMMS45MzYxNyA5LjY4MzQzWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTkuMDc5NTcgMi42ODQ4MUM0LjEyNDgzIDIuNjg0ODEgMS42NDI0NiA1LjI3NTkzIDEuNDQ3NDcgOC4yMzA3N0MxLjM3MjQ4IDkuMzY4MjEgMS43NDI0NiAxMC4xMTMyIDIuNTQ4NjcgMTAuODIzMUMzLjk1NjA5IDEyLjA2MDYgNi41MjQ3MSAxMi44MzU1IDkuMTc4MzIgMTIuODY0M0MxMi4wMjE5IDEyLjg5NTUgMTQuMTc5MyAxMi4xNzU2IDE1LjgzMDUgOS45OTY5M0MxNi4zNjkyIDkuMjg2OTcgMTYuNDI3OSA4Ljg4MzI0IDE2LjQ1MDQgOC4wOTMyOEMxNi41MTkyIDUuNjE1OTEgMTQuMjkwNSAyLjY4NDgxIDkuMDc5NTcgMi42ODQ4MVoiIGZpbGw9InVybCgjcGFpbnQwX3JhZGlhbF83MjFfMTQ1KSIvPgo8cGF0aCBkPSJNNi41Mzg0MiA3LjI0NDU5QzYuNDgyMTcgNy4xMjA4NCA2LjkxNzE1IDYuOTY5NiA3LjEzMzM5IDYuODEyMTFDNy4zNzA4OCA2LjYzODM3IDcuNDE0NjIgNi40MzMzOCA3LjUwMDg3IDYuNDIyMTNDNy41ODcxMSA2LjQxMDg4IDcuOTY1ODQgNi41MjgzNyA4LjkzOTU0IDYuNTQwODdDOS43OTQ1IDYuNTUyMTIgMTAuMzM1NyA2LjMyNDYzIDEwLjQxMDcgNi4zODk2M0MxMC40ODcgNi40NTQ2MyAxMC41OTQ1IDYuNjM4MzcgMTAuODMzMiA2Ljg3NzExQzExLjAzOTQgNy4wODMzNCAxMS4yNzY5IDcuMjIzMzQgMTEuMjg4MiA3LjI5OTU4QzExLjI5OTQgNy4zNzU4MyAxMC43NTE5IDcuNDkyMDcgMTAuNjU1NyA3LjUxMzMyQzEwLjU1ODIgNy41MzQ1NyA3LjQzNDYyIDcuNTI1ODIgNy40MzQ2MiA3LjUyNTgyQzcuNDM0NjIgNy41MjU4MiA3LjI1MjEzIDcuNDcyMDcgNy4xMDA4OSA3LjQyOTU4QzYuOTQ5NjUgNy4zODQ1OCA2LjU5MjE3IDcuMzYzMzMgNi41Mzg0MiA3LjI0NDU5WiIgZmlsbD0iI0ZDREU4QyIvPgo8cGF0aCBkPSJNOC45NzIxMyA3Ljk5MDkxQzkuNTIzMzUgNy45OTA5MSA5LjgyNDU4IDcuODI5NjcgMTAuMTk0NiA3LjY5ODQyQzEwLjU4MDggNy41NjIxOCAxMC44OTgzIDcuNDQ5NjkgMTAuODk4MyA3LjQ0OTY5QzEwLjg5ODMgNy40NDk2OSAxMC4zMjQ2IDYuODY1OTcgOS4wNTgzNyA2Ljg2NTk3QzcuNzkyMTkgNi44NjU5NyA3LjI0MDk3IDcuNDYwOTQgNy4yNDA5NyA3LjQ2MDk0QzcuMjQwOTcgNy40NjA5NCA4LjMyMzQxIDcuOTkwOTEgOC45NzIxMyA3Ljk5MDkxWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTUuNjcyMTUgNC42MjQ5NEM1LjUyMzQxIDQuNjU4NjkgNS41MTg0MSA0Ljg4NjE4IDUuNDczNDEgNS4xMDExN0M1LjM4MjE3IDUuNTMzNjUgNS41ODcxNiA1Ljg4MzYzIDUuOTc1ODkgNS43NzIzOEM2LjQyMDg2IDUuNjQ0ODkgNi4zMTk2MiA1LjIwODY2IDYuMDU4MzggNC45MTg2OEM1LjkwNTg5IDQuNzUxMTkgNS44MjQ2NCA0LjU4OTk0IDUuNjcyMTUgNC42MjQ5NFoiIGZpbGw9IiNGNUU1QzciLz4KPHBhdGggZD0iTTcuNzU5NjUgNC4zMTg2MkM3LjcxOTY1IDQuNDI2MTEgNy4zNjg0MiA0LjQ5OTg2IDcuMDEwOTQgNC40NTk4NkM2Ljc0NTk1IDQuNDI5ODYgNi41MjM0NiA0LjE2NDg4IDYuNjY0NyAzLjg2OTg5QzYuODA4NDUgMy41NzExNiA3LjE2NTkzIDMuNTM2MTYgNy40MDg0MSAzLjc3MzY1QzcuNjU3MTUgNC4wMTczOCA3LjgwMzM5IDQuMTk5ODcgNy43NTk2NSA0LjMxODYyWiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNOC41MjU3NiA0LjEyNDkzQzguNDQwNzcgNC4xMDc0MyA4LjE2MjAzIDQuMzQ2MTcgOC4xMDA3OSA0LjYxMjRDOC4wMzA3OSA0LjkwOTg5IDguMjMwNzggNS4wMzIzOCA4LjM5NTc3IDUuMDQ5ODhDOC41OTk1MSA1LjA3MTEzIDguNzQ3IDQuOTEzNjQgOC43NTk1IDQuNzA5OUM4Ljc2OTUgNC41MDc0MSA4LjYxNzAxIDQuMTQzNjggOC41MjU3NiA0LjEyNDkzWiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNNC4yMDczNyA3LjI1NzExQzQuMjY2MTEgNy4zOTgzNSA0LjQ5MTEgNy4zNjIxIDQuNzExMDkgNy4zNjgzNUM1LjE1MjMyIDcuMzgwODUgNS40NjEwNSA3LjExODM2IDUuMjgzNTYgNi43NTQ2M0M1LjA3OTgyIDYuMzM4NCA0LjY2NzM0IDYuNTE3MTUgNC40Mjk4NSA2LjgyNDYzQzQuMjkxMTEgNy4wMDQ2MiA0LjE0NzM3IDcuMTEzMzYgNC4yMDczNyA3LjI1NzExWiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNMi42Nzc0IDcuMjM2MDNDMi41MjQ5MSA3LjIyODUzIDIuNDYxMTYgNy40NDcyNyAyLjM2MTE3IDcuNjQzNTFDMi4xNTk5MyA4LjAzNTk5IDIuMjY0OTIgOC40Mjg0NiAyLjY2ODY1IDguNDIzNDdDMy4xMzExMyA4LjQxODQ3IDMuMTQ3MzggNy45Njk3NCAyLjk3MjM5IDcuNjIyMjZDMi44Njk4OSA3LjQxOTc3IDIuODMzNjQgNy4yNDIyOCAyLjY3NzQgNy4yMzYwM1oiIGZpbGw9IiNGNUU1QzciLz4KPHBhdGggZD0iTTE0LjAxMTcgNi45Mjk2NUMxNC4xNjMgNi45MTA5IDE0LjE4OTIgNi42ODQ2NiAxNC4yNTY3IDYuNDc0NjdDMTQuMzg5MiA2LjA1MzQ0IDE0LjIyMDUgNS42ODQ3MSAxMy44MjMgNS43NTcyMUMxMy4zNjY4IDUuODM5NyAxMy40MjU1IDYuMjg0NjggMTMuNjU2NyA2LjU5ODQxQzEzLjc5MTcgNi43ODA5IDEzLjg1NjcgNi45NDg0IDE0LjAxMTcgNi45Mjk2NVoiIGZpbGw9IiNGNUU1QzciLz4KPHBhdGggZD0iTTUuNzkzNDYgOS4yMTA3NEM1LjcyMDk2IDkuMTU4MjQgNS40OTA5NyA5LjIyNjk4IDUuMjg1OTggOS4zMDgyM0M0Ljg3NiA5LjQ3MDcyIDQuNjk2MDEgOS44MzQ0NSA1LjAwMjI1IDEwLjA5NjlDNS4zNTM0OCAxMC4zOTgyIDUuNjYyMjEgMTAuMDcxOSA1Ljc1OTcxIDkuNjk1NzFDNS44MTcyIDkuNDc2OTcgNS44ODA5NSA5LjI3NDQ4IDUuNzkzNDYgOS4yMTA3NFoiIGZpbGw9IiNGNUU1QzciLz4KPHBhdGggZD0iTTUuMDcyMzggOC43OTIxNEM1LjEwNzM4IDguNzIyMTQgNS4wMTM2NCA4LjUzMjE1IDQuOTE5ODkgOC4zODA5MUM0LjgxMTE1IDguMjA0NjcgNC42ODc0IDguMDI0NjggNC41MTQ5MSA3Ljk3NTkzQzMuOTU5OTQgNy44MTk2OSAzLjc1OTk1IDguNTI3MTUgNC4xMzI0MyA4Ljc1MzM5QzQuMjY4NjggOC44MzU4OCA0LjQxMjQyIDguODQwODggNC41NzExNiA4Ljg0OTYzQzQuODA4NjUgOC44NTk2MyA1LjAzODY0IDguODU5NjMgNS4wNzIzOCA4Ljc5MjE0WiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNMy44MTExNyA2Ljc1MzM5QzMuNzg4NjcgNi44OTQ2MyAzLjE2OTk2IDcuMDEyMTIgMi44NTM3MiA2LjgyMDg4QzIuNDc0OTkgNi41OTA5IDIuNjc4NzMgNi4wNjA5MiAzLjEyOTk2IDYuMDk5NjdDMy41MTYxOSA2LjEzNDY3IDMuODM2MTcgNi41OTM0IDMuODExMTcgNi43NTMzOVoiIGZpbGw9IiNGNUU1QzciLz4KPHBhdGggZD0iTTQuMTMxMDUgNS45ODIxNkM0LjI3NzI5IDYuMDIyMTYgNC42OTk3NyA1LjcwNTkzIDQuODIzNTEgNS40NzU5NEM0LjkxMzUxIDUuMzA4NDUgNC45ODYgNC44OTIyMiA0LjY5MzUyIDQuNzYwOThDNC4zNjcyOSA0LjYxNDc0IDQuMDg4NTUgNC44Mjk3MiA0LjAwNjA2IDUuMDk4NDZDMy45MDEwNiA1LjQ1MzQ0IDQuMDAxMDYgNS45NDcxNyA0LjEzMTA1IDUuOTgyMTZaIiBmaWxsPSIjRjVFNUM3Ii8+CjxwYXRoIGQ9Ik02LjYzMSA3LjcwNTg3QzYuNjU0NzUgNy44MTQ2MiA2LjQ0NDc2IDguNDA5NTkgNS45NTQ3OCA4LjQzMjA4QzUuNjE3MyA4LjQ0NzA4IDUuMzI0ODIgNy45OTMzNiA1Ljc0NjA0IDcuNjk5NjJDNi4wNjYwMyA3LjQ3ODM4IDYuNjE0NzUgNy42MjcxMyA2LjYzMSA3LjcwNTg3WiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNNy4wNTM0NCA4LjY1NDY4QzYuOTU3MiA4LjU3MzQzIDYuNDUwOTggOC45MzU5MSA2LjI5ODQ4IDkuMjAwOUM2LjIwODQ5IDkuMzU4MzkgNi4xNDU5OSA5LjY0NTg4IDYuNTE4NDcgOS44MTQ2MkM2Ljg3ODQ1IDkuOTc4MzYgNy4wNzU5NCA5LjcwMjEyIDcuMTIwOTQgOS40NzcxM0M3LjE2NTk0IDkuMjUyMTUgNy4xMjcxOSA4LjcxNzE3IDcuMDUzNDQgOC42NTQ2OFoiIGZpbGw9IiNGNUU1QzciLz4KPHBhdGggZD0iTTcuNTYwODggMTAuNDQyMUM3LjY4NDYyIDEwLjY3MzMgOC4zMjU4NCAxMC41NzMzIDguNDc4MzMgMTAuNTI3MUM4LjY5ODMyIDEwLjQ1OTYgOS4wMDk1NSAxMC4yNTQ2IDguODEwODEgOS44MjgzNUM4LjY0NzA3IDkuNDc5NjIgOC4yMTk1OSA5LjU1MjEyIDcuOTYwODUgOS43NzIxQzcuNzM3MTIgOS45NjA4NCA3LjQ3NTg4IDEwLjI4NDYgNy41NjA4OCAxMC40NDIxWiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNOS42MjcxMiA5LjYyNTc2QzkuNDk0NjMgOS43MjMyNiA5LjU5OTYzIDEwLjMxOTUgOS42NzgzNyAxMC40OTgyQzkuNzc5NjIgMTAuNzI5NSAxMC4xMDU4IDEwLjgxOTUgMTAuMzI1OCAxMC42NTA3QzEwLjU3OTYgMTAuNDU1NyAxMC41Mzk2IDEwLjEzMzIgMTAuMzc3MSA5Ljk1ODI1QzEwLjI1MzMgOS44MjcgOS43NTU4NyA5LjUyOTUyIDkuNjI3MTIgOS42MjU3NloiIGZpbGw9IiNGNUU1QzciLz4KPHBhdGggZD0iTTEwLjU3ODIgOS4yNTk1MkMxMC41Mjk1IDkuMzAzMjcgMTAuMDk0NSA5LjMzMzI2IDkuNzg0NTEgOS4wNjgyOEM5LjYwNTc3IDguOTE1NzkgOS42MTA3NyA4LjU1NDU1IDkuNzkwNzYgOC40MjA4MUM5Ljk4ODI1IDguMjc0NTcgMTAuMjU4MiA4LjMxOTU3IDEwLjQzMiA4LjU3MzNDMTAuNTQwNyA4LjcyOTU1IDEwLjYzNDUgOS4yMDgyNyAxMC41NzgyIDkuMjU5NTJaIiBmaWxsPSIjRjVFNUM3Ii8+CjxwYXRoIGQ9Ik0xMS4yMzY5IDkuMDQ1NzNDMTEuMzI0NCA5LjEyMTk4IDExLjg3ODEgOS4wMjgyMyAxMi4wODY4IDguNzg3QzEyLjIwMDYgOC42NTQ1IDEyLjM2MzEgOC4zNzA3NyAxMi4xNDMxIDguMTY4MjhDMTEuOTIzMSA3Ljk2NTc5IDExLjY2NDMgOC4wMDQ1NCAxMS40Mzk0IDguMjgwNzdDMTEuMjcxOSA4LjQ4NDUxIDExLjE0NjkgOC45NjY5OSAxMS4yMzY5IDkuMDQ1NzNaIiBmaWxsPSIjRjVFNUM3Ii8+CjxwYXRoIGQ9Ik0xMy4zODE5IDcuMzg0OEMxMy4zMzk0IDcuNTU0NzkgMTIuNzE4MiA3LjYyNDc5IDEyLjQ2OTUgNy41MDg1NEMxMi4yMTU4IDcuMzg5OCAxMi4yNjA3IDcuMTAzNTcgMTIuMzEyIDYuOTc5ODJDMTIuMzYzMiA2Ljg1NjA4IDEyLjQ4NyA2LjcwOTg0IDEyLjgxMzIgNi43OTk4M0MxMy4xNTgyIDYuODk0ODMgMTMuNDEwNyA3LjI3MjMxIDEzLjM4MTkgNy4zODQ4WiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNMTIuNTE0MyA5LjMwOTU5QzEyLjM3NTYgOS4yNzA4NCAxMi4yOTQzIDkuNDYwODMgMTIuMjA0MyA5LjU0NTgzQzEyLjA5MTggOS42NTMzMiAxMS42MTk0IDEwLjE5OTUgMTIuMjIxOCAxMC4zMzk1QzEyLjgyNTYgMTAuNDgwOCAxMi42NjA2IDkuODE1ODEgMTIuNjM4MSA5LjY4MDgyQzEyLjYyMzEgOS41ODcwOCAxMi42MTU2IDkuMzM4MzQgMTIuNTE0MyA5LjMwOTU5WiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNMTMuMTYxOSA4Ljk1NTc2QzEzLjExMTkgOS4wOTk1IDEzLjM5MzEgOS4zODMyNCAxMy41MjgxIDkuNTQ2OThDMTMuNjYzMSA5LjcxMDcyIDEzLjk2MTggOS44NjE5NiAxNC4xMzU2IDkuNTM1NzNDMTQuMzEwNiA5LjIwOTUgMTQuMDM5MyA5LjAwNzAxIDEzLjc5ODEgOC45NTA3NkMxMy41NTY5IDguODkzMjYgMTMuMjA2OSA4LjgyNTc3IDEzLjE2MTkgOC45NTU3NloiIGZpbGw9IiNGNUU1QzciLz4KPHBhdGggZD0iTTE1LjMxOTIgNy43OTU4MUMxNS4zMTkyIDcuODkyMDYgMTQuOTgxNyA4LjE2MjA0IDE0LjYwNDIgOC4xNzMyOUMxNC4zMjMgOC4xODIwNCAxNC4xMTQzIDguMDc5NTUgMTQuMTE0MyA3LjgwMjA2QzE0LjExNDMgNy41MjA4MyAxNC4zNjggNy4zNjMzMyAxNC42ODMgNy40MzA4M0MxNC44NjQyIDcuNDY4MzMgMTUuMzE5MiA3LjY0MzMyIDE1LjMxOTIgNy43OTU4MVoiIGZpbGw9IiNGNUU1QzciLz4KPHBhdGggZD0iTTE0LjY2NTQgNi45Nzk2MkMxNC42OTQyIDcuMDkyMTEgMTUuMDMxNyA3LjEyNTg2IDE1LjI3MjkgNy4xMzIxMUMxNS41MTU0IDcuMTM4MzYgMTUuODEyOSA3LjA1MzM2IDE1LjcwMDQgNi43MjA4OEMxNS41ODc5IDYuMzg4NCAxNS4yNTU0IDYuNDczMzkgMTUuMTMxNiA2LjUzNDY0QzE1LjAwOTIgNi41OTU4OSAxNC42Mjc5IDYuODI3MTIgMTQuNjY1NCA2Ljk3OTYyWiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNMTMuNjEzMSA0Ljc1NDdDMTMuNTExOSA0LjkwMDk0IDEzLjcwMzEgNS4yMDQ2OCAxMy44NzgxIDUuMzM5NjdDMTQuMDUzMSA1LjQ3NDY2IDE0LjI5NDMgNS41NTk2NiAxNC40MzU2IDUuMzIyMTdDMTQuNTc2OCA1LjA4NTkzIDE0LjMyODEgNC44NzIxOSAxNC4xNzY4IDQuNzc1OTVDMTQuMDI0MyA0LjY4MjIgMTMuNzExOSA0LjYxMjIxIDEzLjYxMzEgNC43NTQ3WiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNMTMuMDQ0NCA1LjAzMDk2QzEyLjk3ODEgNC45ODk3MSAxMi42MjgxIDUuMDc4NDUgMTIuNDI1NyA1LjI2MjE5QzEyLjIxNTcgNS40NTIxOCAxMi4xMzgyIDUuNzI5NjcgMTIuMzkxOSA1LjkyNzE2QzEyLjYzNjkgNi4xMTcxNSAxMi45MDMxIDUuOTMyMTYgMTMuMDEwNiA1LjY3OTY3QzEzLjExMTkgNS40NDIxOCAxMy4xMTk0IDUuMDc3MiAxMy4wNDQ0IDUuMDMwOTZaIiBmaWxsPSIjRjVFNUM3Ii8+CjxwYXRoIGQ9Ik0xMC4zMDgzIDQuODczNjNDMTAuMzc1OCA0Ljc3OTg5IDEwLjI1MjEgNC4yNzc0MiAxMC4wOTQ2IDQuMDg0OTNDOS45MzcxMSAzLjg5MzY5IDkuNzM5NjIgMy44MTk5NCA5LjUyMDg4IDMuOTgzNjhDOS4zNDgzOSA0LjExMjQyIDkuMjg0NjUgNC40NjI0MSA5LjUzMjEzIDQuNjQyNEM5LjgwMDg3IDQuODM5ODkgMTAuMjM0NiA0Ljk3NDg4IDEwLjMwODMgNC44NzM2M1oiIGZpbGw9IiNGNUU1QzciLz4KPHBhdGggZD0iTTEwLjUwNDQgNC4wOTYwN0MxMC41ODgyIDQuMTU3MzIgMTAuODkzMiA0LjExMzU3IDExLjEwMDYgMy45NzczM0MxMS4zMDgxIDMuODQxMDkgMTEuMzM5NCAzLjYyNDg1IDExLjIxOTQgMy40NzYxMUMxMS4wNzMxIDMuMjk2MTEgMTAuODM2OSAzLjM2MzYxIDEwLjY2ODIgMy41ODM2QzEwLjUyMDcgMy43NzQ4NCAxMC4zOTgyIDQuMDE3MzMgMTAuNTA0NCA0LjA5NjA3WiIgZmlsbD0iI0Y1RTVDNyIvPgo8cGF0aCBkPSJNMTEuMDA1OSA0LjgyMjQzQzExLjA3MzQgNS4wMDI0MiAxMS43NDk2IDQuOTYzNjggMTEuOTQ1OCA0Ljg3ODY4QzEyLjE0MzMgNC43OTM2OSAxMi4yODA4IDQuNjE0OTUgMTIuMTcwOCA0LjM1NDk2QzEyLjA0NzEgNC4wNjI0NyAxMS44NTIxIDQuMDYxMjIgMTEuNzA5NiA0LjEwMTIyQzExLjQyODMgNC4xODEyMiAxMC45Mzk2IDQuNjQ2MTkgMTEuMDA1OSA0LjgyMjQzWiIgZmlsbD0iI0Y1RTVDNyIvPgo8ZGVmcz4KPHJhZGlhbEdyYWRpZW50IGlkPSJwYWludDBfcmFkaWFsXzcyMV8xNDUiIGN4PSIwIiBjeT0iMCIgcj0iMSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGdyYWRpZW50VHJhbnNmb3JtPSJ0cmFuc2xhdGUoOS4wMjY1NSA2LjUxMTY2KSByb3RhdGUoLTAuMzc0NDY5KSBzY2FsZSg4LjE5NjczIDUuMzQ0MjcpIj4KPHN0b3Agb2Zmc2V0PSIwLjE2MTYiIHN0b3AtY29sb3I9IiNGMUIxNEEiLz4KPHN0b3Agb2Zmc2V0PSIwLjE2ODgiIHN0b3AtY29sb3I9IiNGMUIwNDkiLz4KPHN0b3Agb2Zmc2V0PSIwLjM3MDQiIHN0b3AtY29sb3I9IiNFODk4MjUiLz4KPHN0b3Agb2Zmc2V0PSIwLjUzNTQiIHN0b3AtY29sb3I9IiNFMjg4MTAiLz4KPHN0b3Agb2Zmc2V0PSIwLjY0MjUiIHN0b3AtY29sb3I9IiNFMDgzMDgiLz4KPHN0b3Agb2Zmc2V0PSIwLjcxMjUiIHN0b3AtY29sb3I9IiNFMTg2MEQiLz4KPHN0b3Agb2Zmc2V0PSIwLjc5MjQiIHN0b3AtY29sb3I9IiNFNTkwMUIiLz4KPHN0b3Agb2Zmc2V0PSIwLjg3NyIgc3RvcC1jb2xvcj0iI0VCQTAzMSIvPgo8c3RvcCBvZmZzZXQ9IjAuOTQ3IiBzdG9wLWNvbG9yPSIjRjFCMTRBIi8+CjwvcmFkaWFsR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg==": dashboardIconPathPrefix + "bagel", + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMuNzg4NTcgMTQuNTI3QzMuNzg4NTcgMTQuNTI3IDYuMTkwOTUgMTcuMDU0MyAxMC4zMTIgMTYuNDEzMUMxNC40MzMgMTUuNzczMSAxNi4wODU0IDEyLjE1OTYgMTYuNDI1NCAxMC4wOTk3QzE2Ljg1NzkgNy40NzYwOCAxNS42NTkyIDUuNjcyNDIgMTUuNTk1NSA1LjU3MzY4QzE1LjU0OCA1LjUwMTE4IDMuNzg4NTcgMTQuNTI3IDMuNzg4NTcgMTQuNTI3WiIgZmlsbD0iI0RDNTgxOSIvPgo8cGF0aCBkPSJNMy42MDEwNCAzLjY4ODU3QzEuMTMyNDIgNi4yODcxOSAwLjcxMzY5MyAxMC4wODMyIDIuNTY5ODQgMTMuMDE2OEMzLjQzMzU1IDE0LjM4MTggMy45NjIyNyAxNC42NTkyIDMuOTYyMjcgMTQuNjU5MkMzLjk2MjI3IDE0LjY1OTIgOC4wMDA4MSAxNy4yNDkxIDEyLjUzOTMgMTMuMzIzMUMxNi40MjY2IDkuOTYwNzQgMTUuNjAyOSA1LjU4MjIyIDE1LjYwMjkgNS41ODIyMkMxNS42MDI5IDUuNTgyMjIgMTUuMTE3OSA0LjUyMjI4IDE0LjA5OCAzLjUxNjA4QzExLjgzNDQgMS4yODEyIDYuNzc0NjIgMC4zNDYyNTEgMy42MDEwNCAzLjY4ODU3WiIgZmlsbD0iI0VGNkMzMiIvPgo8cGF0aCBkPSJNMTMuNDk1NiAzLjAwNDk4QzEzLjQ1OTQgMy4wMDc0OCA5Ljg0ODMyIDMuMjY3NDYgNi4wMjg1MiA4LjQwMjE5QzMuNjE2MTUgMTEuNjQ0NSAzLjczNDg5IDEzLjkzMzIgMy44Mjg2NCAxNC41NjA2QzMuODI4NjQgMTQuNTYwNiAzLjY0NDkgMTQuNDA5NCAzLjUxNDkgMTQuMjY5NEMzLjM4NDkxIDE0LjEzMDYgMy4yODM2NiAxNC4wMDY5IDMuMjgzNjYgMTQuMDA2OUMzLjI3MzY2IDEyLjkwODIgMy42MDI0IDEwLjgyNTggNS42Mjg1NCA4LjEwMzQ2QzguNzEyMTMgMy45NTQ5MyAxMS42NjA3IDIuODczNzQgMTIuODk2OSAyLjU5Mzc1QzEyLjg5NjkgMi41OTM3NSAxMy4wNDgxIDIuNjgyNSAxMy4xOTQ0IDIuNzczNzRDMTMuMzAxOSAyLjg0MTI0IDEzLjQ5NTYgMy4wMDQ5OCAxMy40OTU2IDMuMDA0OThaIiBmaWxsPSIjMkQzMTMwIi8+CjxwYXRoIGQ9Ik0yLjk0NjE2IDQuNDcyNDFDMy4zNDczOCA1LjEzMTEzIDQuMzE2MDggNi41NTQ4IDUuOTkzNDkgOC4wNDA5N0M4LjEwNDYzIDkuOTEzMzcgMTAuMjk5NSAxMS40MzU4IDEyLjUxNDQgMTIuNTY0NUMxMy41NjA2IDEzLjA5ODIgMTQuMzY0MyAxMy40MTA3IDE0LjkzMyAxMy41OTQ0QzE0LjkzMyAxMy41OTQ0IDE0Ljg1MDUgMTMuNzIwNyAxNC43NzMgMTMuODI0NEMxNC42OTU1IDEzLjkyODIgMTQuNjEwNSAxNC4wMTU3IDE0LjYxMDUgMTQuMDE1N0MxNC4wMzE4IDEzLjgxOTQgMTMuMjU5NCAxMy41MDY5IDEyLjI4NTcgMTMuMDA5NUMxMC4wMzMzIDExLjg2MDggNy44MDM0IDEwLjMxNDYgNS42NjEwMSA4LjQxNDdDNC4wNzM2IDcuMDA3MjggMy4xMTExNSA1LjY3NDg1IDIuNjM4NjcgNC45Mjg2NEMyLjYzODY3IDQuOTI4NjQgMi43MDQ5MiA0Ljc5NzM5IDIuNzkyNDEgNC42ODExNUMyLjg3OTkxIDQuNTY0OTEgMi45NDYxNiA0LjQ3MjQxIDIuOTQ2MTYgNC40NzI0MVoiIGZpbGw9IiMyRDMxMzAiLz4KPHBhdGggZD0iTTcuNjQ3MTkgMS41OTYxOUM3LjQ2ODQ1IDEuNzk5OTMgNy4yMTM0NiAyLjEzMTE2IDYuOTE1OTggMi42MzIzOUM2LjY0MzQ5IDMuMDg5ODYgNi41MDM1IDMuNTQ0ODQgNi4zNDEwMSA0LjA3MjMxQzYuMDc0NzcgNC45MzcyNyA1Ljc3MzUzIDUuOTE3MjEgNC43ODczNCA3LjE2OTY1QzMuNDE3NDEgOC45MDk1NiAyLjA3MTIzIDkuNDkwNzggMS40NDM3NiA5LjY3NzAyQzEuNDQzNzYgOS42NzcwMiAxLjQyNjI2IDkuNTE0NTIgMS40MjAwMiA5LjQwMDc4QzEuNDEzNzcgOS4yODcwNCAxLjQxNjI3IDkuMTU4MjkgMS40MTYyNyA5LjE1ODI5QzIuMDEyNDggOC45NTQ1NSAzLjE5MjQyIDguMzg0NTggNC4zOTM2MSA2Ljg2MDkxQzUuMzIyMzEgNS42ODA5OCA1LjYwODU0IDQuNzQ4NTMgNS44NjIyOCAzLjkyNjA3QzYuMDI3MjcgMy4zOTExIDYuMTgyMjYgMi44ODYxMiA2LjQ4NiAyLjM3NzRDNi42Mjk3NCAyLjEzNjE2IDYuNzY0NzMgMS45Mjk5MiA2Ljg4ODQ4IDEuNzU3NDNDNi44ODg0OCAxLjc1NzQzIDcuMDI3MjIgMS43MTExOSA3LjIxNzIxIDEuNjY4NjlDNy4zNjQ3IDEuNjMzNjkgNy42NDcxOSAxLjU5NjE5IDcuNjQ3MTkgMS41OTYxOVoiIGZpbGw9IiMyRDMxMzAiLz4KPHBhdGggZD0iTTkuNTg5NTggMTYuNTAwNEM5LjE3MDg1IDE2LjEyMTcgOC4zMDU4OSAxNS4yNTY4IDcuNjc0NjggMTQuMDUzMUM2Ljk3NTk2IDEyLjcyMTkgNi42NDg0OCAxMC43MTk1IDcuNTgwOTMgOS40NjgzMUM4LjY0NTg4IDguMDQwODggMTAuNjM0NSA3Ljg0NzE0IDEyLjQ0MTkgNy44NDcxNEMxMi40NDQ0IDcuODQ3MTQgMTIuNDQ2OSA3Ljg0NzE0IDEyLjQ0OTQgNy44NDcxNEMxNC43NTA2IDcuODQ4MzkgMTYuNDgxNyA4LjMyOTYyIDE2LjUwMTcgOC4zMzU4N0MxNi41MDE3IDguMzM1ODcgMTYuNDg4IDguMTY3MTIgMTYuNDY5MiA4LjAyMzM4QzE2LjQ1MyA3Ljg5OTY0IDE2LjQzNTUgNy43ODQ2NCAxNi40MzU1IDcuNzg0NjRDMTYuMzQ4IDcuNzYyMTUgMTQuODE4MSA3LjM0ODQyIDEyLjQ0OTQgNy4zNDcxN0MxMi40NDY5IDcuMzQ3MTcgMTIuNDQ0NCA3LjM0NzE3IDEyLjQ0MTkgNy4zNDcxN0MxMC41MTA4IDcuMzQ3MTcgOC4zNzU4OSA3LjU2NDY2IDcuMTc5NyA5LjE2OTU3QzYuNDA5NzQgMTAuMjAyIDYuMTI3MjYgMTIuMTg0NCA3LjIzMjIgMTQuMjg1NkM3Ljc1MjE3IDE1LjI3NTUgOC40MDcxNCAxNi4wMzkyIDguODg1ODYgMTYuNTIwNEM4Ljg4NTg2IDE2LjUyMDQgOS4wMjU4NiAxNi41MjU0IDkuMjA0NiAxNi41MjI5QzkuMzgwODQgMTYuNTIwNCA5LjU4OTU4IDE2LjUwMDQgOS41ODk1OCAxNi41MDA0WiIgZmlsbD0iIzJEMzEzMCIvPgo8L3N2Zz4K": dashboardIconPathPrefix + "basketball", + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTYuOTc1OTIgMS40NDAwNUM2Ljk2NzE3IDEuNjIwMDUgNi44MDcxOCAxNi41MTU1IDYuODI1OTMgMTYuNTkxOEM2Ljg0NDY4IDE2LjY2OCA3LjkzOTYyIDE2LjY0NjggNy45Mzk2MiAxNi42MDNDNy45Mzk2MiAxNi41MDY4IDguMTMzMzYgMS41NDg4IDguMTI5NjEgMS40NTEzQzguMTI1ODYgMS4zOTEzMSA2Ljk3ODQyIDEuMzcyNTYgNi45NzU5MiAxLjQ0MDA1WiIgZmlsbD0iI0ZGRDUxRCIvPgo8cGF0aCBkPSJNNi45NzM0MSAxLjU1MzcxQzYuOTczNDEgMS41NTM3MSA3LjM1MzM5IDIuMDY4NjggNy41OTA4OCAyLjM3NzQyQzcuODAzMzcgMi42NTM2NSA4LjExMjEgMy4wMDM2MyA4LjExMjEgMy4wMDM2M0w4LjEwMjEgMy43NTM1OUM4LjEwMjEgMy43NTM1OSA3LjYyMjEzIDMuMjE2MTIgNy4zNzk2NCAyLjkyNzM5QzcuMTE3MTUgMi42MTYxNSA2Ljk2NDY2IDIuMzY4NjcgNi45NjQ2NiAyLjM2ODY3TDYuOTczNDEgMS41NTM3MVoiIGZpbGw9IiNGREE3MjYiLz4KPHBhdGggZD0iTTYuOTM5NjkgNC4zMzg2MkM2LjkzOTY5IDQuMzM4NjIgNy4xODIxOCA0Ljc2NDg1IDcuNDk3MTYgNS4xNjg1OEM3Ljc1MzQgNS40OTczMSA4LjA3MzM4IDUuNzkzNTUgOC4wNzMzOCA1Ljc5MzU1TDguMDU3MTMgNy4yNzU5N0w2LjkwMzQ0IDcuMDU5NzNMNi45Mzk2OSA0LjMzODYyWiIgZmlsbD0iI0ZEQTcyNiIvPgo8cGF0aCBkPSJNNi44NjQ3MSAxMS42MDk0TDYuODYwOTYgMTIuMTMwNkM2Ljg2MDk2IDEyLjEzMDYgNy4wOTA5NSAxMi41MTQ0IDcuMzgzNDMgMTIuODQ2OEM3LjYyNzE3IDEzLjEyMzEgNy45Nzg0IDEzLjQ5NjggNy45Nzg0IDEzLjQ5NjhMOC4wMDIxNSAxMS42MDk0TDcuNTI5NjggMTEuMzc4Mkw2Ljg2NDcxIDExLjYwOTRaIiBmaWxsPSIjRkRBNzI2Ii8+CjxwYXRoIGQ9Ik02Ljg0MDkzIDE0LjE3MTlDNi44NDA5MyAxNC4xNzE5IDcuMjA5NjcgMTQuNjUzMSA3LjQ2NTkgMTQuOTQwNkM3LjY2OTY0IDE1LjE2OTMgNy45NTQ2MyAxNS40NjkzIDcuOTU0NjMgMTUuNDY5M0w3Ljk0NDYzIDE2LjE4NTVDNy45NDQ2MyAxNi4xODU1IDcuNDY1OSAxNS43MDE4IDcuMjQ5NjYgMTUuNDY5M0M3LjAzMzQyIDE1LjIzNjggNi44MzM0NCAxNS4wMDQzIDYuODMzNDQgMTUuMDA0M0w2Ljg0MDkzIDE0LjE3MTlaIiBmaWxsPSIjRkRBNzI2Ii8+CjxwYXRoIGQ9Ik0yLjEwNjIxIDEzLjQyMDdDMi4xMDYyMSAxMy40MjA3IDEuOTE4NzIgMTMuMTQwNyAyLjAzMzcxIDEyLjc4MkMyLjE1NDk2IDEyLjQwMiAyLjQyOTk0IDEyLjI2MiAyLjU1ODY5IDExLjk1ODNDMi43MzYxOCAxMS41MzgzIDIuNjk0OTMgMTEuMTk4MyAzLjAyNzQxIDEwLjg4MzNDMy4zMjYxNSAxMC42MDA4IDMuNzM4NjIgMTAuNzA1OCA0LjEwMjM2IDEwLjYwMDhDNC40NjYwOSAxMC40OTU4IDQuNjE5ODMgMTAuMjc3MSA0LjYxOTgzIDEwLjI3NzFMNS40Njg1MyAxMC41MzU4QzUuNDY4NTMgMTAuNTM1OCA1LjM1NjA0IDExLjQ4MDggNS4wMTYwNiAxMS40NjQ1QzQuNjc2MDggMTEuNDQ4MyAzLjUwNDg5IDExLjY2NyAzLjQ4MTE0IDExLjcxNDVDMy40NTczOSAxMS43NjMzIDIuNDE0OTQgMTMuNDAzMiAyLjQxNDk0IDEzLjQwMzJMMi4xMDYyMSAxMy40MjA3WiIgZmlsbD0iIzhBODY3RSIvPgo8cGF0aCBkPSJNOS45OTk1MSAxMS4zNjgyQzEwLjAxNTggMTEuNDQwNyAxMC4yOTA3IDExLjgzNjkgMTAuNzI3IDEyLjE5MTlDMTEuMTYzMiAxMi41NDY5IDExLjgwOTQgMTIuODA1NiAxMi4wMjgyIDEyLjk2ODFDMTIuMjQ2OSAxMy4xMjk0IDEyLjQwODEgMTMuMjAxOSAxMi42NzQ0IDEzLjU1ODFDMTIuOTQwNiAxMy45MTQzIDEzLjAyOTQgMTQuMTMxOCAxMy4yMjQzIDE0LjQwNjhDMTMuNDE4MSAxNC42ODE4IDEzLjYyMDYgMTQuODc1NSAxMy43OTgxIDE1LjAxM0MxMy45ODE4IDE1LjE1NDMgMTQuMzU1NSAxNS4zNzY3IDE0LjQyMDUgMTUuMzY4QzE0LjQ4NTUgMTUuMzYwNSAxNC43NjggMTQuNzcwNSAxNC43NjggMTQuNzcwNUMxNC43NjggMTQuNzcwNSAxMy45MTQzIDEzLjc5ODEgMTMuNzIzMSAxMy40NjgxQzEzLjUzMTggMTMuMTM5NCAxMy4xNDkzIDEyLjYxODEgMTIuNzI0NCAxMi4zNzQ0QzEyLjUxMTkgMTIuMjUxOSAxMS4xMjMyIDEwLjUyMDggMTEuMTIzMiAxMC41MjA4TDkuOTk5NTEgMTEuMzY4MloiIGZpbGw9IiM4QTg2N0UiLz4KPHBhdGggZD0iTTEyLjY3MzIgNy42ODcyN0MxMi42NzMyIDcuNjg3MjcgMTIuODExOSA3LjUyNDc4IDEyLjkwMTkgNy40NjIyOEMxMy4wMTU2IDcuMzgyMjggMTMuMTA4MSA3LjMzNjA0IDEzLjI4ODEgNy4zMzIyOUMxMy40NTA2IDcuMzI5NzkgMTMuOTUwNiA3Ljc2NDc2IDEzLjk1MDYgNy43NjQ3NkMxMy45NTA2IDcuNzY0NzYgMTUuMTMzIDkuOTU1OSAxNS4xMzY4IDkuOTUyMTVDMTUuMTQwNSA5Ljk0ODQgMTUuOTA1NSAxMC44MjQ2IDE1Ljg5MTcgMTAuODA3MUMxNS44NzggMTAuNzg4NCAxNi4zNjE3IDExLjAzNzEgMTYuMzYxNyAxMS4wMzcxQzE2LjM2MTcgMTEuMDM3MSAxNi4yNzkyIDExLjU4MjEgMTUuMzE0MyAxMS41NTMzQzE0LjM3MDYgMTEuNTI1OCAxMy43NzQzIDEwLjk4OTYgMTMuNDM5NCAxMC4yNzcxQzEzLjI3ODEgOS45MzQ2NSAxMy4yNTMxIDkuMzEzNDMgMTMuMjUzMSA5LjMxMzQzTDEyLjY3MzIgNy42ODcyN1oiIGZpbGw9IiM3QTU3QkQiLz4KPHBhdGggZD0iTTEzLjA1ODEgNy4zNzA5QzEzLjA1ODEgNy4zNzA5IDEzLjEyNTYgNy42NzIxNCAxMy4zNzA2IDcuODg5NjNDMTMuNjE1NiA4LjEwNTg3IDEzLjgzMDYgOC4yOTU4NiAxNC4wMDA2IDguNTY4MzRDMTQuMTcwNSA4Ljg0MDgzIDE0LjU1NDMgOS44MzQ1MiAxNS4wNDY4IDEwLjQxNDVDMTUuNTM5MiAxMC45OTQ1IDE1LjcyNTUgMTEuMDMyIDE1LjkxNjcgMTEuMTAyQzE2LjA3NzkgMTEuMTYwNyAxNi4yMzQyIDExLjE1NDUgMTYuMjkxNyAxMS4xMzJDMTYuMzgwNCAxMS4wOTk1IDE2LjQ2NDIgMTAuOTI3IDE2LjA1OTIgMTAuNTUzMkMxNS42NTQyIDEwLjE3OTUgMTUuNDAwNSA5LjczNzAzIDE1LjE3OCA5LjI5MjA1QzE0Ljk1NTUgOC44NDcwOCAxNC44NDQzIDcuODUzMzggMTQuMjMzIDcuNDQwOUMxMy42MjE4IDcuMDI5NjcgMTMuMDU4MSA3LjM3MDkgMTMuMDU4MSA3LjM3MDlaIiBmaWxsPSIjQTQ3RkNDIi8+CjxwYXRoIGQ9Ik0xMy4wNDE4IDcuOTI4NDdDMTMuMDQxOCA3LjkyODQ3IDEzLjQyOTMgOC40MzIxOSAxMy40MTMxIDkuMTIyMTVDMTMuMzk2OCA5LjgxMjEyIDEzLjA2MDYgMTAuMzAwOCAxMy4wNjA2IDEwLjcwOTZDMTMuMDYwNiAxMS4xMTgzIDEzLjE0OTMgMTEuMzk5NSAxMy41NDkzIDExLjUxOTVDMTMuOTUwNSAxMS42Mzk1IDE0LjE2NjggMTEuNzEyIDE0LjIzOTMgMTEuODgwOEMxNC4zMTE4IDEyLjA0OTUgMTQuMjk1NSAxMi40NzQ1IDE0LjQ3OTMgMTIuODI3QzE0LjY2MyAxMy4xNzk0IDE0LjgwMyAxMy4yNjA3IDE0Ljg4OCAxMy40NDQ0QzE0Ljk4NDIgMTMuNjQ5NCAxNS4wMDA1IDEzLjk4OTQgMTUuMDAwNSAxMy45ODk0QzE1LjAwMDUgMTMuOTg5NCAxNC44MDA1IDE0Ljk1OTMgMTQuNTY4IDE0LjU1MDZDMTQuMzM1NSAxNC4xNDE5IDEzLjUzMzEgMTIuNDgyIDEzLjMwOTMgMTIuMjg5NUMxMy4wODQzIDEyLjA5NyAxMS44ODE5IDEwLjc3MzMgMTEuODgxOSAxMC43MDIxQzExLjg4MTkgMTAuNjMwOCAxMy4wNDE4IDcuOTI4NDcgMTMuMDQxOCA3LjkyODQ3WiIgZmlsbD0iI0FDQTM5OSIvPgo8cGF0aCBkPSJNMi43NzYxNSAxMy41ODY5TDMuMjMxMTMgMTMuNjUwN0wzLjI1MjM4IDEzLjMyMkMzLjI1MjM4IDEzLjMyMiAzLjQwMTEyIDEzLjAxNDUgMy41NDg2MSAxMi44MTMyQzMuNjk3MzUgMTIuNjEyIDMuODM0ODUgMTIuMzg5NSA0LjExOTgzIDEyLjM0N0M0LjQwNjA3IDEyLjMwNDUgNi4yMTU5NyAxMi4zNDcgNi4zNjQ3MSAxMi4yNjJDNi41MTM0NiAxMi4xNzcgNi44MTk2OSAxMS43OTU4IDYuODE5NjkgMTEuNzk1OEM2LjgxOTY5IDExLjc5NTggNy45MTMzOCAxMS44OTA4IDkuMDQzMzIgMTEuNjU4M0M5Ljg0ODI4IDExLjQ5MiAxMC4zNzcgMTEuNDM1OCAxMC4zNzcgMTEuNDM1OEwxMS4xNzA3IDExLjM2MjFDMTEuMTcwNyAxMS4zNjIxIDEyLjAwNjkgMTAuOTkyMSAxMS45ODU3IDEwLjkxNzFDMTEuOTY0NCAxMC44NDMzIDExLjMzOTUgOS43MDk2NCAxMS4yNzU3IDkuNzQyMTRDMTEuMjEyIDkuNzczMzkgNi40MzcyMSAxMC4yMTg0IDYuMzYzNDYgMTAuMzAzNEM2LjI4OTcyIDEwLjM4ODQgNS40MjEwMSAxMS4yNzcxIDUuNDIxMDEgMTEuMjc3MUM1LjQyMTAxIDExLjI3NzEgMy41MTQ4NiAxMS43NzQ1IDMuNDgzNjIgMTEuNzg1OEMzLjQ1MjM3IDExLjc5NTggMi42NTc0MSAxMy40NDgyIDIuNjU3NDEgMTMuNDQ4MkwyLjc3NjE1IDEzLjU4NjlaIiBmaWxsPSIjQUNBMzk5Ii8+CjxwYXRoIGQ9Ik00LjAyNjA3IDkuMjM3MDZDNC4wMjYwNyA5LjIzNzA2IDQuMDIxMDcgOC44NDgzMyA0LjA4MjMyIDguMzYzMzZDNC4xNDEwNiA3Ljg5MzM4IDQuMjQ5ODEgNy4zNzIxNiA0LjI2MTA2IDYuOTY5NjhDNC4yNzM1NiA2LjUyMDk1IDQuMjIyMzEgNi4yNTQ3MiA0LjE3NDgxIDYuMjM5NzJDNC4xMjg1NyA2LjIyNDcyIDMuOTAyMzMgNi43NTk2OSAzLjkwMjMzIDYuNzU5NjlDMy45MDIzMyA2Ljc1OTY5IDMuOTE2MDggNy4wMDIxOCAzLjgxMjMzIDcuMjQwOTJDMy42ODk4NCA3LjUyMjE1IDMuNDk0ODUgNy40MjIxNiAzLjQ5NDg1IDcuNDIyMTZMMy4yNjM2MSA2LjYzNzJMMy43MTQ4NCA1LjM5MTAxTDMuOTU2MDcgMy42OTM2TDQuNzU4NTMgNC4yNjIzMkw1LjI5NiA2LjQwNDcxTDUuMTA4NTEgOC40MzcxTDQuNTY5NzkgOS40NDk1NUw0LjAyNjA3IDkuMjM3MDZaIiBmaWxsPSIjQUNBMzk5Ii8+CjxwYXRoIGQ9Ik0xMS4yMjQ1IDcuMDUzNDdMMTAuNjk4MiA4LjMxODRMMTEuMjg3IDExLjAxOTVMMTAuNzgzMiAxMS40MTJDMTAuNzgzMiAxMS40MTIgMTEuMTUzMiAxMi4xMjE5IDExLjcyOTQgMTIuNDAxOUMxMi4zMDY5IDEyLjY4MTkgMTIuODIwNiAxMi42MzY5IDEyLjk5MTkgMTIuNjI2OUMxMy4xNjMxIDEyLjYxODIgMTMuMjc4MSAxMi42OTE5IDEzLjM3OTQgMTIuODM0NEMxMy44NTkzIDEzLjUwMzEgMTMuODMwNiAxNC4xMDkzIDEzLjk2NDMgMTQuMzU4MUMxNC4xOTgxIDE0Ljc5MDYgMTQuNDg4IDE0LjgyNjggMTQuNDg4IDE0LjgyNjhDMTQuNDg4IDE0LjgyNjggMTUuMDEwNSAxNC42MzY4IDE0Ljk5MyAxNC42MDE4QzE0Ljk3NTUgMTQuNTY1NiAxNC43NDkzIDE0LjE1OTMgMTQuNzQ5MyAxNC4xNTkzQzE0Ljc0OTMgMTQuMTU5MyAxNC42NTkzIDEzLjc2MzEgMTQuNDUxOCAxMy40MDE5QzE0LjI0NDMgMTMuMDQwNyAxNC4xMjY4IDEyLjg2MDcgMTQuMDczMSAxMi40ODE5QzE0LjAxOTMgMTIuMTAzMiAxNC4wMDA2IDExLjkyMzIgMTMuNTk1NiAxMS44MzMyQzEzLjE4OTQgMTEuNzQzMiAxMi42NjY5IDExLjQ5MDcgMTIuNzAzMSAxMC45NzdDMTIuNzM5NCAxMC40NjMzIDEzLjM4NDQgOS4zNzA4NCAxMy4yODk0IDguNTc4MzlDMTMuMjA4MSA3LjkwMjE3IDEyLjYyMTkgNy4yNjIyMSAxMS43MDE5IDcuMTE3MjFDMTEuNDQ4MiA3LjA3NzIyIDExLjIyNDUgNy4wNTM0NyAxMS4yMjQ1IDcuMDUzNDdaIiBmaWxsPSIjRjFFREVDIi8+CjxwYXRoIGQ9Ik0xNS4wMTE4IDEzLjc5MDdDMTQuOTU1NSAxMy44MDA3IDE0LjgxMyAxNC4xNDE5IDE0LjcwNTUgMTQuMzIzMUMxNC42MjA2IDE0LjQ2NTYgMTQuMzk5MyAxNC43OTE5IDE0LjQxNjggMTQuODM2OUMxNC40MzQzIDE0Ljg4MTkgMTUuMTM5MyAxNS41NjMxIDE1LjE4MyAxNS41OTQzQzE1LjI0NjggMTUuNjM5MyAxNS4zNzMgMTUuNTk0MyAxNS40MjY4IDE1LjM5NTZDMTUuNDgwNSAxNS4xOTY4IDE1Ljc0MyAxNC4wNjA3IDE1Ljc0MyAxNC4wNjA3QzE1Ljc0MyAxNC4wNjA3IDE1LjEyMDUgMTMuNzcxOSAxNS4wMTE4IDEzLjc5MDdaIiBmaWxsPSIjNUU2MzY3Ii8+CjxwYXRoIGQ9Ik0xMS44MTA3IDcuNTY3MjJDMTEuNzY0NCA3LjcwNDcxIDExLjg5MTkgNy44MDIyMSAxMi4xMTY5IDguMDE4NDVDMTIuMzQxOSA4LjIzNDY4IDEyLjQ3ODIgOC40OTU5MiAxMi41MDQ0IDguODAzNEMxMi41MzE5IDkuMTA5NjQgMTIuNTgzMSA5LjMzNzEzIDEyLjc0ODEgOS4zNDQ2M0MxMi45NDY5IDkuMzUzMzcgMTMuMTY2OSA4Ljg5MzQgMTIuODI5NCA4LjIwODQzQzEyLjQ2OTQgNy40NzcyMiAxMS44MzgyIDcuNDg1OTcgMTEuODEwNyA3LjU2NzIyWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTExLjI5NTcgNy4yMzczTDExLjI3ODIgMTAuOTYyMUMxMS4yNzgyIDEwLjk2MjEgMTAuOTY3IDExLjA4MzQgMTAuNTc3IDEwLjk2MjFDMTAuMTg3IDEwLjg0MDkgMTAuMDU3IDEwLjU5ODQgMTAuMDU3IDEwLjU5ODRDMTAuMDU3IDEwLjU5ODQgOS43Nzk1NSAxMS4wNjU5IDkuMDI1ODQgMTEuMDU3MUM4LjI3MjEzIDExLjA0ODQgOC4wODIxNCAxMC42OTM0IDguMDgyMTQgMTAuNjkzNEM4LjA4MjE0IDEwLjY5MzQgNy45NTA4OSAxMC45ODU5IDcuMzk4NDIgMTEuMDU3MUM3LjA4NTk0IDExLjA5NzEgNi44MTk3IDExLjA5NzEgNi43NjcyIDEwLjk3NTlDNi43MTQ3MSAxMC44NTQ2IDUuODQxIDguMTQ0NzYgNS44NDEgOC4xNDQ3Nkw4LjgwMDg1IDcuMzc2MDVMMTEuMjk1NyA3LjIzNzNaIiBmaWxsPSIjQjBFNEZFIi8+CjxwYXRoIGQ9Ik03LjE2NDY4IDguMjMzNDVMNy4xMzg0MyAxMC42MDU4QzcuMTM4NDMgMTAuNjA1OCA3LjM5NzE2IDEwLjYwODMgNy42MzIxNSAxMC40NDk2QzcuODc3MTQgMTAuMjgzMyA3Ljk4NzEzIDEwLjExNzEgOC4wMzA4OCAxMC4xMjA5QzguMDkwODggMTAuMTI1OSA4LjQwMzM2IDEwLjYwNzEgOS4wMDA4MyAxMC42MjMzQzkuNjY4MjkgMTAuNjQwOCA5Ljk2MjAzIDEwLjA2OTYgMTAuMDE0NSAxMC4wNjk2QzEwLjA4MzMgMTAuMDY5NiAxMC4yMjk1IDEwLjMxNzEgMTAuMzc4MyAxMC4zOTgzQzEwLjY0NyAxMC41NDU4IDEwLjg0NTcgMTAuNDc1OCAxMC44NDU3IDEwLjQ3NThMMTAuODU0NSA3Ljc4MjIzTDcuMTY0NjggOC4yMzM0NVoiIGZpbGw9IiMzNEI2RTIiLz4KPHBhdGggZD0iTTIuMDE5OTQgNC40Njk5NEMyLjAxOTk0IDQuNDY5OTQgMS45MDEyIDMuNzI4NzMgMi4xNzc0NCAzLjI5Mzc1QzIuNDUzNjcgMi44NTg3NyAyLjg3ODY1IDIuNzQwMDMgMi44Nzg2NSAyLjc0MDAzTDMuNjU5ODYgMi41NDI1NEMzLjY1OTg2IDIuNTQyNTQgMy45NjIzNCAyLjQxNjI5IDQuNTM5ODEgMi40MzM3OUM1LjEwOTc4IDIuNDUxMjkgNS41OTg1MSAyLjc0Mzc4IDUuODYzNDkgMi44OTg3N0M2LjE2OTczIDMuMDc2MjYgNi40NTU5NiAzLjQ2MjQ5IDYuMzU3MjIgMy42Mjk5OEM2LjI1ODQ3IDMuNzk3NDcgNS45OTIyMyAzLjY3OTk4IDUuOTkyMjMgMy42Nzk5OEM1Ljk5MjIzIDMuNjc5OTggNi43MTM0NSA0LjE4MzcgNy4wNTk2OCA1LjA5MjRDNy4zNDcxNiA1Ljg0ODYxIDcuMzg1OTEgNi42MTM1NyA3LjM4NTkxIDYuNjEzNTdMOC4zMTQ2MSA4LjA2NkwxMS4xMjA3IDYuNjgyMzJDMTEuMTIwNyA2LjY4MjMyIDExLjMwOTUgNi42NzEwNyAxMS40MjY5IDYuNzYxMDdDMTEuNTk0NCA2Ljg4OTgxIDExLjYyNTcgNy4wOTQ4IDExLjYyNTcgNy4wOTQ4QzExLjYyNTcgNy4wOTQ4IDExLjQ0NDQgNy4xNDQ3OSAxMS4yNDgyIDcuNDIzNTNDMTAuNzA0NSA4LjE5NDc0IDEwLjM4ODMgMTAuMDUyMSA4Ljg4NzA4IDkuOTIzNEM3LjU4MjE1IDkuODEyMTUgNy4zNTU5MSA4LjUwMDk3IDcuMjY1OTIgOC4xNjQ3NEM3LjE3NzE3IDcuODI4NTEgNi4wMDg0OCA2Ljc1NzMyIDYuMDA4NDggNi43NTczMkw0Ljg5NDc5IDQuMjEyNDVMMy43MTg2IDMuMTM1MDFMMi40NDM2NyA0LjM4OTk0TDIuMDE5OTQgNC40Njk5NFoiIGZpbGw9IiM3QTU3QkQiLz4KPHBhdGggZD0iTTQuNjQ5ODIgMTAuOTIyMUM0LjY0OTgyIDEwLjkyMjEgNC4zOTczMyAxMS4wNjU4IDQuMDcyMzUgMTEuMTEyMUMzLjc0NzM2IDExLjE1NzEgMy40MDQ4OCAxMS4xMDMzIDMuMTQzNjUgMTEuMzgyMUMyLjg4MjQxIDExLjY2MDggMi45ODk5IDEyLjA1ODMgMi43ODI0MSAxMi4zNjQ1QzIuNTc0OTMgMTIuNjcwOCAyLjQwMzY4IDEyLjg2MDcgMi4yMzI0NCAxMy4wNDk1QzIuMDYxMiAxMy4yMzgyIDIuMDI0OTUgMTMuNDU1NyAyLjAzMzcgMTMuNjM1N0MyLjA0MjQ1IDEzLjgxNTcgMi4xOTYyIDE0LjEzMTkgMi4xOTYyIDE0LjEzMTlMMy4wNjI0IDE0LjA2ODJMMy4wNTM2NSAxMy41MDk1QzMuMDUzNjUgMTMuNTA5NSAyLjkzNzQxIDEzLjMwODIgMy4wOTg2NSAxMy4wMzJDMy4yMDIzOSAxMi44NTQ1IDMuNDg2MTMgMTIuMzgzMyAzLjU3NjEyIDEyLjI2NThDMy42NjYxMiAxMi4xNDgzIDMuNzI2MTIgMTIuMDEwOCAzLjkzNzM1IDExLjk3N0M0LjE2NjA5IDExLjkzOTUgNS45OTM1IDExLjkyMzMgNi4xNjQ3NCAxMS44NDIxQzYuMzM1OTggMTEuNzYwOCA2LjcwNTk2IDExLjM2NDYgNi43MzM0NiAxMS4wNjcxQzYuNzYwOTYgMTAuNzY5NiA2Ljc3ODQ1IDguMzUyMjQgNi43Nzg0NSA4LjM1MjI0TDYuOTQ3MiA3LjAyMTA2QzYuOTQ3MiA3LjAyMTA2IDYuODQ4NDUgNi43NTEwNyA2Ljc0NzIxIDYuNDc5ODNDNi40MDQ3MiA1LjU1OTg4IDUuOTg0NzUgNC41OTc0MyA1LjE4OTc5IDMuNjgyNDhDNC40MDg1OCAyLjc4Mzc4IDMuNjIyMzcgMi44Nzg3NyAzLjYyMjM3IDIuODc4NzdDMy42MjIzNyAyLjg3ODc3IDIuNDA0OTMgMS43NzAwOCAyLjM0MTE5IDEuNjc4ODRDMi4yNzc0NCAxLjU4NzU5IDIuMTg4NyAxLjQ5ODg1IDIuMTY5OTUgMS41NzAwOUMyLjE1MTIgMS42NDEzNCAyLjIwNjIgMi40MTc1NSAyLjIwNjIgMi40MTc1NUwyLjY1NzQyIDMuMTkyNTFDMi42NTc0MiAzLjE5MjUxIDIuNTY3NDMgMy41NjI0OSAyLjQxMzY4IDMuOTMyNDdDMi4yNTk5NCA0LjMwMjQ1IDIuMDE5OTYgNC40Njg2OSAyLjAxOTk2IDQuNDY4NjlMMS45Mjg3MSA2LjEwODZDMS45Mjg3MSA2LjEwODYgMS43Mzc0NyA2LjQwNzM0IDEuNzMxMjIgNi43OTYwN0MxLjcyNDk3IDcuMTc4NTUgMy4yNDk4OSA2Ljc1NjA3IDMuMjU2MTQgNi43MjM1N0MzLjI2MjM5IDYuNjkxMDcgMy43NzczNiA1LjI4ODY1IDMuNzcxMTEgNS4yMTc0QzMuNzY0ODYgNS4xNDYxNiA0LjE1NDg0IDQuMDIzNzEgNC4xNTQ4NCA0LjAyMzcxQzQuMTU0ODQgNC4wMjM3MSA0Ljg0NzMxIDUuMDE4NjYgNC43MzczMSA2LjE3NDg1QzQuNjk5ODEgNi41NzQ4MyA0LjYzMzU3IDYuOTk4NTYgNC41NzczMiA3LjMzOTc5QzQuMzkzNTggOC40NjIyMyA0LjI1ODU5IDkuMjE4NDQgNC4yNTg1OSA5LjIxODQ0TDQuNjQ5ODIgMTAuNjIyMVYxMC45MjIxWiIgZmlsbD0iI0YxRURFQyIvPgo8cGF0aCBkPSJNMy42MDg2MiAxMy44OTgzQzMuNjA4NjIgMTMuOTUyIDMuNDQ0ODggMTQuMjMyIDMuMTAyNCAxNC42OTJDMi43NTk5MiAxNS4xNTE5IDIuNjM3NDIgMTUuMzAxOSAyLjUwODY4IDE1LjExOTRDMi4zNzk5NCAxNC45MzY5IDIuMzMyNDQgMTQuNjI3IDIuMjgzNjkgMTQuNDYzMkMyLjIzOTk0IDE0LjMxNTcgMi4xNjYyIDE0LjA3ODIgMi4xNjYyIDE0LjA3ODJMMy4yNTIzOSAxMy4zMzJDMy4yNTM2NCAxMy4zMzIgMy42MDg2MiAxMy43ODA4IDMuNjA4NjIgMTMuODk4M1oiIGZpbGw9IiM1RTYzNjciLz4KPHBhdGggZD0iTTIuNzkzNjQgNS4yNTYxMUMyLjczNzM5IDUuMjgxMSAyLjY4NzQgNS41Mjk4NCAyLjg4MjM5IDUuNzc0ODNDMy4wNzczOCA2LjAxOTgxIDMuMzAxMTEgNi4xMzczMSAzLjMwMTExIDYuMTM3MzFMMy40NzM2MSA1Ljc1ODU4QzMuNDczNjEgNS43NTg1OCAzLjIzNDg3IDUuNjA4NTkgMy4wOTk4NyA1LjQ4NDg0QzIuOTYxMTMgNS4zNTYxIDIuODQzNjQgNS4yMzM2MSAyLjc5MzY0IDUuMjU2MTFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMi4xNjk5NSAxLjU2OTkyQzIuMTM0OTYgMS41ODYxNyAxLjkyNjIyIDEuODU2MTYgMi4wNjk5NiAyLjQwMjM4QzIuMTU0OTUgMi43MjYxMSAyLjc2MjQyIDMuMzI2MDggMi43NjI0MiAzLjMyNjA4TDMuMTI0OSAzLjU3OTgyQzMuMTI0OSAzLjU3OTgyIDMuMDcxMTYgMy4yMDIzNCAyLjg1NDkyIDIuOTQ4NkMyLjYzODY4IDIuNjk0ODYgMi4zODQ5NCAyLjQwOTg4IDIuMzE2MiAyLjE3MTE0QzIuMjQ3NDUgMS45MzI0MSAyLjI4NjIgMS41MTYxOCAyLjE2OTk1IDEuNTY5OTJaIiBmaWxsPSIjQUNBMzk5Ii8+CjxwYXRoIGQ9Ik0xLjkyNDk4IDYuMTA3MzdDMS45MjQ5OCA2LjEwNzM3IDEuNzEzNzQgNi40ODExIDEuNzIyNDkgNi44NDQ4M0MxLjcyOTk5IDcuMTc3MzEgMS44MzYyNCA3LjM1NjA1IDIuMDQxMjMgNy41MzcyOUMyLjI0NDk3IDcuNzE3MjggMi41OTc0NSA3Ljg0ODUzIDIuODE3NDQgNy43NTk3OEMyLjg0NzQzIDcuNzQ3MjggMi44NTk5MyA3LjUzNzI5IDIuOTMxMTggNy4zODEwNUMyLjk4NjE4IDcuMjYyMzEgMy4xNzc0MiA3LjA3MTA3IDMuMTc3NDIgNy4wNzEwN0MzLjE3NzQyIDcuMDcxMDcgMy4yNjk5MSA3LjI3MTA2IDMuMjg0OTEgNy4zNzEwNUMzLjI5OTkxIDcuNDcxMDUgMy4zMDk5MSA3LjY2OTc5IDMuMzQ2MTYgNy42Nzg1NEMzLjQ4NDkgNy43MTM1MyAzLjY3NjE0IDcuNTc0NzkgMy43OTczOCA3LjI3NDgxQzMuOTI3MzggNi45NTM1NyAzLjkwMTEzIDYuNzY2MDggMy45MDExMyA2Ljc2NjA4TDQuMTA4NjIgNi4yOTExMUM0LjEwODYyIDYuMjkxMTEgNC4wMDExMiA1LjQ1OTkgMy4yOTI0MSA1LjI2NzQxQzIuNTgzNyA1LjA3NDkyIDEuOTYzNzMgNS40Mjk5IDEuOTYzNzMgNS40Mjk5TDEuOTI0OTggNi4xMDczN1oiIGZpbGw9InVybCgjcGFpbnQwX3JhZGlhbF83MTVfMjk1KSIvPgo8cGF0aCBkPSJNMi4yMjI0NiA2LjM5MjJDMi4wMjQ5NyA2LjM2OTcxIDEuOTQ2MjMgNi41OTA5NCAxLjk0NjIzIDYuODM3MThDMS45NDYyMyA3LjA4MzQyIDIuMDc4NzIgNy4xODU5MSAyLjIwNDk2IDcuMTg1OTFDMi4zMzEyMSA3LjE4NTkxIDIuNDQ2MiA3LjAyMzQyIDIuNDQ2MiA2Ljc5NDY4QzIuNDQ0OTUgNi41NjcyIDIuMzc4NzEgNi40MTA5NSAyLjIyMjQ2IDYuMzkyMloiIGZpbGw9IiM4NDVCNTEiLz4KPHBhdGggZD0iTTIuMzU3NDIgNC43NDM2NEMyLjM3OTkyIDQuNzk5ODkgMi40NDc0MiA0Ljg4OTg5IDIuNTQzNjYgNC45MjM2M0MyLjYzOTkxIDQuOTU3MzggMi43NTI0IDQuOTQxMTMgMi43NTI0IDQuOTQxMTNDMi43NTI0IDQuOTQxMTMgMi43ODExNSA1LjE4MzYyIDIuOTU0ODkgNS4xODk4N0MzLjEzNzM4IDUuMTk2MTIgMy4xNTIzOCA0Ljg3MzY0IDMuMTIzNjMgNC43MTYxNUMzLjA5NDg4IDQuNTU4NjUgMi45OTk4OSA0LjQ1MTE2IDIuOTk5ODkgNC40NTExNkMyLjk5OTg5IDQuNDUxMTYgMi43MTI0IDQuNDExMTYgMi41MzExNiA0LjUzNjE2QzIuMzk4NjcgNC42MjYxNSAyLjM1NzQyIDQuNzQzNjQgMi4zNTc0MiA0Ljc0MzY0WiIgZmlsbD0iIzQ2NEQ1MCIvPgo8cGF0aCBkPSJNMy4yNjQ4OCAyLjU1MzczQzMuMjY0ODggMi41NTM3MyAzLjU2NzM2IDIuNzk2MjIgMy42MDQ4NiAzLjIxNDk1QzMuNjQ2MTEgMy42ODM2NyAzLjY0NDg2IDMuNzg5OTIgMy42NDQ4NiAzLjc4OTkyTDIuNTQzNjcgMy4wOTc0NUMyLjU0MzY3IDMuMDk3NDUgMi40OTExNyAzLjIxODcgMi40ODg2NyAzLjM1MzY5QzIuNDg2MTcgMy40OTk5MyAyLjUwNzQyIDMuNjUxMTggMi41MDc0MiAzLjY1MTE4TDMuNjUxMTEgNC4zNTM2NEMzLjY1MTExIDQuMzUzNjQgMy42MDczNiA0Ljk3OTg2IDMuNDIyMzcgNS42MzQ4MkMzLjI1ODYzIDYuMjEyMjkgMy4wMzM2NCA2LjY0MjI3IDIuOTk2MTQgNi43MjIyNkMyLjk1ODY0IDYuODAyMjYgMi44Mjc0IDcuMTQ0NzQgMy4xMzg2MyA3LjIyMjI0QzMuNDExMTIgNy4yODk3MyAzLjQ2NjEyIDYuOTAxIDMuNTQ2MTEgNi43MjcyNkMzLjYyNjExIDYuNTUzNTIgMy44MTk4NSA2LjA2NDggMy45MzQ4NCA1LjYzMzU3QzQuMjQ5ODMgNC40NTM2MyA0LjE4ODU4IDMuNzQ4NjcgNC4xMTk4MyAzLjI2MTJDNC4wNDczNCAyLjczOTk3IDMuODg3MzQgMi42Mzk5OCAzLjY5NjExIDIuNTU4NzNDMy41NTg2MSAyLjUwMjQ5IDMuMzU2MTIgMi40NjQ5OSAzLjI2NDg4IDIuNTUzNzNaIiBmaWxsPSIjMzRCNkUyIi8+CjxwYXRoIGQ9Ik00LjAxNDgzIDguOTk4NDNDNC4wMTQ4MyA4Ljk5ODQzIDMuOTg2MDkgOS4zNDg0MSA0LjA2MjMzIDkuODcwODlDNC4xMjM1OCAxMC4yODQ2IDQuNDQzNTYgMTAuOTYzMyA0LjY1NDggMTAuOTEyMUM0LjcwNjA1IDEwLjg5OTYgNC41NzM1NiAxMC4yNTM0IDQuNzc3MjkgOS45OTU4OEM0LjkxNjA0IDkuODE5NjQgNS4xNjg1MiA5Ljc4ODM5IDUuODE3MjQgOS40MjA5MUM2LjM1OTcxIDkuMTEzNDMgNi44NDA5NCA4LjUxMjIxIDYuODQwOTQgOC41MTIyMUw2LjczOTY5IDcuNjI0NzZMNi4wNTU5OCA3Ljc5MjI1QzYuMDU1OTggNy43OTIyNSA1LjU1NiA4LjI3NTk3IDQuOTY5NzggOC41OTIyQzQuNDQ4NTYgOC44NzQ2OSA0LjAxNDgzIDguOTk4NDMgNC4wMTQ4MyA4Ljk5ODQzWiIgZmlsbD0iI0Q4NTNDOSIvPgo8cGF0aCBkPSJNNi40NTQ3NCA3LjU4NTk4QzYuNDUwOTkgNy41ODcyMyA2LjIxMSA3LjQ4OTc0IDYuMDU2MDEgNy43NDk3M0M1Ljg2NDc3IDguMDcwOTYgNS44NjM1MiA4LjY1MjE4IDUuODQ0NzcgOC42OTU5MkM1LjgyNjAyIDguNzM5NjcgNS43MDk3OCA4Ljg3ODQyIDUuODI2MDIgOC45MDIxNkM1Ljk0MTAxIDguOTI1OTEgNi4zODg0OSA4Ljg4NzE2IDYuNjc1OTggOC42ODA5M0M2Ljk2NDcxIDguNDc0NjkgNy4wMTcyMSA4LjE5MDk1IDcuMDE3MjEgOC4xOTA5NUw2LjQ1NDc0IDcuNTg1OThaIiBmaWxsPSIjMzRCNkUyIi8+CjxwYXRoIGQ9Ik0xMS4yNTY5IDYuNjk3NEMxMS4yNTY5IDYuNjk3NCAxMS4wODMyIDYuNjg4NjUgMTAuNjk4MiA3LjE3ODYyQzEwLjE3NTggNy44NDIzMyA5LjkwODI3IDkuMjA3MjYgOC45NzU4MiA5LjIwNzI2QzguMDQzMzcgOS4yMDcyNiA3Ljg5NDYzIDguMzk0OCA3LjY0ODM5IDcuNzY3MzRDNy40MDIxNSA3LjEzOTg3IDYuOTQ3MTggNi43OTczOSA2Ljk0NzE4IDYuNzk3MzlDNi45NDcxOCA2Ljc5NzM5IDYuNzA4NDQgNi42NDg2NSA2LjgzNTkzIDYuNDU0OTFDNi45MzM0MyA2LjMwNjE3IDcuMjUwOTEgNi40Mzk5MSA3LjQyOTY1IDYuNTIyNEM3LjYwODM5IDYuNjA0OSA3LjkzMDg4IDYuODEzNjQgOC4xMzMzNiA2Ljg5MzYzQzguNjkyMDkgNy4xMTQ4NyA5LjIwODMxIDYuOTc3MzggOS40OTk1NCA2LjkxMTEzQzkuNzkwNzggNi44NDM2NCAxMC4wNjA4IDYuNzE4NjQgMTAuNDk5NSA2LjY3MjRDMTAuOTg5NSA2LjYxODY1IDExLjI1NjkgNi42OTc0IDExLjI1NjkgNi42OTc0WiIgZmlsbD0iI0E0N0ZDQyIvPgo8cGF0aCBkPSJNNi42Njk3MiA2Ljk5ODZDNi40MjU5OSA3LjE2NzM0IDYuMTYzNSA3LjU1MzU3IDYuNDY4NDggOC4wNDcyOUM2Ljc0NzIyIDguNDk3MjcgNy4zMzk2OSA4LjM1MzUzIDcuNTA5NjggOC4xODM1M0M3LjcyMzQyIDcuOTY5OCA3LjgwODQxIDcuNTU2MDcgNy41ODU5MyA3LjE5ODU5QzcuMzYzNDQgNi44NDExMSA2Ljg5NzIxIDYuODM5ODYgNi42Njk3MiA2Ljk5ODZaIiBmaWxsPSIjRkZBNkEzIi8+CjxwYXRoIGQ9Ik02Ljk0NDcxIDcuMzIyMzFDNi44MTU5NiA3LjM1NDgxIDYuNjkzNDcgNy41Mzg1NSA2Ljc1ODQ2IDcuNzM2MDRDNi44MTQ3MSA3LjkwOTc4IDcuMDE4NDUgNy45ODEwMyA3LjE0ODQ0IDcuOTE4NTNDNy4yODU5NCA3Ljg1MjI4IDcuMzczNDMgNy42NjcyOSA3LjI5NTk0IDcuNTAyM0M3LjIzMjE5IDcuMzYzNTYgNy4wOTM0NSA3LjI4NDgxIDYuOTQ0NzEgNy4zMjIzMVoiIGZpbGw9IiNGMTgyQTciLz4KPGRlZnM+CjxyYWRpYWxHcmFkaWVudCBpZD0icGFpbnQwX3JhZGlhbF83MTVfMjk1IiBjeD0iMCIgY3k9IjAiIHI9IjEiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBncmFkaWVudFRyYW5zZm9ybT0idHJhbnNsYXRlKDIuNjE0ODUgNy45NzY0OSkgc2NhbGUoMi4xODYxNikiPgo8c3RvcCBvZmZzZXQ9IjAuNjYyMyIgc3RvcC1jb2xvcj0iI0ZGQThBNiIvPgo8c3RvcCBvZmZzZXQ9IjAuOTE0MiIgc3RvcC1jb2xvcj0iI0ZGQThBNiIgc3RvcC1vcGFjaXR5PSIwIi8+CjwvcmFkaWFsR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg==": dashboardIconPathPrefix + "carousel-horse", + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIuOTk4NjEgNS40Njk4NUwyLjQ2NjE0IDUuNTkyMzRDMi40NjYxNCA1LjU5MjM0IDEuNzI3NDIgNi43MzIyOCAxLjU2NjE4IDcuOTQ0NzJDMS40NjQ5NCA4LjcwNTkzIDEuNDYxMTkgOS42OTk2MiAxLjUzMTE5IDEwLjE5NDZDMS42MDI0MyAxMC42ODk2IDEuODU3NDIgMTEuNzQzMyAyLjI1ODY1IDEyLjQ4NTdDMi42NDQ4OCAxMy4yMDA3IDMuMDIxMTEgMTMuNDgxOSAzLjQzMjMzIDEzLjczMDdDMy42MDg1OCAxMy44MzY5IDMuNzY2MDcgMTQuMjAwNiA0LjA0MTA1IDE0LjM3NjlDNC4yOTg1NCAxNC41NDE5IDQuNjM4NTIgMTQuNTc0NCA0LjYzODUyIDE0LjU3NDRDNC42Mzg1MiAxNC41NzQ0IDUuNTc4NDcgMTYuMDUwNSA3LjYxMjExIDE2LjMwNjhDOS45OTgyNCAxNi42MDY4IDExLjE2NTcgMTUuNTI5MyAxMS4xNjU3IDE1LjUyOTNDMTEuMTY1NyAxNS41MjkzIDEyLjEyMDYgMTUuNTI2OCAxMy4wMjE4IDE1LjE0MDZDMTQuMDMzIDE0LjcwNjkgMTQuNTczIDEzLjgwNDQgMTQuNTczIDEzLjgwNDRDMTQuNTczIDEzLjgwNDQgMTUuODM5MiAxMi43MTQ1IDE2LjI0MDQgMTEuNDA5NUMxNi41NjI5IDEwLjM2MjEgMTYuNTcxNiA4LjM3MDk0IDE2LjMyNDIgNy40MzM0OUMxNi4wNzY3IDYuNDk2MDQgMTUuNTk3OSA1LjcwMjMzIDE1LjIxOCA1LjE5MzYxQzE0LjkxNTUgNC43ODg2MyAxMS41MzgyIDIuOTkzNzMgMTEuMTMwNyAyLjgzMzczQzEwLjcyNDUgMi42NzQ5OSA2Ljk0MDkgMi40Mjc1MSA2Ljc5OTY2IDIuNTMzNzVDNi42NTg0MSAyLjY0MTI1IDIuOTk4NjEgNS40Njk4NSAyLjk5ODYxIDUuNDY5ODVaIiBmaWxsPSIjQUM1ODExIi8+CjxwYXRoIGQ9Ik00LjMxNjE0IDMuNDY4NjlDMy40MzQ5NCA0LjE4NjE1IDIuOTc4NzEgNC41NjExMyAyLjUyMTI0IDUuNDU4NThDMi4wNjM3NiA2LjM1NjAzIDIuMjExMjUgNy41NDcyMiAyLjI5Mzc1IDcuOTg3MkMyLjM3NSA4LjQyNzE4IDIuNTIyNDkgOS4yNzU4OCAyLjYwMzczIDkuNjk5NjFDMi42ODQ5OCAxMC4xMjQ2IDIuOTAxMjIgMTEuMTMyIDMuNDk5OTQgMTEuNzU1OEM0LjMzODY0IDEyLjYyOTUgNS4wOTg2IDEyLjkzMDcgNS4wOTg2IDEyLjkzMDdDNS4wOTg2IDEyLjkzMDcgNS43MTQ4MiAxMy44ODU2IDcuMjAzNDkgMTQuNDMxOUM5LjE2MDg5IDE1LjE0OTMgMTAuMDQyMSAxNC4xMDU2IDEwLjc5MjEgMTMuODI4MUMxMS41NDIgMTMuNTUwNyAxMi40Mzk1IDEzLjUzNDQgMTMuMjA2OSAxMi45OTU3QzEzLjk3MzEgMTIuNDU3IDE0LjgyMTggMTEuODA0NSAxNS4zNzY4IDEwLjQzNDZDMTUuOTMxOCA5LjA2NDY0IDE1LjgxNjggOC40NjA5MiAxNS44MzMgOC4xMTg0NEMxNS44NDkzIDcuNzc1OTYgMTUuODAwNSA2LjEyODU1IDE1LjE2NDMgNS4xMTczNUMxNC41MjgxIDQuMTA2MTUgMTMuNzYzMSAzLjQ5ODY4IDEzLjQ2ODIgMy4zMDYxOUMxMi44MTU3IDIuODgyNDcgMTAuNDg1OCAxLjg2ODc3IDkuODk5NiAxLjc1NTAzQzkuMzEyMTMgMS42NDEyOCA3LjIxNzI0IDEuODE4NzcgNi4zODYwMyAyLjE2MTI1QzUuNTU0ODMgMi41MDM3NCA0LjMxNjE0IDMuNDY4NjkgNC4zMTYxNCAzLjQ2ODY5WiIgZmlsbD0idXJsKCNwYWludDBfcmFkaWFsXzcxOF84OSkiLz4KPHBhdGggZD0iTTguMDM0NzYgOS45MzIxMkM3Ljg5NjAxIDkuODg4MzcgNy43NDk3NyA5LjY4MjEzIDcuNzE0NzcgOS4yNDA5QzcuNjY2MDIgOC42MTIxOSA4LjEyOTc1IDguNDM4NDUgOC4xMjk3NSA4LjQzODQ1QzguMTI5NzUgOC40Mzg0NSA4LjY4MjIyIDguMjkyMiA5LjA3MDk1IDguMTUzNDZDOS40MzIxOCA4LjAyNDcyIDkuNjkyMTcgNy44NTk3MyA5LjY5MjE3IDcuODU5NzNDOS42OTIxNyA3Ljg1OTczIDkuNzI3MTcgNy4xODYwMSA5Ljk1MDkgNi45ODcyN0MxMC4xNzU5IDYuNzg4NTMgMTAuNTcyMSA2LjgwNjAzIDExLjAxMzMgNi44NTcyOEMxMS40NTMzIDYuOTA4NTMgMTIuMTYyIDcuMjAyMjYgMTIuMDU4MyA3LjQ2OTc1QzExLjk1NDUgNy43MzcyMyAxMS44MzMzIDguMDA0NzIgMTEuNDEwOCA4LjI0NzIxQzEwLjk4ODMgOC40ODg0NCAxMC42ODU5IDguNzEzNDMgMTAuNDY5NiA4LjkwMzQyQzEwLjI1MzQgOS4wOTQ2NiA5Ljg1NzE2IDkuMzM0NjUgOS4yMDA5NCA5LjU3NzE0QzguNTQ0NzMgOS44MTk2MiA4LjIzNDc0IDkuOTk0NjEgOC4wMzQ3NiA5LjkzMjEyWiIgZmlsbD0iI0ZGQzg2RSIvPgo8cGF0aCBkPSJNNC4zMTg2NCA0LjMwODYyQzMuOTAzNjYgNC4yMjM2MyAzLjMxNzQ0IDUuMTIyMzMgMy4yMDg3IDUuNDE5ODFDMy4wOTg3IDUuNzE3MyAyLjk4NjIxIDYuNDQyMjYgMi45NzM3MSA2Ljc3MzQ5QzIuOTU3NDYgNy4xODQ3MiAyLjk0OTk2IDcuNTcwOTUgMy4xMjI0NSA3Ljg4NDY4QzMuMjU0OTUgOC4xMjcxNyAzLjU3NjE4IDguMjYzNDEgMy44NjQ5MSA4LjAxMzQzQzQuMTUzNjUgNy43NjM0NCA0LjY1NDg3IDcuMDc4NDggNC43ODczNiA2Ljk4MjIzQzQuOTE5ODYgNi44ODU5OSA1LjMwNzM0IDYuNzU4NDkgNS4xNTQ4NSA2LjEyODUzQzUuMDkyMzUgNS44NzEwNCA0LjQ1ODYzIDUuMzM4NTcgNC40MjczOCA1LjI2NjA3QzQuMzk3MzkgNS4xOTQ4MyA0LjU1MzYzIDQuMzU3MzcgNC4zMTg2NCA0LjMwODYyWiIgZmlsbD0iI0ZGQzg2RSIvPgo8cGF0aCBkPSJNNy4yNTIyNSAzLjE3OTkxQzcuMjY4NSAzLjQyMTE1IDcuNDY1OTkgMy44MDczOCA3LjY3OTczIDQuMDE2MTFDNy45OTk3MSA0LjMyNzM1IDkuNDY3MTMgNS4zNzk3OSA5Ljg0NzEyIDUuMzE5OEMxMC4yMjcxIDUuMjU5OCAxMC42NDA4IDQuNTc3MzMgMTAuNzYyMSA0LjM4NzM0QzEwLjg4MzMgNC4xOTczNiAxMS4xMjQ1IDMuOTI5ODcgMTEuMjE5NSAzLjgwODYzQzExLjMxNDUgMy42ODczOCAxMS41NjQ1IDMuNDExMTUgMTEuMzU4MyAzLjEzNDkxQzExLjE1MDggMi44NTg2OCAxMC42MzMzIDIuNjc3NDQgMTAuMjg4MyAyLjY1MTE5QzkuOTQzMzYgMi42MjQ5NCA5LjQwNzE0IDIuNDAxMiA4Ljk0OTY2IDIuNDE4N0M4LjQ5MjE5IDIuNDM2MiA3LjIwOTc1IDIuNTU3NDQgNy4yNTIyNSAzLjE3OTkxWiIgZmlsbD0iI0ZGQzg2RSIvPgo8cGF0aCBkPSJNMTMuMjkyIDYuMzk4NTNDMTMuMzIwOCA2LjY2MTAyIDEzLjY4OTUgNi43MTg1MSAxMy45ODMyIDYuOTA4NUMxNC4yNzcgNy4wOTg0OSAxNC40NzU3IDcuMjk3MjMgMTQuNjQ4MiA3LjI0NDc0QzE0LjgyMDcgNy4xOTM0OSAxNS4wNTU3IDYuOTE3MjUgMTUuMDAxOSA2LjQzMzUzQzE0LjkzMzIgNS44MTIzMSAxNC41NDQ0IDUuMDE3MzUgMTQuMDAwNyA1LjA0MzZDMTMuNjcyIDUuMDU5ODUgMTMuNzE3IDUuNjMxMDcgMTMuNzE3IDUuNjMxMDdDMTMuNzE3IDUuNjMxMDcgMTMuMjc0NSA2LjI0MzU0IDEzLjI5MiA2LjM5ODUzWiIgZmlsbD0iI0ZGQzg2RSIvPgo8cGF0aCBkPSJNNi4xODM2IDQuNzc4NDVDNi4xODM2IDQuNzc4NDUgNS4wMDQ5MSA0Ljc3MDk1IDUuMDI2MTYgNC44ODg0NEM1LjA0ODY2IDUuMDA1OTQgNS4xNTExNiA1LjM5MzQyIDUuNDU4NjQgNS40MDg0MkM1Ljc2NjEyIDUuNDIzNDEgNi4xNjIzNSA1LjQ1MjE2IDYuMzUyMzQgNS4zOTM0MkM2LjU0MjMzIDUuMzM0NjcgNy4wNjk4MSA0LjkwOTY5IDcuMDY5ODEgNC43MTk3QzcuMDY5ODEgNC41Mjk3MSA2Ljg1NzMyIDQuMjUwOTggNi44NTczMiA0LjI1MDk4TDYuMTgzNiA0Ljc3ODQ1WiIgZmlsbD0iI0NGNzAxRSIvPgo8cGF0aCBkPSJNNy44NjcxNSA2LjkzOTY4QzcuODY3MTUgNi45Mzk2OCA3Ljg1MzQgNi45NDg0MiA3LjgzMzQgNi45NjQ2N0M3Ljc3NDY2IDcuMDEzNDIgNy42NjIxNiA3LjEyNzE3IDcuNzA1OTEgNy4yNTQ2NkM3Ljc2NDY2IDcuNDIzNCA4LjEwODM5IDcuNzM4MzggOC40NDU4NyA3LjczODM4QzguNzgzMzUgNy43MzgzOCA4Ljk2NTg0IDcuNDIzNCA5LjAyNDU5IDcuMjE4NDFDOS4wODMzNCA3LjAxMzQyIDkuMDUzMzQgNi44ODg0MyA5LjA1MzM0IDYuODg4NDNMNy44NjcxNSA2LjkzOTY4WiIgZmlsbD0iI0NGNzAxRSIvPgo8cGF0aCBkPSJNNS4yMzg3MSA3Ljg5OTY5QzUuMjM4NzEgNy44OTk2OSA1LjAzMzcyIDcuOTczNDQgNS4wNDEyMiA4LjEwNDY4QzUuMDQ4NzIgOC4yMzU5MyA1LjA5OTk2IDguNDc4NDEgNS4zNjM3IDguNzM0NjVDNS42Mjk5NCA4Ljk5MzM5IDYuMDE2MTYgOS4wNjQ2MyA2LjM2NzQgOC45Njk2NEM2LjcxODYzIDguODc0NjQgNi45NzQ4NiA4LjYxMDkxIDYuOTQ2MTIgOC4yNTk2N0M2LjkxNzM3IDcuOTA4NDQgNi41Nzk4OSA3LjYzNzIxIDYuNTc5ODkgNy42MzcyMUw1LjIzODcxIDcuODk5NjlaIiBmaWxsPSIjQ0Y3MDFFIi8+CjxwYXRoIGQ9Ik0zLjcyMjU0IDkuODQwOUMzLjcyMjU0IDkuODQwOSAzLjUwMjU1IDkuODkyMTUgMy41MjUwNSAxMC4wMzg0QzMuNTQ3NTUgMTAuMTg0NiAzLjc5NjI5IDEwLjY0NTkgNC4xOTEyNyAxMC41OTQ2QzQuNTg2MjUgMTAuNTQzNCA1LjIwOTk2IDEwLjA4OTYgNS4yMDk5NiA5LjkxMzRDNS4yMDk5NiA5LjczNzE2IDQuODk0OTggOS41MDM0MiA0Ljg5NDk4IDkuNTAzNDJMMy43MjI1NCA5Ljg0MDlaIiBmaWxsPSIjQ0Y3MDFFIi8+CjxwYXRoIGQ9Ik02LjQxNzE3IDExLjY3MTlDNi40MTcxNyAxMS42NzE5IDYuMzM1OTIgMTIuMTQ0NCA2Ljk3MzM5IDEyLjMxNjhDOC4wMzU4NCAxMi42MDMxIDguMTQ1ODMgMTEuOTU4MSA4LjE0NTgzIDExLjk1ODFMNi40MTcxNyAxMS42NzE5WiIgZmlsbD0iI0NGNzAxRSIvPgo8cGF0aCBkPSJNMTAuNTkwOCAxMS44OTA2QzEwLjU5MDggMTEuODkwNiAxMC4zMDU4IDEyLjE3NTYgMTAuNjI5NSAxMi4zOTkzQzEwLjk1MzIgMTIuNjIzMSAxMS4zMjIgMTIuNjk5MyAxMS42Njk1IDEyLjY2MDZDMTIuMDE2OSAxMi42MjE4IDEyLjI1NDQgMTIuMzQ0NCAxMi4yNTQ0IDEyLjM0NDRMMTAuNTkwOCAxMS44OTA2WiIgZmlsbD0iI0NGNzAxRSIvPgo8cGF0aCBkPSJNMTMuMjUwNyA4LjcyNTgzQzEzLjI1MDcgOC43MjU4MyAxMi43MTMzIDguOTk3MDcgMTMuMTI5NSA5LjQ2NTc5QzEzLjQ0NTcgOS44MjMyNyAxNC4xNjk0IDkuNzUwNzggMTQuNDY1NyA5LjU0OTU0QzE0LjY1OTQgOS40MTgyOSAxNC43MTY5IDkuMTg1ODEgMTQuNjQ0NCA5LjA2ODMxQzE0LjU3MDcgOC45NTA4MiAxMy4yNTA3IDguNzI1ODMgMTMuMjUwNyA4LjcyNTgzWiIgZmlsbD0iI0NGNzAxRSIvPgo8cGF0aCBkPSJNMTEuMTcwOCA1LjYyMTAzQzExLjE3MDggNS42MjEwMyAxMS4xNDIgNi4yMzQ3NCAxMS45Njk1IDYuMjE0NzVDMTMuMTI2OSA2LjE4NiAxMi45ODA3IDUuNDY3MjkgMTIuOTgwNyA1LjQ2NzI5TDExLjE3MDggNS42MjEwM1oiIGZpbGw9IiNDRjcwMUUiLz4KPHBhdGggZD0iTTExLjcwNTggNC44NTIyOEMxMS43MDU4IDQuODUyMjggMTEuMTYzMyA1LjE4OTc2IDExLjEwNDYgNS4zMjg1MUMxMS4wNDU5IDUuNDY3MjUgMTAuOTIwOSA1LjkwNzIyIDExLjU1MDggNS44Nzg0OEMxMi4xODA4IDUuODQ5NzMgMTIuODEwOCA1LjgyNzIzIDEzLjAyMzIgNS41NzA5OUMxMy4yMzU3IDUuMzE0NzYgMTMuMTg0NSA1LjA3OTc3IDEzLjA5NyA0Ljg4OTc4QzEzLjAwOTUgNC42OTk3OSAxMi41MTcgMy45ODEwOCAxMi4xMjU4IDMuOTg5ODNDMTEuODY5NiAzLjk5NDgzIDExLjcwNTggNC44NTIyOCAxMS43MDU4IDQuODUyMjhaIiBmaWxsPSIjNTkzMzI5Ii8+CjxwYXRoIGQ9Ik0xMi4xMjMzIDMuOTg3NDRDMTIuMTIzMyAzLjk4NzQ0IDExLjc3OTUgMy45NDM2OSAxMS40NzgzIDQuNDc4NjZDMTEuMTc4MyA1LjAxMzYzIDExLjEwMDggNS4zMzIzNyAxMS4xMDA4IDUuMzMyMzdDMTEuMTAwOCA1LjMzMjM3IDExLjcxMiA1LjMzNDg3IDEyLjA3ODMgNS4yOTExMkMxMi40NDQ1IDUuMjQ3MzcgMTIuNjkzMiA1LjAxOTg4IDEyLjY5MzIgNS4wMTk4OEMxMi42OTMyIDUuMDE5ODggMTIuMzk0NSA0LjA1MzY4IDEyLjEyMzMgMy45ODc0NFoiIGZpbGw9IiM5MjU4NDkiLz4KPHBhdGggZD0iTTcuOTYzMzUgNi40ODU5M0M3Ljk2MzM1IDYuNDg1OTMgNy42NjMzNiA2LjQ2NTkzIDcuNjQwODcgNi41ODIxN0M3LjYxODM3IDYuNjk5NjcgNy42ODQ2MSA2Ljk5ODQgNy43ODcxMSA3LjEyOTY1QzcuODg5NiA3LjI2MDg5IDguMTk3MDkgNy41NTQ2MiA4LjUwNDU3IDcuNDk1ODhDOC44MTIwNSA3LjQzNzEzIDguOTk0NTQgNy4xMzU5IDkuMDQ1NzkgNy4wMzIxNUM5LjA5MjA0IDYuOTM0NjYgOS4xNDIwNCA2LjgyOTY2IDkuMDU0NTQgNi42OTg0MkM4Ljk2NTggNi41NjU5MyA3Ljk2MzM1IDYuNDg1OTMgNy45NjMzNSA2LjQ4NTkzWiIgZmlsbD0iIzU5MzMyOSIvPgo8cGF0aCBkPSJNOC40MDk3MSA2LjAxNzIzQzguMTU0NzMgNS45ODk3MyA3LjUzOTc2IDYuNDc4NDUgNy42NDk3NSA2LjY0NTk1QzcuNzcyMjUgNi44Mjk2OSA3Ljc5MzUgNi43MTM0NCA4LjEzMDk4IDYuODgyMThDOC40MDk3MSA3LjAyMDkzIDguNTMwOTYgNy4xNDk2NyA4LjcwNTk1IDcuMTI3MTdDOC44ODA5NCA3LjEwNDY3IDkuMDg5NjggNi44MTU5NCA5LjA1MzQzIDYuNjk4NDRDOS4wMTcxOCA2LjU4MDk1IDguNzU0NyA2LjA1MzQ4IDguNDA5NzEgNi4wMTcyM1oiIGZpbGw9IiM5MjU4NDkiLz4KPHBhdGggZD0iTTUuNTIzNjYgNC4wMzg2N0w0LjkyMzcgNC40NTM2NEM0LjkyMzcgNC40NTM2NCA0LjkwMTIgNC43MzQ4OCA1LjAyNjE5IDQuODg4NjJDNS4xNTExOCA1LjA0MjM2IDUuMTg3NDMgNS4xMzczNiA1Ljc3MzY1IDUuMTM3MzZDNi4zNTk4NyA1LjEzNzM2IDYuOTA4NTkgNS4wNTczNiA3LjAxMTA5IDQuNjM5ODhDNy4xMTM1OCA0LjIyMjQxIDYuOTMzNTkgMy45NTQ5MiA2LjgyMzYgMy44MDExOEM2LjcxMzYgMy42NDc0NCA1LjUyMzY2IDQuMDM4NjcgNS41MjM2NiA0LjAzODY3WiIgZmlsbD0iIzU5MzMyOSIvPgo8cGF0aCBkPSJNNS41MzEwNCAzLjM4NzQ1QzUuMzk2MDQgMy40NDQ5NCA1LjA3NzMxIDMuODc4NjcgNS4wMDM1NyA0LjA2ODY2QzQuOTI5ODIgNC4yNTg2NSA0LjkxMzU3IDQuNDYxMTQgNC45MjM1NyA0LjQ2MzY0QzUuMDQ0ODEgNC40OTM2NCA1Ljc1ODUzIDQuNTU5ODkgNi4wNzM1MSA0LjUwODY0QzYuMzg4NDkgNC40NTczOSA2LjY0NDczIDQuMzE4NjUgNi43NDcyMiA0LjEzNDkxQzYuODI4NDcgMy45ODg2NyA2LjgyMjIyIDMuNzk0OTMgNi44MjIyMiAzLjc5NDkzQzYuODIyMjIgMy43OTQ5MyA2LjYwODQ4IDMuNDA5OTUgNi4zMTQ3NSAzLjI5MjQ1QzYuMDIyMjYgMy4xNzQ5NiA1LjU4MjI4IDMuMzY0OTUgNS41MzEwNCAzLjM4NzQ1WiIgZmlsbD0iIzkyNTk0OSIvPgo8cGF0aCBkPSJNNS4wMjg1MyA3LjkyNzIxQzUuMDI4NTMgNy45MjcyMSA0Ljk3ODUzIDguMDAzNDYgNS4wNzM1MyA4LjE0OTdDNS4xNjg1MiA4LjI5NTk0IDUuMzU0NzYgOC40NzA5MyA1LjYzMzUgOC41NTIxOEM1LjkxMjIzIDguNjMyMTggNi4xMDk3MiA4Ljc0MjE3IDYuMjkyMjEgOC43MjA5MkM2LjQ3NTk1IDguNjk4NDIgNi44MzQ2OCA4LjU1MjE4IDYuODQ4NDMgOC4wNDcyMUM2Ljg2MzQzIDcuNTQyMjMgNi43NjA5NCA3LjMwNzI1IDYuNTQwOTUgNy4wODcyNkM2LjMyMDk2IDYuODY3MjcgNi4xNjIyMiA2LjkwNDc3IDYuMTYyMjIgNi45MDQ3N0w1LjAyODUzIDcuOTI3MjFaIiBmaWxsPSIjNTkzMzI5Ii8+CjxwYXRoIGQ9Ik01LjA5MjQ2IDcuNDU5NTZDNS4wMTQ5NiA3LjU4OTU1IDQuOTU3NDYgOC4wMTIwMyA1LjA1MjQ2IDguMTA3MDJDNS4xNDc0NSA4LjIwMjAyIDUuNzE4NjcgOC4yMDgyNyA1Ljg2ODY2IDguMTkyMDJDNi4wMTk5MSA4LjE3NTc3IDYuNTEzNjMgNy45NDMyOCA2LjUyMTEzIDcuNzY3MDRDNi41Mjg2MyA3LjU5MDggNi40MzM2MyA3LjM1NzA2IDYuNDMzNjMgNy4yNjk1N0M2LjQzMzYzIDcuMTgyMDcgNi41NDM2MyA2Ljk3NzA4IDYuMjQzNjQgNi45MDMzNEM1Ljk0MzY2IDYuODI5NTkgNS4yNTI0NSA3LjE4ODMyIDUuMDkyNDYgNy40NTk1NloiIGZpbGw9IiM5MjU4NDkiLz4KPHBhdGggZD0iTTQuMTAyNCA5LjM1N0M0LjEwMjQgOS4zNTcgMy42MzExNyA5LjQ3MzI0IDMuNjAxMTggOS42MzQ0OUMzLjU3MjQzIDkuNzk1NzMgMy42Mzk5MiA5Ljk1ODIyIDMuODAxMTYgMTAuMTEyQzMuOTYyNDEgMTAuMjY1NyA0LjA3OTkgMTAuNDI2OSA0LjIwMzY0IDEwLjQxMTlDNC4zMjg2NCAxMC4zOTY5IDQuNjU3MzcgMTAuMTA0NSA0Ljc1OTg2IDEwLjAwOTVDNC44NjIzNiA5LjkxNDQ3IDUuMDA4NiA5Ljc0NTczIDQuOTk0ODUgOS42MDY5OUM0Ljk4MTEgOS40NjgyNCA0LjkyMTExIDkuMzI4MjUgNC43ODk4NiA5LjIxODI2QzQuNjU5ODcgOS4xMDgyNiA0LjEwMjQgOS4zNTcgNC4xMDI0IDkuMzU3WiIgZmlsbD0iIzU5MzMyOSIvPgo8cGF0aCBkPSJNNC4yMTk5IDguOTM5NkMzLjk5OTkxIDguOTQ0NiAzLjgzMTE3IDkuMjEwODMgMy43NTg2NyA5LjMwNTgzQzMuNjg0OTIgOS40MDA4MiAzLjU2MTE4IDkuNjI4MzEgMy42MTI0MyA5LjY2NDU2QzMuNjYzNjggOS43MDA4MSAzLjg3NjE2IDkuNzIzMzEgMy45Nzg2NiA5Ljc4OTU1QzQuMDgxMTUgOS44NTU4IDQuMjM0OSA5Ljk1ODI5IDQuMzUyMzkgOS44NjMzQzQuNDY5ODggOS43NjgzIDQuNTcyMzggOS41MDQ1NyA0LjYzMTEyIDkuNDE3MDdDNC42ODk4NyA5LjMyOTU4IDQuNzkyMzcgOS4yMTk1OCA0Ljc5MjM3IDkuMjE5NThDNC43OTIzNyA5LjIxOTU4IDQuNTcxMTMgOC45MzIxIDQuMjE5OSA4LjkzOTZaIiBmaWxsPSIjOTI1ODQ5Ii8+CjxwYXRoIGQ9Ik02LjkwODQgMTAuNzA0NEM2Ljg2NDY1IDEwLjc1NTYgNi4yNDIxOSAxMS4yNjA2IDYuMjQyMTkgMTEuMjYwNkM2LjI0MjE5IDExLjI2MDYgNi4yNjQ2OSAxMS43MDA2IDYuNDAzNDMgMTEuODAzMUM2LjU0MjE3IDExLjkwNTYgNi45NzQ2NSAxMi4wNTE4IDcuMjUzMzggMTIuMTEwNkM3LjUzMjEyIDEyLjE2OTMgOC4wMjk1OSAxMi4yMzU2IDguMTU0NTkgMTIuMTg0M0M4LjI3OTU4IDEyLjEzMzEgOC40MTA4MiAxMS44NjE4IDguNDE4MzIgMTEuNTEwNkM4LjQyNTgyIDExLjE1OTQgOC4zMDA4MyAxMC44OTY5IDguMTYyMDkgMTAuNzM1NkM4LjAyMzM0IDEwLjU3NDQgNy45MTIxIDEwLjUxNDQgNy45MTIxIDEwLjUxNDRMNi45MDg0IDEwLjcwNDRaIiBmaWxsPSIjNTkzMzI5Ii8+CjxwYXRoIGQ9Ik03LjczNTg4IDExLjM2MzNDNy44MTcxMiAxMS4yNzIgNy45MTIxMiAxMC41MTMzIDcuOTEyMTIgMTAuNTEzM0M3LjkxMjEyIDEwLjUxMzMgNy4zNDg0IDEwLjA4MDggNy4xMTM0MSAxMC4wNTIxQzYuODc4NDIgMTAuMDIzMyA2LjYwMDk0IDEwLjA0NDYgNi41MTk2OSAxMC4xMzIxQzYuNDM5NjkgMTAuMjE5NiA2LjEyNDcxIDEwLjUzNDYgNi4xNTM0NiAxMC43Mzk1QzYuMTgyMjEgMTAuOTQ0NSA2LjI0MDk1IDExLjI1OTUgNi4yNDA5NSAxMS4yNTk1QzYuMjQwOTUgMTEuMjU5NSA3LjU2NzEzIDExLjU1NDUgNy43MzU4OCAxMS4zNjMzWiIgZmlsbD0iIzkyNTg0OSIvPgo8cGF0aCBkPSJNMTAuNDY4NCAxMS43MjQ0QzEwLjQ2ODQgMTEuNzI0NCAxMC4zOTcxIDEyLjE5OTQgMTAuOTE5NiAxMi4yODk0QzExLjQ0MjEgMTIuMzc4MSAxMS44NzU4IDEyLjQ5NjkgMTIuMTAyIDEyLjQ0OTRDMTIuMzI4MyAxMi40MDE5IDEyLjU1MzIgMTIuMDk5NCAxMi41NzcgMTEuODc5NEMxMi42MDA3IDExLjY1OTQgMTIuNjEzMiAxMS4zNDE5IDEyLjU3NyAxMS4yNTU3QzEyLjQ4MiAxMS4wMjcgMTIuMDQyIDEwLjQwMDcgMTEuODI4MyAxMC4zMzQ1QzExLjcwODMgMTAuMjk4MiAxMS41OTQ2IDEwLjMxODIgMTEuNTk0NiAxMC4zMTgyTDEwLjQ2ODQgMTEuNzI0NFoiIGZpbGw9IiM1OTMzMjkiLz4KPHBhdGggZD0iTTEwLjgwNyAxMC44OTJDMTAuNTczMiAxMS4xNTA3IDEwLjQ1MzIgMTEuNTI0NSAxMC40NjU3IDExLjcyMkMxMC40NzgyIDExLjkwODIgMTEuMzY0NCAxMS44MjcgMTEuNDY5NCAxMS43NjMyQzExLjU3NDQgMTEuNjk5NSAxMS44MDMyIDExLjQxNDUgMTEuODEwNyAxMS4zMTk1QzExLjgxODIgMTEuMjI0NSAxMS43NDU3IDEwLjkyNyAxMS43MzMyIDEwLjc0MzNDMTEuNzIwNyAxMC41NTk1IDExLjcxNTcgMTAuMzA5NSAxMS42MTQ0IDEwLjMxNThDMTEuNTEzMiAxMC4zMjIgMTEuMTM5NCAxMC41MjQ1IDEwLjgwNyAxMC44OTJaIiBmaWxsPSIjOTI1ODQ5Ii8+CjxwYXRoIGQ9Ik0xMy44MDQzIDcuNjY3MjRDMTMuODA0MyA3LjY2NzI0IDE0LjAxMDUgNy42OTcyMyAxNC4yNjQzIDcuODQ5NzNDMTQuNTYwNSA4LjAyNzIyIDE0Ljg3MDUgOC40Nzk2OSAxNC44NDY3IDguODQ4NDJDMTQuODIzIDkuMjE3MTUgMTQuNTAxNyA5LjM4MzQgMTQuMTEwNSA5LjM4OTY1QzEzLjcxOTMgOS4zOTU5IDEzLjMzNjggOS4zNzQ2NSAxMy4yNjU2IDkuMTcyMTZDMTMuMTk0MyA4Ljk2OTY3IDEzLjgwNDMgNy42NjcyNCAxMy44MDQzIDcuNjY3MjRaIiBmaWxsPSIjNTkzMzI5Ii8+CjxwYXRoIGQ9Ik0xMy40OTE5IDcuNzkwODVDMTMuNDMwNiA3Ljg3NTg0IDEzLjI3MTkgOC4xMzU4MyAxMy4yMTgxIDguNDY4MzFDMTMuMTY0NCA4LjgwMDc5IDEzLjIwMDYgOS4xODA3NyAxMy4zMTk0IDkuMjQ3MDJDMTMuNDM4MSA5LjMxMjAyIDE0LjA5MTggOC43NDgzIDE0LjEyNjggOC42ODgzQzE0LjE2MzEgOC42MjgzIDEzLjkxODEgNy43MTk2IDEzLjg0MDYgNy42NzgzNUMxMy43NjMxIDcuNjM3MTEgMTMuNTY5NCA3LjY4NDYgMTMuNDkxOSA3Ljc5MDg1WiIgZmlsbD0iIzkyNTg0OSIvPgo8ZGVmcz4KPHJhZGlhbEdyYWRpZW50IGlkPSJwYWludDBfcmFkaWFsXzcxOF84OSIgY3g9IjAiIGN5PSIwIiByPSIxIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgZ3JhZGllbnRUcmFuc2Zvcm09InRyYW5zbGF0ZSg5LjAyMDE5IDguMTk5MjQpIHNjYWxlKDYuNjQ5OSA2LjY0OTkpIj4KPHN0b3Agb2Zmc2V0PSIwLjUwNyIgc3RvcC1jb2xvcj0iI0YyOUY1OCIvPgo8c3RvcCBvZmZzZXQ9IjAuNzE1MSIgc3RvcC1jb2xvcj0iI0YwOUQ1NiIvPgo8c3RvcCBvZmZzZXQ9IjAuODI2MiIgc3RvcC1jb2xvcj0iI0VCOTU1MCIvPgo8c3RvcCBvZmZzZXQ9IjAuOTE0MSIgc3RvcC1jb2xvcj0iI0UxODc0NSIvPgo8c3RvcCBvZmZzZXQ9IjAuOTg5OCIgc3RvcC1jb2xvcj0iI0Q0NzQzNiIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNEMjcxMzMiLz4KPC9yYWRpYWxHcmFkaWVudD4KPC9kZWZzPgo8L3N2Zz4K": dashboardIconPathPrefix + "cookie", + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwLjk3MzIgMTAuMTQ3MUw3LjU2MjA5IDEwLjAxNzFMNi45NTMzNyAxNS44NjE4QzYuOTUzMzcgMTUuODYxOCA3Ljg4ODMyIDE1Ljc5NjggOS4xMjU3NSAxNS43OTY4QzEwLjM2MzIgMTUuNzk2OCAxMS40MDY5IDE1Ljg4NDMgMTEuNDA2OSAxNS44ODQzTDEwLjk3MzIgMTAuMTQ3MVoiIGZpbGw9IiM4QTJFMDgiLz4KPHBhdGggZD0iTTQuMTEyMjYgMTAuOTY0NUwzLjI3NzMxIDEwLjc5MDhDMy4yNzczMSAxMC43OTA4IDMuMDcyMzIgMTIuMTE0NSAyLjg4MzU4IDEyLjkzNDRDMi42OTQ4NCAxMy43NTQ0IDIuMzgxMSAxNC44NjMxIDIuMzgxMSAxNC44NjMxTDQuMzY2IDE1Ljg0OTNDNC4zNjYgMTUuODQ5MyA2LjE2MjE1IDE2LjAwNjcgNi4xOTM0IDE1Ljk0NDJDNi4yMjQ2NSAxNS44ODE4IDcuNTQ4MzMgMTIuODA4MiA3LjU0ODMzIDEyLjgwODJMNy44NjMzMSAxMS42MjU3TDQuMTEyMjYgMTAuOTY0NVoiIGZpbGw9IiNGRjYxMTAiLz4KPHBhdGggZD0iTTEwLjMyMDcgMTEuNjEwOEwxNC40ODA1IDEwLjc0NDZMMTQuNjY5MiAxMS4yNDg0QzE0LjY2OTIgMTEuMjQ4NCAxNC45NDI5IDEyLjM5MzMgMTUuMDk0MiAxMy4wMTMzQzE1LjMzNzkgMTQuMDA4MiAxNS42NTE2IDE0Ljg5OTQgMTUuNjUxNiAxNC44OTk0TDEzLjc4NTUgMTYuMDU0M0wxMS44NjMxIDE1Ljk5MTlMMTAuMzIwNyAxMS42MTA4WiIgZmlsbD0iI0ZGNjExMCIvPgo8cGF0aCBkPSJNMy4yNjczMyAxMC4yOTcxQzMuMjY3MzMgMTAuMzI1OSAzLjI4NzMzIDEwLjUzNDYgMy4yNzczMyAxMC42ODcxQzMuMjY3MzMgMTAuODM5NiAzLjIzODU5IDExLjAyNzEgMy4yMzg1OSAxMS4wMjcxQzMuMjM4NTkgMTEuMDI3MSAzLjM1MjMzIDExLjA1MjEgMy40NTczMiAxMS4xMTk2QzMuNTYyMzIgMTEuMTg3MSAzLjc3MjMxIDExLjQ0NDYgMy43NzIzMSAxMS40NDQ2QzMuNzcyMzEgMTEuNDQ0NiA0LjE4MjI5IDExLjI5MzMgNC40MzEwMiAxMS4zODcxQzQuODEzNSAxMS41MzA4IDQuOTc1OTkgMTEuODg0NSA0Ljk3NTk5IDExLjg4NDVDNC45NzU5OSAxMS44ODQ1IDUuMzY3MjIgMTEuNTIwOCA1Ljg0NTk1IDExLjU5ODNDNi4zMTM0MiAxMS42NzMzIDYuNDk1OTEgMTIuMTE0NSA2LjQ5NTkxIDEyLjExNDVDNi40OTU5MSAxMi4xMTQ1IDYuODQ3MTUgMTEuODMwOCA3LjE1NDYzIDExLjg0N0M3LjUxODM2IDExLjg2NTggNy42NTIxIDEyLjEzMzMgNy42NTIxIDEyLjEzMzNMOC4xOTcwOCAxMS4wMzQ2TDQuMTQ2MDQgMTAuMjk4NEgzLjI2NzMzVjEwLjI5NzFaIiBmaWxsPSIjQUYwRDAzIi8+CjxwYXRoIGQ9Ik0yLjE3NzM3IDE1LjM4MDdMMi4zOTczNiAxNC43OTgyQzIuMzk3MzYgMTQuNzk4MiAzLjM3MjMxIDE1LjI3NTcgNC4wNzg1MiAxNS40ODU3QzQuNzg0NzMgMTUuNjk1NiA1LjYxNzE5IDE1LjcyNDQgNS42MTcxOSAxNS43MjQ0QzUuNjE3MTkgMTUuNzI0NCA1LjYwNzE5IDE1LjM5NDQgNS4zMDA5NSAxNS4xNTU3QzQuOTk0NzIgMTQuOTE2OSA0LjUwOTc1IDE0LjUxNTcgNC41MDk3NSAxNC41MTU3TDUuMTcyMjEgMTQuMjk0NUw1LjU0MDk0IDE0LjE0N0w2LjY5NzEzIDE1LjUyMzJMNi4zMjQ2NSAxNi40NTA2QzYuMzI0NjUgMTYuNDUwNiA1LjUxMjE5IDE2LjU1NTYgNC4yMTM1MSAxNi4yMjE5QzIuOTE0ODMgMTUuODg4MSAyLjE3NzM3IDE1LjM4MDcgMi4xNzczNyAxNS4zODA3WiIgZmlsbD0iI0M5QzlDOSIvPgo8cGF0aCBkPSJNMTAuMjQxOSAxMC44NDJDMTAuMjYwNyAxMC45MTgzIDEwLjI1MTkgMTEuNTIwOCAxMC4yNTE5IDExLjUyMDhMMTAuNjA1NyAxMS45MjJDMTAuNjA1NyAxMS45MjIgMTAuNzk2OSAxMS42NzMyIDExLjIzNjkgMTEuNjkzMkMxMS42NzY5IDExLjcxMiAxMS44NzY5IDExLjkxMzIgMTEuODc2OSAxMS45MTMyQzExLjg3NjkgMTEuOTEzMiAxMi4xNDQzIDExLjU4ODMgMTIuNTA4MSAxMS41MjJDMTIuODcxOCAxMS40NTQ1IDEzLjIyNDMgMTEuNjQ1NyAxMy4yMjQzIDExLjY0NTdDMTMuMjI0MyAxMS42NDU3IDEzLjQ3MyAxMS4xODcgMTMuODM1NSAxMS4wODJDMTQuMTk5MiAxMC45NzcgMTQuNjY5MiAxMS4yNDk1IDE0LjY2OTIgMTEuMjQ5NUwxNC40NzU1IDEwLjA2OTZMMTAuOTExOSAxMC4zMjU4TDEwLjI0MTkgMTAuODQyWiIgZmlsbD0iI0FGMEQwMyIvPgo8cGF0aCBkPSJNMTUuNjEyOSAxNC43Nzk1TDE1Ljg0MTcgMTUuMzYxOUMxNS44NDE3IDE1LjM2MTkgMTUuMTQ0MiAxNS45MDY5IDE0LjI0NTUgMTYuMTU1NkMxMy4zNDY4IDE2LjQwNDQgMTIuMDk2OSAxNi41MDQ0IDEyLjA5NjkgMTYuNTA0NEwxMS43MzE5IDE0LjQ1NTdMMTIuOTY0MyAxNC4wODMzTDEzLjY0MDUgMTQuMzcyQzEzLjY0MDUgMTQuMzcyIDEzLjA0MTggMTQuODc1NyAxMi44NzgxIDE1LjEwNjlDMTIuNTY2OCAxNS41NDQ0IDEyLjY3ODEgMTUuODIxOSAxMi42NzgxIDE1LjgyMTlDMTIuNjc4MSAxNS44MjE5IDEzLjYzMyAxNS43MzU3IDE0LjM2OTIgMTUuNDIwN0MxNS4xMDY3IDE1LjEwMzIgMTUuNjEyOSAxNC43Nzk1IDE1LjYxMjkgMTQuNzc5NVoiIGZpbGw9IiNDOUM5QzkiLz4KPHBhdGggZD0iTTQuOTc3MjMgMTQuNTAxOEM0Ljk3NzIzIDE0LjUwMTggNS40MDA5NiAxNC42OTE3IDUuNjE3MiAxNC44NDU1QzUuODg0NjkgMTUuMDM2NyA2LjE1MjE3IDE1LjMyMyA2LjIzODQyIDE1Ljc4MTdDNi4yOTg0MSAxNi4wOTc5IDYuMzI0NjYgMTYuNDUwNCA2LjMyNDY2IDE2LjQ1MDRDNi4zMjQ2NiAxNi40NTA0IDYuNjIwOSAxNi42NzkxIDcuMjAzMzcgMTUuNzA1NEM3Ljc4NTg0IDE0LjczMTcgOC4wODIwNyAxMy4zODMxIDguMTMwODIgMTIuNDI4MUM4LjE3ODMxIDExLjQ3MzIgOC4xNTk1NyAxMS4yNTMyIDguMTU5NTcgMTEuMjUzMkw3LjI5OTYxIDEyLjY2NjhMNi4wMjg0MyAxMy44MjNMNC45NzcyMyAxNC41MDE4WiIgZmlsbD0iI0Q5MkYwQSIvPgo8cGF0aCBkPSJNOC4xNjcwOSAxMS4yNTgzQzguMTY3MDkgMTEuMjU4MyA4LjI2NzA4IDEyLjEyNTggNy4wOTg0IDEzLjIwMkM2LjM3MjE4IDEzLjg3MDcgNS42NjM0NyAxNC4zMzA3IDUuMTc4NSAxNC41MjA2QzUuMDEyMjYgMTQuNTg1NiA0Ljg2MzUxIDE0LjU1OTQgNC43NTg1MiAxNC41NTgxQzQuNDE2MDQgMTQuNTUzMSA0LjQ3OTc4IDE0LjQ5NDQgNC42NzEwMiAxNC4zOTY5QzQuODk2MDEgMTQuMjgxOSA1LjU0OTczIDE0LjA0MzIgNi43NzM0MSAxMi45MTU3QzcuNzUyMTEgMTIuMDEzMyA3LjgxOTYxIDExLjExMDggNy44MTk2MSAxMS4xMTA4TDguMTY1ODQgMTAuOTIzM1YxMS4yNTgzSDguMTY3MDlaIiBmaWxsPSIjRkZGRUZGIi8+CjxwYXRoIGQ9Ik0xMi4xNDQ0IDE2LjQ5OTJDMTIuMTQ0NCAxNi40OTkyIDExLjcxNDQgMTYuNjUxNyAxMS4wNjQ0IDE1Ljc3M0MxMC40MTQ1IDE0Ljg5NDMgMTAuMjE0NSAxMy4zMzY5IDEwLjE4NTcgMTIuODM5NEMxMC4xNTcgMTIuMzQyIDEwLjE2NyAxMS40MDU4IDEwLjE2NyAxMS40MDU4TDExLjM2MTkgMTIuOTA1N0MxMS4zNjE5IDEyLjkwNTcgMTMuMjE1NiAxNC4zMDA2IDEzLjE4NjggMTQuMzEwNkMxMy4xNTgxIDE0LjMyMDYgMTIuNTgxOCAxNC42ODMxIDEyLjM2NTYgMTUuMDQ2OEMxMS45MjQ0IDE1Ljc4MTggMTIuMTQ0NCAxNi40OTkyIDEyLjE0NDQgMTYuNDk5MloiIGZpbGw9IiNEOTJGMEEiLz4KPHBhdGggZD0iTTEwLjEwMDcgMTAuOTkyMUMxMC4xMDA3IDEwLjk5MjEgMTAuMDgzMiAxMS4zMTk2IDEwLjE0NyAxMS41Njg0QzEwLjI4MDcgMTIuMDkzMyAxMC43NDE5IDEyLjY2MDggMTEuMzQxOSAxMy4xOTMzQzEyLjExNTYgMTMuODgwNyAxMy4wMTQzIDE0LjM1OTUgMTMuMDgwNiAxNC4zNzgyQzEzLjE0OCAxNC4zOTcgMTMuNjI1NSAxNC40MTcgMTMuNjM0MyAxNC4zNjgyQzEzLjYzOCAxNC4zNTIgMTMuNjY2OCAxNC4yODA3IDEzLjUxMDUgMTQuMTY3QzEzLjE4OTMgMTMuOTM1NyAxMi40NzgxIDEzLjUyODMgMTEuODk1NiAxMy4wNjgzQzExLjQ4NDQgMTIuNzQzMyAxMC45NTY5IDEyLjMzOTYgMTAuNDk4MiAxMS41MDcxQzEwLjQxNDQgMTEuMzU1OSAxMC40NDQ0IDEwLjk3MzQgMTAuNDQ0NCAxMC45NzM0TDEwLjEwMDcgMTAuOTkyMVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0zLjc0NjA4IDguMTU5NTlDMy43NDYwOCA4LjE1OTU5IDIuNzUxMTMgOC4yODk1OCAyLjcxOTg4IDguNDMwODJDMi42ODg2MyA4LjU3MzMxIDIuNzk4NjMgMTAuNDUwNyAyLjc5ODYzIDEwLjQ1MDdDMi43OTg2MyAxMC40NTA3IDMuMjE3MzYgMTAuMzczMiAzLjQyODYgMTAuNDE5NUMzLjYzOTgzIDEwLjQ2NTcgNC4wMzczMSAxMC43MjMyIDQuMDM3MzEgMTAuNzIzMkw0LjcwMzUzIDEwLjMxMzJMNS4xODM1IDEwLjk3OTRDNS4xODM1IDEwLjk3OTQgNS41MjIyMyAxMC44MDQ0IDUuOTkwOTYgMTAuODUwN0M2LjQ1OTY5IDEwLjg5NjkgNi43NzQ2NyAxMS4yMTMyIDYuNzc0NjcgMTEuMjEzMkw3LjQyOTYzIDEwLjY3NDVMOC4xNjA4NSAxMS4yNDk0QzguMTYwODUgMTEuMjQ5NCA4LjQ5NDU4IDEwLjg2MDcgOS4xMTQ1NCAxMC44NDk0QzkuNzM0NTEgMTAuODM4MiAxMC4xMDk1IDExLjI5NDQgMTAuMTA5NSAxMS4yOTQ0TDEwLjkyODIgMTAuNTgwN0wxMS43MzU3IDExLjIwMDdDMTEuNzM1NyAxMS4yMDA3IDEyLjA1MTkgMTAuOTQzMiAxMi40MTQ0IDEwLjg2MTlDMTIuNzc2OSAxMC43Nzk0IDEzLjA0NTYgMTAuOTIwNyAxMy4wNDU2IDEwLjkyMDdMMTMuNTYwNiAxMC4wMDgyTDE0LjE0NTUgMTAuNDI5NUMxNC4xNDU1IDEwLjQyOTUgMTQuMzMzIDEwLjIwNyAxNC42MDE4IDEwLjE3MkMxNC44NzA1IDEwLjEzNyAxNS4xOTggMTAuMTI1NyAxNS4xOTggMTAuMTI1N0MxNS4xOTggMTAuMTI1NyAxNS4xNzQyIDguODYyMDUgMTUuMTc0MiA4LjY3NDU2QzE1LjE3NDIgOC40ODcwNyAxNS4xMDQyIDguMTgzMzMgMTUuMDIxNyA4LjEyNDU5QzE0LjkzOTIgOC4wNjU4NCAxNC4yMDMgNy45MDIxIDE0LjIwMyA3LjkwMjFMMy43NDYwOCA4LjE1OTU5WiIgZmlsbD0iI0Q5MkYwQSIvPgo8cGF0aCBkPSJNNS4wNjIyMSA4LjcwNTgxTDQuMDg2MDEgOC45MzIwNUw0LjAzNjAxIDEwLjcyNDVDNC4wMzYwMSAxMC43MjQ1IDQuMzMzNDkgMTAuNjM4MiA0LjYzMDk4IDEwLjY2OTVDNC45Mjg0NiAxMC43MDA3IDUuMTgyMiAxMC45ODE5IDUuMTgyMiAxMC45ODE5QzUuMTgyMiAxMC45ODE5IDUuMTc1OTUgOS44MTcgNS4xODU5NSA5LjYyMDc2QzUuMTk1OTUgOS40MjQ1MiA1LjI3MjIgOS4wNTIwNCA1LjMxMzQ0IDguOTgwOEM1LjM1NDY5IDguOTA4MyA1LjA2MjIxIDguNzA1ODEgNS4wNjIyMSA4LjcwNTgxWiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNNy42MzIwOSA5LjA4NzE2TDYuNzQwODkgOS40MTMzOUM2Ljc0MDg5IDkuNDEzMzkgNi43MTcxNCAxMC4yMzgzIDYuNzM4MzkgMTAuNTY3MUM2Ljc1ODM5IDEwLjg5NTggNi43NzMzOSAxMS4yMTU4IDYuNzczMzkgMTEuMjE1OEM2Ljc3MzM5IDExLjIxNTggNy4xNzk2MiAxMS4wODA4IDcuNTA5NiAxMS4xMDA4QzcuODM4MzMgMTEuMTIwOCA4LjE1OTU2IDExLjI1MiA4LjE1OTU2IDExLjI1Mkw4LjE0NzA3IDkuNDg3MTRMNy42MzIwOSA5LjA4NzE2WiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMTAuMDc5NSA5LjQ1NzAzQzEwLjA3OTUgOS40NTcwMyAxMC4xMTA3IDEwLjEyNTcgMTAuMTIwNyAxMC40NjQ1QzEwLjEzMDcgMTAuODAzMiAxMC4xMDgyIDExLjI5ODIgMTAuMTA4MiAxMS4yOTgyQzEwLjEwODIgMTEuMjk4MiAxMC41OTMyIDExLjA1MDcgMTAuOTc0NCAxMS4wMTk0QzExLjM1NDQgMTAuOTg4MiAxMS43MzQ0IDExLjIwNDQgMTEuNzM0NCAxMS4yMDQ0QzExLjczNDQgMTEuMjA0NCAxMS43MDQ0IDEwLjUwNTcgMTEuNjYzMSA5Ljk5MkMxMS42MzQ0IDkuNjMzMjcgMTEuNTI5NCA5LjMyMzI4IDExLjUyOTQgOS4zMjMyOEwxMC41NTMyIDguNzk5NTZMMTAuMDc5NSA5LjQ1NzAzWiIgZmlsbD0iI0UxRTFFMSIvPgo8cGF0aCBkPSJNMTMuMDQ0MyAxMC45MjMyQzEzLjA0NDMgMTAuOTIzMiAxMy4zNjgxIDEwLjU2NyAxMy42MjU1IDEwLjQ4NDVDMTMuODgzIDEwLjQwMiAxNC4xNDQzIDEwLjQzMiAxNC4xNDQzIDEwLjQzMkMxNC4xNDQzIDEwLjQzMiAxNC4xMjkzIDkuNTkwNzggMTQuMDg4IDkuMjA5NTVDMTQuMDQ2OCA4LjgyOTU3IDE0LjAwOTMgOC42MTA4MyAxNC4wMDkzIDguNjEwODNMMTIuODE0MyA4LjA5OTYxTDEyLjg0ODEgOS4wMzk1NkMxMi44NDgxIDkuMDM5NTYgMTIuOTg5MyA5LjM1MzI5IDEzLjAzMDYgOS44OTgyNkMxMy4wNzA2IDEwLjQ0MzIgMTMuMDQ0MyAxMC45MjMyIDEzLjA0NDMgMTAuOTIzMloiIGZpbGw9IiNFMUUxRTEiLz4KPHBhdGggZD0iTTkuMDUyMDIgNC43MjcyOUw3LjYwOTU5IDUuMzY5NzZDNy42MDk1OSA1LjM2OTc2IDYuMjk4NDEgNi4zNTM0NiA1LjI2MDk3IDcuMDA5NjdDNC4yMjM1MiA3LjY2NTg5IDMuNjI3MyA3LjkxNTg4IDMuNTAzNTYgNy45NjcxMkMzLjMwNjA3IDguMDQ5NjIgMi45ODYwOSA4LjE0NzExIDIuODQ3MzQgOC4yNDIxMUMyLjYxNzM2IDguNDAyMSAyLjcxMzYgOC40NzcxIDIuNzUxMSA4LjQ4OTZDMi43ODg2IDguNTAyMSAzLjkzNjA0IDkuMDE1ODIgNS45Njk2OCA5LjMxNzA1QzguMDAzMzIgOS42MTgyOSAxMC43NjgyIDkuNDk1NzkgMTIuMTg1NiA5LjIyMDgxQzEzLjYwMTggOC45NDU4MiAxNS4wMjE3IDguMTI1ODcgMTUuMDIxNyA4LjEyNTg3QzE1LjAyMTcgOC4xMjU4NyAxNC45MDMgNy45MjcxMyAxNC4xMTY3IDcuNTg1ODlDMTMuMzI5MyA3LjI0NDY2IDEyLjY2MDYgNi44NzcxOCAxMS41MTk0IDYuMTI5NzJDMTAuMzc4MiA1LjM4MjI2IDEwLjEwMzIgNS4wMTQ3OCAxMC4xMDMyIDUuMDE0NzhMOS4wNTIwMiA0LjcyNzI5WiIgZmlsbD0iI0ZGNjExMCIvPgo8cGF0aCBkPSJNNy44MzIwNiA1LjYzMjMyQzcuODMyMDYgNS42MzIzMiA2LjI0NDY1IDcuMjU4NDkgNC4yODk3NSA4LjYwOTY3QzQuMDc2MDEgOC43NTcxNiA0LjA4NjAxIDguOTMyMTUgNC4wODYwMSA4LjkzMjE1QzQuMDg2MDEgOC45MzIxNSA0LjI0MzUgOC45OTIxNSA0LjYzMDk4IDkuMDgyMTRDNS4wMjQ3MSA5LjE3MzM5IDUuMjUwOTUgOS4xOTU4OSA1LjI1MDk1IDkuMTk1ODlDNS4yNTA5NSA5LjE5NTg5IDUuNDI5NjkgOC45OTA5IDUuODEwOTIgOC42MDk2N0M2LjQ5MzM4IDcuOTI3MiA2LjkxNzExIDcuMzg1OTggNy40NTA4MyA2LjY5NDc3QzcuODA0NTYgNi4yMzYwNCA4LjIyNTc5IDUuNjg2MDcgOC4yMjU3OSA1LjY4NjA3TDcuODMyMDYgNS42MzIzMloiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik04LjUxNDU2IDUuODQyMzZDOC41MTQ1NiA1Ljg0MjM2IDcuOTM3MDkgNy4xNzk3OSA3LjY2MjExIDcuNzU3MjZDNy41MzIxMSA4LjAyODUgNy4yNDIxMyA4LjUzMDk3IDYuOTc5NjQgOC45NTA5NUM2Ljc1MDkxIDkuMzE3MTggNi43NDA5MSA5LjQxNDY3IDYuNzQwOTEgOS40MTQ2N0M2Ljc0MDkxIDkuNDE0NjcgNy4yNTQ2MyA5LjQ4ODQyIDcuNTAzMzcgOS40ODg0MkM3Ljc1MjEgOS40ODg0MiA4LjE0NTgzIDkuNDg1OTIgOC4xNDU4MyA5LjQ4NTkyQzguMTQ1ODMgOS40ODU5MiA4LjM0MzMyIDguODk3MiA4LjU1MzMxIDcuNzE3MjZDOC42NzU4IDcuMDMxMDUgOC44NTQ1NCA1LjgyODYxIDguODU0NTQgNS44Mjg2MUw4LjUxNDU2IDUuODQyMzZaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNOS4zMTQ1MSA1Ljg5NDc4QzkuMzE0NTEgNS44OTQ3OCA5LjQ0NDUxIDcuMTgzNDcgOS41ODk1IDcuODYyMThDOS43ODU3NCA4Ljc4MDg4IDEwLjA3ODIgOS40NTU4NSAxMC4wNzgyIDkuNDU1ODVDMTAuMDc4MiA5LjQ1NTg1IDEwLjU3MzIgOS40MzU4NSAxMC44NDgyIDkuNDA5NkMxMS4xMjMyIDkuMzgzMzUgMTEuNTI2OSA5LjMyMjEgMTEuNTI2OSA5LjMyMjFDMTEuNTI2OSA5LjMyMjEgMTEuMDU0NCA4LjU5NDY0IDEwLjU5ODIgNy44NjIxOEMxMC4yOTcgNy4zNzcyMSA5LjczMTk5IDYuMjQ4NTIgOS42NDA3NSA1LjgxNjA0QzkuNTYzMjUgNS40NDM1NiA5LjMxNDUxIDUuODk0NzggOS4zMTQ1MSA1Ljg5NDc4WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTEwLjAyMiA1LjY1ODU0QzEwLjAyMiA1LjY5NzI5IDEwLjQzNyA2LjU4MjI0IDExLjQ2NDQgNy42OTIxOEMxMi4yOTA2IDguNTg0NjMgMTIuODQxOCA5LjA0MzM2IDEyLjg0MTggOS4wNDMzNkMxMi44NDE4IDkuMDQzMzYgMTMuMzEzIDguODk4MzcgMTMuNTI0MyA4LjgyMDg3QzEzLjc3MyA4LjcyOTYzIDE0LjAwOTMgOC42MTA4OCAxNC4wMDkzIDguNjEwODhDMTQuMDA5MyA4LjYxMDg4IDEyLjg5NDMgNy44MzcxNyAxMi4yMzgxIDcuMjk5N0MxMS41ODE5IDYuNzYyMjMgMTAuNjc2OSA2LjAxNDc3IDEwLjQyODIgNS42NzM1NEMxMC4xNzk1IDUuMzMxMDUgMTAuMDIyIDUuNjU4NTQgMTAuMDIyIDUuNjU4NTRaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNOS4wMzgyNiAzLjgzNjIxQzguNzYyMDMgMy44NTYyMSA4LjY3MDc4IDQuMzIxMTkgOC41MjcwNCA0LjQ2NjE4QzguMzgzMyA0LjYwOTkyIDcuNjA4MzQgNS4zNzExMyA3LjYwODM0IDUuMzcxMTNDNy42MDgzNCA1LjM3MTEzIDcuNzQ1ODMgNi4wODk4NSA5LjExMDc2IDYuMDc2MUMxMC4zNTk0IDYuMDYyMzUgMTAuNTk2OSA1LjQ3MTEzIDEwLjU5NjkgNS40NzExM0MxMC41OTY5IDUuNDcxMTMgOS43OTk0NyA0LjYyMzY3IDkuNjE1NzMgNC40MTM2OEM5LjQzMTk5IDQuMjAyNDQgOS40MDU3NCAzLjgwOTk3IDkuMDM4MjYgMy44MzYyMVoiIGZpbGw9IiNEOTJGMEEiLz4KPHBhdGggZD0iTTkuMDg0NTQgMy4yMjg3OEM5LjA4NDU0IDMuMjI4NzggOS4zMjA3OCAzLjE1Mzc4IDkuNjU3MDEgMy4xOTI1M0M5Ljk5OTQ5IDMuMjMyNTMgMTAuMjI3IDMuMzQzNzcgMTAuNTgzMiAzLjMzNjI3QzExLjEyODIgMy4zMjYyNyAxMS41NjY5IDMuMDcwMDMgMTEuNzI0NCAyLjkyNTA0QzExLjg4MTkgMi43ODAwNSAxMi4wODMxIDIuNTQ1MDYgMTEuOTc4MSAyLjQ2NjMyQzExLjg3MzEgMi4zODc1NyAxMS40MjU3IDIuNTA2MzEgMTAuOTI1NyAyLjMwODgzQzEwLjQ4NDUgMi4xMzUwOCAxMC4yNjMyIDEuNjgxMzYgOS42MzcwMSAxLjYwODg2QzkuMDk4MjkgMS41NDc2MiA4Ljk0MDggMS43Njg4NSA4Ljk0MDggMS43Njg4NUw5LjA4NDU0IDMuMjI4NzhaIiBmaWxsPSIjRkY2MTEwIi8+CjxwYXRoIGQ9Ik04Ljg3NDUxIDEuNjExMzNWNC4wNjk5NUw5LjI2ODI0IDMuOTY0OTVMOS4yMjk0OSAxLjYxMTMzSDguODc0NTFaIiBmaWxsPSIjRDkyRjBBIi8+Cjwvc3ZnPgo=": dashboardIconPathPrefix + "circus-tent", + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1Ljg5ODEgMTMuOTM2OEgxMC4zMTcxVjE0LjgwM0gxNS44OTgxVjEzLjkzNjhaIiBmaWxsPSIjQUYwQzFBIi8+CjxwYXRoIGQ9Ik05LjY0NTc1IDE0LjU0OTNWMTUuMjEzTDE2LjQ3OTEgMTUuMjIwNUMxNi40NzkxIDE1LjIyMDUgMTYuNDk0MSAxNC41NzgxIDE2LjQ3MTYgMTQuNTc4MUMxNi40NTA0IDE0LjU3OTMgOS42NDU3NSAxNC41NDkzIDkuNjQ1NzUgMTQuNTQ5M1oiIGZpbGw9IiNEQzBEMkEiLz4KPHBhdGggZD0iTTkuNjQ1NzUgMTMuMzg4Mkw5LjY2MDc1IDE0LjA1OTRDOS42NjA3NSAxNC4wNTk0IDE2LjQ1NzkgMTQuMDk1NiAxNi40NjU0IDE0LjA3NDRDMTYuNDcyOSAxNC4wNTMxIDE2LjQ4MDQgMTMuNDM5NCAxNi40NTA0IDEzLjQyNDRDMTYuNDIxNiAxMy40MDk0IDkuNjQ1NzUgMTMuMzg4MiA5LjY0NTc1IDEzLjM4ODJaIiBmaWxsPSIjREMwRDJBIi8+CjxwYXRoIGQ9Ik0yLjEwOTg2IDYuOTg0NzRDMi4xMDk4NiA2Ljk4NDc0IDIuMTA5ODYgOS4yNDIxMyAyLjEwOTg2IDkuMzE4MzdDMi4xMDk4NiA5LjM5NDYyIDIuMTYyMzYgOS40NjIxMSAyLjI2MTExIDkuNDY5NjFDMi4zNTk4NSA5LjQ3NzExIDMuNjU4NTMgOS40NTQ2MSAzLjczMzUzIDkuNDYyMTFDMy44MDg1MiA5LjQ2OTYxIDMuODkyMjcgOS40MDIxMiAzLjg5MjI3IDkuMjY1ODdDMy44OTIyNyA5LjEyOTYzIDMuODkyMjcgNi45OTM0OSAzLjg5MjI3IDYuOTkzNDlDMy44OTIyNyA2Ljk5MzQ5IDMuNDM5NzkgNi42NTM1MSAzLjM4NjA1IDYuNjYxMDFDMy4zMzIzIDYuNjY4NTEgMi4xMDk4NiA2Ljk4NDc0IDIuMTA5ODYgNi45ODQ3NFoiIGZpbGw9IiM4NTg1ODUiLz4KPHBhdGggZD0iTTIuNjYyMzMgNy41MjQ2NUMyLjU1NzM0IDcuNTM3MTUgMi41MTEwOSA3LjU5OTY0IDIuNTExMDkgNy43MjA4OUMyLjUxMTA5IDcuODQyMTMgMi40OTYwOSA4LjY1NzA5IDIuNDk2MDkgOC43Mzk1OEMyLjQ5NjA5IDguODIyMDggMi41MDM1OSA4LjkwNTgyIDIuNjI0ODQgOC45MTMzMkMyLjc0NjA4IDguOTIwODIgMy4yODk4IDguOTI4MzIgMy4zNTczIDguOTIwODJDMy40MjQ3OSA4LjkxMzMyIDMuNDc4NTQgOC44MzgzMyAzLjQ4NjA0IDguNzM5NThDMy40OTM1NCA4LjY0MDg0IDMuNDg2MDQgNy43NTgzOCAzLjQ4NjA0IDcuNjg5NjRDMy40ODYwNCA3LjU2ODM5IDMuNDAzNTUgNy41MzA5IDMuMjk3MyA3LjUzMDlDMy4xOTEwNiA3LjUzMDkgMi43ODIzMyA3LjUwOTY1IDIuNjYyMzMgNy41MjQ2NVoiIGZpbGw9IiNCMEIwQjAiLz4KPHBhdGggZD0iTTEzLjc0NDQgNy42NTcxTDEzLjc1OTQgNi44Nzk2NEwxMS45MzIgNi44ODcxNEwxMS45MTcgNy42MTk2TDEyLjM2OTUgOC4xMTA4MkgxMi42MzgyQzEyLjYyODIgOC44MzcwNCAxMi41OTU3IDExLjExNTcgMTIuNTk1NyAxMS4xMTU3QzEyLjU5NTcgMTEuMTE1NyAxMi40OTMyIDExLjI2MTkgMTIuMzc3IDExLjM3MTlDMTIuMjI1NyAxMS41MTU2IDExLjk3NyAxMS43MDQ0IDExLjk5OTUgMTIuMTcxOUMxMi4wMjA3IDEyLjYwMTggMTIuNDQ0NSAxMi45MjY4IDEyLjg2MDcgMTIuOTI2OEMxMy4zMDU3IDEyLjkyNjggMTMuNjA4MiAxMi42MDE4IDEzLjYyMzIgMTIuMjkzMUMxMy42MzgyIDExLjk4MzEgMTMuNTE2OSAxMS45NzA2IDEzLjQ0OTQgMTEuOTYwNkMxMy4zNDMyIDExLjk0NTYgMTMuMjYwNyAxMi4wMjA2IDEzLjIzODIgMTIuMTU2OUMxMy4yMTU3IDEyLjI5MzEgMTMuMDc5NCAxMi40NjY4IDEyLjg0NTcgMTIuNDEzMUMxMi42MTIgMTIuMzU5NCAxMi40NjA3IDEyLjEwMzEgMTIuNjQ5NSAxMS44NTQ0QzEyLjc4NDQgMTEuNjc1NiAxMy4wNDE5IDExLjYyMDYgMTMuMDY0NCAxMS40Njk0QzEzLjA4NDQgMTEuMzM5NCAxMy4wNjk0IDguNzk5NTQgMTMuMDY1NyA4LjEwOTU3SDEzLjM0NDRMMTMuNzQ0NCA3LjY1NzFaIiBmaWxsPSIjODU4NTg3Ii8+CjxwYXRoIGQ9Ik0xMS40NTY5IDEzLjI1ODJDMTEuMzEzMSAxMy4zMjQ1IDExLjMzMzEgMTMuNDM4MiAxMS4zMzQ0IDEzLjU1NDVDMTEuMzM5NCAxNC4wNjE5IDExLjM2MzEgMTUuMjE0NCAxMS4zNjMxIDE1LjIxNDRDMTEuMzYzMSAxNS4yMTQ0IDExLjM5NjkgMTUuMzgwNiAxMS42MTU2IDE1LjM4MDZDMTEuODc2OSAxNS4zODA2IDExLjg5NjkgMTUuMjA2OSAxMS44OTY5IDE1LjIwNjlWMTMuNDgyTDEyLjg4MzEgMTIuNjY0NUwxNC4xOTE3IDEzLjYyNTdDMTQuMTkxNyAxMy42MjU3IDE0LjIyOCAxNS4xMTk0IDE0LjIyOCAxNS4yMDU2QzE0LjIyOCAxNS4yOTE5IDE0LjMxNDIgMTUuNDA4MSAxNC41MTY3IDE1LjQwMDZDMTQuNzQ4IDE1LjM5MTkgMTQuNzc2NyAxNS4yMDU2IDE0Ljc3NjcgMTUuMjA1NkMxNC43NzY3IDE1LjIwNTYgMTQuNzY4IDEzLjYwNTcgMTQuNzYwNSAxMy40NzMyQzE0Ljc0OCAxMy4yNzU3IDE0LjQ1MTcgMTMuMjc5NSAxNC40NTE3IDEzLjI3OTVDMTQuNDUxNyAxMy4yNzk1IDEzLjAwOCAxMi4xMzA4IDEyLjg3ODEgMTIuMTI0NUMxMi43MzQzIDEyLjExODMgMTEuNTU2OSAxMy4yMTIgMTEuNDU2OSAxMy4yNTgyWiIgZmlsbD0iI0ZGNTEwRiIvPgo8cGF0aCBkPSJNMTIuMzk0NSA5LjYwMkMxMi4zMDA4IDkuNjE3IDEyLjI3OTUgOS42ODE5OSAxMi4yNzk1IDkuNzY4MjRDMTIuMjc5NSA5Ljg1NDQ4IDEyLjI4MDggMTAuNzExOSAxMi4yODA4IDEwLjc2OTRDMTIuMjgwOCAxMC44MjY5IDEyLjI4MDggMTAuODg0NCAxMi4zNjA4IDEwLjg5MTlDMTIuNDQwOCAxMC44OTk0IDEzLjI2MzIgMTAuODk5NCAxMy4zMDU3IDEwLjg5OTRDMTMuMzcwNyAxMC44OTk0IDEzLjQwNyAxMC44MTk0IDEzLjQwNyAxMC43MTk0QzEzLjQwNyAxMC42MTgyIDEzLjQxMiA5Ljc3NDQ5IDEzLjQwNyA5LjcxNjk5QzEzLjM5OTUgOS42MjMyNSAxMy4zMTMyIDkuNjAyIDEzLjI0MDcgOS41OTQ1QzEzLjE2ODIgOS41ODU3NSAxMi40NTk1IDkuNTkyIDEyLjM5NDUgOS42MDJaIiBmaWxsPSIjRjE5MDFCIi8+CjxwYXRoIGQ9Ik0xLjc3MzY4IDQuNzM2MTZMNi4wMjk3MSAyLjcwNzUyQzYuMDI5NzEgMi43MDc1MiA3LjQ1ODM4IDMuMDI1IDEwLjI0MiAzLjcwODcyQzEzLjQ4MzEgNC41MDQ5MiAxNC4xMzY4IDQuNzk0OTEgMTQuMTM2OCA0Ljc5NDkxTDEzLjE2MDYgNS4wNjM2NUwxMi4zNjY5IDQuNzcxMTZMNi4wNDIyMSAzLjI1NzQ5TDIuNjQ3MzkgNC44MzI0MUwxLjkwMjQyIDQuOTQyNEwxLjc3MzY4IDQuNzM2MTZaIiBmaWxsPSIjODU4NTg1Ii8+CjxwYXRoIGQ9Ik0xNi41NDkyIDYuNjQ0NzdDMTYuNDgxNyA2LjU3MzUyIDE0LjU1OTMgNC42ODg2MiAxNC40NjY4IDQuNjg4NjJDMTQuMzc0MyA0LjY4ODYyIDcuNTEyMTMgNC42ODg2MiA3LjUxMjEzIDQuNjg4NjJDNy41MTIxMyA0LjY4ODYyIDYuMzk4NDQgMy4wMTg3MSA2LjI1NTk1IDIuODEyNDdDNi4wOTU5NiAyLjU3OTk4IDUuOTUyMjIgMi41Njc0OSA1LjgwNzIyIDIuODA2MjJDNS43NDg0OCAyLjkwMzcyIDQuNjc3MjggNC42NzYxMiA0LjY3NzI4IDQuNjc2MTJDNC42NzcyOCA0LjY3NjEyIDEuODUyNDMgNC43MTIzNyAxLjY4OTk0IDQuNjk5ODdDMS41Mjc0NSA0LjY4ODYyIDEuNDgxMiA0Ljc2OTg3IDEuNDgxMiA0LjgzODYyQzEuNDgxMiA0LjkwODYxIDEuNDgxMiA3LjExODUgMS41MTYyIDcuMTQxQzEuNTUxMiA3LjE2MzQ5IDQuMzg2MDUgNy4xNTIyNCA0LjM4NjA1IDcuMTUyMjRMNC4zNjIzIDE1LjU3NjhDNC4zNjIzIDE1LjU3NjggNC4xMDk4MSAxNS41ODE4IDMuOTM2MDcgMTUuNTgxOEMzLjc2MjMzIDE1LjU4MTggMy42NjYwOSAxNS41ODQzIDMuNjY3MzQgMTUuNzE2OEMzLjY2NzM0IDE1Ljc4NjggMy42NjQ4NCAxNi4wODY4IDMuNjY3MzQgMTYuMTkxOEMzLjY3MjM0IDE2LjM5MTggMy42NTEwOSAxNi40OTQzIDMuNzgzNTggMTYuNDkzQzMuODk5ODIgMTYuNDkxOCA3Ljg0NTg3IDE2LjQ5MyA4LjA2NTg1IDE2LjQ5M0M4LjI4NTg0IDE2LjQ5MyA4LjI4NTg0IDE2LjM1NDMgOC4yODU4NCAxNi4yODQzQzguMjg1ODQgMTYuMjE0MyA4LjI4NTg0IDE1Ljg0NDMgOC4yODU4NCAxNS43MjkzQzguMjg1ODQgMTUuNjE0MyA4LjE4MjEgMTUuNTY2OCA4LjEwMDg1IDE1LjU2NjhDOC4wMTk2MSAxNS41NjY4IDcuNjAzMzggMTUuNTY2OCA3LjYwMzM4IDE1LjU2NjhWOS4xNjcxNEM3LjYwMzM4IDkuMTY3MTQgOC4wMzIxIDkuMTkwODkgOC40OTQ1OCA5LjEyMDg5QzguOTU3MDYgOS4wNTA4OSA5LjEwNzA1IDguNTM4NDIgOS4xODgyOSA4LjE2ODQ0QzkuMjY5NTQgNy43OTg0NiA5LjI0NzA0IDcuMTIzNSA5LjI0NzA0IDcuMTIzNUM5LjI0NzA0IDcuMTIzNSAxNi40MDkyIDcuMTQyMjQgMTYuNTYwNCA3LjA4NDc1QzE2LjcxMDQgNy4wMjYgMTYuNzU1NCA2Ljg2NjAxIDE2LjU0OTIgNi42NDQ3N1pNMi4xNjYxNiA1LjI3NjA5SDMuMDAzNjJMMi4xNjYxNiA2LjIxMTA0QzIuMTY2MTYgNS45MjQ4MSAyLjE2NjE2IDUuMjY4NTkgMi4xNjYxNiA1LjI3NjA5Wk0yLjYyMzY0IDYuMzQyMjlMMy4yNjg2MSA1LjYxMTA4TDQuMDQxMDcgNi4zNDIyOUgyLjYyMzY0Wk00LjQyMzU1IDYuMTM2MDVMMy41NTQ4NCA1LjI3NjA5SDQuNDIzNTVWNi4xMzYwNVpNNi44Nzg0MiAxMy4wOTQ0TDUuOTk4NDYgMTMuOTg0NEw1LjExMzUxIDEzLjA5NDRINi44Nzg0MlpNNS4zMzIyNSAxMi42NzgyTDYuMDI0NzEgMTEuOTYzMkw2LjczNTkyIDEyLjY2OTVMNS4zMzIyNSAxMi42NzgyWk01Ljk3MjIxIDguNTkzNDJMNC45NzQ3NyA3LjY1NDcySDYuODkzNDJMNS45NzIyMSA4LjU5MzQyWk03LjEwMjE1IDguMTA4NDRWOS42NTgzNkw2LjMwNTk1IDguOTA4NEw3LjEwMjE1IDguMTA4NDRaTTYuNzkwOTIgMTAuMDY0Nkg1LjE1NzI2TDUuOTY1OTYgOS4yNTA4OEw2Ljc5MDkyIDEwLjA2NDZaTTcuMTAyMTUgMTAuNzkzM1YxMi4zNjU3TDYuMzMwOTUgMTEuNjE0NUw3LjEwMjE1IDEwLjc5MzNaTTYuNzQ4NDIgMTAuNDczM0w1Ljk4NzIxIDExLjMwODNMNS4xNzYwMSAxMC40ODgzTDYuNzQ4NDIgMTAuNDczM1pNNy4xNjU5IDYuNjA0NzdINS40MDIyNEw3LjE2NzE1IDUuNTgyMzNWNi42MDQ3N0g3LjE2NTlaTTYuMDEzNDYgMy40NjM2OUM2LjAxMzQ2IDMuNDYzNjkgNi44MDg0MiA0LjYxNzM4IDYuNzg4NDIgNC42NTYxM0M2Ljc3ODQyIDQuNjc2MTIgNi4zNzM0NCA0LjY2MTEzIDUuOTg4NDYgNC42NTg2M0M1LjYyNTk4IDQuNjU3MzggNS4yODEgNC42Njk4OCA1LjI3NzI1IDQuNjQ2MTNDNS4yNjcyNSA0LjU5ODYzIDYuMDEzNDYgMy40NjM2OSA2LjAxMzQ2IDMuNDYzNjlaTTQuOTY2MDIgNS4yNjczNEg2Ljc1ODQyTDQuOTU4NTIgNi4zNTM1NEw0Ljk2NjAyIDUuMjY3MzRaTTQuODg5NzcgOC4xODg0NEw1LjY0MDk4IDguOTMwOUw0Ljg4OTc3IDkuNjk1ODZWOC4xODg0NFpNNC44ODk3NyAxMC44NDJMNS42NjIyMyAxMS42MjA4TDQuODk0NzcgMTIuNDMwN0w0Ljg4OTc3IDEwLjg0MlpNNC44ODk3NyAxMy41NDU3TDUuNjY1OTggMTQuMzIxOUw0Ljg4OTc3IDE1LjEwNjhWMTMuNTQ1N1pNNS4xMTM1MSAxNS41ODU2TDYuMDAzNDYgMTQuNjU5M0w2LjkxMjE2IDE1LjU2ODFDNi41MDU5NCAxNS41NjY4IDUuNTQ5NzQgMTUuNTc5MyA1LjExMzUxIDE1LjU4NTZaTTcuMTAyMTUgMTUuMDk1Nkw2LjMyOTcgMTQuMzE4MUw3LjEwMjE1IDEzLjUxNDRWMTUuMDk1NlpNNy42MzIxMyA2LjYwNDc3TDcuNjM5NjMgNS40OTM1OEw4LjY5NzA3IDYuNjA3MjdDOC4wNTA4NSA2LjYwNDc3IDcuNjMyMTMgNi42MDQ3NyA3LjYzMjEzIDYuNjA0NzdaTTkuMjU4MjkgNi41MTQ3OEw4LjAwNTg2IDUuMjM2MUwxMC41MjcgNS4yMjg2TDkuMjU4MjkgNi41MTQ3OFpNOS44MjcwMSA2LjYwOTc3TDExLjA0ODIgNS40MTQ4NEwxMi4zMjMxIDYuNjE2MDJDMTEuNDgzMiA2LjYxMzUyIDEwLjU5ODIgNi42MTEwMiA5LjgyNzAxIDYuNjA5NzdaTTExLjQ4MDcgNS4yMjYxTDE0LjE4OTMgNS4yMTg2TDEyLjg1MTkgNi41MjcyOEwxMS40ODA3IDUuMjI2MVpNMTUuNTk4IDYuNjIzNTJDMTUuNTg1NSA2LjYyMzUyIDE0LjY3NjggNi42MjEwMiAxMy40NzgxIDYuNjE4NTJDMTMuNzcwNiA2LjMyMzU0IDE0LjM2OCA1LjcxOTgyIDE0LjYxNDMgNS40NzEwOEMxNC45NDU1IDUuODA3MzIgMTUuNTAwNSA2LjM3MjI5IDE1LjU4OCA2LjQ1OTc4QzE1LjcxNDIgNi41ODQ3NyAxNS42MzY3IDYuNjIzNTIgMTUuNTk4IDYuNjIzNTJaIiBmaWxsPSIjRjM5MTFDIi8+CjxwYXRoIGQ9Ik04LjEzOTY1IDcuMzM0NjZMOC4xNDQ2NSA4LjIzODM3TDguOTM5NjEgOC44MzA4NEM4LjkzOTYxIDguODMwODQgOS4xMjk2IDguNjA5NiA5LjIxNTg0IDguMTE3MTJDOS4yODgzNCA3LjcwNTg5IDkuMjQ4MzQgNy4zMjM0MSA5LjI0ODM0IDcuMzIzNDFDOS4yNDgzNCA3LjMyMzQxIDguMTUyMTUgNy4zMTk2NiA4LjEzOTY1IDcuMzM0NjZaIiBmaWxsPSIjQTdEMEQ2Ii8+CjxwYXRoIGQ9Ik03LjU5MjA4IDcuMDkzNTZDNy41OTIwOCA3LjA5MzU2IDcuNjAyMDggNy45Mjk3NiA3LjU4MjA4IDcuOTM5NzZDNy41NjIwOCA3Ljk0OTc2IDQuMzY2IDcuOTMyMjYgNC4zMDYwMSA3LjkxMjI2QzQuMjQ2MDEgNy44OTIyNiA0LjI0NzI2IDcuMTAzNTYgNC4yNzYwMSA3LjA5MzU2QzQuMzA0NzYgNy4wODM1NiA3LjU5MjA4IDcuMDkzNTYgNy41OTIwOCA3LjA5MzU2WiIgZmlsbD0iI0QxNTExNiIvPgo8L3N2Zz4K": dashboardIconPathPrefix + "crane", + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTguODc5NTQgMS45NDYzM0M1LjU4OTcxIDEuODcxMzQgMS45NjExNSA0LjU5OTk0IDIuMDMxMTUgOS4wMTk3MUMyLjA5MjQgMTIuOTA1OCA1LjAxMjI0IDE1Ljg0NjggOC43OTU3OSAxNS45MDgxQzEyLjU3OTMgMTUuOTY5MyAxNS45MzE3IDEzLjQ2ODIgMTUuOTUxNyA4Ljk5OTcxQzE1Ljk3MjkgNC40OTYyIDEyLjQ5ODEgMi4wMjg4MyA4Ljg3OTU0IDEuOTQ2MzNaIiBmaWxsPSIjRkYyQTIzIi8+CjxwYXRoIGQ9Ik04Ljg4OTUgMy41MjM2MUM2LjI3OTY0IDMuNjEyMzYgMy42NDYwMyA1LjU1OTc1IDMuNTg0NzggOC45OTMzMkMzLjUyMzU0IDEyLjQyNjkgNi41MjQ2MyAxNC4zMzkzIDguODg5NSAxNC4zMzkzQzExLjg5MTggMTQuMzM5MyAxNC40MDA1IDExLjk3NDQgMTQuMzM4IDguODI4MzNDMTQuMjc2NyA1LjY4MzUgMTEuOTExOCAzLjQyMTEyIDguODg5NSAzLjUyMzYxWiIgZmlsbD0iI0ZGRkRGRSIvPgo8cGF0aCBkPSJNOC45NTA3OCA0Ljg2MTEyQzYuNTg1OSA0LjgwNzM4IDQuODM4NSA2Ljg5NzI3IDQuODE3MjUgOC44OTA5MUM0Ljc5NzI1IDEwLjkyNzEgNi4wNzA5MyAxMy4wMDMyIDguOTA5NTMgMTMuMTA1N0MxMS43NDgxIDEzLjIwODIgMTMuMDgzMSAxMC45NDcxIDEzLjEyNDMgOS4wMzQ2NUMxMy4xNjY4IDcuMTIyMjUgMTEuNjQ0NCA0LjkyMjM3IDguOTUwNzggNC44NjExMloiIGZpbGw9IiNGRjJBMjMiLz4KPHBhdGggZD0iTTYuMjQ3MTggOC45NjU4QzYuMjI3MTggMTAuNzU0NSA3LjUwMDg2IDExLjc4MzIgOS4wNDMyOCAxMS43ODMyQzEwLjQ0NDUgMTEuNzgzMiAxMS44MTk0IDEwLjg5OTUgMTEuODE5NCA5LjAyODNDMTEuODE5NCA3LjM0MjE0IDEwLjU0NDUgNi4zMzQ2OSA5LjEyNTc4IDYuMjczNDVDNy43MDcxIDYuMjEwOTUgNi4yNjcxOCA3LjIzOTY0IDYuMjQ3MTggOC45NjU4WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTcuNTEyMDggOC45NTIwOEM3LjQzNTgzIDEwLjAxODMgOC4zMDMyOCAxMC41MjgzIDkuMDg0NDkgMTAuNTI4M0M5LjgyNDQ1IDEwLjUyODMgMTAuNTU0NCAxMC4wNDIgMTAuNTc1NyA5LjA1NDU4QzEwLjU5NDQgOC4xMjcxMyA5Ljg2Njk1IDcuNTYwOTEgOS4wODQ0OSA3LjUzMzQxQzguMjc3MDQgNy41MDU5MSA3LjU3MzMyIDguMDg1ODggNy41MTIwOCA4Ljk1MjA4WiIgZmlsbD0iI0ZCMkIyMiIvPgo8cGF0aCBkPSJNOS4zNDIwMSA5LjAzNTg5TDguOTIzMjggOS4yNjgzOEw5LjcyMTk5IDEwLjQwNDZDOS43MjE5OSAxMC40MDQ2IDEwLjMzOTUgMTAuNDUyMSAxMC40ODE5IDEwLjI5MDhDMTAuNjI0NCAxMC4xMjk2IDEwLjQ2MzIgOS42NTQ2MSAxMC40NjMyIDkuNjU0NjFMOS4zNDIwMSA5LjAzNTg5WiIgZmlsbD0iI0NDMTkzNSIvPgo8cGF0aCBkPSJNMTEuOTI2OCA3LjgwMDk1QzExLjgxNDMgNy41Njg0NiAxMS42OTkzIDcuMzgyMjIgMTEuMzk0NCA3LjQzOTcyQzExLjA5MDYgNy40OTcyMiAxMC41Mjk0IDcuODE5NyAxMC40MjQ0IDcuOTcyMTlDMTAuMzI4MiA4LjExMjE5IDEwLjIzNDQgOC4yODU5MyAxMC4yMzQ0IDguMjg1OTNDMTAuMjM0NCA4LjI4NTkzIDkuMTU5NDcgOC44NzM0IDguOTg5NDggOC45Njk2NEM4LjcyMzI1IDkuMTIyMTMgOC45MDY5OSA5LjQzMjEyIDkuMDk0NDggOS4zOTcxMkM5LjM1MDcxIDkuMzQ5NjIgMTAuNDk1NyA4Ljg4MzQgMTAuNDk1NyA4Ljg4MzRDMTAuNDk1NyA4Ljg4MzQgMTAuNzM4MSA4Ljk0MDg5IDEwLjg4MDYgOC45NDA4OUMxMS4wMjMxIDguOTQwODkgMTEuODM1NiA4LjUzOTY2IDExLjk0NTYgOC40NjU5MkMxMi4xMjY4IDguMzQyMTcgMTIuMDQwNiA4LjAzODQ0IDExLjkyNjggNy44MDA5NVoiIGZpbGw9IiNBQ0IxQjUiLz4KPHBhdGggZD0iTTEwLjQ1OTQgOS42NTIxQzEwLjQ1OTQgOS42NTIxIDEwLjM5OTQgOS44NjQ1OSAxMC4xODA3IDEwLjA4NDZDOS45NTMyMiAxMC4zMTIxIDkuNzE5NDggMTAuNDAyMSA5LjcxOTQ4IDEwLjQwMjFMMTAuNDQwNyAxMS40NTk1QzEwLjQ0MDcgMTEuNDU5NSAxMC45NzY5IDExLjQ5ODMgMTEuMzY2OSAxMS4xMTgzQzExLjc1NjkgMTAuNzM4MyAxMS41ODY5IDEwLjI0MDggMTEuNTg2OSAxMC4yNDA4TDEwLjQ1OTQgOS42NTIxWiIgZmlsbD0iI0M4QzhDOCIvPgo8cGF0aCBkPSJNMTEuMTc0NCAxMi41NzA2TDEwLjQzOTUgMTEuNDU4MkMxMC40Mzk1IDExLjQ1ODIgMTAuNTYxOSAxMS4zOTMyIDEwLjcyMzIgMTEuMjgzMkMxMS4wMTk0IDExLjA4MDcgMTEuMTg0NCAxMC45MTk1IDExLjM5NTcgMTAuNTg5NUMxMS41MTA2IDEwLjQxMiAxMS41ODQ0IDEwLjIzOTUgMTEuNTg0NCAxMC4yMzk1TDEyLjcyMDYgMTAuODAwN0MxMi43MjA2IDEwLjgwMDcgMTIuNjk1NiAxMS42NzgyIDEyLjMyNDQgMTIuMTA1N0MxMS45NTMxIDEyLjUzMzEgMTEuMTc0NCAxMi41NzA2IDExLjE3NDQgMTIuNTcwNloiIGZpbGw9IiNDQzE5MzUiLz4KPHBhdGggZD0iTTExLjE3NDQgMTIuNTcwN0wxMS44MDU2IDEzLjUxNjlDMTEuODA1NiAxMy41MTY5IDEyLjYyOTMgMTMuNTk4MiAxMy4zMDQzIDEyLjc4OTVDMTMuOTc5MiAxMS45ODA3IDEzLjc5OTIgMTEuMzE1OCAxMy43OTkyIDExLjMxNThMMTIuNzIwNSAxMC43OTk2QzEyLjcyMDUgMTAuNzk5NiAxMi41MTA2IDExLjMwNDUgMTIuMDkzMSAxMS43ODk1QzExLjY3NTYgMTIuMjc0NSAxMS4xNzQ0IDEyLjU3MDcgMTEuMTc0NCAxMi41NzA3WiIgZmlsbD0iI0M4QzhDOCIvPgo8cGF0aCBkPSJNMTIuNzQwNSAxNC45MTU3TDExLjgwNjggMTMuNTE4MkMxMS44MDY4IDEzLjUxODIgMTIuNTEwNSAxMy4xMDcgMTMuMDEwNSAxMi41MTU4QzEzLjUyOTIgMTEuOTAyMSAxMy44MDA1IDExLjMxODQgMTMuODAwNSAxMS4zMTg0TDE1LjMxMTYgMTIuMDY5NkMxNS4zMTE2IDEyLjA2OTYgMTQuOTcxNyAxMi45NDgzIDE0LjIxNzkgMTMuNzQyQzEzLjQ4MDUgMTQuNTE1NyAxMi43NDA1IDE0LjkxNTcgMTIuNzQwNSAxNC45MTU3WiIgZmlsbD0iI0NDMTkzNSIvPgo8cGF0aCBkPSJNMTEuMjAwNiA4LjE3MDkzQzExLjM0MTkgOC40MzQ2NiAxMS41NzU2IDguNDM3MTYgMTEuODIzMSA4LjMzMjE3QzEyLjA3MDYgOC4yMjcxNyAxMy4yMzkzIDcuNDAwOTcgMTMuMjM5MyA3LjQwMDk3QzEzLjIzOTMgNy40MDA5NyAxNC44MjY3IDguMTk5NjggMTUuODUyOSA3Ljc5OTdDMTYuODc5MSA3LjQwMDk3IDE2Ljg1MDMgNi4zMTcyOCAxNi44NTAzIDYuMzE3MjhMMTYuMjQyOSA1LjkzMjNDMTYuMjQyOSA1LjkzMjMgMTYuNjg1NCA1LjcwMjMxIDE2LjgyNzggNS42MTczMUMxNi44OTc4IDUuNTc0ODEgMTcuMDAxNiA1LjQxNzMyIDE2Ljk0NDEgNS4yNzYwOEMxNi44OTkxIDUuMTY2MDkgMTYuNzI2NiA1LjEyODU5IDE2LjU4NDEgNS4xODYwOUMxNi40NDE2IDUuMjQzNTggMTYuMDMwNCA1LjM3MjMzIDE2LjAzMDQgNS4zNzIzM0MxNi4wMzA0IDUuMzcyMzMgMTYuMTk0MSA0LjY5MTExIDE2LjE2NTQgNC42NTM2MUMxNi4xMzY2IDQuNjE2MTIgMTUuODQxNiA0LjQ2MzYyIDE1Ljg0MTYgNC40NjM2MkMxNS44NDE2IDQuNDYzNjIgMTQuODgxNyA0LjQ2MzYyIDE0Ljc1OCA0LjUxMTEyQzE0LjYzNDIgNC41NTg2MiAxMy42ODQzIDUuNDk5ODIgMTMuNjg0MyA1LjQ5OTgyTDEzLjIyOCA2LjQzMTAyTDEzLjAzMyA2Ljg0NkMxMy4wMzMgNi44NDYgMTEuNzAxOSA3LjQ0MjIyIDExLjUwMTkgNy41MjcyMUMxMS4zMDMxIDcuNjEzNDYgMTEuMDI2OSA3Ljg0OTY5IDExLjIwMDYgOC4xNzA5M1oiIGZpbGw9IiMwMDZDQTkiLz4KPHBhdGggZD0iTTEzLjMxNjggNi43MzEwOEMxMy4zMTY4IDYuNzMxMDggMTMuODc2OCA2LjMyNjEgMTQuMjc1NSA2LjEyNjExQzE0LjY3NDIgNS45MjYxMiAxNS40MTQyIDUuNjgyMzkgMTUuNjMyOSA1LjY1NjE0QzE1Ljg1MTcgNS42Mjk4OSAxNS45NDE3IDUuNjg4NjQgMTUuOTgwNCA1Ljc5MTEzQzE2LjAxOTIgNS44OTM2MyAxNS44OTA0IDYuMDIyMzcgMTUuNzgwNCA2LjA4MTEyQzE1LjY3MTcgNi4xMzg2MSAxNS4wOTE3IDYuNDAyMzUgMTQuNDQ5MiA2Ljc0MzU4QzEzLjgwNjggNy4wODQ4MSAxMy41MDE4IDcuMzM0OCAxMy4yNDE4IDcuMzkzNTVDMTMuMTEwNiA3LjQyMzU1IDEyLjk0MTggNy4zMDYwNSAxMi45MzE4IDcuMTA5ODFDMTIuOTI0MyA2Ljk1NzMyIDEyLjk1NDMgNi44Mjk4MyAxMy4wNTQzIDYuNTM3MzRDMTMuMTc2OCA2LjE3NzM2IDEzLjQ0NTUgNS42MzYxNCAxMy42NzE4IDUuMjgzNjZDMTQuMDA2OCA0Ljc2MjQ0IDE0LjQ4OTIgNC40MDg3MSAxNS4wMjMgNC4zMTI0NkMxNS41NTY3IDQuMjE2MjIgMTUuOTk0MiA0LjM1MTIxIDE2LjEwNDIgNC40NDEyQzE2LjIxMjkgNC41MzEyIDE2LjIwNjYgNC42MjM2OSAxNi4xNjc5IDQuNjU0OTRDMTYuMTI5MiA0LjY4NjE5IDE2LjA3MTcgNC43MTExOSAxNS43ODE3IDQuNjA4NjlDMTUuNTgwNCA0LjUzNzQ1IDE0LjkzOTIgNC41Mzc0NSAxNC40ODkyIDQuODcyNDNDMTQuMDM0MyA1LjIwOTkxIDEzLjkyMyA1LjQxODY1IDEzLjcyNDMgNS43ODYxM0MxMy41MjU1IDYuMTUzNjEgMTMuMzE2OCA2LjczMTA4IDEzLjMxNjggNi43MzEwOFoiIGZpbGw9IiMxRjg3RkQiLz4KPC9zdmc+Cg==": dashboardIconPathPrefix + "dartboard", + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEuNzY4NzEgMTMuNDI2OUMxLjc2ODcxIDEyLjIyMDggNS4wMDYwNCAxMS41NjA4IDguOTk5NTggMTEuNTYwOEMxMi45OTMxIDExLjU2MDggMTYuMjMwNCAxMi4yMjA4IDE2LjIzMDQgMTMuNDI2OUMxNi4yMzA0IDE1LjEyNDQgMTIuOTkzMSAxNi40OTkzIDguOTk5NTggMTYuNDk5M0M1LjAwNjA0IDE2LjQ5OTMgMS43Njg3MSAxNS4xMjQ0IDEuNzY4NzEgMTMuNDI2OVoiIGZpbGw9IiM5NEQxRTAiLz4KPHBhdGggZD0iTTIuMzkzNjggNy4zMzQ3MlYxMy4wMjgySDIuMzk3NDNDMi40ODExNyAxNC41MDkzIDUuMzg3MjcgMTUuNjk5MyA4Ljk2MjA4IDE1LjY5OTNDMTIuNTM2OSAxNS42OTkzIDE1LjQ0MyAxNC41MDkzIDE1LjUyNjcgMTMuMDI4MkgxNS41MzA1VjcuMzM0NzJIMi4zOTM2OFoiIGZpbGw9IiNFNjNGMzMiLz4KPHBhdGggZD0iTTMuOTI4NTkgMTAuNDU0NEM1LjI4OTc3IDExLjAyMTkgNy4wNzcxOCAxMS4zMzQ0IDguOTYyMDggMTEuMzM0NEMxMC44NDcgMTEuMzM0NCAxMi42Mzk0IDExLjAzMzEgMTMuOTk1NiAxMC40NTQ0QzE1LjIxNDIgOS45MzQ0NSAxNS4zMzggOS41ODk0NyAxNS41MzA1IDkuMzkwNzNWOC42MzQ1MkMxNS4xNzQzIDkuMTA3IDE0LjU3MyA5LjUzMDcyIDEzLjc1MTggOS44NzE5NkMxMi40NjU2IDEwLjQwODIgMTAuNzY0NSAxMC43MDMyIDguOTYwODMgMTAuNzAzMkM3LjE1NzE3IDEwLjcwMzIgNS40NTYwMSAxMC40MDgyIDQuMTY5ODMgOS44NzE5NkMzLjM0OTg3IDkuNTI5NDcgMi43NDg2NiA5LjEwNyAyLjM5MTE3IDguNjM0NTJWOS41NTU3MkwzLjAyMjM5IDEwLjAwMDdDMy4yOTQ4OCAxMC4xNjQ0IDMuNTk2MTEgMTAuMzE1NyAzLjkyODU5IDEwLjQ1NDRaIiBmaWxsPSIjQkRCREJEIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNi4zOTM0NSAxMC4zNDM4QzYuNTM0NzIgMTAuMzQzOCA2LjY0OTI0IDEwLjQ1ODMgNi42NDkyNCAxMC41OTk1VjE1LjY2MDVDNi42NDkyNCAxNS44MDE4IDYuNTM0NzIgMTUuOTE2MyA2LjM5MzQ1IDE1LjkxNjNDNi4yNTIxOCAxNS45MTYzIDYuMTM3NjYgMTUuODAxOCA2LjEzNzY2IDE1LjY2MDVWMTAuNTk5NUM2LjEzNzY2IDEwLjQ1ODMgNi4yNTIxOCAxMC4zNDM4IDYuMzkzNDUgMTAuMzQzOFoiIGZpbGw9IiM5NEQxRTAiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMS41MzA3IDEwLjM0MzhDMTEuNjcyIDEwLjM0MzggMTEuNzg2NSAxMC40NTgzIDExLjc4NjUgMTAuNTk5NVYxNS42NjA1QzExLjc4NjUgMTUuODAxOCAxMS42NzIgMTUuOTE2MyAxMS41MzA3IDE1LjkxNjNDMTEuMzg5NCAxNS45MTYzIDExLjI3NDkgMTUuODAxOCAxMS4yNzQ5IDE1LjY2MDVWMTAuNTk5NUMxMS4yNzQ5IDEwLjQ1ODMgMTEuMzg5NCAxMC4zNDM4IDExLjUzMDcgMTAuMzQzOFoiIGZpbGw9IiM5NEQxRTAiLz4KPHBhdGggZD0iTTYuMjUwOTggMTQuMjU1NkM2LjEzNDc0IDE0LjIxMzEgNi4wMzA5OSAxNC4xMjQ0IDUuOTcxIDE0LjAxNjlDNS44NjQ3NSAxMy44MjU2IDUuODczNSAxMy41ODA2IDUuODY5NzUgMTMuMzM1NkM1Ljg2MjI1IDEyLjgwMTkgNi4wMDk3NCAxMi42Njk0IDYuMTgzNDggMTIuNjY5NEg2LjYwMjIxQzYuOTE1OTUgMTIuNzk4MiA2LjkxNTk1IDEzLjA1NjkgNi45MTU5NSAxMy4yMzA3QzYuOTE1OTUgMTMuNDExOSA2LjkxNTk1IDEzLjM0NjkgNi45MTU5NSAxMy41MjgxQzYuOTE1OTUgMTMuNzkwNiA2LjkyOTY5IDE0LjA1MzEgNi42Nzg0NiAxNC4yMTk0QzYuNjU0NzEgMTQuMjM1NiA2LjQ1OTcyIDE0LjMzMTggNi4yNTA5OCAxNC4yNTU2WiIgZmlsbD0iIzk0RDFFMCIvPgo8cGF0aCBkPSJNMTEuNjY5NCAxNC4yNTU2QzExLjc4MTkgMTQuMjA0NCAxMS44ODA3IDE0LjExODEgMTEuOTQ5NCAxNC4wMTY5QzEyLjA5ODEgMTMuNzk4MSAxMi4wNDY5IDEzLjU4MDYgMTIuMDUwNyAxMy4zMzU2QzEyLjA1ODIgMTIuODMwNyAxMS45MTA3IDEyLjY2OTQgMTEuNzM2OSAxMi42Njk0SDExLjMxODJDMTEuMDA0NSAxMi43OTgyIDExLjAwNDUgMTMuMDU2OSAxMS4wMDQ1IDEzLjIzMDdDMTEuMDA0NSAxMy40MTE5IDExLjAwNDUgMTMuMzQ2OSAxMS4wMDQ1IDEzLjUyODFDMTEuMDA0NSAxMy43OTA2IDExLjAwOTUgMTQuMDI2OSAxMS4yNDE5IDE0LjIxOTRDMTEuMzQ5NCAxNC4zMDgxIDExLjU1NTcgMTQuMzA4MSAxMS42Njk0IDE0LjI1NTZaIiBmaWxsPSIjOTREMUUwIi8+CjxwYXRoIGQ9Ik0xNi4wNDA1IDExLjM4NTZDMTYuMDI4IDEwLjc5NjkgMTUuOTIwNSAxMC42OTU3IDE1LjY0MDUgMTAuNzE5NEMxNS42NDA1IDkuNjM5NDcgMTUuNjQwNSA4LjY2NTc3IDE1LjY0MDUgOC42NjU3N0wxNS4yMjggOC45MjA3NkwxNS4yNTggMTMuODI1NUMxNS4yNTggMTMuODI1NSAxNS42MzkyIDEzLjk0MyAxNS42MzkyIDEzLjUzMThDMTUuNjM5MiAxMy40MTA1IDE1LjYzOTIgMTIuOTM4IDE1LjYzOTIgMTIuMzMxOEMxNi4wNDY3IDEyLjE4OTMgMTYuMDY4IDExLjc1NTYgMTYuMDQwNSAxMS4zODU2WiIgZmlsbD0iIzk0RDFFMCIvPgo8cGF0aCBkPSJNMi42ODYxNSA4Ljg1NzA0TDIuMjczNjcgOC42MDIwNUMyLjI3MzY3IDguNjAyMDUgMi4yNzM2NyA5LjYxNDUgMi4yNzM2NyAxMC43MTk0QzEuOTkzNjkgMTAuNjk1NyAxLjg4NjE5IDEwLjc5NjkgMS44NzM2OSAxMS4zODU3QzEuODczNjkgMTEuNjMwNiAxLjgzMjQ0IDExLjg0ODEgMS45NjExOSAxMi4wNjY5QzIuMDM0OTMgMTIuMTg5NCAyLjE0MTE4IDEyLjI5MzEgMi4yNzM2NyAxMi4zMzE5QzIuMjczNjcgMTIuOTE0MyAyLjI3MzY3IDEzLjM2NTUgMi4yNzM2NyAxMy40ODNDMi4yNzM2NyAxMy44OTQzIDIuNjU0OSAxMy43NzY4IDIuNjU0OSAxMy43NzY4TDIuNjg2MTUgOC44NTcwNFoiIGZpbGw9IiM5NEQxRTAiLz4KPHBhdGggZD0iTTE1Ljg5NDIgNy41NjIxMUMxNS42OTA1IDUuMTU1OTkgMTEuMzQ5NCA0LjQ3NzI3IDguOTYyMDcgNC40ODQ3N0M2LjU5NTk1IDQuNDY0NzcgMi4wNTExOSA1LjE5NTk4IDIuMDMyNDQgNy42NjcxQzAuNzI4NzU5IDkuOTk0NDggNC44NjEwNCAxMS41NTE5IDguOTk5NTcgMTEuNTYxOUMxMy4xNTE5IDExLjU3MTkgMTcuNDQwNCA5LjkxNjk5IDE1Ljg5NDIgNy41NjIxMVoiIGZpbGw9IiM5NEQxRTAiLz4KPHBhdGggZD0iTTguOTYyMDggMTAuMzIzMUMxMi41ODk3IDEwLjMyMzEgMTUuNTMwNSA5LjA5NzU0IDE1LjUzMDUgNy41ODU3NEMxNS41MzA1IDYuMDczOTQgMTIuNTg5NyA0Ljg0ODM5IDguOTYyMDggNC44NDgzOUM1LjMzNDQ1IDQuODQ4MzkgMi4zOTM2OCA2LjA3Mzk0IDIuMzkzNjggNy41ODU3NEMyLjM5MzY4IDkuMDk3NTQgNS4zMzQ0NSAxMC4zMjMxIDguOTYyMDggMTAuMzIzMVoiIGZpbGw9IiNGRkNDODAiLz4KPHBhdGggZD0iTTguOTYyMDUgMTAuMzY1N0MxMi4zMTA4IDEwLjM2NTcgMTUuMDI1NSA5LjIzNDczIDE1LjAyNTUgNy44Mzk1OUMxNS4wMjU1IDYuNDQ0NDYgMTIuMzEwOCA1LjMxMzQ4IDguOTYyMDUgNS4zMTM0OEM1LjYxMzMxIDUuMzEzNDggMi44OTg2MiA2LjQ0NDQ2IDIuODk4NjIgNy44Mzk1OUMyLjg5ODYyIDkuMjM0NzMgNS42MTMzMSAxMC4zNjU3IDguOTYyMDUgMTAuMzY1N1oiIGZpbGw9IiNGRUY2RTAiLz4KPHBhdGggZD0iTTcuNzM4MzkgOS4zODQ1MkwzLjM2OTg3IDYuNDgyMTdMNC4wNTk4NCA2LjE0ODQ0TDcuOTQyMTMgOS4wOTgyOEw3LjczODM5IDkuMzg0NTJaIiBmaWxsPSIjRkZDQzgwIi8+CjxwYXRoIGQ9Ik0xMC4yOTMyIDkuMzg0NTJMMTQuNjYwNSA2LjQ4MjE3TDEzLjk3MTggNi4xNDg0NEwxMC4wODgzIDkuMDk4MjhMMTAuMjkzMiA5LjM4NDUyWiIgZmlsbD0iI0ZGQ0M4MCIvPgo8cGF0aCBkPSJNOC4yMDQ2MiA4LjgzMDg0QzguMDI4MzggOC41OTIxIDcuNzQyMTQgOC41MzgzNiA3LjcwNTg5IDguNDcyMTFMNy41NjM0IDguMjE3MTJDNy41MTQ2NSA4LjEyOTYzIDcuNDc4NCA4LjA3NDYzIDcuNDM4NDEgOC4wMjIxM0wyLjQ4MzY3IDEuNjQyNDdDMi4zNjYxNyAxLjQ4OTk4IDIuMTQ5OTQgMS40NTQ5OCAxLjk4ODY5IDEuNTYyNDhDMS44MDk5NSAxLjY4MzcyIDEuNzczNzEgMS45MzI0NiAxLjkxMTIgMi4wOTg3TDYuOTEzNDMgOC4zOTk2MUM2Ljk3OTY4IDguNDgyMTEgNy4wNTQ2OCA4LjU1NzExIDcuMTM4NDIgOC42MjA4NUw3LjM0ODQxIDguNzgyMDlDNy40MDQ2NiA4LjgyNDU5IDcuMzk0NjYgOS4wOTU4MyA3LjU5MzQgOS4yNzgzMkM3Ljc1ODM5IDkuNDI5NTYgOC4wMTU4OCA5LjQ4NDU2IDguMTc5NjIgOS4zMTIwN0M4LjMwNTg2IDkuMTgwODIgOC4zMDgzNiA4Ljk3MDgzIDguMjA0NjIgOC44MzA4NFoiIGZpbGw9IiNCQTc5M0UiLz4KPHBhdGggZD0iTTkuODUzMjYgOC44MzA4NEMxMC4wMjk1IDguNTkyMSAxMC4zMTU3IDguNTM4MzYgMTAuMzUyIDguNDcyMTFMMTAuNDk0NSA4LjIxNzEyQzEwLjU0MzIgOC4xMjk2MyAxMC41Nzk1IDguMDc0NjMgMTAuNjE5NSA4LjAyMjEzTDE1LjU3NDIgMS42NDI0N0MxNS42OTMgMS40ODk5OCAxNS45MDkyIDEuNDU0OTggMTYuMDY5MiAxLjU2MjQ4QzE2LjI0NzkgMS42ODM3MiAxNi4yODQyIDEuOTMyNDYgMTYuMTQ2NyAyLjA5ODdMMTEuMTQ0NCA4LjQwMDg2QzExLjA3ODIgOC40ODMzNiAxMS4wMDMyIDguNTU4MzYgMTAuOTE5NSA4LjYyMjFMMTAuNzA5NSA4Ljc4MzM0QzEwLjY1MzIgOC44MjU4NCAxMC42NjMyIDkuMDk3MDggMTAuNDY0NSA5LjI3OTU3QzEwLjI5OTUgOS40MzA4MSAxMC4wNDIgOS40ODU4MSA5Ljg3ODI2IDkuMzEzMzJDOS43NTIwMiA5LjE4MDgyIDkuNzQ5NTIgOC45NzA4MyA5Ljg1MzI2IDguODMwODRaIiBmaWxsPSIjQkE3OTNFIi8+CjxwYXRoIGQ9Ik0zLjM4MTA4IDEwLjAyOTVDMi42NTQ4NyA5LjU3ODI0IDIuMzIxMTQgOS4xMjIwMiAyLjM3OTg5IDguOTcwNzhDMi40MTczOCA4Ljg3NDUzIDIuNTU2MTMgOC44NzQ1MyAyLjYyMjM3IDguOTUzMjhDMy4yNzg1OSA5LjcyNTc0IDQuNjIzNTIgMTAuMjY5NSA1LjU5NDcyIDEwLjQ0OTRDNS42OTU5NiAxMC40NjgyIDUuNzczNDYgMTAuNjU2OSA1LjUwMDk3IDEwLjY4MzJDNS4yNjQ3MyAxMC43MDQ0IDQuMzM5NzggMTAuNjI1NyAzLjM4MTA4IDEwLjAyOTVaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTEuNDQ5NCAxNC4wMDE5QzExLjM2NjkgMTQuMDM1NiAxMS4yNzQ0IDEzLjkzNTYgMTEuMjM1NyAxMy44MDQ0QzExLjE5NjkgMTMuNjczMSAxMS4xNzQ0IDEzLjIxNTYgMTEuMTk2OSAxMy4wODE5QzExLjIxODIgMTIuOTQ5NCAxMS4yNDU3IDEyLjg2ODIgMTEuMzQ5NCAxMi44NjgyQzExLjQ1MzIgMTIuODY4MiAxMS41MzY5IDEyLjk1MTkgMTEuNTM2OSAxMy4wNTU3QzExLjUzNjkgMTMuMDU1NyAxMS41MjQ0IDEzLjU1NTYgMTEuNTMwNyAxMy42NzY5QzExLjUzNjkgMTMuODAwNiAxMS41MzE5IDEzLjk2ODEgMTEuNDQ5NCAxNC4wMDE5WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTYuMzA1OTYgMTQuMDAxOUM2LjIyMzQ3IDE0LjAzNTYgNi4xMzA5NyAxMy45MzU2IDYuMDkyMjIgMTMuODA0NEM2LjA1MzQ4IDEzLjY3MzEgNi4wMzA5OCAxMy4yMTU2IDYuMDUzNDggMTMuMDgxOUM2LjA3NTk3IDEyLjk0ODIgNi4xMDIyMiAxMi44NjgyIDYuMjA1OTcgMTIuODY4MkM2LjMwOTcxIDEyLjg2ODIgNi4zOTM0NiAxMi45NTE5IDYuMzkzNDYgMTMuMDU1N0M2LjM5MzQ2IDEzLjA1NTcgNi4zODA5NiAxMy41NTU2IDYuMzg3MjEgMTMuNjc2OUM2LjM5MzQ2IDEzLjgwMDYgNi4zODg0NiAxMy45NjgxIDYuMzA1OTYgMTQuMDAxOVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=": dashboardIconPathPrefix + "drum", + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEuNTM4NjYgMi40MzM5MkMxLjMyNDkyIDIuODA3NjUgMS42NTk5IDMuNjAyNjEgMy4yOTczMSAzLjkyNzU5QzUuMzM1OTYgNC4zMzI1NyA3LjUzMjA5IDMuNTEyNjEgOS4xODgyNSAzLjgwMTM1QzEwLjEzMDcgMy45NjYzNCAxMC43MjgyIDQuMTgwMDggMTEuMDcwNyA0LjMzNTA3QzExLjMzNjkgNC40NTYzMSAxMS4zMzY5IDQuNDY2MzEgMTEuMzM2OSA0LjQ2NjMxTDExLjc1OTQgMy4xMjYzOEMxMS43NTk0IDMuMTI2MzggMTAuNTgzMiAyLjQ4ODkyIDkuMDA1NzYgMi40MTg5MkM3LjQyODM1IDIuMzQ4OTMgNi4yNTU5MSAyLjg2NTE1IDQuNjkyMjQgMi44MjM5QzIuMzM0ODcgMi43NjE0IDEuODczNjQgMS44NDc3IDEuNTM4NjYgMi40MzM5MloiIGZpbGw9IiM5NjY3MzgiLz4KPHBhdGggZD0iTTEzLjc3OTIgNi43MTYxM0wxNS4wNzY3IDYuMTg2MTZDMTUuMDc2NyA2LjE4NjE2IDE1Ljc0OTEgNy4xMjQ4NiAxNS45ODY2IDguOTY3MjZDMTYuMjcwNCAxMS4xNjg0IDE1LjU4MTYgMTIuNjI0NiAxNS44MDU0IDEzLjg2N0MxNi4wMjkxIDE1LjEwOTQgMTYuNTE3OCAxNS43Nzk0IDE2LjQ3NTMgMTYuMDczMUMxNi40MzI4IDE2LjM2NjkgMTYuMjI0MSAxNi42ODY5IDE1Ljk0NTQgMTYuNTM0NEMxNS42NjY2IDE2LjM4MDYgMTQuNzk2NyAxNS4yNjMyIDE0Ljc1OTIgMTMuNzE0NUMxNC43MTY3IDExLjk5NzEgMTUuMTkyOSAxMC41NjQ3IDE0LjgwMTcgOS4wMzg1MUMxNC4zNjY3IDcuMzQ3MzUgMTMuNzc5MiA2LjcxNjEzIDEzLjc3OTIgNi43MTYxM1oiIGZpbGw9IiM5NjY3MzgiLz4KPHBhdGggZD0iTTExLjY1OTQgMy4wNzYyOUwxMS4wOTgxIDQuMzQ4NzNDMTEuMDk4MSA0LjM0ODczIDExLjg0OTQgNC42Nzk5NiAxMi42NzgxIDUuNDM0OTJDMTMuNDAxOCA2LjA5NDg4IDEzLjkyMDUgNi45MDEwOSAxMy45MjA1IDYuOTAxMDlMMTUuMDc5MiA2LjE4ODYzQzE1LjA3OTIgNi4xODg2MyAxNC40NzggNS4xMjM2OSAxMy41OTkzIDQuMzU5OThDMTIuNTUzMSAzLjQ1MDAyIDExLjY1OTQgMy4wNzYyOSAxMS42NTk0IDMuMDc2MjlaIiBmaWxsPSIjRDI5RjZDIi8+CjxwYXRoIGQ9Ik0xMy44MzY4IDMuNjI3NTZMNS4yMTQ3MiAxMi40NTQ2TDUuMjUyMjIgMTIuODY0Nkw1LjYyNDcgMTIuOTIwOEwxNC4zNDA1IDQuMTEyNTRMMTMuODM2OCAzLjYyNzU2WiIgZmlsbD0iI0YzQzk3NiIvPgo8cGF0aCBkPSJNMTQuMjE2NyAzLjIwMjZMMTMuNzY4IDMuNzAzODJMMTQuMjYwNSA0LjE5NjNMMTQuNzgwNSAzLjcyMTMyQzE0Ljc4MDUgMy43MjEzMiAxNS42MDc5IDMuODgwMDYgMTUuNjk2NyAzLjgyNzU3QzE1LjgxNzkgMy43NTUwNyAxNi41NDQxIDEuNTA4OTQgMTYuNTA2NiAxLjQ1MDE5QzE2LjQ0NTQgMS4zNTM5NSAxNS42ODY3IDEuNjU3NjggMTUuMjA0MiAxLjgyODkyQzE0LjYzMTcgMi4wMzE0MSAxNC4xNDggMi4yMjY0IDE0LjEwNDIgMi4yODY0QzE0LjAwNTUgMi40MTg4OSAxNC4yMTY3IDMuMjAyNiAxNC4yMTY3IDMuMjAyNloiIGZpbGw9IiM4NTg1ODUiLz4KPHBhdGggZD0iTTE1LjQxOTIgMTYuMjUzMUMxNS4zOTc5IDE2LjI1MzEgMTUuMzc1NCAxNi4yNTA2IDE1LjM1NDIgMTYuMjQ0NEw0LjkzODQ4IDEzLjQyMkM0Ljg1MzQ5IDEzLjM5ODMgNC43ODU5OSAxMy4zMzIxIDQuNzYzNDkgMTMuMjQ3MUwyLjEwODYzIDMuNjE3NTZDMi4wOTYxMyAzLjU3MTMyIDIuMDk2MTMgMy41MjM4MiAyLjExMTEzIDMuNDc3NTdMMi40MzM2MSAyLjQxMTM4TDIuOTExMDkgMi41NjYzN0wyLjY2MzYgMy43MzAwNkw1LjE4NzIyIDEyLjkxNDZMMTUuMzE5MiAxNS43NDU3TDE2LjA3NDEgMTQuODc0NUwxNi4yNjQxIDE1LjM2NTdMMTUuNjEyOSAxNi4xNjQ0QzE1LjU2MjkgMTYuMjIxOSAxNS40OTI5IDE2LjI1MzEgMTUuNDE5MiAxNi4yNTMxWiIgZmlsbD0iI0I4Q0VENCIvPgo8cGF0aCBkPSJNNS4yMjU5OCAxMi41MjQ1TDUuNjI0NyAxMi45MjA4QzUuNjI0NyAxMi45MjA4IDUuMjAzNDggMTMuNDMyIDUuMTM1OTggMTMuNDM4MkM1LjA5NTk4IDEzLjQ0MiA0Ljc0OTc1IDEzLjE1ODMgNC43NzcyNSAxMy4wOTU4QzQuODA0NzUgMTMuMDM0NSA1LjIyNTk4IDEyLjUyNDUgNS4yMjU5OCAxMi41MjQ1WiIgZmlsbD0iIzVENjI2NSIvPgo8cGF0aCBkPSJNNy40NjIwOCAxMC4yMjFDNy40NTgzMyAxMC4yMzEgNi43OTcxMiAxMC45MjcyIDYuMTg3MTUgMTEuNTcwOUM1LjcxNDY4IDEyLjA2OTcgNS4yNjcyIDEyLjU1MzQgNS4yMzM0NSAxMi41NTM0QzUuMTU3MjEgMTIuNTUzNCA0LjQ2ODQ5IDExLjkwMzQgNC40NTcyNCAxMS43NTIyQzQuNDQ1OTkgMTEuNjAwOSA1LjQ5OTY5IDEwLjY5MjIgNS45OTk2NiAxMC4zMzg1QzcuMDMyMTEgOS42MDcyOCA3LjQ2MjA4IDEwLjIyMSA3LjQ2MjA4IDEwLjIyMVoiIGZpbGw9IiNEQjBEMkEiLz4KPHBhdGggZD0iTTcuODU0NTMgMTAuNjI0NkM3LjgyODI5IDEwLjY0ODQgNy4wNzA4MyAxMS40MjA4IDYuNDQyMTEgMTIuMDYyMUM2LjAxMDg4IDEyLjUwMiA1LjYyODQgMTIuODc4MyA1LjYyMzQgMTIuOTE0NUM1LjYxNzE1IDEyLjk2NTggNi4yMDA4NyAxMy42NDA3IDYuMjYyMTIgMTMuNjQwN0M2LjM3NDYxIDEzLjY0MDcgNy40NzgzIDEyLjQ2MzMgNy44MjA3OSAxMS44Njk2QzguMzI1NzYgMTAuOTk0NiA3Ljg1NDUzIDEwLjYyNDYgNy44NTQ1MyAxMC42MjQ2WiIgZmlsbD0iI0RCMEQyQSIvPgo8L3N2Zz4K": dashboardIconPathPrefix + "bow", + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTcuMDIyMTcgOS41NTU4MUwxLjgzNzQ0IDkuNzQzM0MxLjgzNzQ0IDkuNzQzMyAxLjUwODcxIDkuNDE0NTYgMS41NTYyMSA5LjI5NzA3QzEuNjAzNyA5LjE3OTU4IDUuODk3MjMgNS44MjQ3NSA2LjA2MDk3IDUuODQ4NUM2LjIyNDcxIDUuODcyMjUgNi42MDA5NCA2LjI3MDk4IDYuOTA1OTIgNi4xMjk3NEM3LjIxMDkxIDUuOTg4NDkgNi45MDU5MiA1LjE2NzI5IDYuOTk5NjcgNS4wNTEwNEM3LjA5MzQxIDQuOTMzNTUgMTAuNDQ4MiAzLjEyNzM5IDEwLjY1OTUgMy4wMzM2NUMxMC44NzA3IDIuOTM5OSAxMS4wNTgyIDIuNzI4NjcgMTEuNDgwNyAyLjgyMjQxQzExLjkwMzIgMi45MTYxNiAxMi45NTk0IDMuMTAzNjUgMTQuMTA4IDMuODc4NkMxNS4yNTY3IDQuNjUzNTYgMTYuNDU0MiA1LjU0NDc3IDE2LjM2MDQgNi4wMzcyNEMxNi4yNjY3IDYuNTI5NzIgNy4wMjIxNyA5LjU1NTgxIDcuMDIyMTcgOS41NTU4MVoiIGZpbGw9IiNGRkUyNjUiLz4KPHBhdGggZD0iTTMuNDMyMzMgMTUuNDQ1NUM0LjU4MjI3IDE1LjMwNDIgMTUuMzA0MiAxMy4wMDU2IDE1Ljc1MDQgMTIuOTM1NkMxNi4xOTY3IDEyLjg2NTYgMTYuNDc3OSAxMi40NDMxIDE2LjQ3NzkgMTEuOTczMkMxNi40Nzc5IDExLjUwMzIgMTYuMzYwNCA2LjAzNzIzIDE2LjM2MDQgNi4wMzcyM0MxNi4zNjA0IDYuMDM3MjMgMTUuODQ0MiA2LjIwMDk3IDE1LjIzNDIgNi4zNDIyMkMxNC42MjQyIDYuNDgyMjEgOS42MjU3NiA3LjcwMjE0IDcuNjU1ODYgOC4xNDgzN0M1LjY4NTk2IDguNTk0NiAyLjU0MTEzIDkuNDE1OCAyLjE4ODY1IDkuNDE1OEMxLjgzNjE3IDkuNDE1OCAxLjU1NDkzIDkuMjk4MzEgMS41NTQ5MyA5LjI5ODMxQzEuNTU0OTMgOS4yOTgzMSAxLjY0ODY4IDE0LjE3ODEgMS42NzI0MyAxNC41MzA1QzEuNjk2MTcgMTQuODgzIDEuODU5OTIgMTUuMzA0MiAyLjI1ODY0IDE1LjQyMTdDMi42NTczNyAxNS41MzkyIDIuOTE3MzYgMTUuNTA4IDMuNDMyMzMgMTUuNDQ1NVoiIGZpbGw9IiNGRUI1MDIiLz4KPHBhdGggZD0iTTEwLjM3MDggNC4xOTQ4M0MxMC4zNzA4IDQuNDk0ODIgMTAuNjc1OCA0LjY5MzU2IDExLjE4MDggNC42Nzg1NkMxMS43MzIgNC42NjIzMSAxMS44ODQ1IDQuMzM0ODMgMTEuODY3IDQuMTI0ODRDMTEuODQ5NSAzLjkxMzYgMTEuNTk1OCAzLjcyODYxIDExLjA5MzMgMy43MzczNkMxMC42MjcxIDMuNzQ2MTEgMTAuMzcwOCAzLjkzMTEgMTAuMzcwOCA0LjE5NDgzWiIgZmlsbD0iI0ZGOEIwRCIvPgo8cGF0aCBkPSJNNC43Mzk4OCA3LjkzNDU4QzMuOTI3NDIgOC4yMDgzMiA0LjAxODY3IDguOTE5NTMgNC4yNTYxNiA5LjA3ODI3QzQuNDkzNjQgOS4yMzcwMSA0LjgwOTg4IDkuMjI4MjYgNS4xNzk4NiA5LjA5NTc3QzUuNTQ5ODQgOC45NjMyOCA1Ljc4NDgzIDguODEyMDMgNS44MjIzMiA4LjU2NDU1QzUuODU3MzIgOC4zMjk1NiA1LjU0OTg0IDcuNjYwODQgNC43Mzk4OCA3LjkzNDU4WiIgZmlsbD0iI0VENkIzMSIvPgo8cGF0aCBkPSJNMTIuMTIyIDUuOTAyMjVDMTEuNTc3MSA1LjkyNzI1IDEwLjk3NDYgNi4zMTg0OCAxMC43NDk2IDYuNjk0NzFDMTAuNTEyMSA3LjA5MDk0IDEwLjQ5NDYgNy41NTcxNiAxMS4wMzk2IDcuODQ3MTVDMTEuNTg0NiA4LjEzNzEzIDEzLjUxMiA3LjM5ODQyIDEzLjQxNTcgNi43MjA5NkMxMy4zMTk1IDYuMDQyMjQgMTIuNjk0NSA1Ljg3NiAxMi4xMjIgNS45MDIyNVoiIGZpbGw9IiNFRDZCMzEiLz4KPHBhdGggZD0iTTEwLjIzMDcgMTEuMzgzMkMxMC4yMzA3IDExLjM4MzIgMTEuMDA0NSAxMC44MTA3IDExLjAwNDUgMTAuNDA3QzExLjAwNDUgMTAuMDAyIDEwLjY3MDcgOS4wNDU4MyA5LjI1MzI5IDguOTczMzRDNy45MjQ2MSA4LjkwNDU5IDcuMTA1OTEgMTAuNzE1NyA3Ljk3NzExIDExLjY0ODJDOC44NDgzMiAxMi41ODA2IDkuMzc1NzkgMTIuMTU4MiA5LjcwMjAyIDEyLjA3MDdDMTAuMDI4MyAxMS45ODE5IDEwLjIzMDcgMTEuMzgzMiAxMC4yMzA3IDExLjM4MzJaIiBmaWxsPSIjRUQ2QjMxIi8+CjxwYXRoIGQ9Ik02LjQwMzM5IDEzLjAxMDZDNi40MDMzOSAxMy4wMTA2IDcuMTcyMDkgMTIuNTI5NCA2LjU4MjEzIDEyLjAwMTlDNi4xNjcxNSAxMS42MzA3IDUuNjEyMTggMTEuNzk4MiA1LjMyOTY5IDEyLjA4NjlDNC45Njg0NiAxMi40NTY5IDUuMDY1OTYgMTMuMDQ1NiA1LjM5OTY5IDEzLjMwOTNDNS43MzQ2NyAxMy41NzQzIDYuNDAzMzkgMTMuMDEwNiA2LjQwMzM5IDEzLjAxMDZaIiBmaWxsPSIjRUQ2QjMxIi8+CjxwYXRoIGQ9Ik0zLjc5ODU3IDEwLjg0NjlDMy43OTg1NyAxMC44NDY5IDMuNzExMDggMTAuNTcwNyAzLjM5NzM0IDEwLjQ1OTRDMy4wNDQ4NiAxMC4zMzQ1IDIuNTE4NjQgMTAuNzczMiAyLjc0MzYzIDExLjI0MTlDMi45NDYxMiAxMS42NjQ0IDMuMzk0ODQgMTEuNTQwNiAzLjM5NDg0IDExLjU0MDZMMy43OTg1NyAxMC44NDY5WiIgZmlsbD0iI0VENkIzMSIvPgo8cGF0aCBkPSJNMTUuMDUxOCAxMS4yNTA3QzE1LjEyODEgMTEuMTY4MiAxNS42MjY4IDEwLjgwODMgMTUuNjI2OCAxMC41MzQ1QzE1LjYyNjggMTAuMjYyIDE0Ljg3NTYgOS40MTk1OCAxMy45MTY5IDEwLjA1MzNDMTIuOTU4MiAxMC42ODcgMTMuNDU5NCAxMS42NDU3IDEzLjcyMzIgMTEuODA0NUMxMy45ODY5IDExLjk2MzIgMTQuNzE4MSAxMS42MTIgMTUuMDUxOCAxMS4yNTA3WiIgZmlsbD0iI0VENkIzMSIvPgo8cGF0aCBkPSJNNC44NTczNSA4LjM2NTg1QzQuMzIzNjMgOC41NTA4NCA0LjEyMjM5IDguOTI4MzIgNC4yNTYxMyA5LjA3ODMxQzQuMzk3MzcgOS4yMzcwNSA0LjcwNDg2IDkuMzQyMDUgNS4yNDEwOCA5LjE0ODMxQzUuNjU2MDYgOC45OTgzMSA1Ljg2NjA1IDguNzQ3MDggNS44MzEwNSA4LjUyMzM0QzUuODA2MDUgOC4zNjA4NSA1LjM0MTA4IDguMTk4MzYgNC44NTczNSA4LjM2NTg1WiIgZmlsbD0iI0ZGOEIwRCIvPgo8cGF0aCBkPSJNMy4wNzczNiAxMC44MjgyQzIuODQzNjIgMTEuMTIwNyAzLjAzMzYxIDExLjQ1MzIgMy4yNTM2IDExLjUzMTlDMy40NzM1OSAxMS42MTA3IDMuODYxMDYgMTEuMzczMiAzLjgzNDgyIDEwLjk3ODJDMy44MDczMiAxMC41ODIgMy4yODg2IDEwLjU2NDUgMy4wNzczNiAxMC44MjgyWiIgZmlsbD0iI0ZGOEIwRCIvPgo8cGF0aCBkPSJNNS43OTU5OSAxMi40MjE5QzUuMjkzNTIgMTIuODI5NCA1LjI3OTc3IDEzLjIyODEgNS40NzM1MSAxMy4zNjMxQzUuNjA4NSAxMy40NTgxIDYuMTA1OTcgMTMuNjU1NiA2LjU2MDk1IDEzLjMxOTRDNi44OTg0MyAxMy4wNzA2IDYuOTA0NjggMTIuNTQ4MiA2Ljc5ODQ0IDEyLjI5ODJDNi43MDk2OSAxMi4wODU3IDYuMjE4NDcgMTIuMDc4MiA1Ljc5NTk5IDEyLjQyMTlaIiBmaWxsPSIjRkY4QjBEIi8+CjxwYXRoIGQ9Ik04Ljg0NTgxIDEwLjIwMDdDOC4xNzMzNCAxMC44NTQ0IDguMDEzMzUgMTEuOTE1NiA4LjY3MzMyIDEyLjE2NTZDOS4yNTQ1NCAxMi4zODU2IDEwLjA4OTUgMTIuMjUzMSAxMC41ODIgMTEuNjQ2OUMxMS4wOTk0IDExLjAwOTQgMTEuMTU0NCAxMC4zMDU3IDEwLjgyMiA5Ljg5MDcyQzEwLjQyNDUgOS4zOTIgOS4zNjU3OCA5LjY5Njk4IDguODQ1ODEgMTAuMjAwN1oiIGZpbGw9IiNGRjhCMEQiLz4KPHBhdGggZD0iTTExLjkwMTkgNi41NTM0N0MxMS4wMjIgNi44NTIyMSAxMC41MDMzIDcuNDg1OTIgMTAuOTUyIDcuODAzNDFDMTEuNDAwNyA4LjExOTY0IDExLjg2NjkgOC4wNzMzOSAxMi4zNzY5IDcuODk5NjVDMTIuOTU1NiA3LjcwMzQxIDEzLjM5ODEgNy4yNDg0NCAxMy40MTU2IDYuODE3MjFDMTMuNDI5NCA2LjQ2MzQ4IDEyLjY3NjkgNi4yODk3NCAxMS45MDE5IDYuNTUzNDdaIiBmaWxsPSIjRkY4QjBEIi8+CjxwYXRoIGQ9Ik0xNC4yNTE4IDEwLjU5OTVDMTMuNjUzMSAxMS4wODcgMTMuNDc4MSAxMS41NzU3IDEzLjcyNDMgMTEuODA0NEMxMy45NzA2IDEyLjAzMzIgMTQuNTYwNiAxMi4xMzgyIDE1LjEwNTUgMTEuNzYwN0MxNS42NzE3IDExLjM2ODIgMTUuNzEzIDEwLjkyMzIgMTUuNjUwNSAxMC41NjQ1QzE1LjU4MDUgMTAuMTYwOCAxNC44NjY4IDEwLjA5ODMgMTQuMjUxOCAxMC41OTk1WiIgZmlsbD0iI0ZGOEIwRCIvPgo8L3N2Zz4K": dashboardIconPathPrefix + "cheese", + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTguNzk0NiAxNi4wNDU1QzguNzk0NiAxNi4wNDU1IDEzLjM1MTkgMTUuNTczIDE0LjIxMTggMTQuODIwNUMxNC42NDE4IDE0LjQ0NDMgMTUuMTMwNSAxMy42NDA2IDE1LjQxNTUgMTMuMTY1NkMxNS43MDA1IDEyLjY4OTQgMTUuODg4IDEyLjM0ODIgMTYuMDYwNSAxMS44NzU3QzE2LjQwNDIgMTAuOTI5NSAxNi41NzY3IDguMDkyMTQgMTYuMDYwNSA2LjY3MzQ3QzE1LjU0NDIgNS4yNTQ3OSAxNC44NzggMy45ODYxMSAxMy45NTMxIDMuMjMzNjVDMTMuMDQ0NCAyLjQ5MTE5IDExLjQzODIgMS40ODk5OSA4Ljk4NzA5IDEuNDg5OTlDNy44OTM0IDEuNDg5OTkgNy4wNjQ2OSAxLjcxOTk4IDYuMzY0NzMgMS45NjI0N0M1LjMxNzI4IDIuMzI2MiA0LjcyMjMxIDIuNzIzNjggNC4zMjIzNCAzLjA1ODY2QzMuODQyMzYgMy40NjIzOSAyLjg5MjQxIDQuMjk2MDkgMi4yMzc0NSA1Ljc4OTc2QzEuOTgxMjEgNi4zNzIyMyAxLjY5ODcyIDcuMTg4NDQgMS42MTM3MyA3LjkxODRDMS4zMTI0OSAxMC41MTk1IDEuNzg0OTcgMTIuNzEzMiAzLjI4OTg5IDE0LjI2MDZDNC43OTQ4MSAxNS44MDggNS4zNTM1MyAxNS41OTMgNS4zNTM1MyAxNS41OTNMOC43OTQ2IDE2LjA0NTVaIiBmaWxsPSIjRjQ3QTAwIi8+CjxwYXRoIGQ9Ik00LjkwOTczIDE0LjEwM0M0LjkwOTczIDE0LjEwMyA0LjY3MzUgMTQuMTAzIDQuMzkzNTEgMTMuOTc0M0M0LjExMzUzIDEzLjg0NTYgMy4wODIzMyAxMi40MDQ0IDIuODg4NTkgMTEuODQ1N0MyLjY5NDg1IDExLjI4NjkgMi4yNTM2MyA5LjMxOTU1IDIuNDU4NjEgOC4zODQ2QzIuNTc2MTEgNy44NTA4OCAyLjcxNjEgNy40Mzg0IDIuNzE2MSA3LjQzODRDMi43MTYxIDcuNDM4NCAzLjIzNjA3IDYuMDM1OTcgMy4zMDEwNyA1LjU4NDc1QzMuMzY2MDcgNS4xMzM1MiAzLjk0MjI5IDQuNjIyMyA0LjIyMTAyIDQuMjk5ODFDNC40OTk3NiAzLjk3NzMzIDQuNjkzNSAzLjY1NDg1IDUuMjA5NzIgMy4zNTM2MUM1LjcyNTk0IDMuMDUyMzggNi4yNjM0MSAzLjA1MjM4IDYuNjA3MTUgMi44ODExNEM2Ljk1MDg4IDIuNzA4NjUgNy40NjA4NSAyLjM5NzQxIDcuOTYyMDcgMi4yNTc0MkM4LjkxMzI3IDEuOTkxMTkgOS40ODgyNCAyLjI3ODY3IDEwLjAwNDUgMi4yNTc0MkMxMC41MjA3IDIuMjM2MTcgMTEuMDA5NCAyLjM5ODY2IDExLjY0MzEgMi43Mzg2NUMxMi4wOTQ0IDIuOTgxMTMgMTIuNDQzMSAzLjI1OTg3IDEyLjg1MDYgMy41ODIzNUMxMy4yNTggMy45MDQ4MyAxMy44MjQzIDQuMDk4NTcgMTQuMjM5MiA0LjYyMTA1QzE0LjYzNjcgNS4xMjEwMiAxNC43MTE3IDUuNjk1OTkgMTQuOTQ5MiA2LjM0MDk2QzE1LjE4NTQgNi45ODU5MiAxNS40NDU0IDcuMzI1OSAxNS40ODkyIDcuNzU1ODhDMTUuNTMxNyA4LjE4NTg2IDE1LjQ5NDIgOC41MjMzNCAxNS41MTU0IDkuMDE3MDdDMTUuNTM2NyA5LjUxMDc5IDE1LjY0NjcgOS44ODA3NyAxNS42MjE3IDEwLjE2ODNDMTUuNTY5MiAxMC43ODgyIDE1LjMzMTcgMTAuOTI0NSAxNS4yNjc5IDExLjM1NDRDMTUuMjAyOSAxMS43ODQ0IDE0LjkyOCAxMi4yODY5IDE0Ljc4OTIgMTIuNTYwNkMxNC40MTQyIDEzLjMwMTggMTMuNzI1NSAxMy43MDMxIDEzLjQzNTUgMTMuODk1NkMxMi45MzE4IDE0LjIzNDMgNC45MDk3MyAxNC4xMDMgNC45MDk3MyAxNC4xMDNaIiBmaWxsPSIjRkZBNzI2Ii8+CjxwYXRoIGQ9Ik0zLjEzOTc2IDguNzQ0NTJDMi42OTYwMyA4LjY4NTc3IDIuMjkxMDUgOS4xMzk0OSAyLjMxOTggOS42NzMyMkMyLjM0OTggMTAuMjA2OSAyLjcwNDc4IDEwLjM4NDQgMy4wODk3NiAxMC4yODU3QzMuNTc3MjQgMTAuMTYwNyAzLjc1MDk4IDkuNzEzMjEgMy43NDA5OCA5LjM4Njk4QzMuNzMwOTggOS4wNjA3NSAzLjU4MjI0IDguODAzMjYgMy4xMzk3NiA4Ljc0NDUyWiIgZmlsbD0iI0YyN0IwMCIvPgo8cGF0aCBkPSJNNS45NTg0OCA1LjQxODQ1QzUuNTY5NzUgNS4xMDM0NyA1LjcxMjI1IDQuNjc0NzQgNS45MjIyNCA0LjQwMjI1QzYuMTMyMjIgNC4xMjk3NyA2LjcxMDk0IDQuMDAzNTMgNi45NTIxOCA0LjQyMzVDNy4xOTM0MiA0Ljg0MzQ4IDcuMDEzNDMgNS4xMzU5NyA2LjgyMjE5IDUuMzM1OTZDNi42MzQ3IDUuNTMzNDQgNi4yMTcyMiA1LjYyODQ0IDUuOTU4NDggNS40MTg0NVoiIGZpbGw9IiNGMjdCMDAiLz4KPHBhdGggZD0iTTcuNjAzNDEgNC4yNzg1NEM3Ljc3MDkgNC42MTQ3NyA4LjI0NDYyIDQuNjczNTIgOC41NjA4NiA0LjQ5NjAzQzguODc3MDkgNC4zMTg1NCA4Ljk4NTgzIDMuOTcyMzEgOC43OTgzNCAzLjY1NzMyQzguNjEzMzUgMy4zNDYwOSA4LjEzNzEzIDMuMjAzNiA3Ljc5MDkgMy40Nzk4M0M3LjQ0NTkyIDMuNzU0ODIgNy41MDQ2NiA0LjA4MTA1IDcuNjAzNDEgNC4yNzg1NFoiIGZpbGw9IiNGMjdCMDAiLz4KPHBhdGggZD0iTTkuNTQxODYgNS41MDcyMkM5LjY5NTYxIDUuODYyMiAxMC4xMzMxIDUuOTE5NyAxMC40NzE4IDUuNzUwOTZDMTAuODEwNSA1LjU4MjIyIDExLjA0OTMgNS4xMzA5OSAxMC44NDY4IDQuNzgyMjZDMTAuNjQzMSA0LjQzNDc4IDEwLjE4NjggNC40MDM1MyA5Ljg4MzEgNC42MTM1MkM5LjU3OTM2IDQuODIzNTEgOS4zNzA2MiA1LjExMjI0IDkuNTQxODYgNS41MDcyMloiIGZpbGw9IiNGMjdCMDAiLz4KPHBhdGggZD0iTTExLjM5NyA0LjU5MzUyQzExLjY4MDcgNC44MjEwMSAxMi4xNzU3IDQuNjg5NzcgMTIuMzU2OSA0LjM1NDc4QzEyLjQ5NTYgNC4wOTg1NSAxMi41NzE5IDMuODAzNTYgMTIuMjg1NyAzLjUxNzMzQzExLjk5OTQgMy4yMzEwOSAxMS41MzE5IDMuMzU4NTkgMTEuMzE3IDMuNjYxMDdDMTEuMTQ4MiAzLjkwMTA2IDExLjEwMDcgNC4zNTcyOCAxMS4zOTcgNC41OTM1MloiIGZpbGw9IiNGMjdCMDAiLz4KPHBhdGggZD0iTTQuNDc3MjIgNi4yNzM0QzQuMjY0NzMgNi4yODg0IDQuMjk1OTggNi43MjQ2MyA0LjI4NTk4IDcuMzQ3MUM0LjI3NTk4IDcuOTQyMDcgNC4xNTg0OSA5LjE5MDc1IDQuMjI0NzQgMTAuNTMwN0M0LjI1MjIzIDExLjA5NDQgNC4xOTM0OSAxMi42ODQzIDQuMjQ3MjMgMTMuNDgzQzQuMzAyMjMgMTQuMjgzIDQuMjY5NzMgMTUuMTc2NyA0LjI2OTczIDE1LjE3NjdDNC4yNjk3MyAxNS4xNzY3IDUuNTUwOTIgMTYuNTA0MSA4Ljk5NDQ4IDE2LjU1NDFDMTIuNjIxOCAxNi42MDY2IDE0LjEwMTcgMTQuODcxNyAxNC4xMDE3IDE0Ljg3MTdDMTQuMTAxNyAxNC44NzE3IDE0LjA2MTcgMTIuNjgzMSAxNC4wNjU1IDExLjgyMTlDMTQuMDcwNSAxMC41NDgyIDEzLjk5MTcgOC4xMjU4MSAxMy45OTE3IDcuNzM0NThDMTMuOTkxNyA3LjM0MzM1IDE0LjAyNDIgNi40MjA5IDEzLjg4MyA2LjI5NTlDMTMuNzQxNyA2LjE3MDkxIDEyLjczOCA2LjIyNzE2IDExLjgyMzEgNi4yNDk2NkMxMS4wMDQ0IDYuMjY5NjUgOC4xNDgyOCA2LjI3NzE1IDcuMDk4MzMgNi4yNzcxNUM2LjA0ODM5IDYuMjc3MTUgNC45MTU5NSA2LjI0MjE2IDQuNDc3MjIgNi4yNzM0WiIgZmlsbD0iIzNDM0MzQyIvPgo8cGF0aCBkPSJNNC45Njk3NCA3LjEyMzQzQzQuODM1OTkgNy4yMDQ2NyA0LjgxMzQ5IDguMjgyMTIgNC44NDQ3NCA5LjI4NTgxQzQuODc1OTkgMTAuMjg5NSA0Ljg3NTk5IDExLjI0NDUgNS4yMDQ3MiAxMS4zMDdDNS41MzM0NiAxMS4zNjk1IDUuODYyMTkgMTAuNjMzMiA2LjIwNzE3IDEwLjM1MDhDNi41NTIxNSAxMC4wNjgzIDYuOTU5NjMgOS44NDk1MyA3LjIyNTg3IDkuNDU4M0M3LjQ5MjEgOS4wNjcwNyA3Ljc4OTU5IDguNDM5NjEgNy45MzA4MyA4LjEyNzEyQzguMDcyMDcgNy44MTMzOSA4LjUxMDggNy4zOTA5MSA4LjQ2MzMgNy4xMzk2OEM4LjQxNTggNi44ODg0NCA2Ljg4MDg4IDYuOTgzNDMgNi40MTA5MSA2Ljk4MzQzQzUuOTQwOTMgNi45ODM0MyA1LjI1MjIyIDYuOTUyMTkgNC45Njk3NCA3LjEyMzQzWiIgZmlsbD0iIzg1ODU4NSIvPgo8cGF0aCBkPSJNOS4zNDA4MyA2Ljk2NzE3QzkuMTQyMDkgNy4xMDg0MSA4LjkzMzM1IDcuNjU1ODggOC43OTIxMSA3LjkwNzEyQzguNjUwODcgOC4xNTgzNiA4LjI1OTY0IDguNzA1ODMgOC41NTcxMiA4Ljg5NDU3QzguODU0NjEgOS4wODIwNiA5LjQ0OTU3IDguNjQzMzMgOS42MzgzMSA4LjUxODM0QzkuODI1OCA4LjM5MzM1IDEwLjA3NyA4LjMzMDg1IDEwLjE3MDggOC4yNTIxQzEwLjI2NDUgOC4xNzMzNiAxMC43MzQ1IDcuNDg0NjQgMTAuODQ0NSA3LjM3NDY1QzEwLjk1NDUgNy4yNjQ2NiAxMS4xODk1IDcuMTM5NjYgMTEuMTg5NSA3LjAxNDY3QzExLjE4OTUgNi44ODk2NyAxMC42MDk1IDYuODg5NjggMTAuMDkzMyA2LjkwNDY3QzkuNTc3MDcgNi45MTk2NyA5LjQ1MDgyIDYuODg4NDMgOS4zNDA4MyA2Ljk2NzE3WiIgZmlsbD0iIzg1ODU4NSIvPgo8cGF0aCBkPSJNNy41Mzk0NyAxMC4xNDdDNy4zOTQ0OCAxMC4zNDQ1IDcuMTQ4MjQgMTAuNzczMyA3LjQyOTQ4IDEwLjk3N0M3LjcxMTk2IDExLjE4MDcgOC4wMjQ0NSAxMC45NjA3IDguMjQ0NDQgMTAuNzI1OEM4LjQ2NDQyIDEwLjQ5MDggOC41NzMxNyAxMC4xNzcgOC4zMDY5MyA5Ljg5NThDOC4wNDA3IDkuNjE0NTYgNy43MTA3MSA5LjkxMjA1IDcuNTM5NDcgMTAuMTQ3WiIgZmlsbD0iIzg1ODU4NSIvPgo8cGF0aCBkPSJNNi41OTk3IDExLjI3NTdDNi40NDM0NiAxMS4yNjMyIDYuMTEzNDcgMTEuNzYyIDUuODE1OTkgMTEuODg3QzUuNTE4NTEgMTIuMDEyIDUuMDYzNTMgMTEuOTM0NSA0Ljk4NjAzIDEyLjIxNTdDNC45MDcyOSAxMi40OTgyIDQuNzM0OCAxMy42ODgxIDQuOTg2MDMgMTMuODI5NEM1LjIzNzI3IDEzLjk3MDYgNS45MjU5OCAxMy43MzU2IDYuMDgyMjMgMTMuNjQxOUM2LjIzODQ3IDEzLjU0ODEgNi4yMjM0NyAxMy4zMjgxIDYuNTM1OTUgMTMuMTI0NEM2Ljg0OTY5IDEyLjkyMDcgNi44OTU5MyAxMi4zODgyIDYuOTEyMTggMTIuMDEyQzYuOTI4NDMgMTEuNjM1NyA2LjgwMjE5IDExLjI5MDcgNi41OTk3IDExLjI3NTdaIiBmaWxsPSIjODU4NTg1Ii8+Cjwvc3ZnPgo=": dashboardIconPathPrefix + "orange", + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTcuMTAwOTQgMTUuNTQ5MUw1LjU2MzUyIDE2LjIyOTFMMi4zNDM2OSA4Ljk0NTcxTDMuODgxMTEgOC4yNjU3NUw3LjEwMDk0IDE1LjU0OTFaIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfNzQ2Xzk1OSkiLz4KPHBhdGggZD0iTTIuODc3NDQgOS42NDE5MkMyLjkzMzY5IDkuNTIxOTIgMy4wMDg2OCA5LjQzOTQzIDMuMDg3NDMgOS4zNjk0M0MzLjE2NzQzIDkuMzAwNjggMy4yNTM2NyA5LjI0Njk0IDMuMzQ0OTIgOS4yMDU2OUMzLjQzNzQxIDkuMTY0NDQgMy41MzQ5MSA5LjEzNjk0IDMuNjM5OSA5LjEyNDQ0QzMuNzQ0OSA5LjExMzE5IDMuODU2MTQgOS4xMTQ0NCAzLjk4MjM4IDkuMTUzMTlDMy45MjYxNCA5LjI3NDQ0IDMuODUyMzkgOS4zNTU2OCAzLjc3MjM5IDkuNDI2OTNDMy42OTI0IDkuNDk1NjcgMy42MDYxNSA5LjU0OTQyIDMuNTEzNjYgOS41OTA2N0MzLjQyMTE2IDkuNjMwNjcgMy4zMjM2NyA5LjY1ODE3IDMuMjE4NjcgOS42NzA2N0MzLjExNjE4IDkuNjgxOTEgMy4wMDQ5MyA5LjY4MTkxIDIuODc3NDQgOS42NDE5MloiIGZpbGw9IiNGRkMxMDciLz4KPHBhdGggZD0iTTMuNzM4NjUgMTEuNTg4MUMzLjc5NDg5IDExLjQ2ODEgMy44Njk4OSAxMS4zODU2IDMuOTQ4NjQgMTEuMzE1NkM0LjAyODYzIDExLjI0NjkgNC4xMTQ4OCAxMS4xOTMxIDQuMjA2MTIgMTEuMTUxOUM0LjI5ODYyIDExLjExMDYgNC4zOTYxMSAxMS4wODMxIDQuNTAxMTEgMTEuMDcwNkM0LjYwNjEgMTEuMDU5NCA0LjcxNzM1IDExLjA2MDYgNC44NDM1OSAxMS4wOTk0QzQuNzg3MzQgMTEuMjIwNiA0LjcxMzYgMTEuMzAxOCA0LjYzMzYgMTEuMzczMUM0LjU1MzYgMTEuNDQxOCA0LjQ2NzM2IDExLjQ5NTYgNC4zNzQ4NiAxMS41MzY4QzQuMjgyMzcgMTEuNTc2OCA0LjE4NDg3IDExLjYwNDMgNC4wNzk4OCAxMS42MTY4QzMuOTc2MTMgMTEuNjI4MSAzLjg2NjE0IDExLjYyNjggMy43Mzg2NSAxMS41ODgxWiIgZmlsbD0iI0ZGQzEwNyIvPgo8cGF0aCBkPSJNMy4xNjQ5MiAxMC4yOTA2QzMuMjIxMTYgMTAuMTcwNiAzLjI5NjE2IDEwLjA4ODEgMy4zNzQ5MSAxMC4wMTgxQzMuNDU0OSA5Ljk0OTM3IDMuNTQxMTUgOS44OTU2MiAzLjYzMjM5IDkuODU0MzdDMy43MjQ4OSA5LjgxMzEyIDMuODIyMzggOS43ODU2MyAzLjkyNzM4IDkuNzczMTNDNC4wMzIzNyA5Ljc2MTg4IDQuMTQzNjIgOS43NjMxMyA0LjI2OTg2IDkuODAxODdDNC4yMTM2MSA5LjkyMzEyIDQuMTM5ODcgMTAuMDA0NCA0LjA1OTg3IDEwLjA3NTZDMy45Nzk4NyAxMC4xNDQ0IDMuODkzNjMgMTAuMTk4MSAzLjgwMTEzIDEwLjIzOTRDMy43MDg2NCAxMC4yNzkzIDMuNjExMTQgMTAuMzA2OCAzLjUwNjE1IDEwLjMxOTNDMy40MDI0IDEwLjMzMDYgMy4yOTI0MSAxMC4zMjkzIDMuMTY0OTIgMTAuMjkwNloiIGZpbGw9IiNGRkMxMDciLz4KPHBhdGggZD0iTTQuMzEyMzggMTIuODg1NEM0LjM2ODYyIDEyLjc2NTUgNC40NDM2MiAxMi42ODMgNC41MjIzNyAxMi42MTNDNC42MDIzNiAxMi41NDQyIDQuNjg4NjEgMTIuNDkwNSA0Ljc3OTg1IDEyLjQ0OTJDNC44NzIzNSAxMi40MDggNC45Njk4NCAxMi4zODA1IDUuMDc0ODQgMTIuMzY4QzUuMTc5ODMgMTIuMzU2NyA1LjI4OTgzIDEyLjM1OCA1LjQxNzMyIDEyLjM5NjdDNS4zNjEwNyAxMi41MTggNS4yODczMyAxMi41OTkyIDUuMjA3MzMgMTIuNjcwNUM1LjEyNzMzIDEyLjczOTIgNS4wNDEwOSAxMi43OTMgNC45NDg1OSAxMi44MzQyQzQuODU2MSAxMi44NzQyIDQuNzU4NiAxMi45MDE3IDQuNjUzNjEgMTIuOTE0MkM0LjU0OTg3IDEyLjkyNTQgNC40Mzk4NyAxMi45MjQyIDQuMzEyMzggMTIuODg1NFoiIGZpbGw9IiNGRkMxMDciLz4KPHBhdGggZD0iTTQuMDI0OSAxMi4yMzY4QzQuMDgxMTUgMTIuMTE2OCA0LjE1NjE1IDEyLjAzNDMgNC4yMzQ4OSAxMS45NjQzQzQuMzE0ODkgMTEuODk1NSA0LjQwMTEzIDExLjg0MTggNC40OTIzOCAxMS44MDA1QzQuNTg0ODcgMTEuNzU5MyA0LjY4MjM3IDExLjczMTggNC43ODczNiAxMS43MTkzQzQuODkyMzYgMTEuNzA4IDUuMDAyMzUgMTEuNzA5MyA1LjEyOTg0IDExLjc0OEM1LjA3MzYgMTEuODY5MyA0Ljk5OTg1IDExLjk1MDUgNC45MTk4NiAxMi4wMjE4QzQuODM5ODYgMTIuMDkwNSA0Ljc1MzYxIDEyLjE0NDMgNC42NjExMiAxMi4xODU1QzQuNTY4NjIgMTIuMjI1NSA0LjQ3MTEzIDEyLjI1MyA0LjM2NjEzIDEyLjI2NTVDNC4yNjIzOSAxMi4yNzY4IDQuMTUyNCAxMi4yNzU1IDQuMDI0OSAxMi4yMzY4WiIgZmlsbD0iI0ZGQzEwNyIvPgo8cGF0aCBkPSJNNS4xNzIzIDE0LjgzMTZDNS4yMjk4IDE0LjcxMTYgNS4zMDM1NSAxNC42MjkxIDUuMzgyMjkgMTQuNTU5MUM1LjQ2MjI5IDE0LjQ5MDQgNS41NDg1MyAxNC40MzY2IDUuNjM5NzggMTQuMzk1NEM1LjczMjI3IDE0LjM1NDEgNS44Mjk3NyAxNC4zMjY2IDUuOTM0NzYgMTQuMzE0MUM2LjAzOTc2IDE0LjMwMjkgNi4xNDk3NSAxNC4zMDQxIDYuMjc3MjQgMTQuMzQyOUM2LjIyMSAxNC40NjQxIDYuMTQ3MjUgMTQuNTQ1NCA2LjA2NzI2IDE0LjYxNjZDNS45ODcyNiAxNC42ODU0IDUuOTAxMDEgMTQuNzM5MSA1LjgwOTc3IDE0Ljc4MDRDNS43MTcyNyAxNC44MjA0IDUuNjE5NzggMTQuODQ3OSA1LjUxNDc4IDE0Ljg2MDRDNS40MDk3OSAxNC44NzE2IDUuMjk5OCAxNC44NzA0IDUuMTcyMyAxNC44MzE2WiIgZmlsbD0iI0ZGQzEwNyIvPgo8cGF0aCBkPSJNMy40NTExNyAxMC45Mzk0QzMuNTA3NDIgMTAuODE5NCAzLjU4MjQxIDEwLjczNjkgMy42NjExNiAxMC42NjY5QzMuNzQxMTYgMTAuNTk4MiAzLjgyNzQgMTAuNTQ0NCAzLjkxODY1IDEwLjUwMzJDNC4wMTExNCAxMC40NjE5IDQuMTA4NjQgMTAuNDM0NCA0LjIxMzYzIDEwLjQyMTlDNC4zMTg2MyAxMC40MTA3IDQuNDI5ODcgMTAuNDExOSA0LjU1NjExIDEwLjQ1MDdDNC40OTk4NyAxMC41NzE5IDQuNDI2MTIgMTAuNjUzMiA0LjM0NjEyIDEwLjcyNDRDNC4yNjYxMyAxMC43OTMyIDQuMTc5ODggMTAuODQ2OSA0LjA4NzM5IDEwLjg4ODJDMy45OTQ4OSAxMC45MjgyIDMuODk3NCAxMC45NTU3IDMuNzkyNCAxMC45NjgyQzMuNjg4NjYgMTAuOTc5NCAzLjU3ODY3IDEwLjk3ODEgMy40NTExNyAxMC45Mzk0WiIgZmlsbD0iI0ZGQzEwNyIvPgo8cGF0aCBkPSJNNC44ODYxMSAxNC4xODI5QzQuOTQyMzYgMTQuMDYyOSA1LjAxNzM1IDEzLjk4MDQgNS4wOTYxIDEzLjkxMDRDNS4xNzYwOSAxMy44NDE3IDUuMjYyMzQgMTMuNzg4IDUuMzUzNTggMTMuNzQ2N0M1LjQ0NjA4IDEzLjcwNTUgNS41NDM1NyAxMy42NzggNS42NDg1NyAxMy42NjU1QzUuNzUzNTYgMTMuNjU0MiA1Ljg2MzU2IDEzLjY1NTUgNS45OTEwNSAxMy42OTQyQzUuOTM0OCAxMy44MTU1IDUuODYxMDYgMTMuODk2NyA1Ljc4MTA2IDEzLjk2NzlDNS43MDEwNyAxNC4wMzY3IDUuNjE0ODIgMTQuMDkwNCA1LjUyMzU3IDE0LjEzMTdDNS40MzEwOCAxNC4xNzE3IDUuMzMzNTggMTQuMTk5MiA1LjIyODU5IDE0LjIxMTdDNS4xMjM2IDE0LjIyMjkgNS4wMTIzNSAxNC4yMjE3IDQuODg2MTEgMTQuMTgyOVoiIGZpbGw9IiNGRkMxMDciLz4KPHBhdGggZD0iTTQuNTk4NjMgMTMuNTM0M0M0LjY1NDg4IDEzLjQxNDMgNC43Mjk4OCAxMy4zMzE4IDQuODA4NjIgMTMuMjYxOEM0Ljg4ODYyIDEzLjE5MyA0Ljk3NDg2IDEzLjEzOTMgNS4wNjYxMSAxMy4wOThDNS4xNTg2IDEzLjA1NjggNS4yNTYxIDEzLjAyOTMgNS4zNjEwOSAxMy4wMTY4QzUuNDY2MDkgMTMuMDA1NSA1LjU3NjA4IDEzLjAwNjggNS43MDM1NyAxMy4wNDU1QzUuNjQ3MzMgMTMuMTY2OCA1LjU3MzU4IDEzLjI0OCA1LjQ5MzU5IDEzLjMxOTNDNS40MTM1OSAxMy4zODggNS4zMjczNCAxMy40NDE4IDUuMjM2MSAxMy40ODNDNS4xNDM2IDEzLjUyMyA1LjA0NjExIDEzLjU1MDUgNC45NDExMSAxMy41NjNDNC44MzYxMiAxMy41NzQyIDQuNzI2MTMgMTMuNTczIDQuNTk4NjMgMTMuNTM0M1oiIGZpbGw9IiNGRkMxMDciLz4KPHBhdGggZD0iTTMuOTE3MzggOC40ODA3N0wyLjQ4OTk1IDkuMTExOTlDMi4zMTc0NiA5LjE4ODI0IDIuMTE3NDcgOS4xMTA3NCAyLjA0MTIzIDguOTM4MjVDMS45NjQ5OCA4Ljc2NTc2IDIuMDQyNDggOC41NjU3NyAyLjIxNDk3IDguNDg5NTJMMy42NDIzOSA3Ljg1ODMxQzMuODE0ODggNy43ODIwNiA0LjAxNDg3IDcuODU5NTYgNC4wOTExMiA4LjAzMjA1QzQuMTY3MzcgOC4yMDMyOSA0LjA4OTg3IDguNDA0NTMgMy45MTczOCA4LjQ4MDc3WiIgZmlsbD0iI0ZGQTAwMCIvPgo8ZyBvcGFjaXR5PSIwLjgxIj4KPHBhdGggb3BhY2l0eT0iMC44MSIgZD0iTTQuNTc3MzIgMTMuOTM5MkwzLjcwNjEyIDExLjk5M0w1LjA5MTA0IDExLjA2OTNMNi4xNjM0OSAxMy4yOTA1TDQuNTc3MzIgMTMuOTM5MloiIGZpbGw9InVybCgjcGFpbnQxX2xpbmVhcl83NDZfOTU5KSIvPgo8L2c+CjxwYXRoIGQ9Ik0yLjExNzQ5IDkuMDQ1NjhDMi4yMzEyNCA5LjE3MDY4IDIuNDI3NDggOS4xMzU2OCAyLjQyNzQ4IDkuMTM1NjhMMi40ODI0NyA5LjI1OTQyTDQuMDIyMzkgOC41ODU3MUwzLjk2ODY1IDguNDU4MjFDMy45Njg2NSA4LjQ1ODIxIDQuMTMxMTQgOC4zNDY5NyA0LjExNzM5IDguMTI1NzNMMi4xMTc0OSA5LjA0NTY4WiIgZmlsbD0idXJsKCNwYWludDJfbGluZWFyXzc0Nl85NTkpIi8+CjxwYXRoIGQ9Ik0yLjgxMjQ0IDE2LjEwNzhWMTYuNDgwM0gxNS45ODY3VjE2LjEwNzhDMTUuOTg2NyAxMy45ODE3IDEyLjY0ODIgMTIuOTI5MiA5LjM5OTU5IDEyLjkzM0M2LjE3MTAxIDEyLjkzNjcgMi44MTI0NCAxMy44NTU0IDIuODEyNDQgMTYuMTA3OFoiIGZpbGw9InVybCgjcGFpbnQzX2xpbmVhcl83NDZfOTU5KSIvPgo8cGF0aCBkPSJNMy43MzIzNiAxMy42NjQyQzMuNzMyMzYgMTQuODMyOSA1LjU3ODUxIDE2LjQ4MDMgNS41Nzg1MSAxNi40ODAzSDEzLjAxMDZDMTMuMDEwNiAxNi40ODAzIDE0LjcyMDUgMTQuNzg3OSAxNC43MjA1IDEzLjQwOEMxNC43MjA1IDEzLjQwOCAxMi44MjQ0IDEyLjg2NDMgOS4zOTk1NiAxMi45MzNDNS41Nzg1MSAxMy4wMDkzIDMuNzMyMzYgMTMuNjY0MiAzLjczMjM2IDEzLjY2NDJaIiBmaWxsPSIjMjEyMTIxIi8+CjxwYXRoIGQ9Ik05Ljk5MzI4IDEzLjMwNjdDMTIuMjE1NyAxMy4zMDY3IDEzLjcwNTYgMTMuNTY5MiAxNC4zMTA2IDEzLjcwMDVDMTQuMTQxOCAxNC41NzkyIDEzLjI5NjkgMTUuNjI2NiAxMi44NDk0IDE2LjEwMDNINS43MjQ3NkM1LjE0MTA0IDE1LjU1NTQgNC4zMzk4MyAxNC42MjI5IDQuMTUyMzQgMTMuOTM1NUM0LjgwMjMgMTMuNzU5MiA2LjUzODQ2IDEzLjM3MDUgOS40MDcwNiAxMy4zMTNDOS42MDMzIDEzLjMwOTIgOS43OTk1NCAxMy4zMDY3IDkuOTkzMjggMTMuMzA2N1pNOS45OTMyOCAxMi45MjY4QzkuODAwNzkgMTIuOTI2OCA5LjYwMzMgMTIuOTI4IDkuMzk5NTYgMTIuOTMzQzUuNTc5NzYgMTMuMDA5MyAzLjczMjM2IDEzLjY2NDIgMy43MzIzNiAxMy42NjQyQzMuNzMyMzYgMTQuODMyOSA1LjU3ODUxIDE2LjQ4MDMgNS41Nzg1MSAxNi40ODAzSDEzLjAxMDZDMTMuMDEwNiAxNi40ODAzIDE0LjcyMDUgMTQuNzg3OSAxNC43MjA1IDEzLjQwOEMxNC43MjA1IDEzLjQwOCAxMy4wNDE5IDEyLjkyNjggOS45OTMyOCAxMi45MjY4WiIgZmlsbD0idXJsKCNwYWludDRfbGluZWFyXzc0Nl85NTkpIi8+CjxwYXRoIGQ9Ik03LjU1NTkxIDEzLjE4NjdDNy41NTU5MSAxMi44MTQyIDcuNTgwOTEgMTIuMTU5MyA4LjAyNzEzIDExLjk1MDVDOC4zMzU4NyAxMS44MDY4IDEwLjQwOTUgMTEuODgwNSAxMC43Mjk1IDEyLjA0NTVDMTEuMTM0NSAxMi4yNTQzIDExLjI0MzIgMTIuODM0MiAxMS4yNDMyIDEzLjE4NTVDMTEuMjQzMiAxMy44MTU0IDEwLjQxODMgMTUuNzAyOCA5LjQwMDgxIDE1LjcwMjhDOC4zODMzNiAxNS43MDI4IDcuNTU1OTEgMTMuODE2NyA3LjU1NTkxIDEzLjE4NjdaIiBmaWxsPSJ1cmwoI3BhaW50NV9saW5lYXJfNzQ2Xzk1OSkiLz4KPHBhdGggb3BhY2l0eT0iMC42MSIgZD0iTTcuNjc4NDEgMTIuMjQ2N0w3LjU5MjE2IDEyLjY1OTJDNy41OTIxNiAxMi42NTkyIDguMjM4MzggMTMuNDQwNCA5LjM5OTU3IDEzLjQ0MDRDMTAuNzQwNyAxMy40NDA0IDExLjE5NDUgMTIuNzYwNCAxMS4xOTQ1IDEyLjc2MDRMMTEuMDc4MiAxMi4zNzQyTDcuNjc4NDEgMTIuMjQ2N1oiIGZpbGw9ImJsYWNrIi8+CjxwYXRoIGQ9Ik0xMC42MDU3IDE2LjQ4MDNDMTEuNTI5NCAxNS40NzE2IDEyLjM4ODIgMTQuMjcwNCAxMi42NDQ0IDEzLjA5MTdMMTEuNjc0NCAxMi44MzU1QzExLjU1MTkgMTIuODAzIDExLjQyNyAxMi44NzMgMTEuMzg5NSAxMi45OTNDMTEuMDI4MiAxNC4xNTc5IDkuODAwNzkgMTUuNDc1MyA4LjY4MzM1IDE2LjQ4MDNIMTAuNjA1N1oiIGZpbGw9ImJsYWNrIi8+CjxwYXRoIGQ9Ik0xMC4zNDgzIDE2LjQ4MjhDMTEuMzEzMiAxNS40NTA0IDEyLjIzMDcgMTQuMjAwNCAxMi40OTY5IDEyLjk3NTVMMTEuNTI3IDEyLjcxOTNDMTEuNDA0NSAxMi42ODY4IDExLjI3OTUgMTIuNzU2OCAxMS4yNDIgMTIuODc2N0MxMC44NjU3IDE0LjA4NzkgOS41NTMzMiAxNS40NjU0IDguNDAwODggMTYuNDgyOEgxMC4zNDgzWiIgZmlsbD0idXJsKCNwYWludDZfbGluZWFyXzc0Nl85NTkpIi8+CjxnIG9wYWNpdHk9IjAuMzEiPgo8cGF0aCBvcGFjaXR5PSIwLjMxIiBkPSJNNy41Nzk3MSAxMi45NjU1QzcuNTc5NzEgMTIuOTY1NSA5LjA2MjEzIDE1LjE3MTYgMTAuODU3IDE2LjQyNzhDMTAuODU3IDE2LjQyNzggOS44NDgzNCAxNi41NDQgOS44MTk1OSAxNi40ODAzQzkuNzkwODUgMTYuNDE2NSA3LjY1ODQ2IDE0LjA4NDEgNy42NTg0NiAxNC4wODQxTDcuNTc5NzEgMTIuOTY1NVoiIGZpbGw9ImJsYWNrIi8+CjwvZz4KPHBhdGggZD0iTTEwLjMzMzMgMTYuNDgwMkM5LjIyNTg0IDE1LjQ4OTEgNy45ODIxNSAxNC4xNTU0IDcuNTc5NjggMTIuOTY1NEM3LjUyNzE4IDEyLjgwOTIgNy4zNjIxOSAxMi43MjI5IDcuMjA1OTUgMTIuNzc0Mkw2LjIzOTc1IDEzLjEwMDRDNi41MDQ3MyAxNC4yOTQxIDcuMzgzNDQgMTUuNDg5MSA4LjMxOTY0IDE2LjQ4MDJIMTAuMzMzM1oiIGZpbGw9InVybCgjcGFpbnQ3X2xpbmVhcl83NDZfOTU5KSIvPgo8cGF0aCBkPSJNOS4zOTk1OSAyLjE3MzU4QzcuMTk0NzEgMi4xNzM1OCA1LjE1MzU2IDQuNTMwOTYgNS4xNTM1NiA3LjkyNDUzQzUuMTUzNTYgMTEuMjk5NCA3LjI1NzIgMTIuODI4IDkuMzk5NTkgMTIuODI4QzExLjU0MiAxMi44MjggMTMuNjQ1NiAxMS4yOTk0IDEzLjY0NTYgNy45MjQ1M0MxMy42NDU2IDQuNTMwOTYgMTEuNjA0NSAyLjE3MzU4IDkuMzk5NTkgMi4xNzM1OFoiIGZpbGw9IiNGOUREQkQiLz4KPHBhdGggZD0iTTcuMzE3MiA4Ljg2ODI0QzcuNjYxNjcgOC44NjgyNCA3Ljk0MDkyIDguNTc4OTIgNy45NDA5MiA4LjIyMjAyQzcuOTQwOTIgNy44NjUxMyA3LjY2MTY3IDcuNTc1ODEgNy4zMTcyIDcuNTc1ODFDNi45NzI3MyA3LjU3NTgxIDYuNjkzNDggNy44NjUxMyA2LjY5MzQ4IDguMjIyMDJDNi42OTM0OCA4LjU3ODkyIDYuOTcyNzMgOC44NjgyNCA3LjMxNzIgOC44NjgyNFoiIGZpbGw9IiMzMTJEMkQiLz4KPHBhdGggZD0iTTExLjQ4MiA4Ljg2ODI0QzExLjgyNjUgOC44NjgyNCAxMi4xMDU3IDguNTc4OTIgMTIuMTA1NyA4LjIyMjAyQzEyLjEwNTcgNy44NjUxMyAxMS44MjY1IDcuNTc1ODEgMTEuNDgyIDcuNTc1ODFDMTEuMTM3NSA3LjU3NTgxIDEwLjg1ODMgNy44NjUxMyAxMC44NTgzIDguMjIyMDJDMTAuODU4MyA4LjU3ODkyIDExLjEzNzUgOC44NjgyNCAxMS40ODIgOC44NjgyNFoiIGZpbGw9IiMzMTJEMkQiLz4KPHBhdGggZD0iTTguMjU3MTQgNy4wODIwNEM4LjEzOTY1IDYuOTI3MDQgNy44Njg0MSA2LjcwMDgxIDcuMzQwOTQgNi43MDA4MUM2LjgxMzQ3IDYuNzAwODEgNi41NDIyMyA2LjkyNzA0IDYuNDI0NzQgNy4wODIwNEM2LjM3MjI0IDcuMTUwNzggNi4zODU5OSA3LjIzMDc4IDYuNDIyMjQgNy4yNzgyOEM2LjQ1NTk5IDcuMzIzMjcgNi41NTQ3MyA3LjM2NDUyIDYuNjYzNDcgNy4zMjcwMkM2Ljc3MjIyIDcuMjg5NTIgNi45ODQ3MSA3LjE3OTUzIDcuMzQyMTkgNy4xNzcwM0M3LjY5ODQyIDcuMTc5NTMgNy45MTA5MSA3LjI4OTUyIDguMDIwOSA3LjMyNzAyQzguMTI5NjUgNy4zNjQ1MiA4LjIyODM5IDcuMzIzMjcgOC4yNjIxNCA3LjI3ODI4QzguMjk1ODkgNy4yMzA3OCA4LjMwOTY0IDcuMTUwNzggOC4yNTcxNCA3LjA4MjA0WiIgZmlsbD0iIzQ1NDE0MCIvPgo8cGF0aCBkPSJNMTIuMzc0NCA3LjA4MjA0QzEyLjI1NjkgNi45MjcwNCAxMS45ODU3IDYuNzAwODEgMTEuNDU4MiA2LjcwMDgxQzEwLjkzMDcgNi43MDA4MSAxMC42NTk1IDYuOTI3MDQgMTAuNTQyIDcuMDgyMDRDMTAuNDg5NSA3LjE1MDc4IDEwLjUwMzIgNy4yMzA3OCAxMC41Mzk1IDcuMjc4MjhDMTAuNTczMiA3LjMyMzI3IDEwLjY3MiA3LjM2NDUyIDEwLjc4MDcgNy4zMjcwMkMxMC44ODk1IDcuMjg5NTIgMTEuMTAyIDcuMTc5NTMgMTEuNDU5NCA3LjE3NzAzQzExLjgxNTcgNy4xNzk1MyAxMi4wMjgyIDcuMjg5NTIgMTIuMTM4MiA3LjMyNzAyQzEyLjI0NjkgNy4zNjQ1MiAxMi4zNDU2IDcuMzIzMjcgMTIuMzc5NCA3LjI3ODI4QzEyLjQxMzEgNy4yMzA3OCAxMi40MjU2IDcuMTUwNzggMTIuMzc0NCA3LjA4MjA0WiIgZmlsbD0iIzQ1NDE0MCIvPgo8ZyBvcGFjaXR5PSIwLjE3Ij4KPHBhdGggb3BhY2l0eT0iMC4xNyIgZD0iTTkuMzk5NTkgMi4wMDYxQzcuMDU4NDYgMi4wMDYxIDUuMTUzNTYgMy41Nzg1MiA1LjE1MzU2IDcuMTUyMDhDNS4xNTM1NiAxMC43NDU2IDYuOTkwOTcgMTIuNzEwNSA5LjM5OTU5IDEyLjcxMDVDMTEuODA4MiAxMi43MTA1IDEzLjg0OTQgMTAuNjk5NCAxMy44NDk0IDcuMTA0NThDMTMuODQ4MSAzLjUzMTAyIDExLjc0MDcgMi4wMDYxIDkuMzk5NTkgMi4wMDYxWk05LjM5OTU5IDEyLjQxMDZDNy44MjIxNyAxMi40MTA2IDUuNzM2MDMgMTEuNTk4MSA1LjczNjAzIDguNjU3QzUuNzM2MDMgNS43MzIxNiA3LjQ4OTY5IDUuODEyMTUgOS4zOTk1OSA1LjgxMjE1QzExLjMwOTUgNS44MTIxNSAxMy4wNjMxIDUuNzMyMTYgMTMuMDYzMSA4LjY1N0MxMy4wNjMxIDExLjU5ODEgMTAuOTc3IDEyLjQxMDYgOS4zOTk1OSAxMi40MTA2WiIgZmlsbD0iYmxhY2siLz4KPC9nPgo8cGF0aCBkPSJNOS40NDU4NSA4LjYwNDQ5QzEwLjgzOTUgOC42MDQ0OSAxMy4zMzE5IDkuOTQwNjcgMTMuMzMxOSA5Ljk0MDY3QzEzLjI1NTcgMTEuMjE4MSAxMS41OTIgMTIuNjg4IDkuNTM0NiAxMi42NThDNi4wODIyOCAxMi42MDggNS40NjYwNiAxMC4wNDMyIDUuNDY2MDYgMTAuMDQzMkM1LjQ2NjA2IDEwLjA0MzIgOC4wNTIxOCA4LjYwNDQ5IDkuNDQ1ODUgOC42MDQ0OVoiIGZpbGw9InVybCgjcGFpbnQ4X2xpbmVhcl83NDZfOTU5KSIvPgo8cGF0aCBvcGFjaXR5PSIwLjQiIGQ9Ik0xMy4zMjgxIDkuOTkxODlDMTMuMzI4MSA5Ljk4ODE0IDEzLjMyOTQgOS45ODQzOSAxMy4zMjk0IDkuOTgxODlDMTMuMTk2OSA5LjkwMzE0IDEzLjA2MzIgOS44MjA2NSAxMi45MjgyIDkuNzM1NjVDMTIuMTY0NSA5LjM2MDY3IDEwLjQ5NDUgOC42MDU3MSA5LjQ0NzEgOC42MDU3MUM4LjQ4MjE1IDguNjA1NzEgNi44NTQ3MyA5LjMxOTQzIDUuODU0NzkgOS44NDMxNUM1LjY1NDggOS45NzY4OSA1Ljc2OTc5IDkuOTg5MzkgNS41NzYwNSAxMC4xMTE5QzYuMjMxMDIgOS44ODE5IDguMTE4NDIgOS4wMjk0NCA5LjQxODM1IDkuMDI5NDRDMTAuNjk0NSA5LjAyODE5IDEyLjYyOTQgOS43NjY5IDEzLjMyODEgOS45OTE4OVoiIGZpbGw9IiNCREJEQkQiLz4KPHBhdGggZD0iTTkuMzk5NTkgMS40OTk4OEM2Ljk1NTk2IDEuNDk5ODggNC41NTYwOSAzLjI2NzI4IDQuNTU2MDkgNi44NDIxQzQuNTU2MDkgMTAuNDM1NyA2Ljg4NDcyIDEyLjkzMyA5LjM5ODM0IDEyLjkzM0MxMS45MTIgMTIuOTMzIDE0LjI0MDYgMTAuNDM1NyAxNC4yNDA2IDYuODQyMUMxNC4yNDE4IDMuMjY3MjggMTEuODQzMiAxLjQ5OTg4IDkuMzk5NTkgMS40OTk4OFpNOS4zOTk1OSAxMi42NTU1QzcuNzUyMTcgMTIuNjU1NSA1LjU3NjA0IDExLjA5MTkgNS41NzYwNCA4LjE1MDc4QzUuNTc2MDQgNS4yMjU5MyA3LjQwNTk0IDUuMzA1OTMgOS4zOTk1OSA1LjMwNTkzQzExLjM5MzIgNS4zMDU5MyAxMy4yMjMxIDUuMjI1OTMgMTMuMjIzMSA4LjE1MDc4QzEzLjIyMzEgMTEuMDkwNiAxMS4wNDU3IDEyLjY1NTUgOS4zOTk1OSAxMi42NTU1WiIgZmlsbD0idXJsKCNwYWludDlfbGluZWFyXzc0Nl85NTkpIi8+CjxwYXRoIGQ9Ik05LjUzNDU2IDIuNzE0NzlDOS41MzQ1NiAyLjcxNDc5IDcuNzI5NjYgMi4xNTM1NyA2LjA4MjI1IDMuOTI3MjNDNC41MDIzMyA1LjYyODM5IDQuOTk0OCA4Ljg3OTQ3IDQuOTk0OCA4Ljg3OTQ3QzQuOTk0OCA4Ljg3OTQ3IDQuODI3MzEgNi4wNDU4NyA2LjY5MjIxIDQuMjcyMjFDOC4zMjMzOCAyLjcxOTc5IDkuNTM0NTYgMi43MTQ3OSA5LjUzNDU2IDIuNzE0NzlaIiBmaWxsPSJ1cmwoI3BhaW50MTBfbGluZWFyXzc0Nl85NTkpIi8+CjxwYXRoIGQ9Ik0xMi41NjY5IDUuMTQyMTVDMTMuNjE5NCA2LjE2NDYgMTMuNjM0NCA4Ljg4OTQ2IDEzLjYzNDQgOC44ODk0NkMxMy42MzQ0IDguODg5NDYgMTQuNDIzMSA2LjE0NTg1IDEyLjk3ODEgNC42NjcxOEMxMS43ODMyIDMuNDQzNDkgMTAuMTI4MyAzLjkzODQ3IDEwLjEyODMgMy45Mzg0N0MxMC4xMjgzIDMuOTM4NDcgMTEuNDU4MiA0LjA2NDcxIDEyLjU2NjkgNS4xNDIxNVoiIGZpbGw9InVybCgjcGFpbnQxMV9saW5lYXJfNzQ2Xzk1OSkiLz4KPGcgb3BhY2l0eT0iMC4zOCI+CjxwYXRoIG9wYWNpdHk9IjAuMzgiIGQ9Ik05LjM5OTU5IDEuNzQ5ODZDMTAuNjEyIDEuNzQ5ODYgMTEuNzM1NyAyLjE5NzM0IDEyLjU2MTkgMy4wMDg1NUMxMy40OTgxIDMuOTI3MjUgMTMuOTkxOCA1LjI1MjE4IDEzLjk5MTggNi44NDIxQzEzLjk5MTggOC4zODgyNiAxMy41Mzk0IDkuODE4MTkgMTIuNzE0NCAxMC44OTY5QzEzLjE3NDQgMTAuMTcwNyAxMy40NzMxIDkuMjUxOTcgMTMuNDczMSA4LjE1MjAzQzEzLjQ3MzEgNi44MDIxIDEzLjA3NTYgNS45MjU4OSAxMi4yNTY5IDUuNDcwOTJDMTEuNTY4MiA1LjA4ODQ0IDEwLjY4OTUgNS4wNTcxOSA5LjY5NTgyIDUuMDU3MTlIOS41NDgzM0g5LjQwMDg0SDkuMjUzMzRIOS4xMDU4NUM3LjQyMjE5IDUuMDU3MTkgNS4zMjczIDUuMDU3MTkgNS4zMjczIDguMTUyMDNDNS4zMjczIDkuMjUzMjIgNS42MjQ3OCAxMC4xNzA3IDYuMDg2MDEgMTAuODk2OUM1LjI2MTA1IDkuODE4MTkgNC44MDg1OCA4LjM4ODI2IDQuODA4NTggNi44NDIxQzQuODA4NTggNS4yNTM0MyA1LjMwMzU1IDMuOTI3MjUgNi4yMzg1IDMuMDA4NTVDNy4wNjM0NiAyLjE5NzM0IDguMTg3MTUgMS43NDk4NiA5LjM5OTU5IDEuNzQ5ODZaTTkuMzk5NTkgMS40OTk4OEM2Ljk1NTk2IDEuNDk5ODggNC41NTYwOSAzLjI2NzI4IDQuNTU2MDkgNi44NDIxQzQuNTU2MDkgMTAuNDM1NyA2Ljg4NDcyIDEyLjkzMyA5LjM5ODM0IDEyLjkzM0MxMS45MTIgMTIuOTMzIDE0LjI0MDYgMTAuNDM1NyAxNC4yNDA2IDYuODQyMUMxNC4yNDE4IDMuMjY3MjggMTEuODQzMiAxLjQ5OTg4IDkuMzk5NTkgMS40OTk4OFpNOS4zOTk1OSA1LjMwNTkzQzkuNDk4MzMgNS4zMDU5MyA5LjU5NzA3IDUuMzA1OTMgOS42OTQ1NyA1LjMwNTkzQzExLjQ5ODIgNS4zMDU5MyAxMy4yMjMxIDUuMzc3MTcgMTMuMjIzMSA4LjE1MDc4QzEzLjIyMzEgMTEuMDkwNiAxMC45NzMzIDEyLjY1NTUgOS4zOTk1OSAxMi42NTU1QzcuODI1OTIgMTIuNjU1NSA1LjU3NjA0IDExLjA5MDYgNS41NzYwNCA4LjE1MDc4QzUuNTc2MDQgNS4zNzcxNyA3LjMwMDk1IDUuMzA1OTMgOS4xMDQ2IDUuMzA1OTNDOS4yMDIxIDUuMzA1OTMgOS4zMDA4NCA1LjMwNTkzIDkuMzk5NTkgNS4zMDU5M1oiIGZpbGw9InVybCgjcGFpbnQxMl9saW5lYXJfNzQ2Xzk1OSkiLz4KPC9nPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzc0Nl85NTkiIHgxPSIzLjQyOTA2IiB5MT0iOS40NzYzMiIgeDI9IjUuMjE2MjQiIHkyPSIxMy4zMDYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iIzY0NjQ2NCIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM0NDQ0NDQiLz4KPC9saW5lYXJHcmFkaWVudD4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDFfbGluZWFyXzc0Nl85NTkiIHgxPSI1LjQ3NTc3IiB5MT0iMTMuOTUyOCIgeDI9IjQuNTAwMDUiIHkyPSIxMS41ODg1IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLW9wYWNpdHk9IjAiLz4KPC9saW5lYXJHcmFkaWVudD4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDJfbGluZWFyXzc0Nl85NTkiIHgxPSIzLjI4NTcxIiB5MT0iOC45OTM4NSIgeDI9IjMuMTE3NjkiIHkyPSI4LjU4NjcyIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLW9wYWNpdHk9IjAiLz4KPC9saW5lYXJHcmFkaWVudD4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDNfbGluZWFyXzc0Nl85NTkiIHgxPSI5LjM5OTQ3IiB5MT0iMTYuNjkxMyIgeDI9IjkuMzk5NDciIHkyPSIxNS4wNjU0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIG9mZnNldD0iMC4xNjk2IiBzdG9wLWNvbG9yPSIjNjQ2NDY0Ii8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzc1NzU3NSIvPgo8L2xpbmVhckdyYWRpZW50Pgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50NF9saW5lYXJfNzQ2Xzk1OSIgeDE9IjkuMjI2MTMiIHkxPSIxMi45MjciIHgyPSI5LjIyNjEzIiB5Mj0iMTYuNDgwMSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjNjQ2NDY0Ii8+CjxzdG9wIG9mZnNldD0iMC4yNjQ5IiBzdG9wLWNvbG9yPSIjNTc1NzU3Ii8+CjxzdG9wIG9mZnNldD0iMC43NTM4IiBzdG9wLWNvbG9yPSIjMzUzNTM1Ii8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzIxMjEyMSIvPgo8L2xpbmVhckdyYWRpZW50Pgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50NV9saW5lYXJfNzQ2Xzk1OSIgeDE9IjkuMzk5NDQiIHkxPSIxMi42Nzk2IiB4Mj0iOS4zOTk0NCIgeTI9IjE1LjQ0NTYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agb2Zmc2V0PSIwLjA3NDQiIHN0b3AtY29sb3I9IiM0NDQ0NDQiLz4KPHN0b3Agb2Zmc2V0PSIwLjY1MyIgc3RvcC1jb2xvcj0iIzIxMjEyMSIvPgo8L2xpbmVhckdyYWRpZW50Pgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50Nl9saW5lYXJfNzQ2Xzk1OSIgeDE9IjExLjY1MzIiIHkxPSIxMy4xNDMxIiB4Mj0iMTAuMjY5OCIgeTI9IjE1LjI5MDIiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iIzY0NjQ2NCIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM0NDQ0NDQiLz4KPC9saW5lYXJHcmFkaWVudD4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDdfbGluZWFyXzc0Nl85NTkiIHgxPSI4LjM4Nzk1IiB5MT0iMTUuMjU2OSIgeDI9IjYuMzUxOTQiIHkyPSIxMi4zMzM0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiM2NDY0NjQiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjOUU5RTlFIi8+CjwvbGluZWFyR3JhZGllbnQ+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQ4X2xpbmVhcl83NDZfOTU5IiB4MT0iOS4zOTk0MiIgeTE9IjkuODkyNjQiIHgyPSI5LjM5OTQyIiB5Mj0iMTIuOTIwOCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBvZmZzZXQ9IjAuMDc0NCIgc3RvcC1jb2xvcj0iIzQ0NDQ0NCIvPgo8c3RvcCBvZmZzZXQ9IjAuNjUzIiBzdG9wLWNvbG9yPSIjMjEyMTIxIi8+CjwvbGluZWFyR3JhZGllbnQ+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQ5X2xpbmVhcl83NDZfOTU5IiB4MT0iOS4zOTkzNCIgeTE9IjEuNTYzOTEiIHgyPSI5LjM5OTM0IiB5Mj0iMTIuNzk5NSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBvZmZzZXQ9IjAuMDc0NCIgc3RvcC1jb2xvcj0iIzQ0NDQ0NCIvPgo8c3RvcCBvZmZzZXQ9IjAuNjUzIiBzdG9wLWNvbG9yPSIjMjEyMTIxIi8+CjwvbGluZWFyR3JhZGllbnQ+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQxMF9saW5lYXJfNzQ2Xzk1OSIgeDE9IjUuNTk4NjQiIHkxPSI2LjMyMzM3IiB4Mj0iOC4zMDEyMiIgeTI9IjIuOTg3MzciIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iIzBEMEMwQyIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwRDBDMEMiIHN0b3Atb3BhY2l0eT0iMCIvPgo8L2xpbmVhckdyYWRpZW50Pgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MTFfbGluZWFyXzc0Nl85NTkiIHgxPSIxMi45MDU5IiB5MT0iNi4zMDAzMyIgeDI9IjExLjA0NzgiIHkyPSI0LjEwNDQ4IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiMwRDBDMEMiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMEQwQzBDIiBzdG9wLW9wYWNpdHk9IjAiLz4KPC9saW5lYXJHcmFkaWVudD4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDEyX2xpbmVhcl83NDZfOTU5IiB4MT0iOS4zOTkzNCIgeTE9IjEuNDk5ODgiIHgyPSI5LjM5OTM0IiB5Mj0iMTIuOTMzIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNCREJEQkQiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNjQ2NDY0Ii8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg==": dashboardIconPathPrefix + "ninja", + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuNjgyMjggOC40ODIwNkw0LjM1NDggNy43NzA4NUwzLjU1MzU5IDQuNjU2MDFMMy4zNDk4NSA0LjE3MTA0TDMuNjQzNTkgMy40NDg1OEMzLjY0MzU5IDMuNDQ4NTggNC4wNTk4MiAzLjUwNzMyIDQuMzc3MyAzLjg2NjA2QzQuNzYzNTMgNC4zMDM1MyA1LjM5MzUgNS42MTIyMSA1LjM5MzUgNS42MTIyMUw1LjI2MSA0LjU3OTc3TDUuOTc5NzEgMi43MTYxMkM1Ljk3OTcxIDIuNzE2MTIgNi4yOTQ3IDIuNjgyMzcgNi4zNzg0NCAyLjc2ODYxQzYuNTI3MTkgMi45MjExMSA2Ljc4MDkyIDMuOTkxMDUgNi44NzA5MiA0LjMyOTc4QzYuOTYwOTEgNC42Njg1MSA3LjA4NTkxIDUuMTMwOTkgNy4yNDM0IDUuMzMzNDhDNy40MDA4OSA1LjUzNTk3IDcuNTkzMzggNS41ODIyMiA3LjU5MzM4IDUuNTgyMjJMOC4xNTcxIDMuNDA0ODNMOC4yOTIwOSAxLjQyOTkzQzguMjkyMDkgMS40Mjk5MyA4LjQ4OTU4IDEuNDI4NjggOC43NjU4MiAxLjQ4NjE4QzguOTc0NTYgMS41Mjk5MyA5LjExNDU1IDEuNjAxMTcgOS4xODIwNSAxLjY4NDkyQzkuMjQ5NTQgMS43Njg2NyA5LjI1MjA0IDMuMDk5ODUgOS4yNTIwNCAzLjA5OTg1QzkuMjUyMDQgMy4wOTk4NSAxMC40NDMyIDIuMDg0OSAxMC40ODIgMi4wOTYxNUMxMC41NjQ1IDIuMTE4NjUgMTAuODIwNyAyLjE3NDg5IDEwLjk1NTcgMi4zMjIzOUMxMS4wOTA3IDIuNDY4NjMgMTAuOTc4MiAzLjAxMTEgMTAuODc3IDMuMzcyMzNDMTAuNzc1NyAzLjczMzU2IDEwLjQyNTcgNC45MDcyNSAxMC40MjU3IDQuOTA3MjVMMTAuNzQyIDQuNzg3MjZMMTAuODEyIDQuODU0NzVMMTEuNDAwNyAzLjk1MTA1QzExLjQwMDcgMy45NTEwNSAxMS42MTMyIDMuOTQ5OCAxMS43ODU3IDQuMDM0OEMxMS45NzMxIDQuMTI3MjkgMTIuMDg1NiA0LjM1NDc4IDEyLjA4NTYgNC4zNTQ3OEwxMy41OTgxIDIuMTY2MTVDMTMuNTk4MSAyLjE2NjE1IDEzLjcxMzEgMi4yNjg2NCAxMy44MTQzIDIuMzY5ODhDMTMuOTE1NSAyLjQ3MTEzIDEzLjk4NjggMi41NDIzOCAxMy45OTE4IDIuNjQ0ODdDMTMuOTk2OCAyLjc0NjExIDEzLjYzMTggMy40MTk4MyAxMy40NjMxIDMuNzU4NTZDMTMuMjk0MyA0LjA5NzI5IDEzLjAxMTggNC43NzQ3NiAxMy4wMTE4IDQuNzc0NzZMMTQuMjQxOCAzLjgzNjA2QzE0LjI0MTggMy44MzYwNiAxNC42NDkzIDMuODkzNTUgMTQuNjYxOCA0LjAyODU1QzE0LjY3MyA0LjE2MzU0IDE0LjQ1NjggNC40MTM1MyAxNC4yNDE4IDQuNzg2MDFDMTQuMDI2OCA1LjE1ODQ5IDEzLjY2NjggNS44ODA5NSAxMy41NDE4IDYuMTI4NDRDMTMuNDE2OCA2LjM3NTkyIDEzLjU4NjggNi4zODg0MiAxMy41ODY4IDYuMzg4NDJMMTUuMDYwNSA1LjQ1NzIyQzE1LjA2MDUgNS40NTcyMiAxNS4zNzggNS41MjIyMiAxNS40MDggNS42Njk3MUMxNS40NDE3IDUuODM4NDUgMTUuMDc2NyA2LjE3NDY4IDE0LjgzOTIgNi40NDQ2N0MxNC42MDE4IDYuNzE1OTEgMTQuMDk0MyA3LjM2OTYyIDEzLjgzNTUgNy44NzgzNEMxMy41NzY4IDguMzg3MDcgMTMuNDAzMSA4LjcwNDU1IDEzLjQwMzEgOC43MDQ1NUwxMS4xNTk0IDEwLjkzMDdMNS4xODk3NiAxMC41Njk1TDQuNjgyMjggOC40ODIwNloiIGZpbGw9InVybCgjcGFpbnQwX3JhZGlhbF83MThfODgpIi8+CjxwYXRoIGQ9Ik0xMi4xNDU4IDYuMDI0NjNDMTEuOTk1OCA2LjA1OTYzIDExLjUyMjEgNi44Nzk1OCAxMS4wNzk2IDguMTI3MDJDMTAuODEzNCA4Ljg3Njk4IDEwLjYzNzEgOS42OTY5MyAxMC42NDcxIDEwLjA4OTRDMTAuNjU3MSAxMC40ODE5IDExLjI0OTYgMTAuMzYxOSAxMS4yNDk2IDEwLjM2MTlMMTEuNTAwOCA5LjU0Njk0QzExLjUwMDggOS41NDY5NCAxMS43ODIxIDguNTkwNzQgMTIuMTE0NiA3Ljg3NzAzQzEyLjUwMzMgNy4wMzk1NyAxMy4wMDcgNi41MDcxIDEyLjk2OTUgNi4zNzgzNkMxMi45MTk1IDYuMjA1ODcgMTIuMzU3IDUuOTc0NjMgMTIuMTQ1OCA2LjAyNDYzWiIgZmlsbD0iI0ZCREU5MyIvPgo8cGF0aCBkPSJNMTMuNjM0MyAzLjg5MjI4QzEzLjQyNDMgNC4wMTQ3OCAxMi45NTA2IDQuODA3MjQgMTIuODA5NCA1LjEwOTcyQzEyLjY2ODEgNS40MTIyIDEyLjY2MzEgNS45ODA5MiAxMi44MTgxIDYuMDU0NjdDMTMuMDA5MyA2LjE0NDY3IDEzLjIyMTggNS41MDIyIDEzLjU0NDMgNC45Njg0OEMxMy44NjY4IDQuNDM0NzYgMTQuMjQ4IDMuODMxMDQgMTQuMjQ4IDMuODMxMDRDMTQuMjQ4IDMuODMxMDQgMTMuODc1NSAzLjc1MTA0IDEzLjYzNDMgMy44OTIyOFoiIGZpbGw9IiNGQkRFOTMiLz4KPHBhdGggZD0iTTE0LjQyOTMgNS40NjIyQzE0LjEwODEgNS41OTg0NSAxMy41MDk0IDYuMjM3MTYgMTMuMzAxOSA2LjU1ODRDMTIuOTE5NCA3LjE1MjEyIDEyLjYzODEgNy44MTU4MyAxMi41MzY5IDguMDg3MDdDMTIuNDM2OSA4LjM1ODMgMTIuMzU1NyA4LjczMDc4IDEyLjY2ODEgOC41NTk1NEMxMi45ODA2IDguMzg4MyAxMy4zODE5IDcuNTgzMzQgMTMuODk1NiA2Ljg1OTYzQzE0LjQ5MDUgNi4wMTk2OCAxNS4wNjMgNS40NTA5NiAxNS4wNjMgNS40NTA5NkMxNS4wNjMgNS40NTA5NiAxNC42OTA1IDUuMzUwOTYgMTQuNDI5MyA1LjQ2MjJaIiBmaWxsPSIjRkJERTkzIi8+CjxwYXRoIGQ9Ik0xMy4wODA3IDEuODc5OTVDMTIuOTU2OSAxLjg2NDk1IDEyLjYxODIgMi43NjQ5IDEyLjQyNyAzLjE5NzM4QzEyLjIzNTcgMy42Mjk4NSAxMS45ODA3IDQuMjAyMzIgMTEuOTgwNyA0LjIwMjMyQzExLjk4MDcgNC4yMDIzMiAxMS44MzMzIDUuMzQxMDEgMTIuMDQ0NSA1LjM2MTAxQzEyLjI1NTcgNS4zODEwMSAxMi42Njk1IDMuOTkyMzMgMTIuOTc4MiAzLjM1NjEyQzEzLjI3OTQgMi43MzYxNSAxMy41OTY5IDIuMTYyNDMgMTMuNTk2OSAyLjE2MjQzQzEzLjU5NjkgMi4xNjI0MyAxMy4zMjE5IDEuOTA5OTQgMTMuMDgwNyAxLjg3OTk1WiIgZmlsbD0iI0ZCREU5MyIvPgo8cGF0aCBkPSJNMTAuODU1OCA0LjEzNDgxQzEwLjc0NDUgNC4yNTIzMSAxMC44MDcgNC44NTcyOCAxMC44MDcgNC44NTcyOEMxMC44MDcgNC44NTcyOCAxMS4yMzk1IDUuMTc5NzYgMTEuMzIwOCA1LjEwODUxQzExLjQwMiA1LjAzNzI3IDExLjQwNTggMy45NDQ4MiAxMS40MDU4IDMuOTQ0ODJDMTEuNDA1OCAzLjk0NDgyIDExLjAzNyAzLjk0MzU3IDEwLjg1NTggNC4xMzQ4MVoiIGZpbGw9IiNGQkRFOTMiLz4KPHBhdGggZD0iTTkuOTYyMTIgNC45NTg0OEM5LjgwNTg3IDUuMDg5NzMgOS45MTIxMiA1LjcwNDY5IDkuODcyMTIgNi4yNTU5MkM5LjgxMjEyIDcuMDgwODcgOS41NzA4OSA4Ljc3MDc4IDkuNDk5NjQgOS4yNjQ1MUM5LjQyODM5IDkuNzU4MjMgOS4zNTg0IDEwLjMwMDcgOS4zNTg0IDEwLjMwMDdMMTAuMTMzNCAxMC4xNzk1QzEwLjEzMzQgMTAuMTc5NSAxMC40MzQ2IDcuODc1ODMgMTAuNTU1OCA2Ljk4OTYzQzEwLjY3NzEgNi4xMDM0MiAxMC44NTcxIDQuODQ3MjQgMTAuNzM3MSA0Ljc3NTk5QzEwLjYxNTggNC43MDcyNSAxMC4xNTM0IDQuNzk3MjQgOS45NjIxMiA0Ljk1ODQ4WiIgZmlsbD0iI0ZCREU5MyIvPgo8cGF0aCBkPSJNOS44NjgyNyAyLjAxOTkxQzkuNjcwNzggMi4wNTc0MSA5LjI1MzMgMy4wOTk4NSA5LjI1MzMgMy4wOTk4NUM5LjI1MzMgMy4wOTk4NSA5LjIyODMgMy44NTIzMSA5LjIyODMgNC4yMTQ3OUM5LjIyODMgNC41NzcyNyA5LjE1ODMxIDUuMDA5NzUgOS4yNzgzIDUuMDA5NzVDOS4zOTgyOSA1LjAwOTc1IDkuNjMwNzggNC4zMTYwNCA5Ljg3MjAyIDMuNzEyMzJDMTAuMTEzMyAzLjEwODYgMTAuNDgzMiAyLjA5NjE1IDEwLjQ4MzIgMi4wOTYxNUMxMC40ODMyIDIuMDk2MTUgMTAuMTQ4MyAxLjk2NzQxIDkuODY4MjcgMi4wMTk5MVoiIGZpbGw9IiNGQkRFOTMiLz4KPHBhdGggZD0iTTcuNTk3MjcgMS42Mzg2MUM3LjQ2MTAzIDEuNzgyMzUgNy42NzcyNiAyLjE1MjMzIDcuNjk3MjYgMy4xNjcyOEM3LjcxNzI2IDQuMTgzNDcgNy41OTIyNyA1LjU4MDkgNy41OTIyNyA1LjU4MDlDNy41OTIyNyA1LjU4MDkgNy42OTk3NiA1LjU3MzQgNy43NjcyNiA1Ljc4MjE0QzcuODI0NzYgNS45NTgzOCA3LjkxODUgNy4wMjk1NyA4LjAzODQ5IDYuOTg5NThDOC4xNTk3NCA2Ljk0OTU4IDguMzA5NzMgNi4yMzQ2MiA4LjQzMDk3IDUuMDI3MThDOC41NTIyMiAzLjgxOTc0IDguNTAwOTcgMi44ODQ3OSA4LjQ2MDk3IDIuNTAyMzFDOC40MjA5NyAyLjExOTgzIDguMjkwOTggMS40MjczNyA4LjI5MDk4IDEuNDI3MzdDOC4yOTA5OCAxLjQyNzM3IDcuNzk4NTEgMS40MjczNyA3LjU5NzI3IDEuNjM4NjFaIiBmaWxsPSIjRkJERTkzIi8+CjxwYXRoIGQ9Ik01Ljk3OTgxIDIuNzE2MDZDNS45Nzk4MSAyLjcxNjA2IDUuMzk4NTkgMi43NTczMSA1LjIzODYgMy4wNjk4QzUuMDc3MzUgMy4zODIyOCA1LjEyODYgMy41MzM1MiA1LjE1ODYgMy44MzQ3NkM1LjE4ODYgNC4xMzU5OSA1LjI4MzU5IDQuNzM3MjEgNS4yODM1OSA0LjczNzIxQzUuMjgzNTkgNC43MzcyMSA1LjgxNzMyIDUuNDAwOTIgNi4xMDg1NSA1LjIzOTY4QzYuMzk5NzggNS4wNzk2OSA1Ljk3OTgxIDIuNzE2MDYgNS45Nzk4MSAyLjcxNjA2WiIgZmlsbD0iI0ZCREU5MyIvPgo8cGF0aCBkPSJNMy42NDM2MyAzLjQ0ODU0QzMuNjQzNjMgMy40NDg1NCAzLjMyMTE0IDMuMzg4NTUgMy4xMjk5IDMuNDI4NTRDMi45Mzg2NiAzLjQ2ODU0IDIuOTQ4NjYgMy42NDk3OCAzLjAzOTkxIDMuNzcxMDNDMy4xMjk5IDMuODkyMjcgMy41NDYxMyA0LjY0NDczIDMuNTQ2MTMgNC42NDQ3M0MzLjU0NjEzIDQuNjQ0NzMgMy42Njk4OCA0LjY0NzIzIDMuODMxMTIgNC43MDcyM0MzLjk5MjM2IDQuNzY3MjIgNC4yNjg1OSA0LjkxNzIyIDQuMjU3MzQgNC42NzU5OEM0LjIzNzM1IDQuMjU0NzUgMy42NDM2MyAzLjQ0ODU0IDMuNjQzNjMgMy40NDg1NFoiIGZpbGw9IiNGQkRFOTMiLz4KPHBhdGggZD0iTTQuNTUyMzEgOC4xOTk1N0M0LjU1MjMxIDguMTk5NTcgMy45NDQ4NCA2Ljk3MDg4IDMuNDkyMzYgNi4yOTU5MkMzLjAzOTg5IDUuNjIwOTUgMi41NjYxNiA1LjA5ODQ4IDIuNTY2MTYgNC45ODg0OUMyLjU2NjE2IDQuODc3MjQgMi44Nzg2NSA0Ljc2NzI1IDMuMDU4NjQgNC43MTcyNUMzLjIzODYzIDQuNjY3MjYgMy41NTIzNiA0LjY1NjAxIDMuNTUyMzYgNC42NTYwMUMzLjU1MjM2IDQuNjU2MDEgNC4yOTYwNyA1LjkxMzQ0IDQuNTk4NTUgNi41OTg0QzQuOTAxMDQgNy4yODMzNyA1LjE4MjI3IDguMDgwODMgNS4xMTIyOCA4LjE3ODMyQzUuMDQ4NTMgOC4yNjU4MiA0LjU5NzMgOC4yMDcwNyA0LjU1MjMxIDguMTk5NTdaIiBmaWxsPSIjRkJERTkzIi8+CjxwYXRoIGQ9Ik00Ljk2MTIxIDYuMDA0NjVDNC45MTM3MiA2LjE4MDg5IDUuMTQyNDUgNi45ODA4NSA1LjI4MzcgNy41NTQ1N0M1LjQyNDk0IDguMTI4MjkgNS41OTYxOCA4LjczMiA1LjU5NjE4IDguNzMyTDYuMDc4NjUgOS43MTgyTDYuNjEyMzggOS41MjY5NkM2LjYxMjM4IDkuNTI2OTYgNi4zMzExNCA4Ljk4MzI0IDYuMTY5OSA4LjQyMDc3QzYuMDA4NjYgNy44NTU4IDUuNzk5OTIgNi4xMDMzOSA1LjU4NDkzIDUuODc0NjVDNS40MzQ5NCA1LjcxMzQxIDQuOTkyNDYgNS44OTQ2NSA0Ljk2MTIxIDYuMDA0NjVaIiBmaWxsPSIjRkJERTkzIi8+CjxwYXRoIGQ9Ik02LjYzMjMyIDUuODAzNDhDNi41MDYwNyA1Ljg4NTk4IDYuNzAyMzEgNy4yMjIxNiA2LjgwMzU2IDcuNzc1ODhDNi45MDQ4IDguMzI5NiA3LjExNjA0IDguOTEzMzIgNy4yMjYwNCA5LjE4NDU2QzcuMzM3MjggOS40NTU3OSA3LjY1ODUxIDEwLjE0MDggNy42NTg1MSAxMC4xNDA4QzcuNjU4NTEgMTAuMTQwOCA3Ljk5MSAxMC41MzMyIDguMDAxIDEwLjQ5MzJDOC4wMTA5OSAxMC40NTMyIDguMTUyMjQgOS45NTk1MSA4LjE1MjI0IDkuOTU5NTFDOC4xNTIyNCA5Ljk1OTUxIDcuNzA4NTEgOC42NTA4MyA3LjYzNzI2IDguMDg3MTFDNy41NjcyNyA3LjUyMzM5IDcuNDU2MDIgNS44MTM0OCA3LjM0NjAzIDUuNzUzNDlDNy4yMzYwNCA1LjY5MzQ5IDYuODMzNTYgNS42NzM0OSA2LjYzMjMyIDUuODAzNDhaIiBmaWxsPSIjRkJERTkzIi8+CjxwYXRoIGQ9Ik04Ljg3NTgyIDUuODMzNDRDOC43MjQ1OCA1Ljk3MzQzIDguNTc0NTkgNy44MjU4NCA4LjUxMzM0IDguNTA5NTVDOC40NTIxIDkuMTkzMjYgOC40MDIxIDEwLjE5OTUgOC40MDIxIDEwLjE5OTVMOS4wNzU4MSAxMC4xNDk1QzkuMDc1ODEgMTAuMTQ5NSA5LjMyNzA1IDcuNjY0NTkgOS4zNjcwNSA3LjM0MjExQzkuNDA3MDUgNy4wMTk2MyA5LjU4ODI5IDYuMjg1OTIgOS40OTgyOSA1Ljk5MzQzQzkuNDA4MyA1LjcwMzQ1IDkuMDA1ODIgNS43MTM0NSA4Ljg3NTgyIDUuODMzNDRaIiBmaWxsPSIjRkJERTkzIi8+CjxwYXRoIGQ9Ik00LjM1MjMzIDguNDI4MzRDNC4zMDk4MyA4LjUyNDU5IDUuNjIzNTEgMTUuNzEzIDUuNjQ0NzYgMTUuODIwNUM1LjY2NjAxIDE1LjkyNzkgNS42MjM1MSAxNi4wMjU0IDUuODI3MjUgMTYuMTIxN0M2LjAzMjI0IDE2LjIxNzkgNi4zNTIyMiAxNi4yNjkyIDYuNTU3MjEgMTYuMjkxN0M2Ljc2MjIgMTYuMzEyOSA4LjE3MzM4IDE2LjMxNTQgOC4xODQ2MyAxNi4xNDQyQzguMTk1ODcgMTUuOTcxNyAxMi40MTQ0IDkuODI3MDIgMTIuNDE0NCA5LjgyNzAyQzEyLjQxNDQgOS44MjcwMiAxMy4xOTE5IDguNTIzMzQgMTMuMTM4MSA4LjQxNTg0QzEzLjA4NDQgOC4zMDgzNSAxMi44NzQ0IDguMzU4MzUgMTIuNjQ1NiA4LjUxNDU5QzEyLjQxNjkgOC42NzIwOCAxMi4yMDU3IDkuMDYwODEgMTEuNTYzMiA5LjQ5MjA0QzEwLjg2NyA5Ljk1OTUxIDkuOTQ0NTMgMTAuMDU4MyA5LjAxNzA4IDEwLjA0N0M4LjE2MjEzIDEwLjAzNTggNy4xNDU5MyA5Ljg4MDc3IDYuNDE4NDcgOS4zNDIwNEM1Ljc3ODUgOC44NjcwNyA1LjU4OTc2IDguNTI0NTkgNS4wMTk3OSA4LjM3MzM1QzQuNTE3MzIgOC4yMzk2IDQuMzUyMzMgOC40MjgzNCA0LjM1MjMzIDguNDI4MzRaIiBmaWxsPSIjRkQ3MDNGIi8+CjxwYXRoIGQ9Ik01LjM0NjE5IDguODk4MjVMNi41Mzg2MyAxNi4yOTQxQzYuNTM4NjMgMTYuMjk0MSA3Ljk1OTggMTYuNDk5MSA5LjA2NzI1IDE2LjUyMDNDMTAuMTc1OSAxNi41NDE2IDExLjQ5NzEgMTYuMzEwNCAxMS40OTcxIDE2LjMxMDRMMTIuNDU3MSAxMy4wNjU1TDEzLjI3MiA4Ljc4NDVDMTMuMjcyIDguNzg0NSAxMy4wNzA4IDguNTgzMjYgMTIuODQ0NSA4Ljc0OTVDMTIuNjcwOCA4Ljg3NyAxMi4xMzQ2IDkuNzA2OTUgMTEuMTg3MSAxMC4wNzMyQzEwLjExODQgMTAuNDg1NyA4LjE5MjI5IDEwLjUxNTcgNi45ODk4NSAxMC4wMDgyQzUuODg3NDEgOS41NDQ0NiA1LjU1MTE4IDguOTQwNzQgNS4zNDYxOSA4Ljg5ODI1WiIgZmlsbD0iI0ZGMkEyMyIvPgo8cGF0aCBkPSJNMTIuMTg4MyAxNi4wODI5QzExLjk0NTggMTYuMjI3OSAxMS4zODA5IDE2LjMyNTMgMTEuMzgwOSAxNi4zMjUzQzExLjM4MDkgMTYuMzI1MyAxMS45NDA4IDEzLjYwMyAxMi4yNTIxIDEyLjE4MThDMTIuNTYzMyAxMC43NjA2IDEzLjAxMiA4LjM1NzAxIDEzLjAxMiA4LjM1NzAxQzEzLjAxMiA4LjM1NzAxIDEzLjE5MiA4LjMyODI3IDEzLjM3NyA4LjM1MDc2QzEzLjU4NyA4LjM3NzAxIDEzLjcxMiA4LjQzODI2IDEzLjc1NDUgOC41MDQ1MUMxMy44MjMyIDguNjEyIDEzLjQwOTUgMTAuNjE5NCAxMy4wMDQ1IDEyLjU3OTNDMTIuNjM0NSAxNC4zNzkyIDEyLjMxNzEgMTYuMDA2NiAxMi4xODgzIDE2LjA4MjlaIiBmaWxsPSIjREMwRDI1Ii8+CjxkZWZzPgo8cmFkaWFsR3JhZGllbnQgaWQ9InBhaW50MF9yYWRpYWxfNzE4Xzg4IiBjeD0iMCIgY3k9IjAiIHI9IjEiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBncmFkaWVudFRyYW5zZm9ybT0idHJhbnNsYXRlKDkuMDcyOSAwLjAxNTk3MDYpIHNjYWxlKDEwLjM5MDUgMTAuMzkwNSkiPgo8c3RvcCBvZmZzZXQ9IjAuNDQ3IiBzdG9wLWNvbG9yPSIjRkNDMjE2Ii8+CjxzdG9wIG9mZnNldD0iMC41ODk3IiBzdG9wLWNvbG9yPSIjRkFCQjE2Ii8+CjxzdG9wIG9mZnNldD0iMC43OTM5IiBzdG9wLWNvbG9yPSIjRjNBODE0Ii8+CjxzdG9wIG9mZnNldD0iMC45MjEyIiBzdG9wLWNvbG9yPSIjRUQ5OTEzIi8+CjwvcmFkaWFsR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg==": dashboardIconPathPrefix + "fries", + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTIuNzM4NTcgMTIuMjMzMUMyLjczODU3IDEyLjIzMzEgMi42MTg1OCAxMy4wMTgxIDIuODk3MzIgMTMuNzEwNUMzLjEyOTggMTQuMjkwNSAzLjczMjI3IDE1LjA1NDIgNC44ODQ3MSAxNS42MzY3QzYuMDM3MTUgMTYuMjE5MSA3LjM2MDgzIDE2LjQyNDEgOS4zMTQ0OCAxNi40NjQxQzExLjI2ODEgMTYuNTA1NCAxMy4yNzU1IDE2LjA0OTIgMTQuMjM5MiAxNS4yMDNDMTUuMjA0MiAxNC4zNTU1IDE1LjU0MTYgMTMuNjQzIDE1LjUxNDIgMTIuODY5M0MxNS40ODY3IDEyLjA5NTYgMTUuMDc1NCAxMS44MDE5IDE1LjA3NTQgMTEuODAxOUwyLjczODU3IDEyLjIzMzFaIiBmaWxsPSIjRkNCNzQ1Ii8+CjxwYXRoIGQ9Ik0zLjI5NzQgMTEuNDQ0M0wyLjcyNzQzIDEyLjAxNDNDMi43Mjc0MyAxMi4wMTQzIDIuNTcyNDQgMTIuNzc2OCAzLjQzMjQgMTMuNjAxN0M0LjA4MzYxIDE0LjIyNTQgNS4yMzYwNSAxNS4zMDkxIDkuMDYzMzUgMTUuMzM3OUMxMi42NDU3IDE1LjM2NTQgMTMuNzcxOSAxNC41Nzc5IDE0LjQ2MzEgMTQuMDIxN0MxNS4xNTU1IDEzLjQ2NTUgMTUuNDE1NSAxMi40OTkzIDE1LjM0NjggMTIuMjI4QzE1LjI3OTMgMTEuOTU2OCAxNS4wNjY4IDExLjgwMDYgMTUuMDY2OCAxMS44MDA2TDMuMjk3NCAxMS40NDQzWiIgZmlsbD0iI0U3OEIyMCIvPgo8cGF0aCBkPSJNMy41NDExMyAxMi4yNTgxQzMuNTQxMTMgMTIuMjU4MSAzLjM3OTg5IDEyLjUzMTkgMy40MzIzOCAxMi43MDU2QzMuNDczNjMgMTIuODQxOSAzLjU1NDg4IDEzLjM5ODEgNS4xNjg1NCAxNC4xMDNDNi4wMjIyNSAxNC40NzY4IDcuNDc0NjcgMTQuNzgxOCA5LjM2MDgyIDE0LjcyNjhDMTEuNTk4MiAxNC42NjE4IDEzLjM5MDYgMTQuMjExOCAxNC4yNTgxIDEzLjM5NjhDMTUuMTQ2OCAxMi41NjMxIDE0Ljk3NjggMTIuMDY2OSAxNC45NzY4IDEyLjA2NjlMMy41NDExMyAxMi4yNTgxWiIgZmlsbD0iI0ZGRDI5MCIvPgo8cGF0aCBkPSJNMy4wNzk3OSAxMC4zNTgyQzMuMDc5NzkgMTAuMzU4MiAyLjU0NDgyIDEwLjkwMzEgMi40NTYwNyAxMS4xNDU2QzIuMzcyMzMgMTEuMzc0NCAyLjM4MzU4IDExLjcwNjggMi41MzEwNyAxMS45NzU2QzIuNzI5ODEgMTIuMzM1NSAyLjk3NzMgMTIuMzczIDMuMjgzNTMgMTIuNTU2OEMzLjYxNDc2IDEyLjc1NDMgNC4xMzg0OSAxMy4zNzA1IDUuNDI3MTcgMTMuNjQxN0M2LjcxNTg1IDEzLjkxMyA4LjQxMjAxIDE0LjAzNTUgOS4zMjA3MSAxNC4wMzU1QzEwLjIyOTQgMTQuMDM1NSAxMS40MzY5IDE0LjAyMTcgMTIuNTYzIDEzLjY0MTdDMTMuNjg5MiAxMy4yNjE4IDE0Ljk3NzkgMTIuNTE1NSAxNS4yNzY2IDExLjg1MDZDMTUuNTc1NCAxMS4xODU2IDE1LjI5MDQgMTAuNzkxOSAxNS4yOTA0IDEwLjc5MTlMMTQuNjM5MiAxMC4zNTgySDMuMDc5NzlaIiBmaWxsPSIjNkQ1NDREIi8+CjxwYXRoIGQ9Ik0yLjc2NzMyIDkuNjY2OTNDMi43NjczMiA5LjY2NjkzIDIuMzUyMzUgOS42NDgxOCAyLjI1MjM1IDEwLjI3NjlDMi4xNzk4NiAxMC43MzQ0IDIuNTA5ODQgMTEuMDIzMSAyLjUwOTg0IDExLjAyMzFDMi41MDk4NCAxMS4wMjMxIDIuMzM0ODUgMTEuMjc0MyAyLjUzNzM0IDExLjU5MzFDMi43MjczMyAxMS44OTE4IDIuOTMxMDcgMTEuOTU5MyAzLjM1MTA0IDEyLjAyNjhDMy42NTM1MyAxMi4wNzU2IDMuODEyMjcgMTEuOTg1NiA0LjIwNiAxMi4yMDNDNC42OTk3MiAxMi40NzU1IDQuOTEwOTYgMTIuOTYzIDUuNTg5NjggMTMuMTExN0M2LjI2ODM5IDEzLjI2MDUgNy4wMjgzNSAxMy4xMjU1IDcuMDU0NiAxMy4wMTY4QzcuMDgwODUgMTIuOTA4IDYuMTA0NjUgMTIuNTAxOCA0LjM0MDk5IDExLjUxMDZDMy43NDYwMiAxMS4xNzY5IDMuMjAxMDUgMTAuNzIzMSAzLjA1MjMxIDEwLjQ5MzFDMi45MDM1NyAxMC4yNjMxIDIuNzY3MzIgOS42NjY5MyAyLjc2NzMyIDkuNjY2OTNaIiBmaWxsPSIjOUI3MTY5Ii8+CjxwYXRoIGQ9Ik0xMS4zMDIgMTIuOTc2OEMxMS4zMDIgMTMuMTUzMSAxMS44ODU3IDEzLjA3MTggMTIuMTU3IDEyLjkyMzFDMTIuNDI4MiAxMi43NzQzIDEyLjg2MTkgMTIuMzk0MyAxMy4yNTU2IDEyLjIzMDZDMTMuNjQ5NCAxMi4wNjgxIDEzLjc5ODEgMTIuMjE2OSAxNC4zNjgxIDExLjk0NTZDMTQuOTM4MSAxMS42NzQ0IDE1LjI1MDUgMTEuMDM2OSAxNS4yNTA1IDExLjAzNjlDMTUuMjUwNSAxMS4wMzY5IDE1LjUyOCAxMC42OTQ0IDE1LjU1MDUgMTAuNDU0NEMxNS41ODU1IDEwLjA4ODIgMTUuNTQ5MyA5LjkxMDcyIDE1LjI5MTggOS44Mjk0OEMxNS4wMzQzIDkuNzQ4MjMgMTQuNDczMSAxMC41OTE5IDE0LjM5MTggMTAuNjg2OUMxNC4zMTA2IDEwLjc4MTkgMTMuMTA4MiAxMS42NjA2IDEyLjYzMzIgMTEuOTk5NEMxMi4xNTgyIDEyLjMzODEgMTEuMzAyIDEyLjg4MTggMTEuMzAyIDEyLjk3NjhaIiBmaWxsPSIjOUI3MTY5Ii8+CjxwYXRoIGQ9Ik0zLjI5NzMzIDkuMjA1NzdDMy4yOTczMyA5LjIwNTc3IDIuNjE4NjIgOS4yODcwMiAyLjUyMzYyIDkuNTcyQzIuNDI4NjMgOS44NTY5OSAyLjcyNzM2IDEwLjI3NyAzLjUxMzU3IDEwLjgwNjlDNC4xNTg1MyAxMS4yNDA3IDYuMzY4NDIgMTIuMjg0NCA3LjI0NDYyIDEyLjgyODFDOC4yNDgzMiAxMy40NTE4IDkuMDk4MjcgMTQuMzIzIDkuMjM5NTIgMTQuMzM0M0M5LjQxNTc2IDE0LjM0OCAxMC42Mzk0IDEzLjA0ODEgMTIuMzMzMSAxMS45MDU2QzEzLjk0MyAxMC44MTgyIDE1LjA0NjcgMTAuNTQ5NCAxNS40NDA0IDkuNzQ4MjRDMTUuNjUxNyA5LjMxODI2IDE1LjE0MTcgOS4wNjk1MyAxNS4xNDE3IDkuMDY5NTNMOC42MDIwNSA2LjU4NzE2TDMuMjk3MzMgOS4yMDU3N1oiIGZpbGw9IiNGRkU4NjkiLz4KPHBhdGggZD0iTTkuMzIwODggMTMuNjAxOEM5LjUyMzM3IDEzLjU3OTMgMTAuNTgyMSAxMi41NTY4IDExLjgzMDcgMTEuNzcwNkMxMy4wNzk0IDEwLjk4NDQgMTQuOTI0MyAxMC4xNDIgMTUuMTk1NiA5LjY2ODIzQzE1LjQ2NjggOS4xOTQ1IDE0LjgxNTYgOC44ODA3NyAxNC44MTU2IDguODgwNzdMNy4yMTcyNCA3LjYwNTgzTDIuNzY3NDcgOS4zNTU3NEMyLjc2NzQ3IDkuMzU1NzQgMi41MjM3NCA5LjQzNjk5IDIuNjg2MjMgOS42NDA3M0MyLjg0ODcyIDkuODQ0NDcgMy4xMDYyMSAxMC4xODMyIDMuNjc2MTggMTAuNTM1N0M0LjM2ODY0IDEwLjk2NDQgNi4xNDYwNCAxMS42NzU2IDcuMjcwOTkgMTIuMjg1NkM4LjM5ODQzIDEyLjg5NTYgOS4wNzcxNCAxMy42MjggOS4zMjA4OCAxMy42MDE4WiIgZmlsbD0iI0ZGQkUwMSIvPgo8cGF0aCBkPSJNMy4zNzk4OCA5LjU1ODI3TDMuNjE2MTIgMTAuMTkwN0MzLjYxNjEyIDEwLjE5MDcgNS4zMTYwMyAxMS45MDQ0IDguNjk3MSAxMS45NzE5QzEyLjA2MTkgMTIuMDM5NCAxMy45NDgxIDEwLjQzODIgMTMuOTQ4MSAxMC40MzgyTDE0LjM3ODEgOS4zNTgyOEwzLjM3OTg4IDkuNTU4MjdaIiBmaWxsPSIjRjQ0ODJCIi8+CjxwYXRoIGQ9Ik03LjgxMjIyIDEwLjYwMzFMNy41NDU5OCAxMS4wMjk0QzcuNTQ1OTggMTEuMDI5NCA4LjExNDcgMTEuMzcwNiA4LjI3ODQ0IDExLjQyOTRDOC41MTM0MyAxMS41MTMxIDguNzQ0NjcgMTEuNDgzMSA5LjA5MDkgMTEuMzc1NkM5LjQzNzEzIDExLjI2OTQgMTAuODc0NiAxMC42OTY5IDEwLjg3NDYgMTAuNjk2OUMxMC44NzQ2IDEwLjY5NjkgMTIuMDYzMiAxMC43MTQ0IDEyLjgwOTUgMTAuNzI4MUMxMy40NzA3IDEwLjc0MDYgMTQuMTMzMSAxMC41MDMyIDE0LjQ3OTQgMTAuMTAzMkMxNC44MjU2IDkuNzAzMiAxNS4zNjMxIDguODQ1NzQgMTUuMzYzMSA4Ljg0NTc0QzE1LjM2MzEgOC44NDU3NCAxNS42NDMxIDguNzc5NSAxNS44MjkzIDguNTY1NzZDMTYuMDE1NSA4LjM1MjAyIDE2LjAyOTMgOC4xNjU3OCAxNi4wMjkzIDguMTY1NzhDMTYuMDI5MyA4LjE2NTc4IDE1Ljk3NTUgNy44MDU4IDE1Ljc3NjggNy43MTMzQzE1LjU3NjggNy42MTk1NiA2LjY2OTc4IDcuMDc0NTggNi42Njk3OCA3LjA3NDU4TDMuMTAxMjIgNy4zNDA4MkwyLjUgOC41Nzk1MUw0LjQ4MzY1IDEwLjI3MDdMNy4yNzk3NSAxMC40MzA3TDcuODEyMjIgMTAuNjAzMVoiIGZpbGw9IiNBNkI3MzIiLz4KPHBhdGggZD0iTTIuNjg2MTQgNy40ODgzN0MyLjQ4NDkgNy4zOTcxMyAyLjI3MzY2IDcuNDA4MzggMi4xNzk5MiA3LjY3NDYxQzIuMDg2MTcgNy45NDA4NSAyLjExNDkyIDguMDU4MzQgMi4xMTQ5MiA4LjA1ODM0QzIuMTE0OTIgOC4wNTgzNCAxLjg0MTE4IDguMTE1ODQgMS44MDc0NCA4LjQ1OTU3QzEuNzY3NDQgOC44NzIwNSAyLjIxOTkxIDkuMDE4MjkgMi4yMTk5MSA5LjAxODI5QzIuMjE5OTEgOS4wMTgyOSAyLjI0MTE2IDkuMTc4MjggMi40MTk5IDkuMzM4MjhDMi42MjM2NCA5LjUyMDc3IDIuODI3MzggOS41MDIwMiAyLjgyNzM4IDkuNTAyMDJDMi44MjczOCA5LjUwMjAyIDMuMzY0ODUgMTAuMDMwNyAzLjcyNDgzIDEwLjI0MzJDNC4wODQ4MiAxMC40NTU3IDQuNjAzNTQgMTAuNjAzMiA1LjEyMzUxIDEwLjU4OTVDNS42NDM0OCAxMC41NzU3IDUuODE1OTcgMTAuMzM3IDYuMjAyMiAxMC40Njk1QzYuNTg4NDMgMTAuNjAyIDcuNjEyMTMgMTEuMTQwNyA4LjAyNDYxIDExLjE0OTRDOC42NzcwNyAxMS4xNjMyIDEwLjA4ODIgMTAuMTUwNyAxMC40MDgyIDEwLjE2NDVDMTAuNzI4MiAxMC4xNzgyIDEyLjE3OTQgMTAuNDA0NSAxMi45NTE4IDEwLjQwNDVDMTMuNzI0MyAxMC40MDQ1IDE0LjAwNDMgMTAuMTc4MiAxNC4xNzY4IDkuOTUxOTlDMTQuMzQ5MyA5LjcyNTc2IDE0LjY0MyA5LjE1MzI5IDE0LjU0OTMgOS4xMzk1NEMxNC40NTU1IDkuMTI1NzkgMTMuODQzIDkuNjMyMDEgMTMuNDA0MyA5LjU5MjAxQzEyLjk2NDMgOS41NTIwMSAxMi41Nzk0IDkuMjk5NTMgMTIuMDMzMSA5LjI5OTUzQzExLjQ4NjkgOS4yOTk1MyA4LjgyNDU2IDkuNjQ1NzYgOC42NTA4MiA5LjcyNTc2QzguNDc3MDggOS44MDU3NSA3LjczMjEyIDEwLjM3ODIgNy41ODU4OCAxMC4zNTJDNy40Mzk2NCAxMC4zMjU3IDYuOTMzNDEgOS44OTk1IDYuNzYwOTIgOS44MDU3NUM2LjU4ODQzIDkuNzEyMDEgNC43NzIyOCA5LjY1MjAxIDQuNTExMDQgOS42OTk1MUM0LjMyMzU1IDkuNzMzMjYgNC4wNTg1NyA5Ljc3OTUgNC4wNTg1NyA5Ljc3OTVDNC4wNTg1NyA5Ljc3OTUgNC4wODQ4MiA5LjM2NzAyIDMuODU4NTggOS4xMjcwNEMzLjYzMjM0IDguODg3MDUgMi44ODYxMyA4LjQ3NDU3IDIuODg2MTMgOC40NzQ1N0MyLjg4NjEzIDguNDc0NTcgMi45Nzg2MiA3LjYyMDg3IDIuNjg2MTQgNy40ODgzN1oiIGZpbGw9IiNDMkREMUYiLz4KPHBhdGggZD0iTTE0LjQ5NTYgOC4yMDcwN0MxNC40OTU2IDguMjA3MDcgMTUuMzA4MSA4LjIwNzA3IDE1LjUzNDMgOC4xOTMzMkMxNS43NjA1IDguMTc5NTcgMTYuMDI2OCA4LjE2NzA3IDE2LjAyNjggOC4xNjcwN0MxNi4wMjY4IDguMTY3MDcgMTUuOTQ5MyA3Ljc5ODM0IDE1LjgzMTggNy42MDIxQzE1LjU5MTggNy4xOTk2MiAxNS4wMTQzIDcuMDg4MzggMTUuMDE0MyA3LjA4ODM4TDE0LjQ5NTYgOC4yMDcwN1oiIGZpbGw9IiNDMkREMUYiLz4KPHBhdGggZD0iTTIuNDk5ODggNi45OTU4OEwyLjQxMTEzIDcuMzg4MzZDMi40MTExMyA3LjM4ODM2IDMuNjM5ODIgOS41NjQ0OSA5LjE2MjAzIDkuNjQ2OTlDMTIuMzk0NCA5LjY5NTc0IDE0LjI1MTggOC43MTQ1NCAxNS4wNzE3IDcuOTEyMDhDMTUuNTYyOSA3LjQzMDg1IDE1LjU3OTIgNi45NjMzOCAxNS41NzkyIDYuOTYzMzhMMi40OTk4OCA2Ljk5NTg4WiIgZmlsbD0iI0RFODAxMCIvPgo8cGF0aCBkPSJNMi4yMjAwMiA1LjkxMzQyQzIuMTUwMDMgNy4wMDA4NiAyLjQxNTAxIDcuMzkwODQgMi40MTUwMSA3LjM5MDg0QzIuNDE1MDEgNy4zOTA4NCA0LjI0MjQyIDkuMDQ1NzUgOC45MDk2NyA5LjEyMkMxMy42OTMyIDkuMTk5NSAxNS4yNDgxIDcuNjEzMzMgMTUuNTIwNiA3LjE1MDg1QzE1LjY5NDMgNi44NTcxMiAxNS44MzMxIDYuMjgwOSAxNS43MjMxIDUuNDQwOTRDMTUuNTQwNiA0LjA1MzUyIDEzLjk0MzIgMS40MjExNSA4Ljg4NzE3IDEuNDQxMTVDMy44MzExOSAxLjQ2MTE1IDIuMzIyNTIgNC4zMzQ3NSAyLjIyMDAyIDUuOTEzNDJaIiBmaWxsPSJ1cmwoI3BhaW50MF9yYWRpYWxfNzIxXzE4NCkiLz4KPHBhdGggZD0iTTcuMDIzNSAyLjc5NDg0QzcuMDYyMjUgMy4wMDczMyA2LjEwMzU1IDMuNzE0OCA1Ljk2NDgxIDMuMDM2MDhDNS44MzQ4MiAyLjM5NzM2IDYuOTg3MjUgMi41OTg2IDcuMDIzNSAyLjc5NDg0WiIgZmlsbD0iI0ZERUFDOCIvPgo8cGF0aCBkPSJNNy4zNjU4NiA0LjEyNjA1QzcuNDE1ODYgNC4yNTcyOSA4LjM5ODMxIDQuNDQ2MDMgOC4yOTMzMSAzLjg5MjMxQzguMTgzMzIgMy4zMTYwOSA3LjI2MzM3IDMuODU2MDYgNy4zNjU4NiA0LjEyNjA1WiIgZmlsbD0iI0ZERUFDOCIvPgo8cGF0aCBkPSJNOC4yMjcwOSAyLjYwNjA0QzguMjUyMDkgMi43OTQ3OCA5LjM5NzAzIDMuMDE0NzcgOS4zNzMyOCAyLjQ4MjNDOS4zNDQ1NCAxLjgyNDgzIDguMTk4MzUgMi4zODczIDguMjI3MDkgMi42MDYwNFoiIGZpbGw9IiNGREVBQzgiLz4KPHBhdGggZD0iTTYuMzM4NDYgNC41NjM1MkM2LjM0NDcxIDQuNjA2MDIgNi4wNjM0OCA0LjgyNzI1IDUuNzc1OTkgNC45MTM1QzUuNTIzNSA0Ljk4OTc1IDUuMjY0NzcgNC45NTcyNSA1LjIzNjAyIDQuNjQzNTFDNS4yMDg1MiA0LjM0MTAzIDUuNDIzNTEgNC4yMzEwNCA1LjY4MSA0LjI1NzI4QzUuOTc3MjMgNC4yODYwMyA2LjMyNTk2IDQuNDgyMjcgNi4zMzg0NiA0LjU2MzUyWiIgZmlsbD0iI0ZERUFDOCIvPgo8cGF0aCBkPSJNOC43MTcwNSA1LjQ0NzI0QzguNjY5NTUgNS4yNDg1IDcuNTkzMzYgNC45NzIyNiA3LjY2NTg2IDUuNjAwOThDNy43Mzk2IDYuMjI3MiA4Ljc2ODMgNS42NTg0OCA4LjcxNzA1IDUuNDQ3MjRaIiBmaWxsPSIjRkRFQUM4Ii8+CjxwYXRoIGQ9Ik05LjcyNDQ4IDQuMDE2QzkuNjcxOTggMy45NTQ3NSA5LjQyNTc0IDMuOTY2IDkuMTI1NzYgNC4wNDQ3NUM4LjkwOTUyIDQuMTAyMjQgOC43NDk1MyA0LjM0NTk4IDguODk5NTIgNC41NzcyMkM5LjA3NDUxIDQuODQ3MjEgOS4zNjgyNCA0LjczMzQ2IDkuNTU1NzMgNC41MDQ3MkM5LjcwODIzIDQuMzE5NzMgOS43ODMyMiA0LjA4NDc1IDkuNzI0NDggNC4wMTZaIiBmaWxsPSIjRkRFQUM4Ii8+CjxwYXRoIGQ9Ik0xMC44NzcxIDIuNTU0ODJDMTAuODQzNCAyLjUxODU4IDEwLjU5NzEgMi40OTM1OCAxMC4zOTU5IDIuNTkxMDdDMTAuMTkzNCAyLjY4ODU3IDEwLjAxODQgMi45MjEwNiAxMC4yMDU5IDMuMTM4NTRDMTAuMzk1OSAzLjM1NzI4IDEwLjY3ODQgMy4yMzEwNCAxMC44MTIxIDIuOTk5OEMxMC45MTg0IDIuODEzNTYgMTAuOTI4NCAyLjYwOTgyIDEwLjg3NzEgMi41NTQ4MloiIGZpbGw9IiNGREVBQzgiLz4KPHBhdGggZD0iTTExLjY0OTUgNC40NTcyN0MxMS42MTcgNC4yNTM1MyAxMC43MTIgMy44OTEwNSAxMC43MTgzIDQuNDQ2MDJDMTAuNzI0NSA1LjAxNDc0IDExLjY3ODIgNC42NDEwMSAxMS42NDk1IDQuNDU3MjdaIiBmaWxsPSIjRkRFQUM4Ii8+CjxwYXRoIGQ9Ik0xMi45MDA1IDMuNDUyMjhDMTIuODcwNSAzLjI5NjA0IDExLjcxOTMgMy4xNjk3OSAxMS45MTU2IDMuNzUxMDFDMTIuMTE5MyA0LjM1NzIzIDEyLjkzNjggMy42NDIyNyAxMi45MDA1IDMuNDUyMjhaIiBmaWxsPSIjRkRFQUM4Ii8+CjxwYXRoIGQ9Ik0xMS4zMTU3IDUuNDk3MTRDMTEuMzIzMiA1LjMyMjE1IDEwLjQ0NyA0LjkyMDkyIDEwLjM4MiA1LjQ0NTg5QzEwLjMxNyA1Ljk3MDg3IDExLjMwNTcgNS43MjQ2MyAxMS4zMTU3IDUuNDk3MTRaIiBmaWxsPSIjRkRFQUM4Ii8+CjxkZWZzPgo8cmFkaWFsR3JhZGllbnQgaWQ9InBhaW50MF9yYWRpYWxfNzIxXzE4NCIgY3g9IjAiIGN5PSIwIiByPSIxIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgZ3JhZGllbnRUcmFuc2Zvcm09InRyYW5zbGF0ZSg4LjkzMjggMi45NDAxOCkgcm90YXRlKDg5LjM1ODMpIHNjYWxlKDUuNTA0ODEgOS41OTE5OCkiPgo8c3RvcCBvZmZzZXQ9IjAuNTE3IiBzdG9wLWNvbG9yPSIjREY4MDE2Ii8+CjxzdG9wIG9mZnNldD0iMC42NDI2IiBzdG9wLWNvbG9yPSIjRTU4QzIxIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0Y1QUMzQyIvPgo8L3JhZGlhbEdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo=": dashboardIconPathPrefix + "hamburger", + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMuNTMxNDggMTUuMDA0OEM0LjAzMjk3IDE0LjkwNzUgNC4zMjM4NCAxNC4yMzI2IDQuMTgxMTUgMTMuNDk3M0M0LjAzODQ3IDEyLjc2MiAzLjUxNjI2IDEyLjI0NDggMy4wMTQ3OCAxMi4zNDIxQzIuNTEzMjkgMTIuNDM5NCAyLjIyMjQyIDEzLjExNDQgMi4zNjUxIDEzLjg0OTdDMi41MDc3OSAxNC41ODUgMy4wMjk5OSAxNS4xMDIxIDMuNTMxNDggMTUuMDA0OFoiIGZpbGw9IiM2MTYxNjEiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yLjgwNTAxIDEzLjc3NDNDMi41OTc2MSAxMi4wMDgyIDIuNDcwMjQgMTAuMTU5MiAyLjU1MTY2IDguNDg2MzNMMy4zMDA3MyA4LjUyMjc4QzMuMjIyMTYgMTAuMTM3MyAzLjM0NDc4IDExLjk0MDUgMy41NDk4NSAxMy42ODY4TDIuODA1MDEgMTMuNzc0M1oiIGZpbGw9IiM2MTYxNjEiLz4KPHBhdGggZD0iTTE1LjYzMjcgMTMuODQ4MkMxNS43NzU0IDEzLjExMjkgMTUuNDg0NSAxMi40Mzc5IDE0Ljk4MyAxMi4zNDA2QzE0LjQ4MTUgMTIuMjQzMyAxMy45NTkzIDEyLjc2MDUgMTMuODE2NiAxMy40OTU4QzEzLjY3NCAxNC4yMzExIDEzLjk2NDggMTQuOTA2IDE0LjQ2NjMgMTUuMDAzNEMxNC45Njc4IDE1LjEwMDcgMTUuNDkgMTQuNTgzNSAxNS42MzI3IDEzLjg0ODJaIiBmaWxsPSIjNjE2MTYxIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTQuNDQ5NCAxMy42ODY4QzE0LjY1NDUgMTEuOTQwNSAxNC43NzcxIDEwLjEzNzMgMTQuNjk4NSA4LjUyMjc4TDE1LjQ0NzYgOC40ODYzM0MxNS41MjkgMTAuMTU5MiAxNS40MDE2IDEyLjAwODIgMTUuMTk0MiAxMy43NzQzTDE0LjQ0OTQgMTMuNjg2OFoiIGZpbGw9IiM2MTYxNjEiLz4KPHBhdGggZD0iTTE1LjI0MDUgOS4zMjU3MUMxNC41OTQzIDkuMjk1NzIgMTQuMDU4MSA4LjgxMDc0IDEzLjk3NDMgOC4xNjk1M0MxMy44ODA2IDcuNDQzMzEgMTMuNjM5NCA2LjYwNDYxIDEzLjAzNDQgNS43OTk2NUMxMi4wODMyIDQuNTM1OTcgMTAuNjkwOCAzLjg2NzI1IDkuMDA1ODYgMy44NjcyNUM3LjgyNDY3IDMuODY3MjUgNi4xNjQ3NiA0LjIwMjI0IDQuOTYzNTcgNS43OTk2NUM0LjM1MTEgNi42MTQ2MSA0LjExMjM3IDcuNDcwODEgNC4wMjExMiA4LjIwODI3QzMuOTQ0ODggOC44MTk0OSAzLjQ0OTkgOS4yOTMyMiAyLjgzMzY4IDkuMzIxOTdMMi43NTg2OSA5LjMyNTcxQzIuMzI2MjEgOS4zNDU3MSAxLjk2ODczIDguOTg4MjMgMS45OTM3MyA4LjU1N0MyLjA1MzczIDcuNTAzMzEgMi4zMTg3MSA1LjYzMjE2IDMuMzc0OTEgNC4yMjcyM0M0LjY5NzM0IDIuNDY3MzMgNi42OTcyMyAxLjQ5OTg4IDkuMDA1ODYgMS40OTk4OEMxMS4zMDU3IDEuNDk5ODggMTMuMzAwNiAyLjQ2NzMzIDE0LjYyMzEgNC4yMjQ3M0MxNS42ODA1IDUuNjMwOTEgMTUuOTQ0MiA3LjUwMzMxIDE2LjAwNTUgOC41NTU3NkMxNi4wMzA1IDguOTg4MjMgMTUuNjczIDkuMzQ1NzEgMTUuMjQwNSA5LjMyNTcxWiIgZmlsbD0iIzYxNjE2MSIvPgo8cGF0aCBkPSJNNS44NDEwNCAxNi40MTAzQzQuNjI2MSAxNi41NzkxIDMuMjMyNDMgMTUuNjgyOSAyLjk1MTE5IDEzLjYwOTJDMi42NzEyMSAxMS41NDQzIDMuNjUyNDEgMTAuMjg5NCA0LjY0NzM1IDEwLjA5NjlMNS44NDEwNCAxNi40MTAzWiIgZmlsbD0iIzQyNDI0MiIvPgo8cGF0aCBkPSJNNS45MTEwNSA5LjY2OTQ2TDQuODUzNiA5Ljg1MDdDNC40MjYxMyA5LjkzMzE5IDQuMDkxMTQgMTAuMjY0NCA0LjAwMzY1IDEwLjY5MTlDMy44ODYxNiAxMS4yNjgxIDMuODExMTYgMTIuMTg5MyA0LjA1MzY1IDEzLjQ0NTVDNC4yOTczOCAxNC43MDA0IDQuNzExMTEgMTUuNTI2NiA1LjAzNzM0IDE2LjAxNzlDNS4yNzczMyAxNi4zODA0IDUuNzEyMzEgMTYuNTYyOCA2LjEzOTc5IDE2LjQ3OTFMNy4xOTcyMyAxNi4yOTc5QzcuNzA4NDUgMTYuMTk5MSA3LjgzNDcgMTQuNjM0MiA3LjQ3ODQ3IDEyLjgwNDNDNy4xMjM0OCAxMC45NzQ0IDYuNDIyMjcgOS41NzA3MSA1LjkxMTA1IDkuNjY5NDZaIiBmaWxsPSIjNzU3NTc1Ii8+CjxwYXRoIG9wYWNpdHk9IjAuNSIgZD0iTTcuMzAwOTQgMTYuMDE2MkM3LjcyNzIxIDE1LjkzMzUgNy44MDY3OCAxNC40OTU4IDcuNDc4NjcgMTIuODA0OUM3LjE1MDU3IDExLjExNDEgNi41MzkwMyA5LjgxMDQ4IDYuMTEyNzYgOS44OTMyQzUuNjg2NSA5Ljk3NTkxIDUuNjA2OTMgMTEuNDEzNyA1LjkzNTAzIDEzLjEwNDVDNi4yNjMxNCAxNC43OTUzIDYuODc0NjggMTYuMDk4OSA3LjMwMDk0IDE2LjAxNjJaIiBmaWxsPSIjNDI0MjQyIi8+CjxwYXRoIGQ9Ik03LjIzMDk2IDEyLjg1M0M3LjQ1MzQ1IDEzLjk5NzkgNy41MDIyIDE0Ljk1MTYgNy4zNDA5NSAxNC45ODI5QzcuMTc5NzEgMTUuMDE0MSA2Ljg2NzIzIDE0LjExMTcgNi42NDU5OSAxMi45NjY3QzYuNDIzNSAxMS44MjE4IDYuMzc0NzYgMTAuODY4MSA2LjUzNiAxMC44MzY4QzYuNjk3MjQgMTAuODA1NiA3LjAwODQ3IDExLjcwODEgNy4yMzA5NiAxMi44NTNaIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfNzE1XzMwMikiLz4KPHBhdGggZD0iTTEyLjE1ODIgMTYuNDEwM0MxMy4zNzMxIDE2LjU3OTEgMTQuNzY2OCAxNS42ODI5IDE1LjA0ODEgMTMuNjA5MkMxNS4zMjggMTEuNTQ0MyAxNC4zNDY4IDEwLjI4OTQgMTMuMzUxOSAxMC4wOTY5TDEyLjE1ODIgMTYuNDEwM1oiIGZpbGw9IiM0MjQyNDIiLz4KPHBhdGggZD0iTTEyLjA4ODIgOS42Njk0NkwxMy4xNDU2IDkuODUwN0MxMy41NzMxIDkuOTMzMTkgMTMuOTA4MSAxMC4yNjQ0IDEzLjk5NTYgMTAuNjkxOUMxNC4xMTMxIDExLjI2ODEgMTQuMTg4MSAxMi4xODkzIDEzLjk0NTYgMTMuNDQ1NUMxMy43MDE4IDE0LjcwMDQgMTMuMjg4MSAxNS41MjY2IDEyLjk2MTkgMTYuMDE3OUMxMi43MjE5IDE2LjM4MDQgMTIuMjg2OSAxNi41NjI4IDExLjg1OTQgMTYuNDc5MUwxMC44MDIgMTYuMjk3OUMxMC4yOTA4IDE2LjE5OTEgMTAuMTY0NSAxNC42MzQyIDEwLjUyMDggMTIuODA0M0MxMC44NzU3IDEwLjk3NDQgMTEuNTc3IDkuNTcwNzEgMTIuMDg4MiA5LjY2OTQ2WiIgZmlsbD0iIzc1NzU3NSIvPgo8cGF0aCBvcGFjaXR5PSIwLjUiIGQ9Ik0xMi4wNjM3IDEzLjEwMzVDMTIuMzkxOCAxMS40MTI3IDEyLjMxMjIgOS45NzQ5NCAxMS44ODYgOS44OTIyMkMxMS40NTk3IDkuODA5NSAxMC44NDgyIDExLjExMzEgMTAuNTIwMSAxMi44MDRDMTAuMTkxOSAxNC40OTQ4IDEwLjI3MTUgMTUuOTMyNSAxMC42OTc4IDE2LjAxNTNDMTEuMTI0MSAxNi4wOTggMTEuNzM1NiAxNC43OTQzIDEyLjA2MzcgMTMuMTAzNVoiIGZpbGw9IiM0MjQyNDIiLz4KPHBhdGggZD0iTTEwLjc2ODMgMTIuODUzQzEwLjU0NTggMTMuOTk3OSAxMC40OTcxIDE0Ljk1MTYgMTAuNjU4MyAxNC45ODI5QzEwLjgxOTYgMTUuMDE0MSAxMS4xMzIgMTQuMTExNyAxMS4zNTMzIDEyLjk2NjdDMTEuNTc1OCAxMS44MjE4IDExLjYyNDUgMTAuODY4MSAxMS40NjMzIDEwLjgzNjhDMTEuMzAyIDEwLjgwNTYgMTAuOTkwOCAxMS43MDgxIDEwLjc2ODMgMTIuODUzWiIgZmlsbD0idXJsKCNwYWludDFfbGluZWFyXzcxNV8zMDIpIi8+CjxwYXRoIGQ9Ik0xNS44ODU1IDguNzYxOThDMTUuNTMxOCA5LjAwMTk3IDE1LjA5NTUgOC43MzE5OCAxNC45NjE4IDguMzY3QzE0LjgwNjggNy40NTcwNSAxNC41MDA2IDYuNDAwODYgMTMuOTAwNiA1LjUyOTY1QzEyLjc3NjkgMy44MDU5OSAxMC44ODgyIDIuOTQyMjkgOC45OTk2IDIuOTQxMDRDNy4xMTA5NSAyLjk0MjI5IDUuMjIyMyAzLjgwNTk5IDQuMDk4NjEgNS41Mjk2NUMzLjQ5ODY0IDYuNDAwODYgMy4xOTM2NSA3LjQ1ODMgMy4wMzc0MSA4LjM2N0MyLjkwMzY3IDguNzMxOTggMi40Njc0NCA5LjAwMTk3IDIuMTEzNzEgOC43NjE5OEMyLjA4NDk2IDguNzQxOTggMi4wNDEyMSA4LjY5MTk5IDEuOTk2MjIgOC42NjQ0OUMyLjAzMTIxIDkuMDQ1NzIgMi4zNjM3IDkuMzQxOTUgMi43NTg2OCA5LjMyNDQ1TDIuODY3NDIgOS4zMTk0NUMzLjYzOTg4IDkuMzI1NyAzLjk0NjExIDguODM0NDggNC4wMTczNiA4LjI0MDc2QzQuMTA2MSA3LjQ5NzA1IDQuMzQyMzQgNi42MjU4NSA0Ljk2NDgxIDUuNzk5NjRDNi4xNjM1IDQuMjA1OTcgNy44MTg0MSAzLjg2ODQ5IDguOTk5NiAzLjg2NzI0QzEwLjE4MDggMy44Njg0OSAxMS44MzU3IDQuMjA1OTcgMTMuMDM0NCA1Ljc5OTY0QzEzLjY0NjkgNi42MTQ2IDEzLjg4NTYgNy40NzA4IDEzLjk3NjggOC4yMDgyNkMxNC4wNTQzIDguODI2OTggMTQuMzU0MyA5LjMyNTcgMTUuMTk4IDkuMzIzMkwxNS4yMzkzIDkuMzI1N0MxNS42MzQyIDkuMzQ0NDUgMTUuOTY2NyA5LjA0Njk3IDE2LjAwMTcgOC42NjU3NEMxNS45NTggOC42OTE5OSAxNS45MTQyIDguNzQxOTggMTUuODg1NSA4Ljc2MTk4WiIgZmlsbD0iIzQyNDI0MiIvPgo8cGF0aCBkPSJNMi44MzEyMSA2LjIwMDkxQzIuNzE0OTcgNi4xMTg0MSAyLjYyMzcyIDYuMDI3MTcgMi43ODk5NiA1LjM5OTdDMi45OTEyIDQuNjQzNDkgMy45MjQ5IDMuMTc5ODIgNS44MDYwNSAyLjIwOTg3QzYuMTU0NzggMi4wMjk4OCA3LjAwMjI0IDEuNzYxMTQgNy4yNjA5OCAyLjIyNzM3QzcuNTA1OTYgMi42Njk4NCA2LjMyMjI4IDIuOTkyMzMgNS45MDQ4IDMuMjE2MDdDNC44MTIzNiAzLjgwMzUzIDQuMDIzNjUgNC42MzIyNCAzLjU5OTkyIDUuMjg3MjFDMy40NjQ5MyA1LjQ5NTk1IDMuMTA4NyA2LjM5NTkgMi44MzEyMSA2LjIwMDkxWiIgZmlsbD0iIzc1NzU3NSIvPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzcxNV8zMDIiIHgxPSI2LjY3OTY5IiB5MT0iMTIuOTYwOCIgeDI9IjcuMDM5MjMiIHkyPSIxMi44OTEiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agb2Zmc2V0PSIwLjIwMTYiIHN0b3AtY29sb3I9IiMyMTIxMjEiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMjEyMTIxIiBzdG9wLW9wYWNpdHk9IjAiLz4KPC9saW5lYXJHcmFkaWVudD4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDFfbGluZWFyXzcxNV8zMDIiIHgxPSIxMS4zMTk2IiB5MT0iMTIuOTYwOSIgeDI9IjEwLjk2IiB5Mj0iMTIuODkxMSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBvZmZzZXQ9IjAuMjAxNiIgc3RvcC1jb2xvcj0iIzIxMjEyMSIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMyMTIxMjEiIHN0b3Atb3BhY2l0eT0iMCIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo=": dashboardIconPathPrefix + "headphones", + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1LjkxNTMgMTQuNDA5MkwxNS4xOTY2IDE0LjA5NDNDMTUuMTkxNiAxNC4wNTU1IDE1LjE4NTQgMTQuMDE2OCAxNS4xNzU0IDEzLjk3OTNDMTUuMTIxNiAxMy43NzA1IDE0LjkyNDEgMTMuNTE2OCAxNC41OTc5IDEzLjQ0M0wxNC41MzE2IDEyLjUwNjhMMTMuODMxNyAxMi41NDY4TDEzLjkxOTIgMTMuNTQzQzEzLjY1OTIgMTMuNjg1NSAxMy41MzkyIDEzLjkzNjggMTMuNTQ3OSAxNC4yMjU1TDEyLjgxMyAxNC44MjE3TDEzLjI1MDUgMTUuMzcwNEwxMy44NDE3IDE0Ljg3M0MxMy45ODkyIDE0Ljk4NjcgMTQuMTgyOSAxNS4wODE3IDE0LjQyMjkgMTUuMDY4QzE0LjcxMTYgMTUuMDUxNyAxNC45MDkxIDE0LjkwMTcgMTUuMDMxNiAxNC43NDkyTDE1LjY2NzggMTUuMDkwNUwxNS45MTUzIDE0LjQwOTJaIiBmaWxsPSIjODc4Nzg3Ii8+CjxwYXRoIGQ9Ik02LjAxMzUgMTQuMTM3OUM1Ljg4MjI2IDEyLjQ2NDMgNC41MDIzMyAxMS45MTE4IDMuNDk5ODkgMTEuOTc0M0MyLjM1NDk1IDEyLjA0NjggMS4yOTM3NSAxMi44ODQzIDEuMzEyNSAxNC4zMTI5QzEuMzI4NzUgMTUuNTY5MSAyLjExNjIxIDE2LjU1NTMgMy43MjczNyAxNi41NTUzQzUuMDE3MzEgMTYuNTU2NiA2LjEwODUgMTUuMzU1NCA2LjAxMzUgMTQuMTM3OVpNMy42NzExMyAxNS43Mjc5QzIuNjk5OTMgMTUuNzQ1NCAyLjEzOTk2IDE1LjE3OTEgMi4xMzEyMSAxNC4zMjkyQzIuMTIzNzEgMTMuNTMzIDIuNzc0OTIgMTIuODQxOCAzLjU5NDg4IDEyLjg2NTVDNC4zODIzNCAxMi44ODggNC45NjIzMSAxMy4zMzQyIDUuMDU4NTUgMTQuMTYxN0M1LjE1NDggMTQuOTg5MSA0LjUyOTgzIDE1LjcxMjkgMy42NzExMyAxNS43Mjc5WiIgZmlsbD0iIzRENDUzQyIvPgo8cGF0aCBkPSJNMy41ODcyMyAxMy4xNjA1QzIuOTUxMDEgMTMuMTI2OCAyLjM5MzU0IDEzLjU2NTUgMi4zODYwNCAxNC4zNjkyQzIuMzc4NTQgMTUuMjIwNCAzLjE0OTc1IDE1LjUxNDEgMy41OTQ3MyAxNS41MjI5QzQuMDM5NyAxNS41MzE2IDQuNzQwOTIgMTUuMjA1NCA0Ljc2MzQyIDE0LjQxNzlDNC43ODg0MSAxMy41ODE3IDQuMzM0NjkgMTMuMTk5MyAzLjU4NzIzIDEzLjE2MDVaIiBmaWxsPSIjODU4NTg1Ii8+CjxwYXRoIGQ9Ik0xNC4xNzMxIDExLjY3MzFDMTIuOTQxOSAxMS43NTU2IDExLjg2NyAxMi44MTU1IDExLjg4MiAxNC4xNDY3QzExLjg5ODIgMTUuNTQ2NiAxMi45NzE5IDE2LjU0MDMgMTQuMzMxOSAxNi41NDAzQzE1LjY2MDUgMTYuNTQwMyAxNi43NjQyIDE1LjU2MTYgMTYuNzQxNyAxNC4wNzQyQzE2LjcxOCAxMi40NjA2IDE1LjQ2OTMgMTEuNTg1NiAxNC4xNzMxIDExLjY3MzFaTTE0LjM2NDQgMTUuNTc3OUMxMy42MDk0IDE1LjU3MDQgMTMuMDM1NyAxNC45NzI5IDEyLjk2NDQgMTQuMzIxN0MxMi44Njk0IDEzLjQ1MDUgMTMuNDE1NyAxMi44MTY4IDE0LjE4OTQgMTIuNzU0M0MxNC45OTMxIDEyLjY5MDUgMTUuNjYwNSAxMy4xNjggMTUuNzQ4IDE0LjAyNjdDMTUuODM1NSAxNC44ODU0IDE1LjE1OTMgMTUuNTg2NiAxNC4zNjQ0IDE1LjU3NzlaIiBmaWxsPSIjNEQ0NTNDIi8+CjxwYXRoIGQ9Ik0xLjg5MzY3IDEyLjI1MzFDMS45Mzk5MiAxMi4zMzQzIDIuNDU4NjQgMTIuMjkzMSAyLjg1NjEyIDEyLjQ2ODFDMy4xMTQ4NiAxMi41ODE4IDMuMzk3MzQgMTIuODgxOCAzLjM1NzM1IDEzLjI5NTVDMy4zMTczNSAxMy43MDkyIDMuMjM4NiAxNC4wOTA1IDMuMjM4NiAxNC4wOTA1TDMuOTY5ODEgMTQuMzI5MkMzLjk2OTgxIDE0LjMyOTIgNC4yNTYwNSAxMy42ODU1IDQuNDE0NzkgMTMuNTE4QzQuNTczNTMgMTMuMzUwNSA0LjkzMjI2IDEzLjAxNjggNS40NTcyMyAxMy4wODhDNS44NjA5NiAxMy4xNDMgNi4zMTU5NCAxMy41MDkzIDYuMzE1OTQgMTMuNTA5M0M2LjMxNTk0IDEzLjUwOTMgNi4wMjk3IDExLjc3NTYgNS45OTcyMSAxMS43NTk0QzUuOTY1OTYgMTEuNzQzMSA0LjM5MTA0IDExLjgyMzEgNC4zOTEwNCAxMS44MjMxQzQuMzkxMDQgMTEuODIzMSAzLjY3NDgzIDExLjUyOTQgMi45NDM2MiAxMS43MTE5QzIuMjExMTYgMTEuODk1NiAxLjg2MTE3IDEyLjE5ODEgMS44OTM2NyAxMi4yNTMxWiIgZmlsbD0iI0RCMEQyQSIvPgo8cGF0aCBkPSJNNC4yMzk5IDExLjQ5OEwzLjQ4NDk0IDEzLjc4OTJDMy40ODQ5NCAxMy43ODkyIDMuMTk2MiAxMy43OTA0IDMuMDg3NDYgMTQuMTg2N0MyLjk5OTk2IDE0LjUwNTQgMy4yMTQ5NSAxNC43MTE2IDMuNDEzNjkgMTQuNzY2NkMzLjY0ODY4IDE0LjgzMjkgMy45NTI0MSAxNC43Mjc5IDQuMDI2MTYgMTQuNDU2NkM0LjEyMTE1IDE0LjEwNjcgMy45MzExNiAxMy45NDc5IDMuOTMxMTYgMTMuOTE1NEMzLjkzMTE2IDEzLjg4MjkgNC43NzM2MiAxMS41NjE4IDQuNzczNjIgMTEuNTYxOEw0LjIzOTkgMTEuNDk4WiIgZmlsbD0iI0UxRDlEQyIvPgo8cGF0aCBkPSJNNi44NDk2MSAxMy4zMTU1TDguMjA4MjkgMTQuODQ3OUwxMC44NTE5IDE0LjgzNTRMMTIuNDcwNiAxNC4zNTI5TDEyLjI0OTMgMTMuNDg4TDEwLjkzOTQgMTIuODcwNUMxMC45Mzk0IDEyLjg3MDUgMTEuNTk0NCAxMS4wMTY5IDExLjQ1ODEgMTAuOTU1NkMxMS4zMjE5IDEwLjg5NDQgOS43OTA3IDkuODY4MTYgOS43OTA3IDkuODY4MTZMNi44ODcxMSAxMC44NDQ0TDYuODQ5NjEgMTMuMzE1NVoiIGZpbGw9IiMyRjJGMkYiLz4KPHBhdGggZD0iTTEyLjM4NjggMTEuMjI5NEMxMi4zODY4IDExLjIyOTQgMTEuOTM5MyAxMS42MzY5IDExLjY3OTMgMTEuOTk4MUMxMS40OTY5IDEyLjI1MzEgMTEuNDQ1NiAxMi41NDgxIDExLjQ0NTYgMTIuNTQ4MUwxMS44MjgxIDEyLjcyMDZDMTEuODI4MSAxMi43MjA2IDEyLjA1OTMgMTIuMTkwNiAxMi42MDA1IDExLjg3NTZDMTMuMjIxOCAxMS41MTMxIDEzLjY5NDIgMTEuNTU5NCAxMy43NjggMTEuNzE5NEMxMy44NDE3IDExLjg3OTQgMTMuNTM2NyAxMS45MzMxIDEzLjEwMDUgMTIuMzg2OEMxMi43NzkzIDEyLjcyMDYgMTIuNjA2OCAxMy4wNDE4IDEyLjYwNjggMTMuMDQxOEMxMi42MDY4IDEzLjA0MTggMTQuMDUzIDEzLjYzNDMgMTQuMDUzIDEzLjcwOTNDMTQuMDUzIDEzLjc4MyAxMy43OTMgMTQuNTI0MiAxMy43OTMgMTQuNTI0MkwxMi4xNDkzIDE0LjEyOTJDMTIuMTQ5MyAxNC4xMjkyIDExLjA4NjkgMTMuODA4IDEwLjk3NTYgMTQuMTI5MkMxMC44NjQ0IDE0LjQ1MDUgMTEuMzU4MSAxNC42MzU1IDExLjM1ODEgMTQuNjM1NUwxMC45MDA2IDE0LjkzMTdMOS40Nzk0NiAxNC4zMTQyQzkuNDc5NDYgMTQuMzE0MiA5LjM5NTcxIDEzLjkyMTggOS44MzgxOSAxMy4wNTQzQzEwLjUwNjkgMTEuNzQwNiAxMS41MDU2IDExLjA3NjkgMTEuNTA1NiAxMS4wNzY5TDEyLjM4NjggMTEuMjI5NFoiIGZpbGw9IiM1RTYyNjgiLz4KPHBhdGggZD0iTTEwLjkzNjkgMTQuNzgwNUMxMC45NDA2IDE0LjgzMyAxMC45ODA2IDE0Ljg5NTUgMTEuMTQ4MSAxNC44ODY3QzExLjMxNTYgMTQuODc4IDEyLjk5MyAxNC45MDQyIDEzLjE5NTUgMTQuODYwNUMxMy4zOTggMTQuODE2NyAxNi4xMDkxIDEzLjY0MDUgMTYuMTYyOCAxMy41MzQzQzE2LjIwMTYgMTMuNDU2OCAxNi4yMDc4IDEzLjE5NTUgMTYuMDgxNiAxMi45MjY4QzE1Ljk0MDQgMTIuNjI2OCAxNS43OTkxIDEyLjQ3NjggMTUuNjE0MSAxMi41MTE4QzE1LjQyOTEgMTIuNTQ2OCAxMS42NTE4IDE0LjM0OCAxMS4zNjkzIDE0LjQ2MTdDMTEuMDg2OSAxNC41NzY3IDEwLjkyODEgMTQuNjU2NyAxMC45MzY5IDE0Ljc4MDVaIiBmaWxsPSIjRTBFMEUwIi8+CjxwYXRoIGQ9Ik0xMi45MjE4IDE0LjI3NjdDMTIuOTUxOCAxNC4zNDggMTMuMjQ1NSAxNC4zMzggMTMuNTg2OCAxNC4xODE3QzEzLjkyOCAxNC4wMjU1IDE2LjA5OTEgMTIuOTYzIDE2LjA5OTEgMTIuOTYzQzE2LjA5OTEgMTIuOTYzIDE2LjA2MjkgMTIuODU0MyAxNS45OTkxIDEyLjc1M0MxNS45NDkxIDEyLjY3NDMgMTUuODY1NCAxMi41OTE4IDE1Ljg2NTQgMTIuNTkxOEMxNS44NjU0IDEyLjU5MTggMTMuMzk2OCAxMy44NDkyIDEzLjI1OCAxMy45MThDMTMuMDc4IDE0LjAwOCAxMi44NjU2IDE0LjE0MyAxMi45MjE4IDE0LjI3NjdaIiBmaWxsPSIjRkVGRUZFIi8+CjxwYXRoIGQ9Ik0xNS4wNjMgOS45NTE4OEwxNS45NjQyIDEwLjc0ODFDMTUuOTY0MiAxMC43NDgxIDE2LjE2NTQgMTAuNDI1NiAxNi4xNjU0IDEwLjM5NDRDMTYuMTY1NCAxMC4zNjE5IDE1LjU2MTcgOS41OTgxNCAxNS41NjE3IDkuNTk4MTRMMTUuMDYzIDkuOTUxODhaIiBmaWxsPSIjNDg0RDUxIi8+CjxwYXRoIGQ9Ik0xMi41NjY5IDExLjI5NTZDMTIuNTY2OSAxMS4yOTU2IDEzLjc5MyAxMC43Njk0IDE0LjMwOCAxMC41Mzk0QzE1LjExNDIgMTAuMTgwNyAxNS45MzkyIDkuNzE4MjEgMTYuMDg2NyA5LjYzMDcyQzE2LjM1MjkgOS40NzE5OCAxNi4zNTA0IDkuMzIzMjMgMTYuMjc2NyA5LjI2MTk5QzE2LjIwMjkgOS4yMDA3NCAxNi4wMDU0IDkuMjg2OTggMTUuOTA1NCA5LjI5OTQ4QzE1LjgwNTQgOS4zMTE5OCAxNS41MTA1IDkuMzI0NDggMTUuNTEwNSA5LjMyNDQ4TDEzLjI5OTMgOS45Mjk0NUMxMy4yOTkzIDkuOTI5NDUgMTEuODI5NCAxMC44NTU3IDExLjg2NTYgMTAuODU1N0MxMS45MDE5IDEwLjg1NTcgMTIuNTY2OSAxMS4yOTU2IDEyLjU2NjkgMTEuMjk1NloiIGZpbGw9IiNEQzBEMkEiLz4KPHBhdGggZD0iTTkuNzcwNzUgMTAuMTcxOUM5Ljc3MDc1IDEwLjE3MTkgMTAuNzM3IDExLjExMTkgMTAuODc1NyAxMS4xODE5QzExLjAxNTcgMTEuMjUxOSAxMS4yODE5IDExLjI4MzEgMTEuNTA0NCAxMS4yMzk0QzExLjcyNjkgMTEuMTk1NiAxMi45NTMxIDEwLjI2NjkgMTMuNDgwNiAxMC4xMDE5QzE0LjAwOCA5LjkzNjkzIDE0LjkxNjcgOS45MTE5MyAxNS4yMTU1IDkuNzQ1NjlDMTUuNTE0MiA5LjU3OTQ1IDE1LjUxMDQgOS4zMjQ0NiAxNS41MTA0IDkuMzI0NDZMMTIuOTE1NiA5LjUwNDQ1QzEyLjkxNTYgOS41MDQ0NSAxMi4zNjU2IDEwLjM4ODIgMTEuMzMzMiAxMC4zMzA3QzEwLjUzOTUgMTAuMjg1NyA5Ljk0MTk5IDkuNjE5NDUgOS45NDE5OSA5LjYxOTQ1TDkuNzcwNzUgMTAuMTcxOVoiIGZpbGw9IiM0NjRDNEYiLz4KPHBhdGggZD0iTTEyLjc4MzEgMTEuOTk0M0MxMy4wMTE5IDEyLjExNTYgMTMuMTIwNiAxMS44NTE4IDEzLjA2MzEgMTEuNzM0M0MxMi45NDE5IDExLjQ4NjkgMTIuNDkxOSAxMC43ODMxIDEyLjM3NjkgMTAuNzM2OUMxMi4yODE5IDEwLjY5OTQgMTEuNzY3IDExLjA3MzEgMTEuNzE1NyAxMS4xMTE5QzExLjY2NDUgMTEuMTQ5NCAxMS42ODMyIDExLjIzMDYgMTEuNzkyIDExLjMwOTRDMTEuOTMxOSAxMS40MTA2IDEyLjYwNTcgMTEuOTAwNiAxMi43ODMxIDExLjk5NDNaIiBmaWxsPSIjQzhDOEM4Ii8+CjxwYXRoIGQ9Ik01LjAwOTY2IDExLjAxNjlMMy40NTg1IDEwLjcwNTdMNC4yMjU5NiA5LjIyNjk5TDUuMDQ5NjYgOC40MDMyOUw1LjUzNzE0IDguMjI3MDVDNS41MzcxNCA4LjIyNzA1IDUuMTI1OTEgOC43NzMyNyA0LjkwNDY3IDkuMTE3QzQuNzY3MTggOS4zMzA3NCA0LjYyMjE4IDkuNjgxOTcgNC42MjIxOCA5LjY4MTk3QzQuNjIyMTggOS42ODE5NyA0Ljc5MzQzIDkuNjQ0NDcgNC45Mjk2NyA5LjYyODIyQzUuMDY1OTEgOS42MTE5NyA1LjIxNzE1IDkuNjAwNzIgNS4yMTcxNSA5LjYwMDcyQzUuMjE3MTUgOS42MDA3MiA1LjE3MjE2IDkuMzA4MjQgNS4yMzU5IDkuMTU3QzUuMjk5NjUgOS4wMDQ1IDUuNDMyMTQgOC45MDU3NiA1LjUyODM5IDguODk3MDFDNS42MjQ2MyA4Ljg4OTUxIDUuNjg4MzggOC45MzcwMSA1LjY4ODM4IDguOTM3MDFDNS42ODgzOCA4LjkzNzAxIDUuNjc0NjMgOC43MDU3NyA1LjY5NTg4IDguNjQwNzdDNS43MzU4OCA4LjUyMDc4IDYuMDcyMTEgOC4zNzk1NCA2LjQ4ODM0IDguMjI3MDVDNi45MDcwNiA4LjA3MzMgNy4zNzcwNCA3Ljg1OTU2IDcuNTAzMjggNy44ODk1NkM3LjU4MjAzIDcuOTA4MzEgNy43NjcwMiA4LjI2NTc5IDcuNjcwNzcgOC4zNjk1NEM3LjU3NDUzIDguNDczMjggNi4wODgzNiA5LjE2OTUgNi4wODgzNiA5LjE2OTVMNi4zMzU4NCA5Ljg0OTQ2TDUuMDA5NjYgMTEuMDE2OVoiIGZpbGw9IiM0NjRDNEYiLz4KPHBhdGggZD0iTTcuNzA5NzQgMTAuNzk0M0w5LjczMDg4IDEwLjcyMDZMMTAuMTI0NiAxMS4xMzE4TDguODAyMTggMTEuOTUzTDcuNTIxIDExLjY0MDVMNy40MDYwMSAxMC44NDQzTDcuNzA5NzQgMTAuNzk0M1oiIGZpbGw9IiM0NjRDNEYiLz4KPHBhdGggZD0iTTYuODQ3MDkgMTIuMDU5NEw1Ljc4MzM5IDEzLjIyMDVDNS43ODMzOSAxMy4yMjA1IDYuMTczMzcgMTMuOTEzIDYuMjgwODcgMTQuMjEwNUM2LjM5MzM2IDE0LjUyMDUgNi41NTIxIDE1LjA4MDUgNi41NTIxIDE1LjA4MDVDNi41NTIxIDE1LjA4MDUgMTAuNTg0NCAxNS4xOTU0IDEwLjc3MzEgMTUuMTcwNEMxMC45NjE5IDE1LjE0NTQgMTEuMTc1NiAxNC45MjQyIDExLjA3NjkgMTQuODM0MkMxMC45NzgxIDE0Ljc0NDIgOS40NDE5NSAxNC4xMTE4IDkuNDQxOTUgMTQuMTExOEM5LjQ0MTk1IDE0LjExMTggOC4yMzQ1MSAxNC4xOTQyIDguMDM3MDIgMTQuMTIwNUM3LjgzOTUzIDE0LjA0NjggNy4xOTU4MiAxMy4xNTkzIDcuMjMwODIgMTMuMDkxOEM3LjMxNTgxIDEyLjkyNDMgOC44ODMyMyAxMS45MDMxIDguODgzMjMgMTEuOTAzMUw4Ljc1MTk5IDExLjc2MzFMNy41NDQ1NSAxMS40OTE5TDcuNjE4MyAxMC45OTk0QzcuNjE4MyAxMC45OTk0IDguMTYwNzcgMTAuNTY0NCA4LjIyNTc2IDEwLjU2NDRDOC4yOTA3NiAxMC41NjQ0IDguOTQwNzMgMTAuNDU4MiA5LjMwMTk2IDEwLjM3NTdDOS42NjMxOSAxMC4yOTMyIDEwLjEwNjkgMTAuMTk0NSAxMC4yMTMyIDEwLjEzN0MxMC4zMTk0IDEwLjA3OTUgMTAuMzk4MSA5Ljk4MDcyIDEwLjM2OTQgOS44MjQ0OEMxMC4zNDY5IDkuNzA2OTggMTAuMDQwNyA5LjYwMzI0IDEwLjA0MDcgOS42MDMyNEw2Ljg3MDg0IDkuNDIyTDYuMjE5NjIgOS40OTA3NEM2LjIxOTYyIDkuNDkwNzQgNi4xMzIxMyA5Ljc4MzIzIDUuNjYzNCAxMC4wNzA3QzUuMTk0NjcgMTAuMzU4MiA0LjM5NzIyIDEwLjU0NDQgNC4zOTcyMiAxMC41NDQ0TDYuODQ3MDkgMTIuMDU5NFoiIGZpbGw9IiNEQzBEMkEiLz4KPHBhdGggZD0iTTMuNjIyMzEgOS4wNDQ0M0MzLjYyMjMxIDkuMDQ0NDMgNC41NTg1MSA4LjAzMzI0IDUuMDU0NzQgNy41ODk1MUM1LjQyMjIyIDcuMjYwNzggNS43MDQ3IDcuMDQ4MjkgNS44MDk3IDcuMDA4MjlDNS44NjU5NSA2Ljk4NzA0IDYuNDA5NjcgNi45NjgyOSA2LjQ2NDY2IDYuOTg0NTRDNi41MjA5MSA3LjAwMDc5IDYuNTc3MTYgNy4wODgyOSA2LjQ4ODQxIDcuMTc3MDNDNi40MDA5MiA3LjI2NDUzIDYuMTIzNDMgNy41ODIwMSA1Ljg3MzQ1IDcuODQ0NUM1Ljc1ODQ1IDcuOTY0NDkgNS41MzcyMSA4LjIyMzIzIDUuNTM3MjEgOC4yMjMyM0w1LjEyOTczIDguMzc0NDdMNC4xMDYwNCA4Ljg4NTY5TDMuNjIyMzEgOS4wNDQ0M1oiIGZpbGw9IiNBRUUzRkQiLz4KPHBhdGggZD0iTTUuNTI1OTEgOC4yMjU3MUM1LjUyNTkxIDguMjI1NzEgNC40NjIyMiA4LjQwNDQ1IDMuNzM5NzYgOC45NDY5MkMzLjAxNzMgOS40ODkzOSAyLjczNzMxIDEwLjU4MTggMi43MjEwNiAxMC42ODgxQzIuNzA0ODEgMTAuNzk0MyAyLjU2NzMyIDExLjE0ODEgMi44Mzg1NiAxMS4zMThDMi45Njk4IDExLjQwMDUgMy4yMzEwNCAxMS4zNjE4IDMuNTA5NzcgMTEuNDI4QzMuNzQxMDEgMTEuNDgxOCA0LjI2NDczIDExLjY1OCA0LjY2NzIxIDEyLjAxOTNDNS4wNjk2OSAxMi4zODA1IDUuNzg0NjUgMTMuMjI2NyA1Ljc4NDY1IDEzLjIyNjdMOC43NTE5OSAxMS43NjQzQzguNzUxOTkgMTEuNzY0MyA3Ljg5NTc5IDEwLjc2MzEgNS45MTU4OSAxMC42MzE4QzMuOTM2IDEwLjUwMDYgMy43MzEwMSAxMC41NDkzIDMuNzMxMDEgMTAuNTQ5M0MzLjczMTAxIDEwLjU0OTMgMy45Nzk3NSA5LjgzNDM3IDQuMzMwOTggOS4zNjY5QzQuNzk5NyA4Ljc0MTkzIDUuNTI1OTEgOC4yMjU3MSA1LjUyNTkxIDguMjI1NzFaIiBmaWxsPSIjRkUyQTIyIi8+CjxwYXRoIGQ9Ik04LjM3NzE0IDEzLjIxMThDOC4zNzcxNCAxMy4yMTE4IDguMTg1OSAxMi42NzY4IDguNjM1ODcgMTIuMzY4MUM4Ljk3NTg1IDEyLjEzNDMgOS40MDcwOCAxMi4xNjU2IDkuNjk5NTcgMTIuNTU0M0M5Ljk5MjA1IDEyLjk0MyA5LjczMDgxIDEzLjM5OTMgOS41NzU4MiAxMy41MjhDOS4wNjk2IDEzLjk1MDUgOC42MDgzNyAxMy41NTA1IDguNjA4MzcgMTMuNTUwNUM4LjYwODM3IDEzLjU1MDUgOC42MzgzNyAxMy43NzA1IDguNDM5NjMgMTMuOTIxN0M4LjI5ODM5IDE0LjAyOTIgNy45NDQ2NiAxNC4wNjA1IDcuNzk4NDIgMTMuNzkxN0M3LjY0MDkzIDEzLjUwMTggNy44MDIxNyAxMy4yODY4IDcuOTY3MTYgMTMuMTk1NUM4LjE3OTY1IDEzLjA3NjggOC4zNzcxNCAxMy4yMTE4IDguMzc3MTQgMTMuMjExOFoiIGZpbGw9IiM0NjRDNEYiLz4KPHBhdGggZD0iTTMuMDQ3MjggOS44MTE5NEMzLjA0NzI4IDkuODExOTQgMy43MjEgOS44MDgxOSAzLjcyNiA5LjgzNTY5QzMuNzMxIDkuODYzMTkgMy40Nzg1MSAxMC41MDE5IDMuNDc4NTEgMTAuNTAxOUMzLjQ3ODUxIDEwLjUwMTkgMi43MDczIDEwLjkwMTkgMi42ODM1NSAxMC44OTY5QzIuNjU5ODEgMTAuODkxOSAyLjY3ODU1IDEwLjU1MzIgMi43Nzg1NSAxMC4yNzgyQzIuODc4NTQgMTAuMDAzMiAzLjA0NzI4IDkuODExOTQgMy4wNDcyOCA5LjgxMTk0WiIgZmlsbD0iI0Q5RTNERiIvPgo8cGF0aCBkPSJNNi4yMTg1MSA5LjQ5MzE2QzYuMjE4NTEgOS40OTMxNiA2LjgwNTk3IDkuOTkwNjQgNy44NjU5MiAxMC4wODA2QzguOTI1ODYgMTAuMTcwNiAxMC4wNDIxIDkuNjA0NDEgMTAuMDQyMSA5LjYwNDQxQzEwLjA0MjEgOS42MDQ0MSA4LjI1NDY1IDguNzc2OTUgOC4wMzg0MSA4Ljc5MTk1QzcuODA1OTIgOC44MDY5NSA2LjIyNzI2IDkuNDQzMTYgNi4yMTg1MSA5LjQ5MzE2WiIgZmlsbD0iI0ZFMkEyMiIvPgo8cGF0aCBkPSJNMy4zMjA5OSA4LjY0MkMzLjM1MjIzIDguNzU0NDkgMy44MTk3MSA5LjEwNjk3IDQuMTIzNDQgOS4wNzk0N0M0LjQwMjE4IDkuMDU0NDggNC4zNTQ2OCA4LjUzNTc1IDQuMzUyMTggOC40MDgyNkM0LjM0ODQzIDguMTUwNzcgNC4zMjU5MyA3LjkzMzI4IDQuMzEwOTMgNy43OTgyOUM0LjI5NDY4IDcuNjUwOCA0LjE3MjE5IDcuNjM0NTUgNC4wODU5NSA3LjcwNDU1QzMuOTk5NyA3Ljc3MzI5IDMuMjg3MjQgOC41MjMyNSAzLjMyMDk5IDguNjQyWiIgZmlsbD0iIzQ2NEM0RiIvPgo8L3N2Zz4K": dashboardIconPathPrefix + "motorcycle", + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEzLjIzOTUgMTMuOTMwNUMxMy4yMzk1IDEzLjkzMDUgMTMuMjI4MyAxNS4yNjQyIDEzLjIyODMgMTUuNTI5MkMxMy4yMjgzIDE1Ljc5NDEgMTMuMzc4MyAxNS45MzE2IDEzLjU1MDggMTUuOTIwNEMxMy43MjMyIDE1LjkwOTEgMTQuNTUyIDE1LjkyMDQgMTQuNzM1NyAxNS45MjA0QzE0LjkxOTQgMTUuOTIwNCAxNS4xMjY5IDE1Ljc0NzkgMTUuMTI2OSAxNS40OTU0QzE1LjEyNjkgMTUuMjQyOSAxNS4xNjE5IDEyLjk4OCAxNS4xNjE5IDEyLjk4OEwxMy4yMzk1IDEzLjkzMDVaIiBmaWxsPSIjNEU0MzNEIi8+CjxwYXRoIGQ9Ik00LjQyOTk3IDEzLjE0OTJMMi42MDEzMiAxMi44OTY3QzIuNjAxMzIgMTIuODk2NyAyLjU5MDA3IDE1LjIzMTYgMi41Nzg4MiAxNS40NTAzQzIuNTY3NTcgMTUuNjY5MSAyLjY5MzgxIDE1LjgyNzggMi44Nzc1NiAxNS44NDE2QzMuMjAwMDQgMTUuODY0MSAzLjgwODc2IDE1Ljg2NDEgNC4wMjc0OSAxNS44NTI4QzQuMjQ2MjMgMTUuODQxNiA0LjM5NDk3IDE1LjY4MDMgNC40MDc0NyAxNS40NzI4QzQuNDE5OTcgMTUuMjY1NCA0LjQwNzQ3IDE0LjAyNDIgNC40MDc0NyAxNC4wMjQyTDQuNDI5OTcgMTMuMTQ5MloiIGZpbGw9IiM0RTQzM0QiLz4KPHBhdGggZD0iTTE0LjA2MDYgOC4wODk2M0wxNS4yMDkzIDguMDM4MzhDMTUuMjA5MyA4LjAzODM4IDE1LjYzOCA4LjcxOTU5IDE1LjU4NDMgMTEuMDI0NUMxNS41MzMgMTMuMjEzMSAxNC45NjgxIDE0LjA2ODEgMTQuNDY4MSAxNC4xODkzQzEzLjk2ODEgMTQuMzEwNSAxMi4wODA3IDE0LjQ5NjggOS4wOTA4OCAxNC41MTQzQzYuMTAxMDMgMTQuNTMxOCAzLjg3OTkgMTQuMzYwNSAzLjQ1MjQyIDE0LjI1ODFDMy4wMjQ5NSAxNC4xNTU2IDIuNTYyNDcgMTMuNTIzMSAyLjUxMjQ3IDEzLjA2MTlDMi40MzYyMyAxMi4zNTQ0IDIuNDMxMjMgMTEuNDAzMiAyLjQ2NDk3IDEwLjA1MzNDMi40OTg3MiA4LjcwMzM0IDIuNzkyNDYgOC4wMjU4OCAyLjc5MjQ2IDguMDI1ODhMMTQuMDYwNiA4LjA4OTYzWiIgZmlsbD0iIzQ3NEM0RiIvPgo8cGF0aCBkPSJNNC4zNTExNSAxMS44OTgxQzMuNzAzNjkgMTEuNzkwNiAyLjc2Mzc0IDExLjM4MzEgMi42OTI0OSAxMS42ODMxQzIuNjIxMjUgMTEuOTgzMSAyLjkwNzQ4IDEyLjM0MDYgMy41OTM3IDEyLjU2OTNDNC4yNzk5MSAxMi43OTgxIDUuODUyMzMgMTIuOTY5MyA5LjE1NDY1IDEyLjg5ODFDMTIuNDU3IDEyLjgyNjggMTMuNTcxOSAxMi43NDA2IDE0LjIyOTQgMTIuNTU1NkMxNC44ODY4IDEyLjM2OTMgMTUuNjQ0MyAxMS44Njk0IDE1LjQwMTggMTEuNTgzMUMxNS4xNTkzIDExLjI5NjkgMTQuNTE1NiAxMS43MTE5IDEzLjkyOTQgMTEuODI1NkMxMy4zNDMyIDExLjkzOTQgMTEuMTQwOCAxMi4wMDA2IDkuMTEyMTUgMTIuMDQwNkM2LjkxMTAyIDEyLjA4MzEgNS4wMzczNyAxMi4wMTE4IDQuMzUxMTUgMTEuODk4MVoiIGZpbGw9IiM4NTg1ODUiLz4KPHBhdGggZD0iTTUuNTcxMTQgMTAuMDIwNkM1LjM1NzQgOS45NjQzNSAyLjcwNTA0IDkuOTAzMSAyLjU4ODc5IDEwLjAyODFDMi41Mzc1NSAxMC4wODMxIDIuNTUwMDUgMTAuODc5MyAyLjU2MDA0IDExLjAyNDNDMi41NzAwNCAxMS4xNjkzIDIuNjgxMjkgMTEuMjE5MyAyLjg3Mzc4IDExLjIyOTNDMy4wNjc1MiAxMS4yMzkzIDUuNTQyMzkgMTEuMjU4IDUuNzA2MTMgMTEuMjM5M0M1Ljg2OTg3IDExLjIyMDUgNS45MDg2MiAxMS4wNTU1IDUuODc5ODcgMTAuOTEwNUM1Ljg1MTEyIDEwLjc2NDMgNS43NTQ4OCAxMC4wNjkzIDUuNTcxMTQgMTAuMDIwNloiIGZpbGw9IiNGRUE4MjYiLz4KPHBhdGggZD0iTTE1LjM1NDQgOS44OTY5OUMxNS4yMzMxIDkuODI1NzQgMTIuNTQyIDkuOTMzMjQgMTIuNDU1OCAxMC4wMTA3QzEyLjM2ODMgMTAuMDg4MiAxMS45MDIgMTEuMDUwNyAxMi4xMzcgMTEuMTQxOUMxMi4zNTk1IDExLjIyOTQgMTUuMjY4MSAxMS4xODMyIDE1LjM2NDQgMTEuMTM0NEMxNS41MTQzIDExLjA1OTQgMTUuNDY5NCA5Ljk2NDQ5IDE1LjM1NDQgOS44OTY5OVoiIGZpbGw9IiNGRUE4MjYiLz4KPHBhdGggZD0iTTYuOTUzNDUgMTEuNDQxOUM2LjY5MjIxIDExLjQ1NDQgNi4zMjQ3MyAxMS4zMTU2IDYuMTY5NzQgMTAuOTEwN0M2LjAxNDc1IDEwLjUwNDQgNS45OTYgMTAuMjYzMiA2LjA0MzUgMTAuMDUwN0M2LjA5MjI0IDkuODM4MjIgNi4yMzcyNCA5Ljc2MDcyIDYuNTc0NzIgOS43MzE5OEM2LjkxMzQ1IDkuNzAzMjMgNy41NzA5MiA5LjY1NDQ4IDguOTgyMDkgOS42NDQ0OEMxMC4zOTQ1IDkuNjM0NDggMTEuMzcwNyA5LjY2MzIzIDExLjYyMiA5LjY5MzIzQzExLjg3MzIgOS43MjE5OCAxMi4wMTgyIDkuODA5NDcgMTEuOTc5NCAxMC4wNzA3QzExLjk0MDcgMTAuMzMxOSAxMS44MDU3IDEwLjg1NDQgMTEuNjk5NCAxMS4xMDU3QzExLjYyMiAxMS4yODk0IDExLjM4MDcgMTEuNDI0NCAxMS4wNjA3IDExLjQzNDRDMTAuNzQzMiAxMS40NDE5IDcuMTU3MTkgMTEuNDMxOSA2Ljk1MzQ1IDExLjQ0MTlaIiBmaWxsPSIjMkYyRjJGIi8+CjxwYXRoIGQ9Ik0xNC42NjggNy40MTk2NUwxNS4wNDQyIDcuNDI5NjVDMTUuMDQ0MiA3LjQyOTY1IDE1LjA1MjkgNi45NzcxNyAxNS4xMDQyIDYuODM3MThDMTUuMTU1NCA2LjY5NzE5IDE1LjI1OTIgNi42MjIxOSAxNS40OTQyIDYuNjIyMTlDMTUuNzI5MiA2LjYyMjE5IDE2LjE5MDQgNi42Mzk2OSAxNi4zNDU0IDYuNjQ3MTlDMTYuNTAwNCA2LjY1NDY5IDE2LjgwMTYgNi45MTk2OCAxNi44MTY2IDcuMzE3MTZDMTYuODMxNiA3LjcxNDY0IDE2LjU1OTEgNy45NjQ2MiAxNi4yODY2IDcuOTk0NjJDMTYuMDE0MSA4LjAyNDYyIDE0Ljk5MTcgNy45OTQ2MiAxNC45OTE3IDcuOTk0NjJMMTQuNjY4IDcuNDE5NjVaIiBmaWxsPSIjNDc0QzRGIi8+CjxwYXRoIGQ9Ik0zLjQ1MTA2IDcuMzU3MTRMMy4wMzM1OCA3LjM4MzM5QzMuMDMzNTggNy4zODMzOSAzLjA0NjA4IDYuOTc0NjYgMy4wMTczMyA2LjgyNzE3QzIuOTg4NTggNi42Nzk2OCAyLjgxMTA5IDYuNTkyMTkgMi42Nzg2IDYuNTg0NjlDMi41NDYxMSA2LjU3NzE5IDEuOTI3MzkgNi41NzcxOSAxLjc1MTE1IDYuNTk5NjhDMS41NzQ5MSA2LjYyMjE4IDEuMjcyNDIgNi43OTg0MiAxLjI3OTkyIDcuMjQ3MTVDMS4yODc0MiA3LjY5NTg4IDEuNTc0OTEgNy45NDU4NiAxLjgxNzM5IDcuOTM5NjFDMi4wNTk4OCA3LjkzMzM2IDIuNjQ3MzUgNy45NTIxMSAyLjgyNjA5IDcuOTQ3MTFDMy4wNTQ4MyA3LjkzOTYxIDMuNTU0OCA3Ljk2MjExIDMuNTU0OCA3Ljk2MjExTDMuNDUxMDYgNy4zNTcxNFoiIGZpbGw9IiM0NzRDNEYiLz4KPHBhdGggZD0iTTcuOTU5NzIgMy4wOTgzOUw3Ljk3ODQ3IDMuNjg3MTFIMTAuMzU3MUwxMC4zNzU4IDMuMTE3MTRMNy45NTk3MiAzLjA5ODM5WiIgZmlsbD0iIzQ3NEM0RiIvPgo8cGF0aCBkPSJNMTEuNjIzMyAzLjA3ODU4QzExLjYyMzMgMy4yMjM1NyAxMS42MjMzIDMuODQyMjkgMTEuNjIzMyAzLjg0MjI5TDEyLjQwNyAzLjg2MTA0TDEyLjM1ODMgMy4wNDg1OEwxMS42MjMzIDMuMDc4NThaIiBmaWxsPSIjNDc0QzRGIi8+CjxwYXRoIGQ9Ik01LjczNDg2IDMuODMyMjNDNS43NTM2MSAzLjcxNTk4IDUuNzYzNjEgMi44OTQ3OCA1Ljc2MzYxIDIuODk0NzhMNi42MDQ4MiAzLjAxMTAyTDYuNTc2MDcgMy43NjQ3M0w1LjczNDg2IDMuODMyMjNaIiBmaWxsPSIjNDc0QzRGIi8+CjxwYXRoIGQ9Ik0yLjc4ODggOC4wMjU4MkMyLjgxMDA1IDcuOTg1ODIgMy4wNjc1NCA3Ljg5NDU4IDMuMjIyNTMgNy42MjIwOUMzLjMzMTI4IDcuNDMwODUgMy42NDc1MSA2LjQwMzQxIDQuMDYzNzQgNS41ODIyQzQuNDc5OTYgNC43NjA5OSA0LjY1MzcxIDQuNDAyMjYgNC43Njk5NSA0LjIxODUyQzQuODg2MTkgNC4wMzQ3OCA1LjA1OTkzIDMuODg5NzkgNS4zNTk5MiAzLjgwMjI5QzUuNjU5OSAzLjcxNDggNi43MDQ4NSAzLjQ3ODU2IDkuMjE4NDYgMy41MjczMUMxMS43MzIxIDMuNTc2MDYgMTIuMzY5NSAzLjY1MTA1IDEyLjYxNDUgMy43Mjk4QzEyLjgxMiAzLjc5MjI5IDEzLjE4MiAzLjk4NjAzIDEzLjQyMzIgNC40MzEwMUMxMy42NjQ1IDQuODc1OTkgMTQuNjg4MiA3LjE1OTYyIDE0Ljc4NTcgNy4zNjIxMUMxNC44ODE5IDcuNTY0NiAxNS4yMTA2IDguMDM1ODIgMTUuMjEwNiA4LjAzNTgyQzE1LjIxMDYgOC4wMzU4MiAxNC4zNDE5IDguOTM5NTIgMTIuMzEyMSA5LjE3NDUxQzEwLjQ4NzEgOS4zODU3NSA3LjgzNzI5IDkuNTc0NDkgNS4xMDExOCA5LjEwMjAyQzIuOTUzOCA4LjczNDUzIDIuNzY3NTYgOC4wNjU4MiAyLjc4ODggOC4wMjU4MloiIGZpbGw9IiNFM0U0REUiLz4KPHBhdGggZD0iTTUuMDE5OTEgNC41NTcyNUM0Ljc5NzQzIDQuNzc5NzQgNC4yOTYyIDUuODc5NjggNC4wNDc0NyA2LjUxMjE1QzMuNzk4NzMgNy4xNDQ2MSAzLjY1MjQ5IDcuNjY0NTggMy45OTEyMiA3LjY3NTgzQzQuMzI5OTUgNy42ODcwOCA3LjU1MTAzIDcuNjQyMDkgOC45NzU5NiA3LjYzMDg0QzEwLjQwMDkgNy42MTk1OSAxMy44MDIgNy42OTgzMyAxNC4wNTA3IDcuNjk4MzNDMTQuMjk5NCA3LjY5ODMzIDE0LjQ0NTcgNy40ODMzNCAxNC4zMzMyIDcuMjEyMTFDMTQuMjIwNyA2Ljk0MDg3IDEzLjM4MzIgNS4wODcyMiAxMy4xOTIgNC43ODIyNEMxMi45OTk1IDQuNDc3MjUgMTIuNzI4MyA0LjM3NDc2IDEyLjI1MzMgNC4zNTIyNkMxMS43NzgzIDQuMzI5NzYgMTAuMDAzNCA0LjIyODUyIDguNzgzNDcgNC4yMjg1MkM3LjU2MzUzIDQuMjI4NTIgNS4yMjM2NSA0LjM1NDc2IDUuMDE5OTEgNC41NTcyNVoiIGZpbGw9IiMyRDJFMzUiLz4KPHBhdGggZD0iTTQuMjYzNDYgNy4xNTA2OUM0LjMxMjIxIDcuMjI0NDQgNy4zNzA4IDcuMDkwNyA5LjIxMzIgNy4xMjQ0NEMxMS4wNTU2IDcuMTU4MTkgMTMuNTE5MiA3LjIxNDQ0IDEzLjY0NDIgNy4yMDMxOUMxMy43NjggNy4xOTE5NCAxMy43ODA1IDcuMDEwNyAxMy43NjggNi45NTQ0NUMxMy43NTY3IDYuODk4MjEgMTMuNTQxNyA2LjQ1Njk4IDEzLjU0MTcgNi40NTY5OEMxMy41NDE3IDYuNDU2OTggMTIuNDIzIDUuMTkwOCAxMi4zMzE4IDUuMTkwOEMxMi4yNDE4IDUuMTkwOCA5LjA3Njk2IDUuMzk0NTQgOS4wNzY5NiA1LjM5NDU0TDUuNTE3MTUgNS4yOTMyOUw0LjQ0MzQ1IDYuNjE4MjJDNC40NDM0NSA2LjYxODIyIDQuMjQwOTYgNy4xMTY5NCA0LjI2MzQ2IDcuMTUwNjlaIiBmaWxsPSIjOTZDOEVEIi8+CjxwYXRoIGQ9Ik00LjQ0MzYgNi42MTg0NEM0LjQ0MzYgNi42MTg0NCA0Ljk0MjMzIDUuNDI0NzUgNS4xNDM1NyA1LjEyMjI3QzUuMjIyMzEgNS4wMDQ3OCA1LjI3OTgxIDQuODk2MDMgNS41NTEwNSA0Ljg2MjI4QzUuODIyMjggNC44Mjg1MyA3LjIyMzQ2IDQuNjI0OCA5LjA5OTYxIDQuNjU4NTRDMTAuOTc1OCA0LjY5MjI5IDEyLjA2MDcgNC43NjcyOSAxMi4yODY5IDQuNzgyMjlDMTIuNTU0NCA0Ljc5OTc5IDEyLjczOTQgNC44OTQ3OCAxMi44NDA3IDUuMDY0NzdDMTIuOTQxOSA1LjIzNDc2IDEzLjY1NDQgNi42OTIxOSAxMy42NTQ0IDYuNjkyMTlMMTIuNTA0NCA2LjU4NzE5QzEyLjUwNDQgNi41ODcxOSAxMi40OTY5IDYuMDU3MjIgMTIuNDc5NCA1Ljg3NDczQzEyLjQ2NDQgNS43MTcyNCAxMi4zOTA3IDUuNTg3MjQgMTIuMTQ5NCA1LjU3MzVDMTEuOTM0NSA1LjU2MjI1IDExLjQ1OTUgNS41NTEgMTEuMzAyIDUuNTYyMjVDMTEuMTQzMiA1LjU3MzUgMTEuMDM1OCA1LjcwNDc0IDExLjAwNDUgNS45Mjk3M0MxMC45NzMzIDYuMTU1OTYgMTAuOTQwOCA2LjU1OTY5IDEwLjk0MDggNi41NTk2OUw3LjM3OTcgNi41ODA5NEM3LjM3OTcgNi41ODA5NCA3LjM3OTcgNi4wMjM0NyA3LjM0NzIgNS44MjIyM0M3LjMyMzQ1IDUuNjc3MjQgNy4yMzIyMSA1LjU4MjI0IDcuMDQ1OTcgNS41ODM0OUM2LjgyOTczIDUuNTg0NzQgNi40MTk3NSA1LjU2MjI1IDYuMTI2MDEgNS41OTU5OUM1Ljk4OTc3IDUuNjEyMjQgNS44ODg1MyA1Ljc3NzIzIDUuODc3MjggNi4wMTQ3MkM1Ljg2ODUzIDYuMTg1OTYgNS44NTQ3OCA2LjU2ODQ0IDUuODU0NzggNi41Njg0NEw0LjQ0MzYgNi42MTg0NFoiIGZpbGw9IiNBRkUzRkIiLz4KPHBhdGggZD0iTTguMDU2MDcgMS45ODYyMUgxMC4zMjg1TDEwLjc0MzQgMi43MzExN0wxMC4zMDg1IDMuMjcyMzlIOC4wNTYwN0w3LjYyMTA5IDIuNTk0OTJMOC4wNTYwNyAxLjk4NjIxWiIgZmlsbD0iI0NFQ0VDRiIvPgo8cGF0aCBkPSJNMTAuMzI4MyAxLjk4NjIxQzEwLjMyODMgMS45ODYyMSAxMi4yODA3IDIuMDE0OTUgMTIuNDE2OSAyLjAzNDk1QzEyLjU1MzIgMi4wNTQ5NSAxMi42ODgyIDIuMjE4NjkgMTIuNzA2OSAyLjQxMjQzQzEyLjcyNTcgMi42MDYxNyAxMi42ODgyIDMuMDc5OSAxMi42ODgyIDMuMTY2MTRDMTIuNjg4MiAzLjI1MjM5IDEyLjU3NjkgMy4zMDQ4OSAxMi4zOTQ0IDMuMzA0ODlDMTIuMjEwNyAzLjMwNDg5IDEwLjMxMDggMy4yNzIzOSAxMC4zMTA4IDMuMjcyMzlMMTAuMzI4MyAxLjk4NjIxWiIgZmlsbD0iI0REMEMyNiIvPgo8cGF0aCBkPSJNNS40MjYwMyAzLjAyMTE1QzUuNDI2MDMgMy4xNDc0IDUuNDM2MDMgMy4yNDM2NCA1LjU2MTAyIDMuMjUzNjRDNS42ODcyNiAzLjI2MzY0IDguMDU1ODkgMy4yNzIzOSA4LjA1NTg5IDMuMjcyMzlWMS45ODYyMUM4LjA1NTg5IDEuOTg2MjEgNS44OTk3NSAxLjk4NjIxIDUuNzQ0NzYgMS45ODYyMUM1LjU4OTc3IDEuOTg2MjEgNS40MjYwMyAyLjE2OTk1IDUuNDI2MDMgMi4zMTQ5NEM1LjQyNjAzIDIuNTE3NDMgNS40MjYwMyAzLjAyMTE1IDUuNDI2MDMgMy4wMjExNVoiIGZpbGw9IiMwRjY0QzciLz4KPC9zdmc+Cg==": dashboardIconPathPrefix + "police-car", +} + +// defaultDashboardIconPath mirrors the frontend's DEFAULT_DASHBOARD_ICON_PATH: the +// fallback icon for a dashboard whose image can't be carried over as-is. +const defaultDashboardIconPath = dashboardIconPathPrefix + "eight-ball" + +// resolveV1Image maps a v1 dashboard's `image` to a value valid under the v2 schema. +// A preset base64 icon becomes its v2 icon path; a custom base64 upload or an empty +// image (both valid v2) pass through. Anything else — a stale URL, a corrupt data +// URI — would fail validation and sink the whole dashboard migration, so it falls +// back to the default icon instead. +func resolveV1Image(image string) string { + if path, ok := v1Base64IconToAssetPath[image]; ok { + return path + } + if (DashboardV2MetadataBase{Image: image}).validateImage() == nil { + return image + } + return defaultDashboardIconPath +} diff --git a/pkg/types/dashboardtypes/perses_v1_to_v2_layouts.go b/pkg/types/dashboardtypes/perses_v1_to_v2_layouts.go new file mode 100644 index 00000000000..13fa2cee6a9 --- /dev/null +++ b/pkg/types/dashboardtypes/perses_v1_to_v2_layouts.go @@ -0,0 +1,352 @@ +package dashboardtypes + +import ( + "sort" + "strings" + + "github.com/perses/spec/go/common" + "github.com/perses/spec/go/dashboard" +) + +// panelRefPrefix is the JSON-ref prefix a grid item uses to point at a panel: +// "#/spec/panels/". +const panelRefPrefix = "#/spec/panels/" + +// sanitizePanelID rewrites a widget id to something valid in a panel $ref. Perses +// accepts only [a-zA-Z0-9_-] per ref segment (common.jsonRefMatching), so every +// other rune (em dash, spaces, dots, unicode, …) is mapped to a hyphen. +func sanitizePanelID(id string) string { + return strings.Map(func(r rune) rune { + switch { + case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '_', r == '-': + return r + default: + return '-' + } + }, id) +} + +// sanitizeWidgetIDs rewrites every widget id in the raw v1 data — widgets[].id, +// layout[].i, panelMap keys and their widgets[].i — through sanitizePanelID, so a +// panel's map key and the layout $ref pointing at it stay identical (an illegal char +// in one but not the other would dangle the ref). Runs before panels/layouts build. +func sanitizeWidgetIDs(data StorableDashboardData) { + sanitizeField := func(raw any, field string) { + items, _ := raw.([]any) + for _, it := range items { + if m, ok := it.(map[string]any); ok { + if s, ok := m[field].(string); ok { + m[field] = sanitizePanelID(s) + } + } + } + } + sanitizeField(data["widgets"], "id") + sanitizeField(data["layout"], "i") + if panelMap, ok := data["panelMap"].(map[string]any); ok { + for key, v := range panelMap { + if s := sanitizePanelID(key); s != key { + panelMap[s] = v + delete(panelMap, key) + } + if m, ok := v.(map[string]any); ok { + sanitizeField(m["widgets"], "i") + } + } + } +} + +// ══════════════════════════════════════════════ +// Layouts (data.layout + data.panelMap) +// ══════════════════════════════════════════════ + +// convertV1Layouts groups v1 react-grid-layout entries into v2 grid layouts. +// Membership is positional (as the frontend renders): each row widget owns the +// panels below it until the next row; panels above the first row form an unnamed +// grid with no section header. Collapsed rows are the exception — their children +// live in panelMap[rowID].widgets, not `layout`. +func (d *v1Decoder) convertV1Layouts(data StorableDashboardData, panels map[string]*Panel) []Layout { + layout := d.readObjects(data, "layout") + if len(layout) == 0 { + return []Layout{} + } + + // react-grid-layout can persist the same widget id more than once. Keep the first + // occurrence in stored order (mirroring getUpdatedLayout — the losing entry's + // geometry is discarded, not merged) and drop the rest. Dedupe before sortByPosition + // so "first" means first-in-stored-order, not topmost. Entries with no id are left + // for the main loop to drop. + seenWidgetIds := make(map[string]bool, len(layout)) + dedupedLayouts := layout[:0] + for _, item := range layout { + if id := d.readString(item, "i"); id != "" { + if seenWidgetIds[id] { + continue + } + seenWidgetIds[id] = true + } + dedupedLayouts = append(dedupedLayouts, item) + } + layout = dedupedLayouts + + rows := d.extractRowsAndCollapsedWidgets(data) + + // ids placed directly in `layout`. A collapsed child also listed here is rendered from + // layout (the open section), so it's dropped from its collapsed section below. + placedInLayout := make(map[string]bool, len(layout)) + for _, item := range layout { + if id := d.readString(item, "i"); id != "" { + placedInLayout[id] = true + } + } + + d.sortByPosition(layout) + + type section struct { + row *rowInfo // nil for the unnamed grid of ungrouped panels + items []map[string]any + } + topSectionWithoutHeader := §ion{} + sectionsWithHeader := make([]*section, 0, len(rows)) + currentRowHeader := topSectionWithoutHeader + for _, item := range layout { + id := d.readString(item, "i") + if id == "" { + continue + } + if row, ok := rows[id]; ok { + newRowHeader := §ion{row: row, items: d.extractValidLayoutItemsForCollapsedSection(row.collapsedWidgets, panels, placedInLayout)} + sectionsWithHeader = append(sectionsWithHeader, newRowHeader) + // A collapsed row owns only its stashed children; later panels → ungrouped. + if row.collapsed { + currentRowHeader = topSectionWithoutHeader + } else { + currentRowHeader = newRowHeader + } + continue + } + // Keep a layout entry only if its widget became a panel; otherwise (skipped + // widget, deleted id, or the "__dropping-elem__" drag placeholder) it would + // reference a panel that does not exist. Rows are handled above. + if _, ok := panels[id]; !ok { + continue + } + currentRowHeader.items = append(currentRowHeader.items, item) + } + + out := make([]Layout, 0, len(sectionsWithHeader)+1) + if len(topSectionWithoutHeader.items) > 0 { + out = append(out, d.buildV2GridLayout(nil, topSectionWithoutHeader.items)) + } + for _, sec := range sectionsWithHeader { + out = append(out, d.buildV2GridLayout(sec.row, sec.items)) + } + return out +} + +// retainPlacedWidgets drops widgets the v1 layout never places, returning the +// filtered widgets. v1 doesn't render an unplaced widget, so converting it — and +// noting any problems it has — is pure noise; filter before conversion so only +// rendered widgets reach convertV1Panels. A non-array widgets value is returned +// untouched for convertV1Panels to flag; non-map entries are kept so it still +// flags them as malformed. +func retainPlacedWidgets(data StorableDashboardData) any { + widgets, ok := data["widgets"].([]any) + if !ok { + return data["widgets"] + } + placed := placedWidgetIDs(data) + kept := make([]any, 0, len(widgets)) + for _, w := range widgets { + wm, ok := w.(map[string]any) + if !ok { + kept = append(kept, w) // malformed entry — leave it for convertV1Panels to note + continue + } + if id, _ := wm["id"].(string); placed[id] { + kept = append(kept, w) + } + } + return kept +} + +// placedWidgetIDs returns the set of widget ids the v1 layout actually renders: +// every id in `layout`, plus the collapsed-row children stashed in panelMap. +// Read leniently (no malformed notes) — convertV1Layouts re-reads these and +// reports any genuine problems. +func placedWidgetIDs(data StorableDashboardData) map[string]bool { + ids := make(map[string]bool) + if layout, ok := data["layout"].([]any); ok { + for _, e := range layout { + if m, ok := e.(map[string]any); ok { + if i, ok := m["i"].(string); ok && i != "" { + ids[i] = true + } + } + } + } + if panelMap, ok := data["panelMap"].(map[string]any); ok { + for _, v := range panelMap { + m, ok := v.(map[string]any) + if !ok { + continue + } + widgets, ok := m["widgets"].([]any) + if !ok { + continue + } + for _, w := range widgets { + if wm, ok := w.(map[string]any); ok { + if i, ok := wm["i"].(string); ok && i != "" { + ids[i] = true + } + } + } + } + } + return ids +} + +// extractValidLayoutItemsForCollapsedSection keeps only the collapsed-row children +// backed by a real panel and not already placed in `layout`, dropping ghosts and any +// child the open layout renders instead. These come from panelMap and skip the main +// loop's per-item panel check, so a grid never references a missing or twice-placed panel. +func (d *v1Decoder) extractValidLayoutItemsForCollapsedSection(items []map[string]any, panels map[string]*Panel, placedInLayout map[string]bool) []map[string]any { + out := make([]map[string]any, 0, len(items)) + seen := make(map[string]bool, len(items)) + for _, item := range items { + id := d.readString(item, "i") + if id == "" { + continue + } + if _, ok := panels[id]; !ok { + continue + } + if placedInLayout[id] || seen[id] { + continue + } + seen[id] = true + out = append(out, item) + } + return out +} + +type rowInfo struct { + title string + collapsed bool + collapsedWidgets []map[string]any +} + +// extractRowsAndCollapsedWidgets returns the row widgets keyed by id; collapsed +// rows also carry their children stashed under panelMap[id].widgets. +func (d *v1Decoder) extractRowsAndCollapsedWidgets(data StorableDashboardData) map[string]*rowInfo { + panelMap := d.readObject(data, "panelMap") + rows := make(map[string]*rowInfo) + for _, w := range d.readObjects(data, "widgets") { + // Read id directly (not via readString): a non-string id is skipped silently by + // convertV1Panels, so flagging it malformed here would fail the migration for a + // widget that's already been dropped. + id, _ := w["id"].(string) + if d.readString(w, "panelTypes") != "row" || id == "" { + continue + } + row := &rowInfo{title: d.readString(w, "title")} + // Some templates store panelMap[id] as a bare []widgetID instead of the + // canonical {widgets, collapsed}. The frontend treats such a non-object + // entry as "not collapsed" (see GridCardLayout), so read it leniently: a + // non-map yields nil, which reads as not collapsed. + pm, _ := panelMap[id].(map[string]any) + if d.readBool(pm, "collapsed") { + row.collapsed = true + row.collapsedWidgets = d.readObjects(pm, "widgets") + } + rows[id] = row + } + return rows +} + +// buildV2GridLayout builds one v2 grid. row is nil for the unnamed grid (no +// display); otherwise the grid takes the row's title and collapse state. Items are +// sorted by (y, x) then vertically compacted (see compactGridItemsVertically). +func (d *v1Decoder) buildV2GridLayout(row *rowInfo, items []map[string]any) Layout { + d.sortByPosition(items) + + spec := dashboard.GridLayoutSpec{Items: make([]dashboard.GridItem, 0, len(items))} + if row != nil { + spec.Display = &dashboard.GridLayoutDisplay{ + Title: clipName(row.title, MaxLayoutTitleLen), + Collapse: &dashboard.GridLayoutCollapse{Open: !row.collapsed}, + } + } + + for _, item := range items { + id := d.readString(item, "i") + spec.Items = append(spec.Items, dashboard.GridItem{ + X: d.readInt(item, "x"), + Y: d.readInt(item, "y"), + Width: d.readInt(item, "w"), + Height: d.readInt(item, "h"), + // Path mirrors what JSONRef.validate derives on decode (each "/segment" + // of the ref; the leading "#" captures nothing). + Content: &common.JSONRef{Ref: panelRefPrefix + id, Path: []string{"spec", "panels", id}}, + }) + } + compactGridItemsVertically(spec.Items) + return Layout{Kind: dashboard.KindGridLayout, Spec: &spec} +} + +// compactGridItemsVertically mirrors react-grid-layout's correctBounds+compact +// (compactType "vertical", allowOverlap false): clamp each item into the grid (x,y>=0; +// x+width<=cols by shifting left), then move sorted-first items up to fill space and +// down past collisions. Fixes overlaps, gaps, and out-of-bounds coords so the migrated +// grid matches the v1 UI and passes v2 validation. +func compactGridItemsVertically(items []dashboard.GridItem) { + collides := func(a, b dashboard.GridItem) bool { + return a.X < b.X+b.Width && b.X < a.X+a.Width && a.Y < b.Y+b.Height && b.Y < a.Y+a.Height + } + firstCollision := func(l dashboard.GridItem, placed []dashboard.GridItem) (dashboard.GridItem, bool) { + for _, p := range placed { + if collides(l, p) { + return p, true + } + } + return dashboard.GridItem{}, false + } + for i := range items { + l := items[i] + if l.X+l.Width > gridColumnCount { // overflows right → shift left to fit + l.X = gridColumnCount - l.Width + } + if l.X < 0 { + l.X = 0 + } + if l.Y < 0 { + l.Y = 0 + } + for l.Y > 0 { // move up to fill space above + up := l + up.Y-- + if _, hit := firstCollision(up, items[:i]); hit { + break + } + l.Y-- + } + for { // then down past any collision with an already-placed item + c, hit := firstCollision(l, items[:i]) + if !hit { + break + } + l.Y = c.Y + c.Height + } + items[i] = l + } +} + +func (d *v1Decoder) sortByPosition(items []map[string]any) { + sort.SliceStable(items, func(i, j int) bool { + if yi, yj := d.readInt(items[i], "y"), d.readInt(items[j], "y"); yi != yj { + return yi < yj + } + return d.readInt(items[i], "x") < d.readInt(items[j], "x") + }) +} diff --git a/pkg/types/dashboardtypes/perses_v1_to_v2_panels.go b/pkg/types/dashboardtypes/perses_v1_to_v2_panels.go new file mode 100644 index 00000000000..aa1fe41c04d --- /dev/null +++ b/pkg/types/dashboardtypes/perses_v1_to_v2_panels.go @@ -0,0 +1,487 @@ +package dashboardtypes + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + "time" + + "github.com/SigNoz/signoz/pkg/types/telemetrytypes" + "github.com/SigNoz/signoz/pkg/valuer" +) + +// ══════════════════════════════════════════════ +// Widgets → Panels +// ══════════════════════════════════════════════ + +// convertV1Panels walks the v1 `widgets` array and produces v2 panels keyed by +// the v1 widget id. WidgetRow entries (panelTypes == "row") are dropped here +// and consumed by convertV1Layouts as section headers. +func (d *v1Decoder) convertV1Panels(raw any) map[string]*Panel { + if raw == nil { + return map[string]*Panel{} + } + widgetsRaw, ok := raw.([]any) + if !ok { + d.noteMalformedField("widgets", raw) + return nil + } + panels := make(map[string]*Panel, len(widgetsRaw)) + for i, widgetRaw := range widgetsRaw { + widget, ok := widgetRaw.(map[string]any) + if !ok { + d.noteMalformedField(fmt.Sprintf("widgets[%d]", i), widgetRaw) + continue + } + // A non-string (or missing) id can't be referenced by any layout entry, and + // v1 doesn't render such widgets either — skip silently, don't flag it as + // malformed. Read directly (not via readString) to avoid a malformed note. + id, ok := widget["id"].(string) + if !ok || id == "" { + continue + } + var panel *Panel + panelType := d.readString(widget, "panelTypes") + switch panelType { + case "graph": + panel = d.convertGraphWidget(widget) + case "time_series", "TIME_SERIES": + // Malformed panelTypes: the canonical v1 value is "graph". Some dashboards + // stored the v2/enum-style name instead; accept it as a time-series graph. + panel = d.convertGraphWidget(widget) + case "bar": + panel = d.convertBarWidget(widget) + case "value": + panel = d.convertValueWidget(widget) + case "pie": + panel = d.convertPieWidget(widget) + case "table": + panel = d.convertTableWidget(widget) + case "histogram": + panel = d.convertHistogramWidget(widget) + case "list": + panel = d.convertListWidget(widget) + case "row": + // "row" (section header) is handled by the layout pass; + continue + default: + // Unknown/unsupported panel type — v1 can't render it either, so skip the + // widget silently rather than failing the whole migration. + continue + } + if panel == nil { + continue + } + if len(panel.Spec.Queries) == 0 { + // No renderable queries — every query was dropped as unrenderable, or none + // were defined. v1 renders nothing, so skip the widget silently. + continue + } + // v1 has no panel links; emit [] (not nil) so the required links field + // round-trips, rather than setting it in every panel constructor. + panel.Spec.Links = []Link{} + panels[id] = panel + } + return panels +} + +func (d *v1Decoder) convertGraphWidget(w map[string]any) *Panel { + return &Panel{ + Kind: "Panel", + Spec: PanelSpec{ + Display: d.widgetDisplay(w), + Plugin: PanelPlugin{ + Kind: PanelKindTimeSeries, + Spec: &TimeSeriesPanelSpec{ + Visualization: TimeSeriesVisualization{ + BasicVisualization: d.basicVisualization(w), + FillSpans: d.readBool(w, "fillSpans"), + }, + Formatting: d.panelFormatting(w), + ChartAppearance: TimeSeriesChartAppearance{ + LineInterpolation: mapV1Enum(d.readString(w, "lineInterpolation"), LineInterpolationSpline, + LineInterpolationLinear, LineInterpolationSpline, LineInterpolationStepAfter, LineInterpolationStepBefore), + ShowPoints: d.readBool(w, "showPoints"), + LineStyle: mapV1Enum(d.readString(w, "lineStyle"), LineStyleSolid, LineStyleSolid, LineStyleDashed), + FillMode: mapV1Enum(d.readString(w, "fillMode"), FillModeNone, FillModeSolid, FillModeGradient, FillModeNone), + SpanGaps: mapV1SpanGaps(w["spanGaps"]), + }, + Axes: d.axesFromWidget(w), + Legend: d.legendFromWidget(w), + Thresholds: d.mapV1ThresholdsWithLabel(w), + }, + }, + Queries: d.convertV1WidgetQuery(w, PanelKindTimeSeries), + }, + } +} + +func (d *v1Decoder) convertBarWidget(w map[string]any) *Panel { + return &Panel{ + Kind: "Panel", + Spec: PanelSpec{ + Display: d.widgetDisplay(w), + Plugin: PanelPlugin{ + Kind: PanelKindBarChart, + Spec: &BarChartPanelSpec{ + Visualization: BarChartVisualization{ + BasicVisualization: d.basicVisualization(w), + FillSpans: d.readBool(w, "fillSpans"), + StackedBarChart: d.readBool(w, "stackedBarChart"), + }, + Formatting: d.panelFormatting(w), + Axes: d.axesFromWidget(w), + Legend: d.legendFromWidget(w), + Thresholds: d.mapV1ThresholdsWithLabel(w), + }, + }, + Queries: d.convertV1WidgetQuery(w, PanelKindBarChart), + }, + } +} + +func (d *v1Decoder) convertValueWidget(w map[string]any) *Panel { + return &Panel{ + Kind: "Panel", + Spec: PanelSpec{ + Display: d.widgetDisplay(w), + Plugin: PanelPlugin{ + Kind: PanelKindNumber, + Spec: &NumberPanelSpec{ + Visualization: d.basicVisualization(w), + Formatting: d.panelFormatting(w), + Thresholds: d.mapV1ComparisonThresholds(w), + }, + }, + Queries: d.convertV1WidgetQuery(w, PanelKindNumber), + }, + } +} + +func (d *v1Decoder) convertPieWidget(w map[string]any) *Panel { + return &Panel{ + Kind: "Panel", + Spec: PanelSpec{ + Display: d.widgetDisplay(w), + Plugin: PanelPlugin{ + Kind: PanelKindPieChart, + Spec: &PieChartPanelSpec{ + Visualization: d.basicVisualization(w), + Formatting: d.panelFormatting(w), + Legend: d.legendFromWidget(w), + }, + }, + Queries: d.convertV1WidgetQuery(w, PanelKindPieChart), + }, + } +} + +func (d *v1Decoder) convertTableWidget(w map[string]any) *Panel { + return &Panel{ + Kind: "Panel", + Spec: PanelSpec{ + Display: d.widgetDisplay(w), + Plugin: PanelPlugin{ + Kind: PanelKindTable, + Spec: &TablePanelSpec{ + Visualization: d.basicVisualization(w), + Formatting: TableFormatting{ + ColumnUnits: d.readStringMap(w, "columnUnits"), + DecimalPrecision: mapV1Precision(w["decimalPrecision"]), + }, + Thresholds: d.mapV1TableThresholds(w), + }, + }, + Queries: d.convertV1WidgetQuery(w, PanelKindTable), + }, + } +} + +func (d *v1Decoder) convertHistogramWidget(w map[string]any) *Panel { + return &Panel{ + Kind: "Panel", + Spec: PanelSpec{ + Display: d.widgetDisplay(w), + Plugin: PanelPlugin{ + Kind: PanelKindHistogram, + Spec: &HistogramPanelSpec{ + HistogramBuckets: HistogramBuckets{ + BucketCount: d.readFloatPtr(w, "bucketCount"), + BucketWidth: d.readFloatPtr(w, "bucketWidth"), + MergeAllActiveQueries: d.readBool(w, "mergeAllActiveQueries"), + }, + Legend: d.legendFromWidget(w), + }, + }, + Queries: d.convertV1WidgetQuery(w, PanelKindHistogram), + }, + } +} + +func (d *v1Decoder) convertListWidget(w map[string]any) *Panel { + return &Panel{ + Kind: "Panel", + Spec: PanelSpec{ + Display: d.widgetDisplay(w), + Plugin: PanelPlugin{ + Kind: PanelKindList, + Spec: &ListPanelSpec{ + SelectFields: d.mapV1SelectFields(w), + }, + }, + Queries: d.convertV1WidgetQuery(w, PanelKindList), + }, + } +} + +// ══════════════════════════════════════════════ +// Panel-spec shared helpers +// ══════════════════════════════════════════════ + +func (d *v1Decoder) widgetDisplay(w map[string]any) Display { + return Display{Name: clipName(d.readString(w, "title"), MaxDisplayNameLen), Description: d.readString(w, "description")} +} + +func (d *v1Decoder) basicVisualization(w map[string]any) BasicVisualization { + return BasicVisualization{TimePreference: mapV1TimePreference(d.readString(w, "timePreferance"))} +} + +func (d *v1Decoder) panelFormatting(w map[string]any) PanelFormatting { + return PanelFormatting{Unit: d.readString(w, "yAxisUnit"), DecimalPrecision: mapV1Precision(w["decimalPrecision"])} +} + +func (d *v1Decoder) axesFromWidget(w map[string]any) Axes { + return Axes{ + SoftMin: d.readFloatPtr(w, "softMin"), + SoftMax: d.readFloatPtr(w, "softMax"), + IsLogScale: d.readBool(w, "isLogScale"), + } +} + +func (d *v1Decoder) legendFromWidget(w map[string]any) Legend { + return Legend{ + Position: mapV1Enum(d.readString(w, "legendPosition"), LegendPositionBottom, LegendPositionBottom, LegendPositionRight), + CustomColors: d.readStringMap(w, "customLegendColors"), + } +} + +func (d *v1Decoder) mapV1SelectFields(w map[string]any) []telemetrytypes.TelemetryFieldKey { + field := "selectedLogFields" + raw := d.readArray(w, field) + if len(raw) == 0 { + field = "selectedTracesFields" + raw = d.readArray(w, field) + } + if len(raw) == 0 { + return nil + } + normalizePreV5FieldKeys(raw) + fields, err := decodeTelemetryFields(raw) + if err != nil { + d.note("widget %q has malformed %s: %v", d.readString(w, "id"), field, err) + return nil + } + // Drop nameless entries (blank column rows) — v2 requires a name, and the v1 + // UI renders nothing for them anyway. + out := fields[:0] + for _, f := range fields { + if f.Name != "" { + out = append(out, f) + } + } + return out +} + +func decodeTelemetryFields(raw []any) ([]telemetrytypes.TelemetryFieldKey, error) { + bytes, err := json.Marshal(raw) + if err != nil { + return nil, err + } + var fields []telemetrytypes.TelemetryFieldKey + if err := json.Unmarshal(bytes, &fields); err != nil { + return nil, err + } + return fields, nil +} + +// ══════════════════════════════════════════════ +// Panel field mappers +// ══════════════════════════════════════════════ + +// v1 stores timePreferance as `GLOBAL_TIME`, `LAST_5_MIN`, … (see +// frontend/src/container/NewWidget/RightContainer/timeItems.ts). v2 uses the +// lowercase form, so the translation is just downcase. +func mapV1TimePreference(s string) TimePreference { + if s == "" { + return TimePreferenceGlobalTime + } + candidate := TimePreference{valuer.NewString(strings.ToLower(s))} + for _, allowed := range candidate.Enum() { + if allowed == candidate { + return candidate + } + } + return TimePreferenceGlobalTime +} + +// mapV1Precision is polymorphic (string|number), so it type-switches the raw +// value rather than reading through a typed accessor. +func mapV1Precision(raw any) PrecisionOption { + switch v := raw.(type) { + case string: + candidate := PrecisionOption{valuer.NewString(v)} + for _, allowed := range candidate.Enum() { + if allowed == candidate { + return candidate + } + } + case float64: + n := int(v) + if n >= 0 && n <= 4 { + return PrecisionOption{valuer.NewString(strconv.Itoa(n))} + } + } + return PrecisionOption2 +} + +// mapV1Enum picks the v1 string value if it matches one of the allowed v2 +// values, otherwise returns the fallback. v1 frontend enums (lineInterpolation, +// lineStyle, fillMode, legendPosition) already use the v2 lowercase form. +func mapV1Enum[T interface{ StringValue() string }](s string, fallback T, allowed ...T) T { + if s == "" { + return fallback + } + for _, a := range allowed { + if a.StringValue() == s { + return a + } + } + return fallback +} + +// v1 spanGaps is `boolean | number`. true → span every gap; false → never span; +// a number is interpreted (per frontend SeriesProps.spanGaps docs) as an +// X-axis threshold in seconds. Polymorphic, so it type-switches the raw value. +func mapV1SpanGaps(raw any) SpanGaps { + switch v := raw.(type) { + case bool: + return SpanGaps{FillOnlyBelow: false} + case float64: + return SpanGaps{FillOnlyBelow: true, FillLessThan: time.Duration(v * float64(time.Second)).String()} + } + return SpanGaps{FillOnlyBelow: false} +} + +func (d *v1Decoder) mapV1ThresholdsWithLabel(w map[string]any) []ThresholdWithLabel { + rawSlice := d.readObjects(w, "thresholds") + if len(rawSlice) == 0 { + return nil + } + out := make([]ThresholdWithLabel, 0, len(rawSlice)) + for _, t := range rawSlice { + color := d.readString(t, "thresholdColor") + if color == "" { + // Only color is required on ThresholdWithLabel; label is optional, so an + // unlabeled threshold must survive. + continue + } + value := d.readFloat(t, "thresholdValue") + out = append(out, ThresholdWithLabel{Value: value, Unit: d.readString(t, "thresholdUnit"), Color: color, Label: d.readString(t, "thresholdLabel")}) + } + if len(out) == 0 { + return nil + } + return out +} + +func (d *v1Decoder) mapV1ComparisonThresholds(w map[string]any) []ComparisonThreshold { + rawSlice := d.readObjects(w, "thresholds") + if len(rawSlice) == 0 { + return nil + } + out := make([]ComparisonThreshold, 0, len(rawSlice)) + for _, t := range rawSlice { + color := d.readString(t, "thresholdColor") + if color == "" { + continue + } + value := d.readFloat(t, "thresholdValue") + out = append(out, ComparisonThreshold{ + Value: value, + Operator: d.mapV1ComparisonOperator(d.readString(t, "thresholdOperator")), + Unit: d.readString(t, "thresholdUnit"), + Color: color, + Format: mapV1ThresholdFormat(t["thresholdFormat"]), + }) + } + if len(out) == 0 { + return nil + } + return out +} + +func (d *v1Decoder) mapV1TableThresholds(w map[string]any) []TableThreshold { + rawSlice := d.readObjects(w, "thresholds") + if len(rawSlice) == 0 { + return nil + } + out := make([]TableThreshold, 0, len(rawSlice)) + for _, t := range rawSlice { + color := d.readString(t, "thresholdColor") + columnName := d.readString(t, "thresholdTableOptions") + if color == "" || columnName == "" { + continue + } + value := d.readFloat(t, "thresholdValue") + out = append(out, TableThreshold{ + ComparisonThreshold: ComparisonThreshold{ + Value: value, + Operator: d.mapV1ComparisonOperator(d.readString(t, "thresholdOperator")), + Unit: d.readString(t, "thresholdUnit"), + Color: color, + Format: mapV1ThresholdFormat(t["thresholdFormat"]), + }, + ColumnName: columnName, + }) + } + if len(out) == 0 { + return nil + } + return out +} + +func (d *v1Decoder) mapV1ComparisonOperator(s string) ComparisonOperator { + switch s { + case ">", "gt": + return ComparisonOperatorAbove + case ">=", "gte": + return ComparisonOperatorAboveOrEqual + case "<", "lt": + return ComparisonOperatorBelow + case "<=", "lte": + return ComparisonOperatorBelowOrEqual + case "=", "==", "eq": + return ComparisonOperatorEqual + case "!=", "neq": + return ComparisonOperatorNotEqual + default: + // v1 often leaves the operator empty or carries an unknown value; default to + // "above" without flagging. + return ComparisonOperatorAbove + } +} + +// mapV1ThresholdFormat reads the raw value (not via readString) so a non-string +// thresholdFormat — some v1 dashboards store it as a number — defaults to text +// silently instead of being flagged malformed. +func mapV1ThresholdFormat(raw any) ThresholdFormat { + s, _ := raw.(string) + switch strings.ToLower(s) { + case "background": + return ThresholdFormatBackground + case "text": + return ThresholdFormatText + } + return ThresholdFormatText +} diff --git a/pkg/types/dashboardtypes/perses_v1_to_v2_queries.go b/pkg/types/dashboardtypes/perses_v1_to_v2_queries.go new file mode 100644 index 00000000000..efc78fdfeee --- /dev/null +++ b/pkg/types/dashboardtypes/perses_v1_to_v2_queries.go @@ -0,0 +1,448 @@ +package dashboardtypes + +import ( + "encoding/json" + "strconv" + "strings" + + "github.com/SigNoz/signoz/pkg/errors" + qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" + "github.com/SigNoz/signoz/pkg/types/telemetrytypes" +) + +// ══════════════════════════════════════════════ +// Queries +// ══════════════════════════════════════════════ + +// convertV1WidgetQuery returns exactly one Query (per Spec.Validate). The kind +// chosen depends on the v1 widget query shape: +// - a single query (promql / clickhouse_sql / builder) → its native kind +// - multiple queries → signoz/CompositeQuery +// +// A single query is never wrapped in a CompositeQuery; in particular List +// panels accept only a bare signoz/BuilderQuery. Builder queries are routed +// through qb.WrapInV5Envelope (in collectV1QueryEnvelopes), which translates v4 +// builder-field names (orderBy/selectColumns/dataSource) into their v5 +// equivalents and adds the `signal` field required by BuilderQuerySpec's +// per-signal dispatch. +func (d *v1Decoder) convertV1WidgetQuery(widget map[string]any, panelKind PanelPluginKind) []Query { + envelopes, signal := d.collectV1QueryEnvelopes(widget, panelKind) + if len(envelopes) == 0 { + return nil + } + // List panels accept only a bare BuilderQuery — never a CompositeQuery. Keep the + // first query and drop the rest so a multi-query v1 list widget still migrates. + if panelKind == PanelKindList && len(envelopes) > 1 { + envelopes = envelopes[:1] + } + requestType := requestTypeForPanel(panelKind) + + // A single query keeps its native kind — never wrapped in a CompositeQuery. + if len(envelopes) == 1 { + if q := singleQueryFromEnvelope(envelopes[0], requestType, signal); q != nil { + return []Query{*q} + } + } + + // Default: wrap in CompositeQuery. + composite, err := parseCompositeFromEnvelopes(envelopes) + if err != nil || composite == nil { + d.note("widget %q: could not build query from %d envelope(s): %s", d.readString(widget, "id"), len(envelopes), detailErr(err)) + return nil + } + return []Query{{ + Kind: requestType, + Spec: QuerySpec{ + Plugin: QueryPlugin{Kind: QueryKindComposite, Spec: composite}, + }, + }} +} + +// dropUnrenderableQueries removes queries whose aggregation can't render (see +// queryIsUnrenderable). If every query is unrenderable the result is empty, so the +// widget produces no panel and is skipped silently (convertV1Panels) — matching v1, +// which renders nothing. +func dropUnrenderableQueries(queries []map[string]any) []map[string]any { + renderable := make([]map[string]any, 0, len(queries)) + for _, q := range queries { + if !queryIsUnrenderable(q) { + renderable = append(renderable, q) + } + } + return renderable +} + +// queryIsUnrenderable reports whether a builder query can't render because of its +// aggregations: a metrics query with none or an empty metric name, or a logs/traces +// query with an empty aggregation expression. No aggregations is valid for a raw +// logs/traces query, so that isn't flagged. +func queryIsUnrenderable(q map[string]any) bool { + aggs, _ := q["aggregations"].([]any) + switch signalFromDataSource(q["dataSource"]) { + case telemetrytypes.SignalMetrics: + if len(aggs) == 0 { + return true + } + for _, a := range aggs { + if agg, ok := a.(map[string]any); ok { + if mn, _ := agg["metricName"].(string); mn == "" { + return true + } + } + } + case telemetrytypes.SignalLogs, telemetrytypes.SignalTraces: + for _, a := range aggs { + if agg, ok := a.(map[string]any); ok { + if expr, _ := agg["expression"].(string); expr == "" { + return true + } + } + } + } + return false +} + +// requestTypeForPanel maps a v2 panel plugin kind to the request type (result +// shape) its queries produce. Mirrors the frontend's panelTypeToRequestType +// (buildQueryRangeRequest.ts): time series for line/bar/histogram (histogram +// bins client-side from raw time series, V1 parity), scalar for +// number/pie/table, raw rows for list. +func requestTypeForPanel(panelKind PanelPluginKind) qb.RequestType { + switch panelKind { + case PanelKindTimeSeries, PanelKindBarChart, PanelKindHistogram: + return qb.RequestTypeTimeSeries + case PanelKindNumber, PanelKindPieChart, PanelKindTable: + return qb.RequestTypeScalar + case PanelKindList: + return qb.RequestTypeRaw + } + return qb.RequestTypeTimeSeries +} + +// collectV1QueryEnvelopes inspects widget.query.queryType and produces a +// flattened list of v5-shaped envelopes. The returned signal is the dominant +// builder signal (if any), used for typed builder-query dispatch. +func (d *v1Decoder) collectV1QueryEnvelopes(widget map[string]any, panelKind PanelPluginKind) ([]map[string]any, telemetrytypes.Signal) { + queryMap := d.readObject(widget, "query") + if queryMap == nil { + d.note("widget %q has no query map", d.readString(widget, "id")) + return nil, telemetrytypes.Signal{} + } + rowLimitPanel := panelKind == PanelKindList || panelKind == PanelKindTable + // Raw (list) panels legitimately have no aggregation; every other panel needs one. + needsAggregation := requestTypeForPanel(panelKind) != qb.RequestTypeRaw + + queryType := d.readString(queryMap, "queryType") + switch queryType { + case "promql": + promQueries := d.readObjects(queryMap, "promql") + var out []map[string]any + for _, q := range promQueries { + // Drop empty queries; if none remain the widget produces no queries and is + // skipped silently (convertV1Panels), as v1 renders nothing. + if d.readString(q, "query") == "" { + continue + } + out = append(out, promQLEnvelope(q)) + } + return out, telemetrytypes.Signal{} + + case "clickhouse_sql": + chQueries := d.readObjects(queryMap, "clickhouse_sql") + var out []map[string]any + for _, q := range chQueries { + // Drop empty queries; if none remain the widget produces no queries and is + // skipped silently (convertV1Panels), as v1 renders nothing. + if d.readString(q, "query") == "" { + continue + } + out = append(out, clickhouseEnvelope(q)) + } + return out, telemetrytypes.Signal{} + + // "builder" plus a blank queryType: v1 defaults an unset type to builder, so a + // missing value still carries a real builder query — try the builder path. + case "builder", "": + builder := d.readObject(queryMap, "builder") + if builder == nil { + d.note("widget %q has no builder data in the query map", d.readString(widget, "id")) + return nil, telemetrytypes.Signal{} + } + var out []map[string]any + var signal telemetrytypes.Signal + widgetType := d.readString(widget, "panelTypes") + queries := d.readObjects(builder, "queryData") + assignQueryDataNames(queries) + for _, q := range queries { + normalizePreV5QueryData(q, widgetType) + normalizePreV5SelectColumns(q) + normalizePreV5GroupBy(q) + normalizePreV5PageSize(q, rowLimitPanel) + normalizeQueryLimit(q) + if needsAggregation { + ensureDefaultAggregation(q) + } + // After the aggregation is settled (incl. an injected default), so a + // value-order key (#SIGNOZ_VALUE) can resolve against it. + normalizeOrderByKeys(q) + } + queries = dropUnrenderableQueries(queries) + for _, q := range queries { + name := d.readString(q, "queryName") + out = append(out, qb.WrapInV5Envelope(name, q, string(qb.QueryTypeBuilder.StringValue()))) + if signal.IsZero() { + signal = signalFromDataSource(q["dataSource"]) + } + } + formulas := d.readObjects(builder, "queryFormulas") + assignMissingFormulaNames(formulas) + for _, f := range formulas { + normalizePreV5QueryData(f, widgetType) + name := d.readString(f, "queryName") + env := qb.WrapInV5Envelope(name, f, string(qb.QueryTypeFormula.StringValue())) + backfillFormulaFields(env, f) + // Drop a formula whose expression the validator rejects (blank/unparseable); + // v1 tolerated it but v2 fails the whole query. Reuse the real validator + // rather than reimplement it, as we do for functions. + if !formulaEnvelopeIsValid(env) { + continue + } + out = append(out, env) + } + for _, op := range d.readObjects(builder, "queryTraceOperator") { + // A trace operator's expression is the operation itself ("A=>B->C") and is + // required (ParseExpression rejects a blank one); drop it if empty. + expression := d.readString(op, "expression") + if expression == "" { + continue + } + normalizePreV5QueryData(op, widgetType) + normalizePreV5GroupBy(op) + normalizeOrderByKeys(op) + name := d.readString(op, "queryName") + out = append(out, traceOperatorEnvelope(name, expression, op)) + } + return out, signal + default: + d.note("widget %q has unknown queryType %q", d.readString(widget, "id"), queryType) + } + return nil, telemetrytypes.Signal{} +} + +// traceOperatorEnvelope builds a v5 builder_trace_operator envelope. WrapInV5Envelope +// would misclassify a trace operator as a formula (its name differs from its +// expression, e.g. "A=>B->C"), so route the map through the builder-query path — +// temporarily aligning expression with name to dodge that heuristic — then restore the +// real expression, drop the builder-only signal (a trace operator's spec has no signal +// field), and set the trace-operator type. +func traceOperatorEnvelope(name, expression string, op map[string]any) map[string]any { + op["expression"] = name + env := qb.WrapInV5Envelope(name, op, string(qb.QueryTypeBuilder.StringValue())) + if spec, ok := env["spec"].(map[string]any); ok { + delete(spec, "signal") + spec["expression"] = expression + } + env["type"] = string(qb.QueryTypeTraceOperator.StringValue()) + return env +} + +// maxQueries mirrors the frontend MAX_QUERIES; builder query names run A..Z. +const maxQueries = 26 + +// assignQueryDataNames names builder data queries the way the frontend does: each +// unnamed query takes the first unused A..Z, deduped against existing names. It also +// forces expression == queryName, since a data query's expression is always its own +// name and WrapInV5Envelope's name != expression heuristic would otherwise +// misclassify the query as a formula. +func assignQueryDataNames(queries []map[string]any) { + taken := make(map[string]bool, len(queries)) + for _, q := range queries { + if name, _ := q["queryName"].(string); name != "" { + taken[name] = true + } + } + for _, q := range queries { + name, _ := q["queryName"].(string) + if name == "" { + for i := 0; i < maxQueries; i++ { + candidate := string(rune('A' + i)) + if !taken[candidate] { + name = candidate + taken[candidate] = true + break + } + } + q["queryName"] = name + } + q["expression"] = name + } +} + +// formulaEnvelopeIsValid reports whether a builder_formula envelope's spec passes +// QueryBuilderFormula.Validate (blank/unparseable expression, blank name, invalid +// functions). Reuses the real validator rather than reimplementing it. +func formulaEnvelopeIsValid(env map[string]any) bool { + spec, ok := env["spec"].(map[string]any) + if !ok { + return false + } + raw, err := json.Marshal(spec) + if err != nil { + return false + } + var f qb.QueryBuilderFormula + if err := json.Unmarshal(raw, &f); err != nil { + return false + } + return f.Validate() == nil +} + +// maxFormulas mirrors the frontend MAX_FORMULAS; formula names run F1..F20. +const maxFormulas = 20 + +// assignMissingFormulaNames fills queryName for unnamed formulas, mirroring the +// frontend: pick the first F{n} (n in 1..20) not already used by another formula. +// Formulas that already have a name keep it. +func assignMissingFormulaNames(formulas []map[string]any) { + taken := make(map[string]bool, len(formulas)) + for _, f := range formulas { + if name, _ := f["queryName"].(string); name != "" { + taken[name] = true + } + } + for _, f := range formulas { + if name, _ := f["queryName"].(string); name != "" { + continue + } + for i := 1; i <= maxFormulas; i++ { + candidate := "F" + strconv.Itoa(i) + if !taken[candidate] { + f["queryName"] = candidate + taken[candidate] = true + break + } + } + } +} + +func promQLEnvelope(q map[string]any) map[string]any { + return map[string]any{ + "type": qb.QueryTypePromQL.StringValue(), + "spec": map[string]any{ + "name": q["name"], + "query": q["query"], + "disabled": q["disabled"], + "legend": q["legend"], + }, + } +} + +func clickhouseEnvelope(q map[string]any) map[string]any { + return map[string]any{ + "type": qb.QueryTypeClickHouseSQL.StringValue(), + "spec": map[string]any{ + "name": q["name"], + "query": q["query"], + "disabled": q["disabled"], + "legend": q["legend"], + }, + } +} + +// singleQueryFromEnvelope returns a typed Query for one envelope, using its +// native query kind (promql/clickhouse_sql/builder) rather than wrapping it in +// a CompositeQuery. A bare signoz/BuilderQuery is valid for every panel kind +// and is the only kind List panels accept. +func singleQueryFromEnvelope(envelope map[string]any, requestType qb.RequestType, signal telemetrytypes.Signal) *Query { + t, _ := envelope["type"].(string) + spec, _ := envelope["spec"].(map[string]any) + switch t { + case qb.QueryTypePromQL.StringValue(): + prom, err := decodeMapInto[qb.PromQuery](spec) + if err != nil { + return nil + } + return &Query{ + Kind: requestType, + Spec: QuerySpec{ + Name: prom.Name, + Plugin: QueryPlugin{Kind: QueryKindPromQL, Spec: &prom}, + }, + } + case qb.QueryTypeClickHouseSQL.StringValue(): + ch, err := decodeMapInto[qb.ClickHouseQuery](spec) + if err != nil { + return nil + } + return &Query{ + Kind: requestType, + Spec: QuerySpec{ + Name: ch.Name, + Plugin: QueryPlugin{Kind: QueryKindClickHouseSQL, Spec: &ch}, + }, + } + case qb.QueryTypeBuilder.StringValue(): + builderSpec := parseBuilderQuerySpec(spec, signal) + if builderSpec == nil { + return nil + } + name, _ := spec["name"].(string) + return &Query{ + Kind: requestType, + Spec: QuerySpec{ + Name: name, + Plugin: QueryPlugin{Kind: QueryKindBuilder, Spec: &BuilderQuerySpec{Spec: builderSpec}}, + }, + } + } + return nil +} + +func parseCompositeFromEnvelopes(envelopes []map[string]any) (*CompositeQuerySpec, error) { + bytes, err := json.Marshal(envelopes) + if err != nil { + return nil, errors.WrapInternalf(err, errors.CodeInternal, "marshal v1 query envelopes") + } + var parsed []qb.QueryEnvelope + if err := json.Unmarshal(bytes, &parsed); err != nil { + return nil, errors.WrapInvalidInputf(err, ErrCodeDashboardInvalidWidgetQuery, "decode v5 query envelopes") + } + return &CompositeQuerySpec{Queries: parsed}, nil +} + +func parseBuilderQuerySpec(rawSpec any, signal telemetrytypes.Signal) any { + spec, ok := rawSpec.(map[string]any) + if !ok { + return nil + } + if !signal.IsZero() { + spec["signal"] = signal.StringValue() + } + bytes, err := json.Marshal(spec) + if err != nil { + return nil + } + parsed, err := qb.UnmarshalBuilderQueryBySignal(bytes) + if err != nil { + return nil + } + return parsed +} + +// signalFromDataSource maps a v1 data-source string to a v5 signal. Casing +// varies by source: builder queries store lowercase ("traces"), while variable +// `dynamicVariablesSource` stores capitalized ("Traces"), so match +// case-insensitively. Unknown values (e.g. "All telemetry") map to the zero +// Signal. +func signalFromDataSource(raw any) telemetrytypes.Signal { + s, _ := raw.(string) + switch strings.ToLower(s) { + case "traces": + return telemetrytypes.SignalTraces + case "logs": + return telemetrytypes.SignalLogs + case "metrics": + return telemetrytypes.SignalMetrics + } + return telemetrytypes.Signal{} +} diff --git a/pkg/types/dashboardtypes/perses_v1_to_v2_queries_malformed.go b/pkg/types/dashboardtypes/perses_v1_to_v2_queries_malformed.go new file mode 100644 index 00000000000..d96374ddf16 --- /dev/null +++ b/pkg/types/dashboardtypes/perses_v1_to_v2_queries_malformed.go @@ -0,0 +1,499 @@ +package dashboardtypes + +import ( + "context" + "encoding/json" + "log/slog" + "regexp" + "strings" + + "github.com/SigNoz/signoz/pkg/transition" + "github.com/SigNoz/signoz/pkg/types/metrictypes" + qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" + "github.com/SigNoz/signoz/pkg/types/telemetrytypes" + "github.com/SigNoz/signoz/pkg/valuer" +) + +// ══════════════════════════════════════════════ +// Malformed-field normalization +// ══════════════════════════════════════════════ +// +// Pre-v5 query-body reshapes for dashboards whose bodies aren't actually v5-shaped +// (e.g. stamped version:"v5" but never upgraded). The bulk of the upgrade is +// delegated to transition.MigrateQueryDataShapeSafe (see normalizePreV5QueryData); +// this file keeps only the reshapes it doesn't cover. + +// preV5Migrator runs transition's shape-safe (idempotent) v4→v5 upgrade. Stateless +// after construction, so a shared instance with a discard logger / no ambiguity +// keys is fine. +var preV5Migrator = transition.NewDashboardMigrateV5(slog.New(slog.DiscardHandler), nil, nil) + +// normalizePreV5QueryData upgrades one builder queryData/formula in place: the +// shared migrator, then a reshape of any existing aggregations[] it leaves alone. +func normalizePreV5QueryData(query map[string]any, widgetType string) { + dropLegacyFilter(query) + normalizeFilterItemOps(query) + preV5Migrator.MigrateQueryDataShapeSafe(context.Background(), query, widgetType) + normalizePreV5LogTraceAggregations(query) + normalizeMetricAggregations(query) + normalizeFunctionArgs(query) + dropInvalidFunctions(query) + // normalizeOrderByKeys runs in the caller, after ensureDefaultAggregation: a + // value-order key resolves against the aggregation that may have just been injected. +} + +// dropInvalidFunctions removes any function the v5 validator would reject — an unknown +// name, or a missing/uncastable required arg (see Function.Validate). v1 tolerated these +// but v2 fails the whole query, so we drop just the offending function. Runs after +// normalizeFunctionArgs so a merely double-wrapped (but otherwise valid) function isn't +// lost. +func dropInvalidFunctions(query map[string]any) { + fns, ok := query["functions"].([]any) + if !ok { + return + } + kept := make([]any, 0, len(fns)) + for _, f := range fns { + raw, err := json.Marshal(f) + if err != nil { + continue + } + var fn qb.Function + if err := json.Unmarshal(raw, &fn); err != nil { + continue + } + if fn.Validate() != nil { + continue + } + kept = append(kept, f) + } + query["functions"] = kept +} + +// normalizeFunctionArgs collapses a doubly-wrapped function arg to a scalar. The +// v4→v5 migration that runs before ConvertV1ToV2 (transition.updateQueryData) wraps every arg as +// {name, value} without checking whether it's already a v5 arg, so a body that was +// already v5 comes back as {value:{value:60}} and fails validation ("must be a floating +// value"). We can't guard it at the source — transition's Migrate is shared and left +// untouched — so unwrap one level of {value:...} nesting here. +func normalizeFunctionArgs(query map[string]any) { + fns, ok := query["functions"].([]any) + if !ok { + return + } + for _, f := range fns { + fn, ok := f.(map[string]any) + if !ok { + continue + } + args, ok := fn["args"].([]any) + if !ok { + continue + } + for _, a := range args { + arg, ok := a.(map[string]any) + if !ok { + continue + } + if inner, ok := arg["value"].(map[string]any); ok { + if v, ok := inner["value"]; ok { + arg["value"] = v + } + } + } + } +} + +// malformedOrderByValueKeys are v4 order-by columnNames meaning "order by the aggregation value" +// that the v5 aggregation validator rejects (validateOrderByForAggregation). All resolve +// to the same aggregation key. Add more as they surface. The frontend passes these +// through (the query-service resolves them), but the v2 dashboard validator only accepts +// a real aggregation key. +var malformedOrderByValueKeys = map[string]bool{ + "#SIGNOZ_VALUE": true, + "A": true, + "A.count()": true, + "__result": true, + "value": true, + "A.p99(duration_nano)": true, + "aws_Kafka_MessagesInPerSec_max": true, + "byte_in_count": true, + "(http_server_request_duration_ms.bucket)": true, +} + +// normalizeOrderByKeys rewrites any orderBy columnName in orderByValueKeys to the +// v5-valid aggregation key. Left untouched if the key can't resolve (no aggregation to +// name). +func normalizeOrderByKeys(query map[string]any) { + orders, ok := query["orderBy"].([]any) + if !ok { + return + } + key, ok := aggregationOrderKey(query) + if !ok { + return + } + for _, o := range orders { + order, ok := o.(map[string]any) + if !ok { + continue + } + if cn, _ := order["columnName"].(string); malformedOrderByValueKeys[cn] { + order["columnName"] = key + } + } +} + +// aggregationOrderKey names the first aggregation the way validateOrderByForAggregation +// expects: "space(metricName)" for metrics, the expression for logs/traces. +func aggregationOrderKey(query map[string]any) (string, bool) { + aggs, ok := query["aggregations"].([]any) + if !ok || len(aggs) == 0 { + return "", false + } + agg, ok := aggs[0].(map[string]any) + if !ok { + return "", false + } + if signalFromDataSource(query["dataSource"]) == telemetrytypes.SignalMetrics { + metricName, _ := agg["metricName"].(string) + space, _ := agg["spaceAggregation"].(string) + if metricName == "" || space == "" { + return "", false + } + return space + "(" + metricName + ")", true + } + expr, _ := agg["expression"].(string) + if expr == "" { + return "", false + } + return expr, true +} + +// backfillFormulaFields restores order/limit/having onto the formula's spec. +// WrapInV5Envelope's formula branch emits only name/expression/disabled/legend/functions +// and drops these three, even though QueryBuilderFormula supports them. +func backfillFormulaFields(env, formula map[string]any) { + spec := env["spec"].(map[string]any) + + // limit and having are already v5-shaped (the shape-safe migrator rewrites having), + // so copy them across unchanged. + if limit, ok := formula["limit"]; ok { + spec["limit"] = limit + } + if having, ok := formula["having"]; ok { + spec["having"] = having + } + + // orderBy is still in the v4 shape ([{columnName, order}]); reshape each entry into + // the v5 order shape ([{key: {name}, direction}]). + orderBy, ok := formula["orderBy"].([]any) + if !ok { + return + } + order := make([]any, 0, len(orderBy)) + for _, item := range orderBy { + ob, ok := item.(map[string]any) + if !ok { + continue + } + order = append(order, map[string]any{ + "key": map[string]any{"name": ob["columnName"]}, + "direction": ob["order"], + }) + } + spec["order"] = order +} + +// dropLegacyFilter removes a v4-shaped filter ({items, op}) stored under the v5 +// `filter` key. The v5 filter is {expression}; the migrator only rewrites the v4 +// `filters` key and skips when `filter` is present, so this stale shape would reach +// WrapInV5Envelope and fail v5 validation. The v1 UI ignores it — it types +// IBuilderQuery.filter as {expression} (frontend queryBuilderData.ts, filter?: Filter) +// and only ever reads filter.expression, so items/op go unread. We drop it before the +// migrator, which can then rebuild `filter` from `filters` if present. +func dropLegacyFilter(query map[string]any) { + filter, ok := query["filter"].(map[string]any) + if !ok { + return + } + _, hasItems := filter["items"] + _, hasOp := filter["op"] + if hasItems || hasOp { + delete(query, "filter") + } +} + +// normalizeFilterItemOps lowercases exists/nexists filter ops (frontend stores them +// uppercase) to the spelling transition's buildCondition (pkg/transition/migrate_common.go) +// matches; otherwise it appends a spurious empty value ("svc EXISTS ''"). Value +// operators already round-trip via that switch's default case. +func normalizeFilterItemOps(query map[string]any) { + filters, ok := query["filters"].(map[string]any) + if !ok { + return + } + items, ok := filters["items"].([]any) + if !ok { + return + } + for _, it := range items { + item, ok := it.(map[string]any) + if !ok { + continue + } + op, ok := item["op"].(string) + if !ok { + continue + } + switch strings.ToLower(strings.ReplaceAll(op, "_", " ")) { + case "exists": + item["op"] = "exists" + case "nexists", "not exists": + item["op"] = "nexists" + } + } +} + +// metricAggregationFields are the JSON keys a metric aggregation accepts (see +// MetricAggregation). The decoder is strict, so any other key (e.g. a logs/traces +// style `expression`) is rejected as an unknown field. +var metricAggregationFields = map[string]bool{ + "metricName": true, + "temporality": true, + "timeAggregation": true, + "spaceAggregation": true, + "comparisonSpaceAggregationParam": true, + "reduceTo": true, +} + +// normalizeMetricAggregations reshapes a metric query's aggregations to the shape v5 +// expects. v1 bodies sometimes carry a logs/traces-style aggregation ({expression}); +// the frontend ignores expression for metrics and builds from the metric fields +// (createAggregation, prepareQueryRangePayloadV5.ts), so we drop every non-metric +// key. A dropped expression leaves metricName empty and the widget is skipped later +// (isUnrenderableMetricQuery), matching what v1 renders. +// +// It also defaults an invalid spaceAggregation to "sum": v1 bodies often leave it +// empty or carry a stale value, which fails validation (SpaceAggregation.IsValid). A +// valid value (including a histogram percentile) is left alone; the metric type isn't +// in the body, so we can't prefer a percentile default for histograms. +func normalizeMetricAggregations(query map[string]any) { + if signalFromDataSource(query["dataSource"]) != telemetrytypes.SignalMetrics { + return + } + aggs, ok := query["aggregations"].([]any) + if !ok { + return + } + for _, a := range aggs { + agg, ok := a.(map[string]any) + if !ok { + continue + } + for k := range agg { + if !metricAggregationFields[k] { + delete(agg, k) + } + } + sa, _ := agg["spaceAggregation"].(string) + if !(metrictypes.SpaceAggregation{String: valuer.NewString(sa)}).IsValid() { + agg["spaceAggregation"] = metrictypes.SpaceAggregationSum.StringValue() + } + } +} + +// normalizePreV5LogTraceAggregations reshapes an existing logs/traces aggregations[] +// via parseAggregations (extract func(args), lift inline "as alias", split +// multi-part, drop metric-only fields; empty → count()). Covers the case the +// migrator skips: it builds from flat fields but leaves a present-but-malformed +// aggregations[] alone. A query with none is left as-is. +func normalizePreV5LogTraceAggregations(query map[string]any) { + switch signalFromDataSource(query["dataSource"]) { + case telemetrytypes.SignalLogs, telemetrytypes.SignalTraces: + default: + return + } + aggs, ok := query["aggregations"].([]any) + if !ok || len(aggs) == 0 { + return + } + out := make([]any, 0, len(aggs)) + for _, a := range aggs { + agg, ok := a.(map[string]any) + if !ok { + continue + } + expr, _ := agg["expression"].(string) + alias, _ := agg["alias"].(string) + parsed := parseAggregations(expr, alias) + if len(parsed) == 0 { + parsed = []any{map[string]any{"expression": "count()"}} + } + out = append(out, parsed...) + } + query["aggregations"] = out +} + +// ensureDefaultAggregation defaults an empty logs/traces aggregations[] to count(), +// mirroring the frontend. Callers gate this to aggregation panels. Metrics are skipped: +// count() can't stand in for a missing metricName. +func ensureDefaultAggregation(query map[string]any) { + switch signalFromDataSource(query["dataSource"]) { + case telemetrytypes.SignalLogs, telemetrytypes.SignalTraces: + default: + return + } + if aggs, ok := query["aggregations"].([]any); ok && len(aggs) > 0 { + return + } + query["aggregations"] = []any{map[string]any{"expression": "count()"}} +} + +// aggExprRe matches one "func(args)" with an optional "as alias". Mirrors the +// frontend's parseAggregations regex; matching only well-formed func(args) +// discards trailing junk ("sum(x) ) )" → "sum(x)"). +var aggExprRe = regexp.MustCompile(`([a-zA-Z0-9_]+\([^)]*\))(?:\s*as\s+('[^']*'|"[^"]*"|[a-zA-Z0-9_-]+))?`) + +// aggExprNestedRe is a backup for aggExprRe that tolerates one level of nested +// parens in args (rate(count())). HACK: the flat aggExprRe (and the frontend it +// mirrors) truncates such exprs to an unbalanced "rate(count()"; the UI fails +// these today, so this is best-effort beyond v1. Tried only when the flat match +// comes back unbalanced. +var aggExprNestedRe = regexp.MustCompile(`([a-zA-Z0-9_]+\((?:[a-zA-Z0-9_]+\([^()]*\)|[^()])*\))(?:\s*as\s+('[^']*'|"[^"]*"|[a-zA-Z0-9_-]+))?`) + +// parseAggregations pulls every func(args) (with inline or passed-through alias, +// quotes stripped) out of a v1 expression. Mirrors the frontend's +// parseAggregations; empty result if none. +func parseAggregations(expression, availableAlias string) []any { + matches := aggExprRe.FindAllStringSubmatch(expression, -1) + if hasUnbalancedParens(matches) { + matches = aggExprNestedRe.FindAllStringSubmatch(expression, -1) + } + out := make([]any, 0, len(matches)) + for _, m := range matches { + alias := m[2] + if alias == "" { + alias = availableAlias + } + agg := map[string]any{"expression": m[1]} + if alias != "" { + agg["alias"] = strings.Trim(alias, `'"`) + } + out = append(out, agg) + } + return out +} + +// hasUnbalancedParens reports whether any matched expression has mismatched +// parens — the signature of aggExprRe truncating a nested expr ("rate(count()"). +func hasUnbalancedParens(matches [][]string) bool { + for _, m := range matches { + if strings.Count(m[1], "(") != strings.Count(m[1], ")") { + return true + } + } + return false +} + +// normalizePreV5SelectColumns / normalizePreV5GroupBy let WrapInV5Envelope (which +// reads the old {key,dataType,type}) handle selectColumns/groupBy stored the v5 way +// ({name,…}) — see backfillPreV5FieldKeys. Inverse of normalizePreV5FieldKeys (the +// two consumers want opposite shapes). +func normalizePreV5SelectColumns(query map[string]any) { + if cols, ok := query["selectColumns"].([]any); ok { + query["selectColumns"] = backfillPreV5FieldKeys(cols) + } +} + +func normalizePreV5GroupBy(query map[string]any) { + if gb, ok := query["groupBy"].([]any); ok { + query["groupBy"] = backfillPreV5FieldKeys(gb) + } +} + +// backfillPreV5FieldKeys copies v5 field names (name/fieldDataType/fieldContext) +// down to their v4 equivalents (key/dataType/type) so WrapInV5Envelope, which reads +// the v4 names, sees a field stored the v5 way. Fields with no resolvable key are +// dropped. +func backfillPreV5FieldKeys(fields []any) []any { + out := make([]any, 0, len(fields)) + for _, f := range fields { + field, ok := f.(map[string]any) + if !ok { + continue + } + if _, ok := field["key"]; !ok { + if name, ok := field["name"]; ok { + field["key"] = name + } + } + if _, ok := field["dataType"]; !ok { + if fdt, ok := field["fieldDataType"]; ok { + field["dataType"] = fdt + } + } + if _, ok := field["type"]; !ok { + if fc, ok := field["fieldContext"]; ok { + field["type"] = fc + } + } + if key, _ := field["key"].(string); key == "" { + continue + } + out = append(out, field) + } + return out +} + +// normalizePreV5FieldKeys renames list-panel field keys {key,dataType,type} → +// {name,fieldDataType,fieldContext} in place (as WrapInV5Envelope does for +// groupBy/orderBy). Entries already carrying "name" are left as-is. +func normalizePreV5FieldKeys(fields []any) { + for _, f := range fields { + field, ok := f.(map[string]any) + if !ok { + continue + } + if _, hasName := field["name"]; hasName { + continue + } + if key, ok := field["key"]; ok { + field["name"] = key + } + if dataType, ok := field["dataType"]; ok { + field["fieldDataType"] = dataType + } + if typ, ok := field["type"]; ok { + field["fieldContext"] = typ + } + } +} + +// normalizePreV5PageSize backfills limit from the legacy pageSize (frontend's +// `limit || pageSize`), for row-limited panels (list/table) only. Leaves a query +// that already has limit, or a non-row-limited panel, untouched. +func normalizePreV5PageSize(query map[string]any, rowLimitPanel bool) { + if !rowLimitPanel { + return + } + if limit, ok := query["limit"]; ok && limit != nil { + return + } + if ps, ok := query["pageSize"]; ok { + query["limit"] = ps + } +} + +// normalizeQueryLimit drops a limit above the v5 maximum (MaxQueryLimit); v1 allowed +// larger/unbounded limits, and an over-max value fails validation. Removing it leaves +// the query unlimited (the field is optional). +func normalizeQueryLimit(query map[string]any) { + limit, ok := coerceFloat(query["limit"]) + if !ok { + return + } + if limit > qb.MaxQueryLimit { + delete(query, "limit") + } +} diff --git a/pkg/types/dashboardtypes/perses_v1_to_v2_tags.go b/pkg/types/dashboardtypes/perses_v1_to_v2_tags.go new file mode 100644 index 00000000000..cd2aa71679e --- /dev/null +++ b/pkg/types/dashboardtypes/perses_v1_to_v2_tags.go @@ -0,0 +1,122 @@ +package dashboardtypes + +import ( + "fmt" + "regexp" + "strings" + + "github.com/SigNoz/signoz/pkg/types/coretypes" + "github.com/SigNoz/signoz/pkg/types/tagtypes" + "github.com/SigNoz/signoz/pkg/valuer" +) + +// ══════════════════════════════════════════════ +// Tags +// ══════════════════════════════════════════════ + +// v1 carries tags as a flat []string; v2 tags are (key, value) pairs. Each v1 +// string is normalized into a pair (separator split, empty-side fallback, +// reserved-key prefix, `/` scrub). Tags that normalize to the same +// (lower(key), lower(value)) within a dashboard are collapsed, first occurrence +// winning the display casing. +// +// Characters still illegal after normalization (spaces, punctuation) are molded +// to fit the tag validators: disallowed runs collapse to "_" (see moldTagField). + +// defaultV1TagKey is the key assigned when a v1 tag string has no usable +// separator (or one side of the split is empty). +const defaultV1TagKey = "tag" + +func (d *v1Decoder) convertV1TagsForOrg(orgID valuer.UUID, raw any) []*tagtypes.Tag { + if raw == nil { + return nil + } + rawTagsList, ok := raw.([]any) + if !ok { + d.noteMalformedField("tags", raw) + return nil + } + seen := make(map[string]struct{}, len(rawTagsList)) + tagsV2 := make([]*tagtypes.Tag, 0, len(rawTagsList)) + for i, rawTag := range rawTagsList { + s, ok := rawTag.(string) + if !ok { + d.noteMalformedField(fmt.Sprintf("tags[%d]", i), rawTag) + continue + } + key, value, ok := normalizeV1Tag(s) + if !ok { + continue + } + dedupKey := strings.ToLower(key) + "\x00" + strings.ToLower(value) + if _, dup := seen[dedupKey]; dup { + continue + } + seen[dedupKey] = struct{}{} + tagsV2 = append(tagsV2, tagtypes.NewTag(orgID, coretypes.KindDashboard, key, value)) + } + return tagsV2 +} + +// normalizeV1Tag derives a (key, value) pair from one v1 tag string. After +// splitting and molding both sides, a lone survivor becomes a value under the +// default key; ok is false if neither survives. +func normalizeV1Tag(s string) (string, string, bool) { + s = strings.TrimSpace(s) + if s == "" { + return "", "", false + } + + var rawKey, rawValue string + switch { + case strings.Contains(s, ":"): + rawKey, rawValue, _ = strings.Cut(s, ":") + // Only the first ":" separates key from value; collapse the rest. + rawValue = strings.ReplaceAll(rawValue, ":", "_") + case strings.Contains(s, "/"): + rawKey, rawValue, _ = strings.Cut(s, "/") + default: + rawValue = s + } + rawKey = strings.TrimSpace(rawKey) + rawValue = strings.TrimSpace(rawValue) + + // Reserved-key collision: prefix "_" so the list-query DSL stays unambiguous. + if _, reserved := reservedDSLKeys[DSLKey(strings.ToLower(rawKey))]; rawKey != "" && reserved { + rawKey = "_" + rawKey + } + + key := moldTagField(rawKey, tagKeyDisallowed, tagKeyNotLead, tagtypes.MAX_LEN_TAG_KEY) + value := moldTagField(rawValue, tagValueDisallowed, nil, tagtypes.MAX_LEN_TAG_VALUE) + switch { + case key == "" && value == "": + return "", "", false + case key == "": + return defaultV1TagKey, value, true + case value == "": + return defaultV1TagKey, key, true + default: + return key, value, true + } +} + +// Inverse of tagKeyRegex/tagValueRegex ("/" always rejected); tagKeyNotLead +// matches a bad first char for a key. TestMoldedV1TagsPassValidation guards drift. +var ( + tagKeyDisallowed = regexp.MustCompile(`[^a-zA-Z0-9$_@#{}:-]+`) + tagValueDisallowed = regexp.MustCompile(`[^a-zA-Z0-9$_@#{}:.+=-]+`) + tagKeyNotLead = regexp.MustCompile(`^[^a-zA-Z$_@{#]`) +) + +// moldTagField collapses disallowed runs to "_", prefixes "_" if notLead hits +// the first char, and caps at max. Keeps a leading "_", trims a trailing one. +func moldTagField(s string, disallowed, notLead *regexp.Regexp, max int) string { + s = strings.TrimRight(disallowed.ReplaceAllString(s, "_"), "_") + if s != "" && notLead != nil && notLead.MatchString(s) { + s = "_" + s + } + if len(s) > max { + s = strings.TrimRight(s[:max], "_") + } + return s +} diff --git a/pkg/types/dashboardtypes/perses_v1_to_v2_test.go b/pkg/types/dashboardtypes/perses_v1_to_v2_test.go new file mode 100644 index 00000000000..e9ab2a31123 --- /dev/null +++ b/pkg/types/dashboardtypes/perses_v1_to_v2_test.go @@ -0,0 +1,2153 @@ +package dashboardtypes + +import ( + "os" + "strings" + "testing" + "time" + + "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/types/coretypes" + qb "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5" + "github.com/SigNoz/signoz/pkg/types/tagtypes" + "github.com/SigNoz/signoz/pkg/valuer" + "github.com/perses/spec/go/dashboard" + "github.com/perses/spec/go/dashboard/variable" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConvertV1TagsForOrg(t *testing.T) { + orgID := valuer.GenerateUUID() + + type kv struct{ key, value string } + + cases := []struct { + scenario string + rawTags any + expectedTags []kv + }{ + { + scenario: "no separator uses the default key", + rawTags: []any{"apm", "latency", "throughput"}, + expectedTags: []kv{{"tag", "apm"}, {"tag", "latency"}, {"tag", "throughput"}}, + }, + { + scenario: "colon splits into key and value", + rawTags: []any{"env:prod", "team : backend"}, + expectedTags: []kv{{"env", "prod"}, {"team", "backend"}}, + }, + { + scenario: "slash splits into key and value when no colon present", + rawTags: []any{"team/backend"}, + expectedTags: []kv{{"team", "backend"}}, + }, + { + scenario: "colon takes precedence over slash and slash is scrubbed", + rawTags: []any{"team/eng:prod", "team/eng:my/path"}, + expectedTags: []kv{{"team_eng", "prod"}, {"team_eng", "my_path"}}, + }, + { + scenario: "empty left side falls back to the default key", + rawTags: []any{":prod"}, + expectedTags: []kv{{"tag", "prod"}}, + }, + { + scenario: "empty right side keeps the left side as the value", + rawTags: []any{"env:"}, + expectedTags: []kv{{"tag", "env"}}, + }, + { + scenario: "extra colons in the value collapse to underscores", + rawTags: []any{"a:b:c"}, + expectedTags: []kv{{"a", "b_c"}}, + }, + { + scenario: "extra slashes in the value are scrubbed", + rawTags: []any{"a/b/c"}, + expectedTags: []kv{{"a", "b_c"}}, + }, + { + scenario: "reserved key gets an underscore prefix", + rawTags: []any{"name:foo", "Source:bar"}, + expectedTags: []kv{{"_name", "foo"}, {"_Source", "bar"}}, + }, + { + scenario: "drops empty, whitespace-only, and bare-separator entries", + rawTags: []any{"", " ", ":", "/", "apm"}, + expectedTags: []kv{{"tag", "apm"}}, + }, + { + scenario: "dedupes case-insensitive duplicates, first casing wins", + rawTags: []any{"Env:Prod", "env:PROD"}, + expectedTags: []kv{{"Env", "Prod"}}, + }, + { + scenario: "spaces in key and value collapse to underscores", + rawTags: []any{"env:spaced out", "spaced key:prod", "spaced out"}, + expectedTags: []kv{{"env", "spaced_out"}, {"spaced_key", "prod"}, {"tag", "spaced_out"}}, + }, + { + scenario: "runs of disallowed punctuation collapse to a single underscore", + rawTags: []any{"team (eng):prod!!one"}, + expectedTags: []kv{{"team_eng", "prod_one"}}, + }, + { + scenario: "key that would start with a non-leading char is prefixed with underscore", + rawTags: []any{"2nd tier:x"}, + expectedTags: []kv{{"_2nd_tier", "x"}}, + }, + { + scenario: "a side that molds to empty falls back to the default key with the survivor", + rawTags: []any{"(:x", "y:!!!", "good:tag"}, + expectedTags: []kv{{"tag", "x"}, {"tag", "y"}, {"good", "tag"}}, + }, + { + scenario: "pairs with no representable content on either side are skipped", + rawTags: []any{"(:!!!", "()", "ok"}, + expectedTags: []kv{{"tag", "ok"}}, + }, + { + scenario: "returns nil for missing tags field", + rawTags: nil, + expectedTags: nil, + }, + } + + for _, tc := range cases { + t.Run(tc.scenario, func(t *testing.T) { + d := &v1Decoder{} + tags := d.convertV1TagsForOrg(orgID, tc.rawTags) + require.NoError(t, d.errIfHasMalformedFields()) + require.Len(t, tags, len(tc.expectedTags)) + for i, expected := range tc.expectedTags { + assert.Equal(t, expected.key, tags[i].Key) + assert.Equal(t, expected.value, tags[i].Value) + assert.Equal(t, orgID, tags[i].OrgID) + assert.Equal(t, coretypes.KindDashboard, tags[i].Kind) + } + }) + } +} + +// TestConvertV1DetectsMalformedFields verifies a field present with the wrong +// type at any depth (not just top-level containers) makes the conversion fail, +// so the migration logs and skips the dashboard. Absent fields are not errors. +func TestConvertV1DetectsMalformedFields(t *testing.T) { + cases := []struct { + scenario string + data map[string]any + }{ + {"variables not a map", map[string]any{"variables": "nope"}}, + {"widgets not an array", map[string]any{"widgets": "nope"}}, + {"tags not an array", map[string]any{"tags": "nope"}}, + {"non-string tag element", map[string]any{"tags": []any{"ok", 42}}}, + {"scalar widget field wrong type", map[string]any{ + "widgets": []any{map[string]any{"id": "w1", "panelTypes": "graph", "title": float64(3)}}, + "layout": []any{map[string]any{"i": "w1", "x": float64(0), "y": float64(0), "w": float64(6), "h": float64(6)}}, + }}, + {"nested query wrong type", map[string]any{ + "widgets": []any{map[string]any{"id": "w1", "panelTypes": "graph", "query": "nope"}}, + "layout": []any{map[string]any{"i": "w1", "x": float64(0), "y": float64(0), "w": float64(6), "h": float64(6)}}, + }}, + {"deep layout coordinate wrong type", map[string]any{ + "widgets": []any{map[string]any{"id": "w1", "panelTypes": "graph", "query": singleLogsBuilderQuery()}}, + "layout": []any{map[string]any{"i": "w1", "x": "0", "y": float64(0)}}, + }}, + } + for _, tc := range cases { + t.Run(tc.scenario, func(t *testing.T) { + _, err := StorableDashboard{Data: tc.data}.ConvertV1ToV2() + require.Error(t, err) + }) + } +} + +// TestMoldedV1TagsPassValidation guards against the mold rules drifting from +// the tag validators: every tag the converter emits for messy input must pass +// the real tagtypes.ValidatePostableTag, and over-long fields must be capped. +func TestMoldedV1TagsPassValidation(t *testing.T) { + orgID := valuer.GenerateUUID() + + raw := []any{ + "env:spaced out", + "team (eng):prod!!one", + "2nd tier:x", + "k:" + strings.Repeat("a", 50), + strings.Repeat("verylongkeysegment ", 4) + ":v", + "weird*&^chars:val#1", + } + + d := &v1Decoder{} + tags := d.convertV1TagsForOrg(orgID, raw) + require.NoError(t, d.errIfHasMalformedFields()) + require.NotEmpty(t, tags) + for _, tag := range tags { + _, _, err := tagtypes.ValidatePostableTag(tagtypes.PostableTag{Key: tag.Key, Value: tag.Value}) + assert.NoError(t, err, "molded tag %q=%q must pass validation", tag.Key, tag.Value) + assert.LessOrEqual(t, len(tag.Key), tagtypes.MAX_LEN_TAG_KEY) + assert.LessOrEqual(t, len(tag.Value), tagtypes.MAX_LEN_TAG_VALUE) + } +} + +// singleLogsBuilderQuery is a minimal renderable widget query — one logs builder +// query (defaults to count()). Stand-in widgets in the layout tests attach it so +// they produce a v2 query and aren't dropped as query-less. Fresh map per call: +// conversion mutates the query in place. +func singleLogsBuilderQuery() map[string]any { + return map[string]any{ + "queryType": "builder", + "builder": map[string]any{ + "queryData": []any{ + map[string]any{"queryName": "A", "expression": "A", "dataSource": "logs"}, + }, + }, + } +} + +// TestConvertV1Panels covers the dispatcher itself: panels are keyed by widget +// id, and row and empty-id widgets are dropped. +func TestConvertV1Panels(t *testing.T) { + widgets := []any{ + map[string]any{"id": "g1", "panelTypes": "graph", "title": "CPU", "query": singleLogsBuilderQuery()}, + map[string]any{"id": "t1", "panelTypes": "table", "query": singleLogsBuilderQuery()}, + map[string]any{"id": "row1", "panelTypes": "row", "title": "section"}, + map[string]any{"id": "", "panelTypes": "graph"}, + } + + d := &v1Decoder{} + panels := d.convertV1Panels(widgets) + require.NoError(t, d.errIfHasMalformedFields()) + + require.Len(t, panels, 2) + require.Contains(t, panels, "g1") + require.Contains(t, panels, "t1") + assert.Equal(t, PanelKindTimeSeries, panels["g1"].Spec.Plugin.Kind) + assert.Equal(t, "CPU", panels["g1"].Spec.Display.Name) + assert.Equal(t, PanelKindTable, panels["t1"].Spec.Plugin.Kind) + assert.NotContains(t, panels, "row1", "row widgets are handled by the layout pass") +} + +// TestConvertV1PanelsSkipsUnknownType verifies an unrecognized panelTypes is +// dropped silently (v1 can't render it either) without failing the migration. +func TestConvertV1PanelsSkipsUnknownType(t *testing.T) { + d := &v1Decoder{} + panels := d.convertV1Panels([]any{map[string]any{"id": "u1", "panelTypes": "somethingelse"}}) + assert.NotContains(t, panels, "u1") + require.NoError(t, d.errIfHasMalformedFields()) +} + +func TestConvertGraphWidgetToTimeSeriesPanel(t *testing.T) { + widget := map[string]any{ + "id": "widget-1", + "panelTypes": "graph", + "title": "Request rate", + "description": "RPS over time", + "timePreferance": "LAST_1_HR", + "fillSpans": true, + "yAxisUnit": "reqps", + "decimalPrecision": float64(3), + "lineInterpolation": "linear", + "lineStyle": "dashed", + "fillMode": "gradient", + "showPoints": true, + "spanGaps": float64(60), + "softMin": float64(0), + "softMax": float64(100), + "isLogScale": true, + "legendPosition": "right", + "customLegendColors": map[string]any{"A": "#ff0000", "B": "#00ff00"}, + "thresholds": []any{ + map[string]any{ + "thresholdValue": float64(90), + "thresholdUnit": "reqps", + "thresholdColor": "#ff0000", + "thresholdLabel": "high", + }, + map[string]any{ + "thresholdValue": float64(50), + "thresholdColor": "", // missing — must be dropped + "thresholdLabel": "missing-color", + }, + }, + } + + panel := (&v1Decoder{}).convertGraphWidget(widget) + require.NotNil(t, panel) + + assert.Equal(t, PanelKindPanel, panel.Kind) + assert.Equal(t, "Request rate", panel.Spec.Display.Name) + assert.Equal(t, "RPS over time", panel.Spec.Display.Description) + + assert.Equal(t, PanelKindTimeSeries, panel.Spec.Plugin.Kind) + spec, ok := panel.Spec.Plugin.Spec.(*TimeSeriesPanelSpec) + require.True(t, ok, "panel plugin spec should be *TimeSeriesPanelSpec") + + assert.Equal(t, TimePreferenceLast1Hr, spec.Visualization.TimePreference) + assert.True(t, spec.Visualization.FillSpans) + + assert.Equal(t, "reqps", spec.Formatting.Unit) + assert.Equal(t, PrecisionOption3, spec.Formatting.DecimalPrecision) + + assert.Equal(t, LineInterpolationLinear, spec.ChartAppearance.LineInterpolation) + assert.True(t, spec.ChartAppearance.ShowPoints) + assert.Equal(t, LineStyleDashed, spec.ChartAppearance.LineStyle) + assert.Equal(t, FillModeGradient, spec.ChartAppearance.FillMode) + assert.True(t, spec.ChartAppearance.SpanGaps.FillOnlyBelow) + assert.Equal(t, "1m0s", spec.ChartAppearance.SpanGaps.FillLessThan) + + require.NotNil(t, spec.Axes.SoftMin) + assert.Equal(t, float64(0), *spec.Axes.SoftMin) + require.NotNil(t, spec.Axes.SoftMax) + assert.Equal(t, float64(100), *spec.Axes.SoftMax) + assert.True(t, spec.Axes.IsLogScale) + + assert.Equal(t, LegendPositionRight, spec.Legend.Position) + assert.Equal(t, map[string]string{"A": "#ff0000", "B": "#00ff00"}, spec.Legend.CustomColors) + + require.Len(t, spec.Thresholds, 1, "threshold with missing color should be dropped") + threshold := spec.Thresholds[0] + require.NotNil(t, threshold.Value) + assert.Equal(t, float64(90), threshold.Value) + assert.Equal(t, "reqps", threshold.Unit) + assert.Equal(t, "#ff0000", threshold.Color) + assert.Equal(t, "high", threshold.Label) +} + +func TestConvertGraphWidgetDefaultsForMissingFields(t *testing.T) { + widget := map[string]any{ + "id": "widget-1", + "panelTypes": "graph", + "title": "minimal", + } + + panel := (&v1Decoder{}).convertGraphWidget(widget) + require.NotNil(t, panel) + + spec, ok := panel.Spec.Plugin.Spec.(*TimeSeriesPanelSpec) + require.True(t, ok) + + assert.Equal(t, TimePreferenceGlobalTime, spec.Visualization.TimePreference) + assert.Equal(t, PrecisionOption2, spec.Formatting.DecimalPrecision) + assert.Equal(t, LineInterpolationSpline, spec.ChartAppearance.LineInterpolation) + assert.Equal(t, LineStyleSolid, spec.ChartAppearance.LineStyle) + assert.Equal(t, FillModeNone, spec.ChartAppearance.FillMode) + assert.Equal(t, LegendPositionBottom, spec.Legend.Position) + assert.False(t, spec.ChartAppearance.SpanGaps.FillOnlyBelow) + assert.Nil(t, spec.Axes.SoftMin) + assert.Nil(t, spec.Axes.SoftMax) + assert.Empty(t, spec.Thresholds) +} + +func TestConvertV1ToV2HappyPath(t *testing.T) { + orgID := valuer.GenerateUUID() + storable := &StorableDashboard{ + Identifiable: types.Identifiable{ID: valuer.GenerateUUID()}, + TimeAuditable: types.TimeAuditable{CreatedAt: time.Now(), UpdatedAt: time.Now()}, + UserAuditable: types.UserAuditable{CreatedBy: "alice", UpdatedBy: "bob"}, + OrgID: orgID, + Source: SourceUser, + Name: "apm-metrics", + Data: StorableDashboardData{ + "title": "APM Metrics", + "description": "service overview", + "image": "data:image/png;base64,abc", + "tags": []any{"apm", "team:platform"}, + "widgets": []any{ + // section header — owned by the layout pass, not a panel + map[string]any{"id": "row-1", "panelTypes": "row", "title": "Overview"}, + // graph widget → TimeSeries panel + map[string]any{ + "id": "panel-1", + "panelTypes": "graph", + "title": "Latency", + "query": map[string]any{ + "queryType": "clickhouse_sql", + "clickhouse_sql": []any{map[string]any{"name": "A", "query": "SELECT now(), 1"}}, + }, + }, + // table widget → Table panel + map[string]any{ + "id": "panel-2", + "panelTypes": "table", + "query": map[string]any{ + "queryType": "clickhouse_sql", + "clickhouse_sql": []any{map[string]any{"name": "A", "query": "SELECT now(), 1"}}, + }, + }, + // widget with missing id — dropped + map[string]any{"panelTypes": "graph", "title": "no id"}, + }, + "layout": []any{ + map[string]any{"i": "panel-1", "x": float64(0), "y": float64(0), "w": float64(6), "h": float64(6)}, + map[string]any{"i": "panel-2", "x": float64(6), "y": float64(0), "w": float64(6), "h": float64(6)}, + }, + }, + } + + dashboard, err := storable.ConvertV1ToV2() + require.NoError(t, err) + require.NotNil(t, dashboard) + + assert.Equal(t, storable.ID, dashboard.ID) + assert.Equal(t, storable.OrgID, dashboard.OrgID) + assert.Equal(t, storable.Source, dashboard.Source) + assert.Equal(t, SchemaVersion, dashboard.SchemaVersion) + assert.Equal(t, "data:image/png;base64,abc", dashboard.Image) + assert.Contains(t, dashboard.Name, "apm-metrics") + + assert.Equal(t, "APM Metrics", dashboard.Spec.Display.Name) + assert.Equal(t, "service overview", dashboard.Spec.Display.Description) + + require.Len(t, dashboard.Tags, 2) + assert.Equal(t, "tag", dashboard.Tags[0].Key) + assert.Equal(t, "apm", dashboard.Tags[0].Value) + assert.Equal(t, "team", dashboard.Tags[1].Key) + assert.Equal(t, "platform", dashboard.Tags[1].Value) + + require.Len(t, dashboard.Spec.Panels, 2, "graph and table map; row and no-id widgets are dropped") + require.Contains(t, dashboard.Spec.Panels, "panel-1") + require.Contains(t, dashboard.Spec.Panels, "panel-2") + assert.Equal(t, PanelKindTimeSeries, dashboard.Spec.Panels["panel-1"].Spec.Plugin.Kind) + assert.Equal(t, PanelKindTable, dashboard.Spec.Panels["panel-2"].Spec.Plugin.Kind) +} + +func TestResolveV1Image(t *testing.T) { + // A real preset icon a v1 dashboard could store (the avocado icon, captured + // from the frontend picker) must migrate to the v2 avocado icon path. + avocadoBase64, err := os.ReadFile("testdata/v1_avocado_icon.txt") + require.NoError(t, err) + assert.Equal(t, "/assets/Icons/avocado", resolveV1Image(string(avocadoBase64))) + + // Values already valid under the v2 schema are returned unchanged: a custom + // base64 upload, an already-migrated icon path, and empty ("no image"). + for _, passthrough := range []string{ + "", + "data:image/png;base64,customuploadblob", + "/assets/Icons/already-migrated", + } { + assert.Equal(t, passthrough, resolveV1Image(passthrough)) + } + + // A value that v2 would reject (a URL, a corrupt data URI) falls back to the + // default eight-ball icon rather than failing the dashboard migration. + for _, invalid := range []string{ + "https://example.com/icon.png", + "data:image/svg+xml;base64,not valid base64!!", + "just-some-string", + } { + assert.Equal(t, "/assets/Icons/eight-ball", resolveV1Image(invalid)) + } +} + +func TestConvertV1ToV2ReplacesInvalidImage(t *testing.T) { + // An image v2 would reject must not sink the whole migration — it falls back + // to the default icon so the dashboard still converts. + storable := &StorableDashboard{ + Identifiable: types.Identifiable{ID: valuer.GenerateUUID()}, + OrgID: valuer.GenerateUUID(), + Source: SourceUser, + Data: StorableDashboardData{ + "title": "Bad Image Dashboard", + "image": "https://example.com/icon.png", + }, + } + + dashboard, err := storable.ConvertV1ToV2() + require.NoError(t, err) + require.NotNil(t, dashboard) + assert.Equal(t, "/assets/Icons/eight-ball", dashboard.Image) +} + +func TestConvertV1ToV2RejectsAlreadyV2(t *testing.T) { + storable := &StorableDashboard{ + Identifiable: types.Identifiable{ID: valuer.GenerateUUID()}, + OrgID: valuer.GenerateUUID(), + Source: SourceUser, + Name: "already-v2", + Data: StorableDashboardData{ + "metadata": map[string]any{"schemaVersion": SchemaVersion}, + "spec": map[string]any{}, + }, + } + + dashboard, err := storable.ConvertV1ToV2() + assert.Nil(t, dashboard) + require.Error(t, err) + assert.Contains(t, err.Error(), "already in") +} + +func TestSpanGapsMapping(t *testing.T) { + cases := []struct { + scenario string + rawSpanGaps any + expectedFillOnlyBelow bool + expectedFillLessThan string + }{ + {scenario: "true spans every gap", rawSpanGaps: true, expectedFillOnlyBelow: false, expectedFillLessThan: ""}, + {scenario: "false has no threshold so fill-only-below stays off", rawSpanGaps: false, expectedFillOnlyBelow: false, expectedFillLessThan: ""}, + {scenario: "number is seconds threshold", rawSpanGaps: float64(30), expectedFillOnlyBelow: true, expectedFillLessThan: "30s"}, + {scenario: "missing defaults to span all", rawSpanGaps: nil, expectedFillOnlyBelow: false, expectedFillLessThan: ""}, + } + + for _, tc := range cases { + t.Run(tc.scenario, func(t *testing.T) { + got := mapV1SpanGaps(tc.rawSpanGaps) + assert.Equal(t, tc.expectedFillOnlyBelow, got.FillOnlyBelow) + assert.Equal(t, tc.expectedFillLessThan, got.FillLessThan) + }) + } +} + +// ══════════════════════════════════════════════ +// Other panel-kind converters +// ══════════════════════════════════════════════ + +func TestConvertBarWidgetToBarChartPanel(t *testing.T) { + widget := map[string]any{ + "id": "bar-1", + "panelTypes": "bar", + "title": "Requests by status", + "fillSpans": true, + "stackedBarChart": true, + "yAxisUnit": "reqps", + "softMin": float64(0), + "isLogScale": true, + "legendPosition": "right", + } + + panel := (&v1Decoder{}).convertBarWidget(widget) + require.NotNil(t, panel) + assert.Equal(t, PanelKindBarChart, panel.Spec.Plugin.Kind) + + spec, ok := panel.Spec.Plugin.Spec.(*BarChartPanelSpec) + require.True(t, ok) + assert.True(t, spec.Visualization.FillSpans) + assert.True(t, spec.Visualization.StackedBarChart) + assert.Equal(t, "reqps", spec.Formatting.Unit) + require.NotNil(t, spec.Axes.SoftMin) + assert.Equal(t, float64(0), *spec.Axes.SoftMin) + assert.True(t, spec.Axes.IsLogScale) + assert.Equal(t, LegendPositionRight, spec.Legend.Position) +} + +func TestConvertValueWidgetToNumberPanel(t *testing.T) { + widget := map[string]any{ + "id": "val-1", + "panelTypes": "value", + "title": "Active services", + "yAxisUnit": "count", + "thresholds": []any{ + map[string]any{ + "thresholdValue": float64(100), + "thresholdOperator": ">=", + "thresholdColor": "#ff0000", + "thresholdFormat": "Background", + "thresholdUnit": "count", + }, + map[string]any{ + // missing color — must be dropped + "thresholdValue": float64(10), + }, + }, + } + + panel := (&v1Decoder{}).convertValueWidget(widget) + require.NotNil(t, panel) + assert.Equal(t, PanelKindNumber, panel.Spec.Plugin.Kind) + + spec, ok := panel.Spec.Plugin.Spec.(*NumberPanelSpec) + require.True(t, ok) + require.Len(t, spec.Thresholds, 1) + require.NotNil(t, spec.Thresholds[0].Value) + assert.Equal(t, float64(100), spec.Thresholds[0].Value) + assert.Equal(t, ComparisonOperatorAboveOrEqual, spec.Thresholds[0].Operator) + assert.Equal(t, "#ff0000", spec.Thresholds[0].Color) + assert.Equal(t, ThresholdFormatBackground, spec.Thresholds[0].Format) +} + +// mapV1ThresholdsWithLabel (graph, bar): color is required, label is optional. +func TestMapV1ThresholdsWithLabel(t *testing.T) { + testCases := []struct { + scenario string + thresholds []any + expectedLen int + expectedValue float64 + expectedColor string + expectedLabel string + }{ + { + scenario: "color and label are kept", + thresholds: []any{map[string]any{"thresholdValue": float64(90), "thresholdColor": "#f00", "thresholdLabel": "high"}}, + expectedLen: 1, + expectedValue: 90, + expectedColor: "#f00", + expectedLabel: "high", + }, + { + scenario: "color without label is kept", + thresholds: []any{map[string]any{"thresholdValue": float64(5), "thresholdColor": "#0f0"}}, + expectedLen: 1, + expectedValue: 5, + expectedColor: "#0f0", + expectedLabel: "", + }, + { + scenario: "missing color is dropped", + thresholds: []any{map[string]any{"thresholdValue": float64(1), "thresholdLabel": "x"}}, + expectedLen: 0, + }, + { + scenario: "empty list yields nil", + thresholds: []any{}, + expectedLen: 0, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.scenario, func(t *testing.T) { + out := (&v1Decoder{}).mapV1ThresholdsWithLabel(map[string]any{"thresholds": testCase.thresholds}) + require.Len(t, out, testCase.expectedLen) + if testCase.expectedLen == 0 { + return + } + assert.Equal(t, testCase.expectedValue, out[0].Value) + assert.Equal(t, testCase.expectedColor, out[0].Color) + assert.Equal(t, testCase.expectedLabel, out[0].Label) + }) + } +} + +// mapV1ComparisonThresholds (value): color is required; operator and format are coerced. +func TestMapV1ComparisonThresholds(t *testing.T) { + testCases := []struct { + scenario string + thresholds []any + expectedLen int + expectedValue float64 + expectedOperator ComparisonOperator + expectedFormat ThresholdFormat + }{ + { + scenario: "full threshold is kept", + thresholds: []any{map[string]any{"thresholdValue": float64(100), "thresholdOperator": ">=", "thresholdColor": "#f00", "thresholdFormat": "Background", "thresholdUnit": "count"}}, + expectedLen: 1, + expectedValue: 100, + expectedOperator: ComparisonOperatorAboveOrEqual, + expectedFormat: ThresholdFormatBackground, + }, + { + scenario: "missing operator and format default", + thresholds: []any{map[string]any{"thresholdValue": float64(0), "thresholdColor": "#f00"}}, + expectedLen: 1, + expectedValue: 0, + expectedOperator: ComparisonOperatorAbove, + expectedFormat: ThresholdFormatText, + }, + { + scenario: "missing color is dropped", + thresholds: []any{map[string]any{"thresholdValue": float64(1), "thresholdOperator": ">"}}, + expectedLen: 0, + }, + { + scenario: "empty list yields nil", + thresholds: []any{}, + expectedLen: 0, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.scenario, func(t *testing.T) { + out := (&v1Decoder{}).mapV1ComparisonThresholds(map[string]any{"thresholds": testCase.thresholds}) + require.Len(t, out, testCase.expectedLen) + if testCase.expectedLen == 0 { + return + } + assert.Equal(t, testCase.expectedValue, out[0].Value) + assert.Equal(t, testCase.expectedOperator, out[0].Operator) + assert.Equal(t, testCase.expectedFormat, out[0].Format) + }) + } +} + +// mapV1TableThresholds (table): color and columnName (thresholdTableOptions) are both required. +func TestMapV1TableThresholds(t *testing.T) { + testCases := []struct { + scenario string + thresholds []any + expectedLen int + expectedColumnName string + expectedOperator ComparisonOperator + }{ + { + scenario: "color and columnName are kept", + thresholds: []any{map[string]any{"thresholdValue": float64(500), "thresholdColor": "#f00", "thresholdTableOptions": "latency", "thresholdOperator": "<"}}, + expectedLen: 1, + expectedColumnName: "latency", + expectedOperator: ComparisonOperatorBelow, + }, + { + scenario: "missing color is dropped", + thresholds: []any{map[string]any{"thresholdValue": float64(1), "thresholdTableOptions": "latency"}}, + expectedLen: 0, + }, + { + scenario: "missing columnName is dropped", + thresholds: []any{map[string]any{"thresholdValue": float64(1), "thresholdColor": "#f00"}}, + expectedLen: 0, + }, + { + scenario: "empty list yields nil", + thresholds: []any{}, + expectedLen: 0, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.scenario, func(t *testing.T) { + out := (&v1Decoder{}).mapV1TableThresholds(map[string]any{"thresholds": testCase.thresholds}) + require.Len(t, out, testCase.expectedLen) + if testCase.expectedLen == 0 { + return + } + assert.Equal(t, testCase.expectedColumnName, out[0].ColumnName) + assert.Equal(t, testCase.expectedOperator, out[0].Operator) + }) + } +} + +// mapV1ComparisonOperator maps known spellings and defaults unknown/empty to "above" +// without flagging (used by value and table thresholds). +func TestMapV1ComparisonOperator(t *testing.T) { + testCases := []struct { + input string + expected ComparisonOperator + }{ + {">", ComparisonOperatorAbove}, + {"gt", ComparisonOperatorAbove}, + {">=", ComparisonOperatorAboveOrEqual}, + {"gte", ComparisonOperatorAboveOrEqual}, + {"<", ComparisonOperatorBelow}, + {"lt", ComparisonOperatorBelow}, + {"<=", ComparisonOperatorBelowOrEqual}, + {"lte", ComparisonOperatorBelowOrEqual}, + {"=", ComparisonOperatorEqual}, + {"==", ComparisonOperatorEqual}, + {"eq", ComparisonOperatorEqual}, + {"!=", ComparisonOperatorNotEqual}, + {"neq", ComparisonOperatorNotEqual}, + {"empty", ComparisonOperatorAbove}, + {"BOGUS", ComparisonOperatorAbove}, + } + + for _, testCase := range testCases { + t.Run(testCase.input, func(t *testing.T) { + d := &v1Decoder{} + input := testCase.input + if input == "empty" { + input = "" + } + assert.Equal(t, testCase.expected, d.mapV1ComparisonOperator(input)) + assert.NoError(t, d.errIfHasMalformedFields(), "operator mapping never flags malformed") + }) + } +} + +// mapV1ThresholdFormat coerces known values and defaults everything else (including a +// non-string) to text. +func TestMapV1ThresholdFormat(t *testing.T) { + testCases := []struct { + scenario string + input any + expected ThresholdFormat + }{ + {"background", "Background", ThresholdFormatBackground}, + {"text", "text", ThresholdFormatText}, + {"empty string defaults to text", "", ThresholdFormatText}, + {"non-string number defaults to text", float64(0), ThresholdFormatText}, + } + + for _, testCase := range testCases { + t.Run(testCase.scenario, func(t *testing.T) { + assert.Equal(t, testCase.expected, mapV1ThresholdFormat(testCase.input)) + }) + } +} + +func TestConvertTableWidgetToTablePanel(t *testing.T) { + widget := map[string]any{ + "id": "tbl-1", + "panelTypes": "table", + "title": "Top services", + "columnUnits": map[string]any{ + "latency": "ms", + "errors": "count", + }, + "thresholds": []any{ + map[string]any{ + "thresholdValue": float64(500), + "thresholdColor": "#ff0000", + "thresholdTableOptions": "latency", + "thresholdOperator": ">", + }, + map[string]any{ + // missing columnName — dropped + "thresholdValue": float64(1), + "thresholdColor": "#00ff00", + }, + }, + } + + panel := (&v1Decoder{}).convertTableWidget(widget) + require.NotNil(t, panel) + assert.Equal(t, PanelKindTable, panel.Spec.Plugin.Kind) + + spec, ok := panel.Spec.Plugin.Spec.(*TablePanelSpec) + require.True(t, ok) + assert.Equal(t, "ms", spec.Formatting.ColumnUnits["latency"]) + assert.Equal(t, "count", spec.Formatting.ColumnUnits["errors"]) + require.Len(t, spec.Thresholds, 1) + assert.Equal(t, "latency", spec.Thresholds[0].ColumnName) + assert.Equal(t, ComparisonOperatorAbove, spec.Thresholds[0].Operator) +} + +func TestConvertPieWidgetToPieChartPanel(t *testing.T) { + widget := map[string]any{ + "id": "pie-1", + "panelTypes": "pie", + "title": "Share", + "legendPosition": "right", + } + + panel := (&v1Decoder{}).convertPieWidget(widget) + require.NotNil(t, panel) + assert.Equal(t, PanelKindPieChart, panel.Spec.Plugin.Kind) + + spec, ok := panel.Spec.Plugin.Spec.(*PieChartPanelSpec) + require.True(t, ok) + assert.Equal(t, LegendPositionRight, spec.Legend.Position) +} + +func TestConvertHistogramWidget(t *testing.T) { + bucketCount := float64(20) + widget := map[string]any{ + "id": "hist-1", + "panelTypes": "histogram", + "title": "Latency distribution", + "bucketCount": bucketCount, + "mergeAllActiveQueries": true, + } + + panel := (&v1Decoder{}).convertHistogramWidget(widget) + require.NotNil(t, panel) + assert.Equal(t, PanelKindHistogram, panel.Spec.Plugin.Kind) + + spec, ok := panel.Spec.Plugin.Spec.(*HistogramPanelSpec) + require.True(t, ok) + require.NotNil(t, spec.HistogramBuckets.BucketCount) + assert.Equal(t, bucketCount, *spec.HistogramBuckets.BucketCount) + assert.Nil(t, spec.HistogramBuckets.BucketWidth) + assert.True(t, spec.HistogramBuckets.MergeAllActiveQueries) +} + +func TestConvertListWidget(t *testing.T) { + widget := map[string]any{ + "id": "list-1", + "panelTypes": "list", + "title": "Recent logs", + "selectedLogFields": []any{ + map[string]any{"name": "body", "fieldDataType": "string", "fieldContext": "log"}, + map[string]any{"name": "severity_text", "fieldDataType": "string", "fieldContext": "log"}, + }, + } + + panel := (&v1Decoder{}).convertListWidget(widget) + require.NotNil(t, panel) + assert.Equal(t, PanelKindList, panel.Spec.Plugin.Kind) + + spec, ok := panel.Spec.Plugin.Spec.(*ListPanelSpec) + require.True(t, ok) + require.Len(t, spec.SelectFields, 2) + assert.Equal(t, "body", spec.SelectFields[0].Name) +} + +// ══════════════════════════════════════════════ +// Query translation +// ══════════════════════════════════════════════ + +func TestConvertV1WidgetQuerySinglePromQL(t *testing.T) { + widget := map[string]any{ + "id": "p-1", + "panelTypes": "graph", + "query": map[string]any{ + "queryType": "promql", + "promql": []any{ + map[string]any{"name": "A", "query": "up", "legend": "{{job}}"}, + }, + }, + } + + queries := (&v1Decoder{}).convertV1WidgetQuery(widget, PanelKindTimeSeries) + require.Len(t, queries, 1) + assert.Equal(t, qb.RequestTypeTimeSeries, queries[0].Kind) + assert.Equal(t, QueryKindPromQL, queries[0].Spec.Plugin.Kind) + + prom, ok := queries[0].Spec.Plugin.Spec.(*qb.PromQuery) + require.True(t, ok) + assert.Equal(t, "A", prom.Name) + assert.Equal(t, "up", prom.Query) + assert.Equal(t, "{{job}}", prom.Legend) +} + +func TestConvertV1WidgetQuerySingleClickHouse(t *testing.T) { + widget := map[string]any{ + "id": "c-1", + "panelTypes": "table", + "query": map[string]any{ + "queryType": "clickhouse_sql", + "clickhouse_sql": []any{ + map[string]any{"name": "Q", "query": "SELECT 1", "legend": "x"}, + }, + }, + } + + queries := (&v1Decoder{}).convertV1WidgetQuery(widget, PanelKindTable) + require.Len(t, queries, 1) + assert.Equal(t, qb.RequestTypeScalar, queries[0].Kind) + assert.Equal(t, QueryKindClickHouseSQL, queries[0].Spec.Plugin.Kind) + + ch, ok := queries[0].Spec.Plugin.Spec.(*qb.ClickHouseQuery) + require.True(t, ok) + assert.Equal(t, "Q", ch.Name) + assert.Equal(t, "SELECT 1", ch.Query) +} + +func TestConvertV1WidgetQuerySingleBuilderUsesBuilderDirectly(t *testing.T) { + widget := map[string]any{ + "id": "b-1", + "panelTypes": "graph", + "query": map[string]any{ + "queryType": "builder", + "builder": map[string]any{ + "queryData": []any{ + map[string]any{ + "queryName": "A", + "expression": "A", + "dataSource": "metrics", + "aggregations": []any{map[string]any{"metricName": "signoz_calls_total"}}, + }, + }, + }, + }, + } + + queries := (&v1Decoder{}).convertV1WidgetQuery(widget, PanelKindTimeSeries) + require.Len(t, queries, 1) + assert.Equal(t, qb.RequestTypeTimeSeries, queries[0].Kind) + assert.Equal(t, QueryKindBuilder, queries[0].Spec.Plugin.Kind, "a lone builder query is not wrapped in a CompositeQuery") + assert.Equal(t, "A", queries[0].Spec.Name) + + _, ok := queries[0].Spec.Plugin.Spec.(*BuilderQuerySpec) + require.True(t, ok) +} + +func TestConvertV1WidgetQueryMultipleBuilderWrapsInComposite(t *testing.T) { + widget := map[string]any{ + "id": "b-1", + "panelTypes": "graph", + "query": map[string]any{ + "queryType": "builder", + "builder": map[string]any{ + "queryData": []any{ + map[string]any{ + "queryName": "A", + "expression": "A", + "dataSource": "metrics", + "aggregations": []any{map[string]any{"metricName": "signoz_calls_total"}}, + }, + map[string]any{ + "queryName": "B", + "expression": "B", + "dataSource": "logs", + "aggregations": []any{map[string]any{"expression": "count()"}}, + }, + }, + "queryFormulas": []any{ + map[string]any{"queryName": "F1", "expression": "A + B"}, + }, + }, + }, + } + + queries := (&v1Decoder{}).convertV1WidgetQuery(widget, PanelKindTimeSeries) + require.Len(t, queries, 1) + assert.Equal(t, qb.RequestTypeTimeSeries, queries[0].Kind) + assert.Equal(t, QueryKindComposite, queries[0].Spec.Plugin.Kind) + + composite, ok := queries[0].Spec.Plugin.Spec.(*CompositeQuerySpec) + require.True(t, ok) + require.Len(t, composite.Queries, 3) + assert.Equal(t, qb.QueryTypeBuilder, composite.Queries[0].Type) + assert.Equal(t, qb.QueryTypeBuilder, composite.Queries[1].Type) + assert.Equal(t, qb.QueryTypeFormula, composite.Queries[2].Type) +} + +func TestConvertV1WidgetQueryListPanelUsesBuilderDirectly(t *testing.T) { + widget := map[string]any{ + "id": "l-1", + "panelTypes": "list", + "query": map[string]any{ + "queryType": "builder", + "builder": map[string]any{ + "queryData": []any{ + map[string]any{ + "queryName": "A", + "expression": "A", + "dataSource": "logs", + }, + }, + }, + }, + } + + queries := (&v1Decoder{}).convertV1WidgetQuery(widget, PanelKindList) + require.Len(t, queries, 1) + assert.Equal(t, qb.RequestTypeRaw, queries[0].Kind) + assert.Equal(t, QueryKindBuilder, queries[0].Spec.Plugin.Kind) + + wrapper, ok := queries[0].Spec.Plugin.Spec.(*BuilderQuerySpec) + require.True(t, ok) + spec, ok := wrapper.Spec.(qb.QueryBuilderQuery[qb.LogAggregation]) + require.True(t, ok, "list builder query should dispatch to LogAggregation, got %T", wrapper.Spec) + assert.Equal(t, "A", spec.Name) +} + +func TestConvertV1WidgetQueryNormalizesV4FiltersAndPageSize(t *testing.T) { + widget := map[string]any{ + "id": "l-1", + "panelTypes": "list", + "query": map[string]any{ + "queryType": "builder", + "builder": map[string]any{ + "queryData": []any{ + map[string]any{ + "queryName": "A", + "expression": "A", + "dataSource": "logs", + "pageSize": float64(50), + "filters": map[string]any{ + "op": "AND", + "items": []any{ + map[string]any{ + "key": map[string]any{"key": "service.name", "dataType": "string", "type": "tag"}, + "op": "=", + "value": "frontend", + }, + map[string]any{ + "key": map[string]any{"key": "status.code", "dataType": "int64", "type": "tag"}, + "op": "nin", + "value": []any{float64(4), float64(5)}, + }, + }, + }, + }, + }, + }, + }, + } + + queries := (&v1Decoder{}).convertV1WidgetQuery(widget, PanelKindList) + require.Len(t, queries, 1) + + wrapper, ok := queries[0].Spec.Plugin.Spec.(*BuilderQuerySpec) + require.True(t, ok) + spec, ok := wrapper.Spec.(qb.QueryBuilderQuery[qb.LogAggregation]) + require.True(t, ok, "list logs query should dispatch to LogAggregation, got %T", wrapper.Spec) + + require.NotNil(t, spec.Filter, "v4 filters should become a v5 filter expression") + assert.Equal(t, "(service.name = 'frontend' AND status.code NOT IN [4, 5])", spec.Filter.Expression, "v4 filters normalize to a v5 expression and the deprecated nin folds to NOT IN") + assert.Equal(t, 50, spec.Limit, "pageSize backfills limit on a list panel") +} + +// When a query carries both a v5 filter ({expression}) and the legacy filters +// ({items, op}), the v5 filter wins and the legacy filters is dropped — matching +// v1's UI, which only ever reads filter.expression. +func TestConvertV1WidgetQueryPrefersV5FilterOverLegacyFilters(t *testing.T) { + widget := map[string]any{ + "id": "l-1", + "panelTypes": "list", + "query": map[string]any{ + "queryType": "builder", + "builder": map[string]any{ + "queryData": []any{ + map[string]any{ + "queryName": "A", + "expression": "A", + "dataSource": "logs", + "filter": map[string]any{"expression": "service.name = 'checkout'"}, + "filters": map[string]any{ + "op": "AND", + "items": []any{ + map[string]any{ + "key": map[string]any{"key": "service.name", "dataType": "string", "type": "tag"}, + "op": "=", + "value": "frontend", + }, + }, + }, + }, + }, + }, + }, + } + + queries := (&v1Decoder{}).convertV1WidgetQuery(widget, PanelKindList) + require.Len(t, queries, 1) + + wrapper, ok := queries[0].Spec.Plugin.Spec.(*BuilderQuerySpec) + require.True(t, ok) + spec, ok := wrapper.Spec.(qb.QueryBuilderQuery[qb.LogAggregation]) + require.True(t, ok, "list logs query should dispatch to LogAggregation, got %T", wrapper.Spec) + + require.NotNil(t, spec.Filter) + assert.Equal(t, "service.name = 'checkout'", spec.Filter.Expression, "the v5 filter wins; the legacy filters ('frontend') is dropped") +} + +// An uppercase EXISTS op migrates to a bare EXISTS, not "host.name EXISTS ”". +func TestConvertV1WidgetQueryNormalizesUppercaseExistsOp(t *testing.T) { + widget := map[string]any{ + "id": "l-1", + "panelTypes": "list", + "query": map[string]any{ + "queryType": "builder", + "builder": map[string]any{ + "queryData": []any{ + map[string]any{ + "queryName": "A", + "expression": "A", + "dataSource": "logs", + "filters": map[string]any{ + "op": "AND", + "items": []any{ + map[string]any{ + "key": map[string]any{"key": "host.name", "dataType": "string", "type": "resource"}, + "op": "EXISTS", + "value": "", + }, + }, + }, + }, + }, + }, + }, + } + + queries := (&v1Decoder{}).convertV1WidgetQuery(widget, PanelKindList) + require.Len(t, queries, 1) + + wrapper, ok := queries[0].Spec.Plugin.Spec.(*BuilderQuerySpec) + require.True(t, ok) + spec, ok := wrapper.Spec.(qb.QueryBuilderQuery[qb.LogAggregation]) + require.True(t, ok, "list logs query should dispatch to LogAggregation, got %T", wrapper.Spec) + + require.NotNil(t, spec.Filter) + assert.Equal(t, "host.name EXISTS", spec.Filter.Expression, `uppercase EXISTS op migrates to a bare EXISTS, not "EXISTS ''"`) +} + +func TestConvertV1WidgetQueryIgnoresPageSizeOnNonRowLimitPanel(t *testing.T) { + widget := map[string]any{ + "id": "b-1", + "panelTypes": "graph", + "query": map[string]any{ + "queryType": "builder", + "builder": map[string]any{ + "queryData": []any{ + map[string]any{ + "queryName": "A", + "expression": "A", + "dataSource": "logs", + "pageSize": float64(50), + "aggregations": []any{map[string]any{"expression": "count()"}}, + }, + }, + }, + }, + } + + queries := (&v1Decoder{}).convertV1WidgetQuery(widget, PanelKindTimeSeries) + require.Len(t, queries, 1) + + wrapper, ok := queries[0].Spec.Plugin.Spec.(*BuilderQuerySpec) + require.True(t, ok) + spec, ok := wrapper.Spec.(qb.QueryBuilderQuery[qb.LogAggregation]) + require.True(t, ok, "got %T", wrapper.Spec) + assert.Zero(t, spec.Limit, "pageSize is a list/table concern; a graph panel must not adopt it as limit") +} + +func TestConvertV1WidgetQueryRebuildsFlatMetricAggregation(t *testing.T) { + // A metric query stored in the flat v4 shape (aggregateOperator/ + // aggregateAttribute/timeAggregation/… instead of aggregations[]) — the shape + // a dashboard mislabeled version:"v5" carries when it skips the v4→v5 migrator. + widget := map[string]any{ + "id": "m-1", + "panelTypes": "graph", + "query": map[string]any{ + "queryType": "builder", + "builder": map[string]any{ + "queryData": []any{ + map[string]any{ + "queryName": "A", + "expression": "A", + "dataSource": "metrics", + "aggregateOperator": "avg", + "aggregateAttribute": map[string]any{"key": "signoz_calls_total"}, + "timeAggregation": "rate", + "spaceAggregation": "sum", + "temporality": "cumulative", + }, + }, + }, + }, + } + + queries := (&v1Decoder{}).convertV1WidgetQuery(widget, PanelKindTimeSeries) + require.Len(t, queries, 1) + + wrapper, ok := queries[0].Spec.Plugin.Spec.(*BuilderQuerySpec) + require.True(t, ok) + spec, ok := wrapper.Spec.(qb.QueryBuilderQuery[qb.MetricAggregation]) + require.True(t, ok, "metric query should dispatch to MetricAggregation, got %T", wrapper.Spec) + + require.Len(t, spec.Aggregations, 1, "flat v4 metric fields should rebuild into one aggregation") + assert.Equal(t, "signoz_calls_total", spec.Aggregations[0].MetricName) + assert.Equal(t, "rate", spec.Aggregations[0].TimeAggregation.StringValue()) + assert.Equal(t, "sum", spec.Aggregations[0].SpaceAggregation.StringValue()) +} + +func TestConvertV1WidgetQueryRebuildsFlatLogsAggregation(t *testing.T) { + // A logs query stored in the flat v4 shape (aggregateOperator/ + // aggregateAttribute instead of aggregations[]). Without a flat-field builder + // the aggregation is dropped entirely (empty panel); it must rebuild into an + // expression aggregation, mirroring the migrator's createAggregations. + widget := map[string]any{ + "id": "l-1", + "panelTypes": "graph", + "query": map[string]any{ + "queryType": "builder", + "builder": map[string]any{ + "queryData": []any{ + map[string]any{ + "queryName": "A", + "expression": "A", + "dataSource": "logs", + "aggregateOperator": "sum", + "aggregateAttribute": map[string]any{"key": "bytes"}, + }, + }, + }, + }, + } + + queries := (&v1Decoder{}).convertV1WidgetQuery(widget, PanelKindTimeSeries) + require.Len(t, queries, 1) + + wrapper, ok := queries[0].Spec.Plugin.Spec.(*BuilderQuerySpec) + require.True(t, ok) + spec, ok := wrapper.Spec.(qb.QueryBuilderQuery[qb.LogAggregation]) + require.True(t, ok, "logs query should dispatch to LogAggregation, got %T", wrapper.Spec) + + require.Len(t, spec.Aggregations, 1, "flat v4 logs fields should rebuild into one aggregation") + assert.Equal(t, "sum(bytes)", spec.Aggregations[0].Expression) +} + +// The v5 shape-safe path preserves an explicit count(attribute) in aggregations[], +// so no dashboardtypes-level normalization is needed for v5 dashboards. (The v4 +// full-migrate path clobbers it to count() — that needs a separate fix.) +func TestConvertV1WidgetQueryPreservesCountAttributeOnV5(t *testing.T) { + testCases := []struct { + scenario string + queryData map[string]any + expectedExpression string + }{ + { + scenario: "aggregations[] count(attribute) only", + queryData: map[string]any{ + "queryName": "A", + "expression": "A", + "dataSource": "logs", + "aggregations": []any{map[string]any{"expression": "count(service.name)"}}, + }, + expectedExpression: "count(service.name)", + }, + { + scenario: "flat count+attr alongside aggregations[] count(attribute)", + queryData: map[string]any{ + "queryName": "A", + "expression": "A", + "dataSource": "logs", + "aggregateOperator": "count", + "aggregateAttribute": map[string]any{"key": "service.name"}, + "aggregations": []any{map[string]any{"expression": "count(service.name)"}}, + }, + expectedExpression: "count(service.name)", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.scenario, func(t *testing.T) { + widget := map[string]any{ + "id": "l-1", + "panelTypes": "graph", + "query": map[string]any{ + "queryType": "builder", + "builder": map[string]any{"queryData": []any{testCase.queryData}}, + }, + } + + queries := (&v1Decoder{}).convertV1WidgetQuery(widget, PanelKindTimeSeries) + require.Len(t, queries, 1) + + wrapper, ok := queries[0].Spec.Plugin.Spec.(*BuilderQuerySpec) + require.True(t, ok) + spec, ok := wrapper.Spec.(qb.QueryBuilderQuery[qb.LogAggregation]) + require.True(t, ok, "logs query should dispatch to LogAggregation, got %T", wrapper.Spec) + + require.Len(t, spec.Aggregations, 1) + assert.Equal(t, testCase.expectedExpression, spec.Aggregations[0].Expression) + }) + } +} + +// A v4 formula carries order/limit/having; WrapInV5Envelope's formula branch drops +// them, so we backfill them onto the envelope. +func TestConvertV1WidgetQueryPreservesFormulaOrderLimitHaving(t *testing.T) { + widget := map[string]any{ + "id": "g-1", + "panelTypes": "graph", + "query": map[string]any{ + "queryType": "builder", + "builder": map[string]any{ + "queryData": []any{ + map[string]any{ + "queryName": "A", + "expression": "A", + "dataSource": "logs", + "aggregations": []any{map[string]any{"expression": "count()"}}, + }, + }, + "queryFormulas": []any{ + map[string]any{ + "queryName": "F1", + "expression": "A * 2", + "legend": "twice", + "limit": float64(10), + "orderBy": []any{map[string]any{"columnName": "F1", "order": "desc"}}, + "having": map[string]any{"expression": "F1 > 5"}, + }, + }, + }, + }, + } + + queries := (&v1Decoder{}).convertV1WidgetQuery(widget, PanelKindTimeSeries) + require.Len(t, queries, 1) + + composite, ok := queries[0].Spec.Plugin.Spec.(*CompositeQuerySpec) + require.True(t, ok, "multi-query widget should be a composite, got %T", queries[0].Spec.Plugin.Spec) + + var formula qb.QueryBuilderFormula + found := false + for _, env := range composite.Queries { + if env.Type == qb.QueryTypeFormula { + formula, ok = env.Spec.(qb.QueryBuilderFormula) + require.True(t, ok, "formula spec should be QueryBuilderFormula, got %T", env.Spec) + found = true + } + } + require.True(t, found, "expected a formula query in the composite") + + assert.Equal(t, "A * 2", formula.Expression) + assert.Equal(t, 10, formula.Limit, "formula limit must survive") + require.Len(t, formula.Order, 1, "formula order must survive") + assert.Equal(t, "F1", formula.Order[0].Key.Name) + require.NotNil(t, formula.Having, "formula having must survive") + assert.Equal(t, "F1 > 5", formula.Having.Expression) +} + +// Every migratable field on a builder query must round-trip through convertV1WidgetQuery. +func TestConvertV1WidgetQueryPreservesAllBuilderFields(t *testing.T) { + widget := map[string]any{ + "id": "b-1", + "panelTypes": "graph", + "query": map[string]any{ + "queryType": "builder", + "builder": map[string]any{ + "queryData": []any{ + map[string]any{ + "queryName": "A", + "expression": "A", + "dataSource": "logs", + "disabled": true, + "legend": "my legend", + "aggregations": []any{map[string]any{"expression": "count()"}}, + "filter": map[string]any{"expression": "service.name = 'checkout'"}, + "groupBy": []any{map[string]any{"key": "service.name", "dataType": "string", "type": "resource"}}, + "orderBy": []any{map[string]any{"columnName": "service.name", "dataType": "string", "type": "resource", "order": "asc"}}, + "selectColumns": []any{map[string]any{"key": "body", "dataType": "string", "type": "tag"}}, + "limit": float64(100), + "offset": float64(10), + "having": map[string]any{"expression": "count() > 5"}, + "functions": []any{map[string]any{"name": "absolute"}}, + }, + }, + }, + }, + } + + queries := (&v1Decoder{}).convertV1WidgetQuery(widget, PanelKindTimeSeries) + require.Len(t, queries, 1) + + wrapper, ok := queries[0].Spec.Plugin.Spec.(*BuilderQuerySpec) + require.True(t, ok) + spec, ok := wrapper.Spec.(qb.QueryBuilderQuery[qb.LogAggregation]) + require.True(t, ok, "logs query should dispatch to LogAggregation, got %T", wrapper.Spec) + + assert.True(t, spec.Disabled, "disabled") + assert.Equal(t, "my legend", spec.Legend, "legend") + require.Len(t, spec.Aggregations, 1, "aggregations") + assert.Equal(t, "count()", spec.Aggregations[0].Expression) + require.NotNil(t, spec.Filter, "filter") + assert.Equal(t, "service.name = 'checkout'", spec.Filter.Expression) + require.Len(t, spec.GroupBy, 1, "groupBy") + assert.Equal(t, "service.name", spec.GroupBy[0].Name) + require.Len(t, spec.Order, 1, "order") + assert.Equal(t, "service.name", spec.Order[0].Key.Name) + require.Len(t, spec.SelectFields, 1, "selectFields") + assert.Equal(t, "body", spec.SelectFields[0].Name) + assert.Equal(t, 100, spec.Limit, "limit") + assert.Equal(t, 10, spec.Offset, "offset") + require.NotNil(t, spec.Having, "having") + assert.Equal(t, "count() > 5", spec.Having.Expression) + require.Len(t, spec.Functions, 1, "functions") +} + +// Every migratable field on a trace operator must round-trip through traceOperatorEnvelope. +func TestConvertV1WidgetQueryPreservesAllTraceOperatorFields(t *testing.T) { + widget := map[string]any{ + "id": "t-1", + "panelTypes": "graph", + "query": map[string]any{ + "queryType": "builder", + "builder": map[string]any{ + "queryData": []any{ + map[string]any{"queryName": "A", "expression": "A", "dataSource": "traces", "aggregations": []any{map[string]any{"expression": "count()"}}}, + map[string]any{"queryName": "B", "expression": "B", "dataSource": "traces", "aggregations": []any{map[string]any{"expression": "count()"}}}, + }, + "queryTraceOperator": []any{ + map[string]any{ + "queryName": "T1", + "expression": "A => B", + "dataSource": "traces", + "disabled": true, + "legend": "op legend", + "aggregations": []any{map[string]any{"expression": "count()"}}, + "filter": map[string]any{"expression": "service.name = 'checkout'"}, + "groupBy": []any{map[string]any{"key": "service.name", "dataType": "string", "type": "resource"}}, + "orderBy": []any{map[string]any{"columnName": "service.name", "dataType": "string", "type": "resource", "order": "asc"}}, + "limit": float64(100), + "offset": float64(10), + "having": map[string]any{"expression": "count() > 5"}, + }, + }, + }, + }, + } + + queries := (&v1Decoder{}).convertV1WidgetQuery(widget, PanelKindTimeSeries) + require.Len(t, queries, 1) + + composite, ok := queries[0].Spec.Plugin.Spec.(*CompositeQuerySpec) + require.True(t, ok, "multi-query widget should be a composite, got %T", queries[0].Spec.Plugin.Spec) + + var op qb.QueryBuilderTraceOperator + found := false + for _, env := range composite.Queries { + if env.Type == qb.QueryTypeTraceOperator { + op, ok = env.Spec.(qb.QueryBuilderTraceOperator) + require.True(t, ok, "trace-operator spec should be QueryBuilderTraceOperator, got %T", env.Spec) + found = true + } + } + require.True(t, found, "expected a trace-operator query in the composite") + + assert.Equal(t, "A => B", op.Expression, "expression") + assert.True(t, op.Disabled, "disabled") + assert.Equal(t, "op legend", op.Legend, "legend") + require.Len(t, op.Aggregations, 1, "aggregations") + assert.Equal(t, "count()", op.Aggregations[0].Expression) + require.NotNil(t, op.Filter, "filter") + assert.Equal(t, "service.name = 'checkout'", op.Filter.Expression) + require.Len(t, op.GroupBy, 1, "groupBy") + assert.Equal(t, "service.name", op.GroupBy[0].Name) + require.Len(t, op.Order, 1, "order") + assert.Equal(t, "service.name", op.Order[0].Key.Name) + assert.Equal(t, 100, op.Limit, "limit") + assert.Equal(t, 10, op.Offset, "offset") + require.NotNil(t, op.Having, "having") + assert.Equal(t, "count() > 5", op.Having.Expression) +} + +// A logs query with no aggregations and an orderBy of #SIGNOZ_VALUE: the value-order +// key must be rewritten to the injected default aggregation (count()), which requires +// normalizeOrderByKeys to run after ensureDefaultAggregation. +func TestConvertV1WidgetQueryRewritesValueOrderKeyAfterDefaultAggregation(t *testing.T) { + widget := map[string]any{ + "id": "l-1", + "panelTypes": "graph", + "query": map[string]any{ + "queryType": "builder", + "builder": map[string]any{ + "queryData": []any{ + map[string]any{ + "queryName": "A", + "expression": "A", + "dataSource": "logs", + "orderBy": []any{ + map[string]any{"columnName": "#SIGNOZ_VALUE", "order": "desc"}, + }, + }, + }, + }, + }, + } + + queries := (&v1Decoder{}).convertV1WidgetQuery(widget, PanelKindTimeSeries) + require.Len(t, queries, 1) + + wrapper, ok := queries[0].Spec.Plugin.Spec.(*BuilderQuerySpec) + require.True(t, ok) + spec, ok := wrapper.Spec.(qb.QueryBuilderQuery[qb.LogAggregation]) + require.True(t, ok, "logs query should dispatch to LogAggregation, got %T", wrapper.Spec) + + require.Len(t, spec.Aggregations, 1) + assert.Equal(t, "count()", spec.Aggregations[0].Expression) + require.Len(t, spec.Order, 1) + assert.Equal(t, "count()", spec.Order[0].Key.Name, "#SIGNOZ_VALUE resolves to the injected default aggregation") +} + +func TestConvertV1WidgetQueryInjectsCountForNoopOnAggregationPanel(t *testing.T) { + // A logs query with the list-style "noop" operator placed on an aggregation + // panel (graph). createAggregationsShapeSafe drops noop, leaving no aggregation; + // an aggregation panel requires one, so it must default to count() (mirrors the + // frontend's noop→count rewrite). + widget := map[string]any{ + "id": "l-1", + "panelTypes": "graph", + "query": map[string]any{ + "queryType": "builder", + "builder": map[string]any{ + "queryData": []any{ + map[string]any{ + "queryName": "A", + "expression": "A", + "dataSource": "logs", + "aggregateOperator": "noop", + }, + }, + }, + }, + } + + queries := (&v1Decoder{}).convertV1WidgetQuery(widget, PanelKindTimeSeries) + require.Len(t, queries, 1) + + wrapper, ok := queries[0].Spec.Plugin.Spec.(*BuilderQuerySpec) + require.True(t, ok) + spec, ok := wrapper.Spec.(qb.QueryBuilderQuery[qb.LogAggregation]) + require.True(t, ok, "logs query should dispatch to LogAggregation, got %T", wrapper.Spec) + + require.Len(t, spec.Aggregations, 1, "noop on an aggregation panel should default to count()") + assert.Equal(t, "count()", spec.Aggregations[0].Expression) +} + +func TestConvertV1WidgetQueryInjectsCountForMissingAggregation(t *testing.T) { + // A traces query with no aggregation fields at all on an aggregation panel: + // the migrator produces no aggregations[], which the v5 backend rejects. It must + // default to count(). + widget := map[string]any{ + "id": "t-1", + "panelTypes": "graph", + "query": map[string]any{ + "queryType": "builder", + "builder": map[string]any{ + "queryData": []any{ + map[string]any{ + "queryName": "A", + "expression": "A", + "dataSource": "traces", + }, + }, + }, + }, + } + + queries := (&v1Decoder{}).convertV1WidgetQuery(widget, PanelKindTimeSeries) + require.Len(t, queries, 1) + + wrapper, ok := queries[0].Spec.Plugin.Spec.(*BuilderQuerySpec) + require.True(t, ok) + spec, ok := wrapper.Spec.(qb.QueryBuilderQuery[qb.TraceAggregation]) + require.True(t, ok, "traces query should dispatch to TraceAggregation, got %T", wrapper.Spec) + + require.Len(t, spec.Aggregations, 1, "a missing aggregation on an aggregation panel should default to count()") + assert.Equal(t, "count()", spec.Aggregations[0].Expression) +} + +func TestConvertV1WidgetQueryListPanelKeepsNoAggregation(t *testing.T) { + // A list panel legitimately has no aggregation; the default-count() injection must + // not touch it (the raw request type skips aggregation validation on the backend). + widget := map[string]any{ + "id": "l-1", + "panelTypes": "list", + "query": map[string]any{ + "queryType": "builder", + "builder": map[string]any{ + "queryData": []any{ + map[string]any{ + "queryName": "A", + "expression": "A", + "dataSource": "logs", + "aggregateOperator": "noop", + }, + }, + }, + }, + } + + queries := (&v1Decoder{}).convertV1WidgetQuery(widget, PanelKindList) + require.Len(t, queries, 1) + assert.Equal(t, qb.RequestTypeRaw, queries[0].Kind) + + wrapper, ok := queries[0].Spec.Plugin.Spec.(*BuilderQuerySpec) + require.True(t, ok) + spec, ok := wrapper.Spec.(qb.QueryBuilderQuery[qb.LogAggregation]) + require.True(t, ok, "list logs query should dispatch to LogAggregation, got %T", wrapper.Spec) + + assert.Empty(t, spec.Aggregations, "a list panel must keep its bare (no-aggregation) query") +} + +func TestConvertV1WidgetQueryNoQuery(t *testing.T) { + widget := map[string]any{"id": "x", "panelTypes": "graph"} + queries := (&v1Decoder{}).convertV1WidgetQuery(widget, PanelKindTimeSeries) + assert.Nil(t, queries) +} + +// ══════════════════════════════════════════════ +// Layouts and sections +// ══════════════════════════════════════════════ + +func TestConvertV1LayoutsRootOnly(t *testing.T) { + data := StorableDashboardData{ + "layout": []any{ + map[string]any{"i": "p-1", "x": float64(0), "y": float64(0), "w": float64(6), "h": float64(6)}, + map[string]any{"i": "p-2", "x": float64(6), "y": float64(0), "w": float64(6), "h": float64(6)}, + }, + "widgets": []any{ + map[string]any{"id": "p-1", "panelTypes": "graph", "query": singleLogsBuilderQuery()}, + map[string]any{"id": "p-2", "panelTypes": "graph", "query": singleLogsBuilderQuery()}, + }, + } + + d := &v1Decoder{} + layouts := d.convertV1Layouts(data, d.convertV1Panels(data["widgets"])) + require.NoError(t, d.errIfHasMalformedFields()) + require.Len(t, layouts, 1) + assert.Equal(t, dashboard.KindGridLayout, layouts[0].Kind) + + spec, ok := layouts[0].Spec.(*dashboard.GridLayoutSpec) + require.True(t, ok) + require.Len(t, spec.Items, 2) + assert.Equal(t, "#/spec/panels/p-1", spec.Items[0].Content.Ref) + assert.Equal(t, 6, spec.Items[1].Width) + assert.Nil(t, spec.Display, "root-only grid should have no display block") +} + +func TestConvertV1LayoutsDropsDuplicateWidgetIDs(t *testing.T) { + // Two layout entries reference the same widget id with different geometry. + // Mirroring the frontend's getUpdatedLayout, the first in stored order wins + // and the rest are dropped whole — the loser's geometry is discarded, not + // merged. + data := StorableDashboardData{ + "layout": []any{ + map[string]any{"i": "p-1", "x": float64(0), "y": float64(0), "w": float64(6), "h": float64(6)}, + map[string]any{"i": "p-1", "x": float64(6), "y": float64(0), "w": float64(3), "h": float64(9)}, + map[string]any{"i": "p-2", "x": float64(6), "y": float64(0), "w": float64(6), "h": float64(6)}, + }, + "widgets": []any{ + map[string]any{"id": "p-1", "panelTypes": "graph", "query": singleLogsBuilderQuery()}, + map[string]any{"id": "p-2", "panelTypes": "graph", "query": singleLogsBuilderQuery()}, + }, + } + + d := &v1Decoder{} + layouts := d.convertV1Layouts(data, d.convertV1Panels(data["widgets"])) + require.NoError(t, d.errIfHasMalformedFields()) + require.Len(t, layouts, 1) + + spec, ok := layouts[0].Spec.(*dashboard.GridLayoutSpec) + require.True(t, ok) + require.Len(t, spec.Items, 2, "the duplicate p-1 entry should be dropped, leaving p-1 and p-2") + + var p1 *dashboard.GridItem + for i := range spec.Items { + if spec.Items[i].Content.Ref == "#/spec/panels/p-1" { + p1 = &spec.Items[i] + } + } + require.NotNil(t, p1) + assert.Equal(t, 6, p1.Width, "first occurrence (w=6) wins, not the dropped duplicate (w=3)") + assert.Equal(t, 6, p1.Height, "first occurrence (h=6) wins, not the dropped duplicate (h=9)") +} + +func TestConvertV1LayoutsWithCollapsedSection(t *testing.T) { + data := StorableDashboardData{ + "widgets": []any{ + map[string]any{"id": "row-1", "panelTypes": "row", "title": "Latency"}, + map[string]any{"id": "p-1", "panelTypes": "graph", "query": singleLogsBuilderQuery()}, + map[string]any{"id": "p-2", "panelTypes": "graph", "query": singleLogsBuilderQuery()}, + }, + "layout": []any{ + map[string]any{"i": "row-1", "x": float64(0), "y": float64(0), "w": float64(12), "h": float64(1)}, + map[string]any{"i": "p-1", "x": float64(0), "y": float64(1), "w": float64(6), "h": float64(6)}, + map[string]any{"i": "p-2", "x": float64(0), "y": float64(7), "w": float64(6), "h": float64(6)}, + }, + "panelMap": map[string]any{ + "row-1": map[string]any{ + "collapsed": true, + "widgets": []any{ + map[string]any{"i": "p-1", "x": float64(0), "y": float64(1), "w": float64(6), "h": float64(6)}, + }, + }, + }, + } + + d := &v1Decoder{} + layouts := d.convertV1Layouts(data, d.convertV1Panels(data["widgets"])) + require.NoError(t, d.errIfHasMalformedFields()) + require.Len(t, layouts, 2, "root grid (p-1 and p-2, both placed from layout) + empty collapsed section") + + // p-1 appears in both `layout` and the collapsed row's panelMap. The layout entry + // wins — the frontend renders it under the open layout, not the collapsed row — so + // p-1 and p-2 both land in the root grid and the collapsed section is left empty. + rootSpec, ok := layouts[0].Spec.(*dashboard.GridLayoutSpec) + require.True(t, ok) + require.Len(t, rootSpec.Items, 2) + assert.Equal(t, "#/spec/panels/p-1", rootSpec.Items[0].Content.Ref) + assert.Equal(t, "#/spec/panels/p-2", rootSpec.Items[1].Content.Ref) + assert.Nil(t, rootSpec.Display) + + sectionSpec, ok := layouts[1].Spec.(*dashboard.GridLayoutSpec) + require.True(t, ok) + require.NotNil(t, sectionSpec.Display) + assert.Equal(t, "Latency", sectionSpec.Display.Title) + require.NotNil(t, sectionSpec.Display.Collapse) + assert.False(t, sectionSpec.Display.Collapse.Open, "collapsed=true → open=false") + assert.Empty(t, sectionSpec.Items, "p-1 is rendered from layout, not the collapsed section") +} + +// TestConvertV1LayoutsExpandedSectionsNoPanelMap covers the common real-world +// shape: multiple expanded row sections, each with its own panels, and no +// panelMap at all. Section membership must come from the layout y/x positions — +// each row owns the panels below it until the next row. +func TestConvertV1LayoutsExpandedSectionsNoPanelMap(t *testing.T) { + data := StorableDashboardData{ + "widgets": []any{ + map[string]any{"id": "row_s1", "panelTypes": "row", "title": "section 1"}, + map[string]any{"id": "s1p1", "panelTypes": "graph", "query": singleLogsBuilderQuery()}, + map[string]any{"id": "s1p2", "panelTypes": "graph", "query": singleLogsBuilderQuery()}, + map[string]any{"id": "row_s2", "panelTypes": "row", "title": "section 2"}, + map[string]any{"id": "s2p1", "panelTypes": "graph", "query": singleLogsBuilderQuery()}, + map[string]any{"id": "s2p2", "panelTypes": "graph", "query": singleLogsBuilderQuery()}, + }, + "layout": []any{ + map[string]any{"i": "row_s1", "x": float64(0), "y": float64(0), "w": float64(12), "h": float64(1)}, + map[string]any{"i": "s1p1", "x": float64(0), "y": float64(1), "w": float64(6), "h": float64(6)}, + map[string]any{"i": "s1p2", "x": float64(6), "y": float64(1), "w": float64(6), "h": float64(6)}, + map[string]any{"i": "row_s2", "x": float64(0), "y": float64(7), "w": float64(12), "h": float64(1)}, + map[string]any{"i": "s2p1", "x": float64(0), "y": float64(8), "w": float64(6), "h": float64(6)}, + map[string]any{"i": "s2p2", "x": float64(6), "y": float64(8), "w": float64(6), "h": float64(6)}, + }, + } + + d := &v1Decoder{} + layouts := d.convertV1Layouts(data, d.convertV1Panels(data["widgets"])) + require.NoError(t, d.errIfHasMalformedFields()) + require.Len(t, layouts, 2, "two row sections, no root grid") + + s1, ok := layouts[0].Spec.(*dashboard.GridLayoutSpec) + require.True(t, ok) + require.NotNil(t, s1.Display) + assert.Equal(t, "section 1", s1.Display.Title) + require.NotNil(t, s1.Display.Collapse) + assert.True(t, s1.Display.Collapse.Open) + require.Len(t, s1.Items, 2) + assert.Equal(t, "#/spec/panels/s1p1", s1.Items[0].Content.Ref) + assert.Equal(t, "#/spec/panels/s1p2", s1.Items[1].Content.Ref) + // y normalized within the section: the row header's row is dropped. + assert.Equal(t, 0, s1.Items[0].Y) + assert.Equal(t, 0, s1.Items[1].Y) + assert.Equal(t, 6, s1.Items[1].X) + + s2, ok := layouts[1].Spec.(*dashboard.GridLayoutSpec) + require.True(t, ok) + assert.Equal(t, "section 2", s2.Display.Title) + require.Len(t, s2.Items, 2) + assert.Equal(t, "#/spec/panels/s2p1", s2.Items[0].Content.Ref) + assert.Equal(t, "#/spec/panels/s2p2", s2.Items[1].Content.Ref) + assert.Equal(t, 0, s2.Items[0].Y) + assert.Equal(t, 0, s2.Items[1].Y) +} + +func TestConvertV1LayoutsCompactsOverlapping(t *testing.T) { + data := StorableDashboardData{ + "widgets": []any{ + map[string]any{"id": "w1", "panelTypes": "graph", "query": singleLogsBuilderQuery()}, + map[string]any{"id": "w2", "panelTypes": "graph", "query": singleLogsBuilderQuery()}, + }, + "layout": []any{ + map[string]any{"i": "w1", "x": float64(0), "y": float64(0), "w": float64(6), "h": float64(6)}, + map[string]any{"i": "w2", "x": float64(3), "y": float64(3), "w": float64(6), "h": float64(6)}, + }, + } + + d := &v1Decoder{} + layouts := d.convertV1Layouts(data, d.convertV1Panels(data["widgets"])) + require.NoError(t, d.errIfHasMalformedFields()) + require.Len(t, layouts, 1) + + grid, ok := layouts[0].Spec.(*dashboard.GridLayoutSpec) + require.True(t, ok) + require.Len(t, grid.Items, 2) + // w1 stays top-left; the overlapping w2 is pushed below it, keeping its x. + assert.Equal(t, "#/spec/panels/w1", grid.Items[0].Content.Ref) + assert.Equal(t, 0, grid.Items[0].Y) + assert.Equal(t, "#/spec/panels/w2", grid.Items[1].Content.Ref) + assert.Equal(t, 3, grid.Items[1].X) + assert.Equal(t, 6, grid.Items[1].Y) +} + +func TestConvertV1LayoutsClampsNegativeY(t *testing.T) { + data := StorableDashboardData{ + "widgets": []any{map[string]any{"id": "w1", "panelTypes": "graph", "query": singleLogsBuilderQuery()}}, + "layout": []any{ + map[string]any{"i": "w1", "x": float64(0), "y": float64(-1), "w": float64(6), "h": float64(6)}, + }, + } + + d := &v1Decoder{} + layouts := d.convertV1Layouts(data, d.convertV1Panels(data["widgets"])) + require.NoError(t, d.errIfHasMalformedFields()) + require.Len(t, layouts, 1) + + grid, ok := layouts[0].Spec.(*dashboard.GridLayoutSpec) + require.True(t, ok) + require.Len(t, grid.Items, 1) + assert.Equal(t, 0, grid.Items[0].Y) // negative y clamped, as react-grid-layout does +} + +func TestConvertV1LayoutsClampsXBounds(t *testing.T) { + data := StorableDashboardData{ + "widgets": []any{ + map[string]any{"id": "w1", "panelTypes": "graph", "query": singleLogsBuilderQuery()}, + map[string]any{"id": "w2", "panelTypes": "graph", "query": singleLogsBuilderQuery()}, + }, + "layout": []any{ + map[string]any{"i": "w1", "x": float64(-2), "y": float64(0), "w": float64(6), "h": float64(6)}, + map[string]any{"i": "w2", "x": float64(10), "y": float64(1), "w": float64(6), "h": float64(6)}, + }, + } + + d := &v1Decoder{} + layouts := d.convertV1Layouts(data, d.convertV1Panels(data["widgets"])) + require.NoError(t, d.errIfHasMalformedFields()) + require.Len(t, layouts, 1) + + grid, ok := layouts[0].Spec.(*dashboard.GridLayoutSpec) + require.True(t, ok) + require.Len(t, grid.Items, 2) + assert.Equal(t, "#/spec/panels/w1", grid.Items[0].Content.Ref) + assert.Equal(t, 0, grid.Items[0].X) // x=-2 clamped to 0 + assert.Equal(t, "#/spec/panels/w2", grid.Items[1].Content.Ref) + assert.Equal(t, 6, grid.Items[1].X) // x+w=16>12 shifted left to 12-6 +} + +// TestConvertV1LayoutsToleratesNonObjectPanelMap covers templates that store +// panelMap as {rowID: []widgetID} instead of the canonical {rowID: {widgets, +// collapsed}}. The frontend reads such an entry as "not collapsed" (it accesses +// .collapsed/.widgets, which are absent on an array — see GridCardLayout), so the +// migration must match: no malformed-field error, and the row becomes an +// expanded section grouping its panels positionally. +func TestConvertV1LayoutsToleratesNonObjectPanelMap(t *testing.T) { + data := StorableDashboardData{ + "widgets": []any{ + map[string]any{"id": "row_overview", "panelTypes": "row", "title": "Overview"}, + map[string]any{"id": "v_up", "panelTypes": "value", "query": singleLogsBuilderQuery()}, + map[string]any{"id": "v_version", "panelTypes": "value", "query": singleLogsBuilderQuery()}, + }, + "layout": []any{ + map[string]any{"i": "row_overview", "x": float64(0), "y": float64(0), "w": float64(12), "h": float64(1)}, + map[string]any{"i": "v_up", "x": float64(0), "y": float64(1), "w": float64(4), "h": float64(5)}, + map[string]any{"i": "v_version", "x": float64(4), "y": float64(1), "w": float64(4), "h": float64(5)}, + }, + "panelMap": map[string]any{ + // non-canonical: a bare []widgetID instead of {widgets, collapsed}. + "row_overview": []any{"v_up", "v_version"}, + }, + } + + d := &v1Decoder{} + layouts := d.convertV1Layouts(data, d.convertV1Panels(data["widgets"])) + require.NoError(t, d.errIfHasMalformedFields(), "a non-object panelMap entry must not be flagged malformed") + require.Len(t, layouts, 1, "one expanded section grid for row_overview") + + section, ok := layouts[0].Spec.(*dashboard.GridLayoutSpec) + require.True(t, ok) + require.NotNil(t, section.Display) + assert.Equal(t, "Overview", section.Display.Title) + require.NotNil(t, section.Display.Collapse) + assert.True(t, section.Display.Collapse.Open, "non-object panelMap entry → row treated as not collapsed") + require.Len(t, section.Items, 2, "both panels grouped under the section positionally") + assert.Equal(t, "#/spec/panels/v_up", section.Items[0].Content.Ref) + assert.Equal(t, "#/spec/panels/v_version", section.Items[1].Content.Ref) +} + +func TestConvertV1LayoutsDropsEntryForUnrenderableWidget(t *testing.T) { + // e-1 is a widget that exists but produces no panel (unknown/EMPTY_WIDGET + // type). Its layout entry must not become a grid item — the ref would point + // at a panel that was never created. (Through ConvertV1ToV2 such a widget also + // records a malformed-field note; here we exercise the layout pass directly.) + data := StorableDashboardData{ + "widgets": []any{ + map[string]any{"id": "p-1", "panelTypes": "graph", "query": singleLogsBuilderQuery()}, + map[string]any{"id": "e-1", "panelTypes": "EMPTY_WIDGET"}, + }, + "layout": []any{ + map[string]any{"i": "p-1", "x": float64(0), "y": float64(0), "w": float64(6), "h": float64(6)}, + map[string]any{"i": "e-1", "x": float64(6), "y": float64(0), "w": float64(6), "h": float64(6)}, + }, + } + + d := &v1Decoder{} + panels := d.convertV1Panels(data["widgets"]) + require.NotContains(t, panels, "e-1", "an unknown widget type produces no panel") + + layouts := d.convertV1Layouts(data, panels) + require.Len(t, layouts, 1) + spec, ok := layouts[0].Spec.(*dashboard.GridLayoutSpec) + require.True(t, ok) + require.Len(t, spec.Items, 1, "e-1 has no panel → its layout entry is dropped, not emitted as a dangling ref") + assert.Equal(t, "#/spec/panels/p-1", spec.Items[0].Content.Ref) +} + +func TestConvertV1LayoutsDropsCollapsedChildWithNoPanel(t *testing.T) { + // A collapsed section lists a child ("ghost") that has no widget at all — a + // deleted widget still referenced in panelMap. It produces no panel and no + // malformed-field note, so the dashboard is NOT skipped; the section grid + // must drop it rather than emit a dangling ref. Collapsed children bypass the + // main layout loop, so this exercises the extractValidLayoutItemsForCollapsedSection filter. + data := StorableDashboardData{ + "widgets": []any{ + map[string]any{"id": "row-1", "panelTypes": "row", "title": "S"}, + map[string]any{"id": "p-1", "panelTypes": "graph", "query": singleLogsBuilderQuery()}, + }, + "layout": []any{ + map[string]any{"i": "row-1", "x": float64(0), "y": float64(0), "w": float64(12), "h": float64(1)}, + }, + "panelMap": map[string]any{ + "row-1": map[string]any{ + "collapsed": true, + "widgets": []any{ + map[string]any{"i": "p-1", "x": float64(0), "y": float64(1), "w": float64(6), "h": float64(6)}, + map[string]any{"i": "ghost", "x": float64(6), "y": float64(1), "w": float64(6), "h": float64(6)}, + }, + }, + }, + } + + d := &v1Decoder{} + layouts := d.convertV1Layouts(data, d.convertV1Panels(data["widgets"])) + require.NoError(t, d.errIfHasMalformedFields(), "a stale collapsed-child ref must not be flagged malformed") + require.Len(t, layouts, 1, "the collapsed section") + + spec, ok := layouts[0].Spec.(*dashboard.GridLayoutSpec) + require.True(t, ok) + require.Len(t, spec.Items, 1, "ghost has no panel → dropped; only p-1 remains") + assert.Equal(t, "#/spec/panels/p-1", spec.Items[0].Content.Ref) +} + +func TestConvertV1LayoutsEmpty(t *testing.T) { + d := &v1Decoder{} + layouts := d.convertV1Layouts(StorableDashboardData{}, nil) + require.NoError(t, d.errIfHasMalformedFields()) + assert.Empty(t, layouts) +} + +// ══════════════════════════════════════════════ +// Variables +// ══════════════════════════════════════════════ + +func TestConvertV1VariablesAllTypes(t *testing.T) { + raw := map[string]any{ + "u-1": map[string]any{ + "name": "service.name", + "description": "the service", + "type": "QUERY", + "queryValue": "SELECT name FROM s", + "multiSelect": true, + "showALLOption": true, + "sort": "ASC", + "order": float64(1), + }, + "u-2": map[string]any{ + "name": "env", + "type": "CUSTOM", + "customValue": "prod,staging,dev", + "order": float64(2), + "selectedValue": "prod", + }, + "u-3": map[string]any{ + "name": "deployment.environment", + "type": "DYNAMIC", + "dynamicVariablesAttribute": "deployment.environment", + "dynamicVariablesSource": "Traces", + "order": float64(0), + }, + "u-4": map[string]any{ + "name": "freetext", + "type": "TEXTBOX", + "textboxValue": "hello", + "order": float64(3), + }, + } + + d := &v1Decoder{} + vars := d.convertV1Variables(raw) + require.NoError(t, d.errIfHasMalformedFields()) + require.Len(t, vars, 4) + + // Ordered by `order` ascending: u-3 (0), u-1 (1), u-2 (2), u-4 (3) + assert.Equal(t, variable.KindList, vars[0].Kind) + dyn, ok := vars[0].Spec.(*ListVariableSpec) + require.True(t, ok) + assert.Equal(t, "deployment.environment", dyn.Name) + assert.Equal(t, VariableKindDynamic, dyn.Plugin.Kind) + dynSpec, ok := dyn.Plugin.Spec.(*DynamicVariableSpec) + require.True(t, ok) + assert.Equal(t, DynamicVariableSignalTraces, dynSpec.Signal) + + q, ok := vars[1].Spec.(*ListVariableSpec) + require.True(t, ok) + assert.Equal(t, "service.name", q.Name) + assert.Equal(t, VariableKindQuery, q.Plugin.Kind) + assert.True(t, q.AllowMultiple) + assert.True(t, q.AllowAllValue) + assert.Equal(t, SortAlphabeticalAsc, q.Sort) + + c, ok := vars[2].Spec.(*ListVariableSpec) + require.True(t, ok) + assert.Equal(t, "env", c.Name) + assert.Equal(t, VariableKindCustom, c.Plugin.Kind) + require.NotNil(t, c.DefaultValue) + assert.Equal(t, "prod", c.DefaultValue.SingleValue) + + assert.Equal(t, variable.KindText, vars[3].Kind) + text, ok := vars[3].Spec.(*TextVariableSpec) + require.True(t, ok) + assert.Equal(t, "freetext", text.Name) + assert.Equal(t, "hello", text.Value) +} + +func TestConvertV1VariablesSkipsUnnamed(t *testing.T) { + raw := map[string]any{ + "u-1": map[string]any{"name": "", "type": "QUERY"}, + "u-3": map[string]any{"name": "good", "type": "CUSTOM", "customValue": "a"}, + } + d := &v1Decoder{} + vars := d.convertV1Variables(raw) + require.NoError(t, d.errIfHasMalformedFields()) + require.Len(t, vars, 1) + spec := vars[0].Spec.(*ListVariableSpec) + assert.Equal(t, "good", spec.Name) +} + +func TestConvertV1VariablesSkipsDynamicMissingAttribute(t *testing.T) { + raw := map[string]any{ + "u-1": map[string]any{"name": "node", "type": "DYNAMIC", "dynamicVariablesSource": "Traces"}, + "u-2": map[string]any{"name": "good", "type": "CUSTOM", "customValue": "a"}, + } + d := &v1Decoder{} + vars := d.convertV1Variables(raw) + require.NoError(t, d.errIfHasMalformedFields()) // skipped silently, dashboard not failed + require.Len(t, vars, 1) + spec := vars[0].Spec.(*ListVariableSpec) + assert.Equal(t, "good", spec.Name) +} + +// TestConvertV1VariablesFlagsUnknownType verifies a named variable with an +// unrecognized (non-empty) type is recorded as a problem (dashboard logged and +// skipped) rather than silently dropped. +func TestConvertV1VariablesFlagsUnknownType(t *testing.T) { + d := &v1Decoder{} + d.convertV1Variables(map[string]any{"u-1": map[string]any{"name": "ok", "type": "WHATEVER"}}) + require.Error(t, d.errIfHasMalformedFields()) +} + +// TestConvertV1VariablesSkipsEmptyType verifies a variable with no type is dropped +// silently, without failing the migration. +func TestConvertV1VariablesSkipsEmptyType(t *testing.T) { + d := &v1Decoder{} + vars := d.convertV1Variables(map[string]any{"u-1": map[string]any{"name": "ok", "type": ""}}) + assert.Empty(t, vars) + require.NoError(t, d.errIfHasMalformedFields()) +} + +func TestConvertV1VariablesDefaultFromSelectedSlice(t *testing.T) { + raw := map[string]any{ + "u-1": map[string]any{ + "name": "svc", + "type": "QUERY", + "queryValue": "SELECT 1", + "selectedValue": []any{"foo", "", "bar"}, + }, + } + d := &v1Decoder{} + vars := d.convertV1Variables(raw) + require.NoError(t, d.errIfHasMalformedFields()) + require.Len(t, vars, 1) + spec := vars[0].Spec.(*ListVariableSpec) + require.NotNil(t, spec.DefaultValue) + assert.Equal(t, []string{"foo", "bar"}, spec.DefaultValue.SliceValues) +} + +// A single-select v1 variable can't offer an "All" option; the migration drops +// allowAllValue (and customAllValue) so v2 validation accepts it. +func TestConvertV1VariablesDropsAllOptionWhenSingleSelect(t *testing.T) { + raw := map[string]any{ + "u-1": map[string]any{ + "name": "svc", + "type": "QUERY", + "queryValue": "SELECT 1", + "showALLOption": true, + "multiSelect": false, + "customAllValue": "*", + }, + } + d := &v1Decoder{} + vars := d.convertV1Variables(raw) + require.NoError(t, d.errIfHasMalformedFields()) + require.Len(t, vars, 1) + spec := vars[0].Spec.(*ListVariableSpec) + assert.False(t, spec.AllowMultiple) + assert.False(t, spec.AllowAllValue, "single-select drops the All option") + assert.Empty(t, spec.CustomAllValue, "customAllValue dropped with allowAllValue") +} diff --git a/pkg/types/dashboardtypes/perses_v1_to_v2_variables.go b/pkg/types/dashboardtypes/perses_v1_to_v2_variables.go new file mode 100644 index 00000000000..f05114b1f2a --- /dev/null +++ b/pkg/types/dashboardtypes/perses_v1_to_v2_variables.go @@ -0,0 +1,243 @@ +package dashboardtypes + +import ( + "sort" + "strconv" + "strings" + + "github.com/perses/spec/go/dashboard/variable" +) + +// ══════════════════════════════════════════════ +// Variables +// ══════════════════════════════════════════════ + +// convertV1Variables walks the v1 `variables` map (UUID-keyed) and produces an +// ordered []Variable. Variables sort by `order` first, then by id for stable +// output. v1 variable types map as follows: +// +// QUERY → ListVariable + signoz/QueryVariable +// CUSTOM → ListVariable + signoz/CustomVariable +// DYNAMIC → ListVariable + signoz/DynamicVariable +// TEXTBOX → TextVariable +func (d *v1Decoder) convertV1Variables(raw any) []Variable { + if raw == nil { + return []Variable{} + } + rawVariablesMap, ok := raw.(map[string]any) + if !ok { + // v1 sometimes stores variables as a list. The frontend consumes it via + // Object.entries/keys, which for an array yields the stringified index as the + // key, so mirror that: [{...}] is treated as {"0":{...}}. An empty list is + // simply "no variables". + rawSlice, isSlice := raw.([]any) + if !isSlice { + d.noteMalformedField("variables", raw) + return nil + } + rawVariablesMap = make(map[string]any, len(rawSlice)) + for i, v := range rawSlice { + rawVariablesMap[strconv.Itoa(i)] = v + } + } + type ordered struct { + variableID string + variableContent map[string]any + order float64 + } + entries := make([]ordered, 0, len(rawVariablesMap)) + for variableID, variableContentRaw := range rawVariablesMap { + variableContent, ok := variableContentRaw.(map[string]any) + if !ok { + // A variable whose content isn't an object (e.g. a stray "list" array) can't + // render in the current UI, so it's useless — skip it instead of failing the + // migration. + continue + } + entries = append(entries, ordered{variableID: variableID, variableContent: variableContent, order: d.readFloat(variableContent, "order")}) + } + sort.SliceStable(entries, func(i, j int) bool { + if entries[i].order != entries[j].order { + return entries[i].order < entries[j].order + } + return entries[i].variableID < entries[j].variableID + }) + + variablesV2 := make([]Variable, 0, len(entries)) + for _, e := range entries { + v, ok := d.convertV1Variable(e.variableContent) + if !ok { + continue + } + variablesV2 = append(variablesV2, v) + } + return variablesV2 +} + +func (d *v1Decoder) convertV1Variable(v map[string]any) (Variable, bool) { + name := d.readString(v, "name") + if name == "" { + return Variable{}, false + } + description := d.readString(v, "description") + // v1 stores the type upper-cased (QUERY/CUSTOM/…); tolerate any casing. + kind := strings.ToUpper(d.readString(v, "type")) + + switch kind { + case "TEXTBOX": + spec := &TextVariableSpec{ + Display: Display{Name: clipName(name, MaxDisplayNameLen), Description: description}, + Value: d.readString(v, "textboxValue"), + Name: name, + } + return Variable{Kind: variable.KindText, Spec: spec}, true + + case "QUERY", "CUSTOM", "DYNAMIC": + // Drop (don't fail on) a dynamic variable with no attribute — it can't resolve. + if kind == "DYNAMIC" && d.readString(v, "dynamicVariablesAttribute") == "" { + return Variable{}, false + } + // Drop a custom variable with no recoverable option list — v2 requires one. + if kind == "CUSTOM" && d.readString(v, "customValue") == "" && d.readString(v, "selectedValue") == "" && d.readString(v, "defaultValue") == "" { + return Variable{}, false + } + // Drop a query variable with no query — it can't resolve. + if kind == "QUERY" && d.readString(v, "queryValue") == "" { + return Variable{}, false + } + listSpec := &ListVariableSpec{ + Display: Display{Name: clipName(name, MaxDisplayNameLen), Description: description}, + AllowAllValue: d.readBool(v, "showALLOption"), + AllowMultiple: d.readBool(v, "multiSelect"), + CustomAllValue: d.readString(v, "customAllValue"), + CapturingRegexp: d.readString(v, "capturingRegexp"), + Sort: mapV1Sort(v["sort"]), + Plugin: d.variablePluginFor(kind, v), + Name: name, + } + // A single-select variable can't offer an "All" option: v2 rejects allowAllValue + // (and thus customAllValue) without allowMultiple. Drop them rather than fail. + if !listSpec.AllowMultiple { + listSpec.AllowAllValue = false + listSpec.CustomAllValue = "" + } + if dv := mapV1VariableDefault(v, listSpec.AllowMultiple); dv != nil { + listSpec.DefaultValue = dv + } + return Variable{Kind: variable.KindList, Spec: listSpec}, true + + case "": + // v1 sometimes stores a variable with no type; it can't render, so drop it + // silently rather than flagging it malformed. + return Variable{}, false + + default: + d.note("variable %q has unknown type %q", name, kind) + return Variable{}, false + } +} + +func (d *v1Decoder) variablePluginFor(kind string, v map[string]any) VariablePlugin { + switch kind { + case "QUERY": + return VariablePlugin{ + Kind: VariableKindQuery, + Spec: &QueryVariableSpec{QueryValue: d.readString(v, "queryValue")}, + } + case "CUSTOM": + // Some v1 dashboards stored the option list in selectedValue/defaultValue + // instead of customValue; fall back so the variable survives migration. + customValue := d.readString(v, "customValue") + if customValue == "" { + customValue = d.readString(v, "selectedValue") + } + if customValue == "" { + customValue = d.readString(v, "defaultValue") + } + return VariablePlugin{ + Kind: VariableKindCustom, + Spec: &CustomVariableSpec{CustomValue: customValue}, + } + case "DYNAMIC": + return VariablePlugin{ + Kind: VariableKindDynamic, + Spec: &DynamicVariableSpec{ + Name: d.readString(v, "dynamicVariablesAttribute"), + Signal: mapV1VariableSignal(v["dynamicVariablesSource"]), + }, + } + } + return VariablePlugin{} +} + +// mapV1VariableSignal maps a v1 dynamicVariablesSource to a dynamic variable's +// signal. v1 stores it capitalized ("Traces"), so match case-insensitively; +// v1's "All telemetry" (and any unrecognized/empty source) means every signal. +func mapV1VariableSignal(raw any) DynamicVariableSignal { + s, _ := raw.(string) + switch strings.ToLower(s) { + case "traces": + return DynamicVariableSignalTraces + case "logs": + return DynamicVariableSignalLogs + case "metrics": + return DynamicVariableSignalMetrics + } + return DynamicVariableSignalAll +} + +// mapV1VariableDefault reads selectedValue/defaultValue, both polymorphic +// (string|array), so it indexes the raw value and lets defaultValueFromAny +// type-switch — no typed accessor, intentionally lenient. +func mapV1VariableDefault(v map[string]any, allowMultiple bool) *VariableDefaultValue { + if raw, ok := v["selectedValue"]; ok { + return defaultValueFromAny(raw, allowMultiple) + } + if raw, ok := v["defaultValue"]; ok { + return defaultValueFromAny(raw, allowMultiple) + } + return nil +} + +func defaultValueFromAny(raw any, allowMultiple bool) *VariableDefaultValue { + switch v := raw.(type) { + case string: + if v == "" { + return nil + } + return &VariableDefaultValue{variable.DefaultValue{SingleValue: v}} + case []any: + if len(v) == 0 { + return nil + } + values := make([]string, 0, len(v)) + for _, item := range v { + if s, ok := item.(string); ok && s != "" { + values = append(values, s) + } + } + if len(values) == 0 { + return nil + } + // A single-select variable can't carry a list default; collapse a lone value. + if !allowMultiple && len(values) == 1 { + return &VariableDefaultValue{variable.DefaultValue{SingleValue: values[0]}} + } + return &VariableDefaultValue{variable.DefaultValue{SliceValues: values}} + } + return nil +} + +// mapV1Sort reads the raw value (not via readString) so a non-string sort — some v1 +// dashboards store it as a number (e.g. 0) — defaults to none silently instead of +// being flagged malformed. +func mapV1Sort(raw any) ListVariableSpecSort { + s, _ := raw.(string) + switch s { + case "ASC": + return SortAlphabeticalAsc + case "DESC": + return SortAlphabeticalDesc + } + return ListVariableSpecSort{} // zero (omitzero) — SortNone is the implicit default +} diff --git a/pkg/types/dashboardtypes/store.go b/pkg/types/dashboardtypes/store.go index 92a27580145..3de1503f0e0 100644 --- a/pkg/types/dashboardtypes/store.go +++ b/pkg/types/dashboardtypes/store.go @@ -25,6 +25,10 @@ type Store interface { Update(context.Context, valuer.UUID, *StorableDashboard) error + // UpdateName updates only the name column of a dashboard, leaving its data + // untouched — used to backfill the name of a dashboard whose data fails to migrate. + UpdateName(ctx context.Context, orgID valuer.UUID, id valuer.UUID, name string) error + UpdatePublic(context.Context, *StorablePublicDashboard) error Delete(context.Context, valuer.UUID, valuer.UUID) error diff --git a/pkg/types/dashboardtypes/testdata/v1_avocado_icon.txt b/pkg/types/dashboardtypes/testdata/v1_avocado_icon.txt new file mode 100644 index 00000000000..003c660412f --- /dev/null +++ b/pkg/types/dashboardtypes/testdata/v1_avocado_icon.txt @@ -0,0 +1 @@ +data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTcuOTg0NCA1LjA1MTFDNy45ODQ0IDUuMDUxMSAzLjE3NDY2IDYuMzg4NTMgMi43OTk2OCA2LjkwNDc2QzIuNDI0NyA3LjQyMDk4IDEuNzc4NDggOC4zNzcxOCAxLjcxOTc0IDEwLjQyNDZDMS42NzIyNCAxMi4wNjcgMi41ODg0NCAxNi4yOTA1IDcuMDkxOTUgMTYuNDMwNUMxMS42OTA1IDE2LjU3NDIgMTMuNDAyOSAxMy41NDQ0IDE0LjIyNDEgMTEuNjY4M0MxNC45NjQgOS45NzU4NCAxNC45NTE1IDguMDMyMiAxNS40MjAzIDcuMTM5NzRDMTUuODg5IDYuMjQ3MjkgMTYuMzExNSA1LjQ3MzU4IDE2LjI4OSA0LjQ2NDg4QzE2LjI2NjUgMy40NTYxOSAxNS44OTAyIDIuNTg3NDggMTUuMjMyOCAyLjE2NTAxQzE0LjU3NTMgMS43NDI1MyAxNC4wMTI4IDEuODEyNTIgMTMuNzMxNiAxLjc0MjUzQzEzLjQ1MDQgMS42NzI1MyA3Ljk4NDQgNS4wNTExIDcuOTg0NCA1LjA1MTFaIiBmaWxsPSIjNzU3RTQwIi8+CjxwYXRoIGQ9Ik04LjgyODE3IDMuMzYyNDFDNi44MzMyOCA0LjE4MzYxIDQuMTgyMTcgNC45NTczMiAyLjkxNTk5IDYuNzQwOThDMS43NzIzIDguMzUwOSAxLjM5MTA3IDEwLjM1MzMgMi45Mzk3MyAxMi42OTk0QzQuMzUzNDEgMTQuODQxOCA4LjEyMTk2IDE1Ljc1NTUgMTAuOTQwNiAxMy43MzE5QzEzLjUyMTcgMTEuODc4MiAxMy43NTU0IDkuMTU3MSAxNC41MzA0IDcuMzQ5N0MxNS4zMDUzIDUuNTQyMjkgMTYuOTM1MiAzLjE2NzQyIDE0Ljk3NjYgMS45OTk5OEMxMi44ODggMC43NTc1NDUgMTAuNDA0MyAyLjcxMzY5IDguODI4MTcgMy4zNjI0MVoiIGZpbGw9IiNBRkI0MkEiLz4KPHBhdGggZD0iTTE0LjUzMDQgMi41MTczM0MxMy41ODE3IDEuOTY5ODYgMTIuMzcxNyAyLjIxMjM0IDExLjAzNDMgMi44Njk4MUM5LjY5Njg4IDMuNTI3MjggOC42NjQ0NCA0LjAxOTc1IDguMTk1NzEgNC4xODM0OUM3LjcyNTc0IDQuMzQ3MjMgMy43MzM0NSA1LjgwOTY2IDIuOTYzNDkgNy43NDk1NUMyLjMyOTc3IDkuMzQ0NDcgMi40OTQ3NiAxMS40MzMxIDQuMTU5NjcgMTIuOTU4QzUuODI0NTkgMTQuNDgyOSAxMC4wMjU2IDE0LjM4OTIgMTEuNjQ0MyAxMi4wODkzQzEzLjI2MjkgOS43ODk0NSAxMy4zMzI5IDcuOTM3MDQgMTMuNjg1NCA3LjExNTg0QzE0LjAzNzkgNi4yOTQ2MyAxNC44MDU0IDUuMDkyMTkgMTUuMDIyOSA0LjQ0MDk4QzE1LjI4MDMgMy42NjcyNyAxNS4xNDAzIDIuODY5ODEgMTQuNTMwNCAyLjUxNzMzWiIgZmlsbD0iI0ZGRjY5RCIvPgo8cGF0aCBkPSJNOS45MzA2NCA2Ljk3NDc1QzkuMTc5NDMgNi4wODM1NSA2LjMxNzA4IDYuMzY0NzggNS4zMzIxNCA3Ljc3MjIxQzQuMzQ3MTkgOS4xNzk2MyA0Ljc1NzE3IDEwLjMyMzMgNS4yMzgzOSAxMC45Mzk1QzUuODI0NjEgMTEuNjkwOCA3LjQyMDc4IDEyLjM3MDcgOC44NzQ0NSAxMS4zNjJDMTAuMzI4MSAxMC4zNTMzIDEwLjc1NDMgNy45NTIyIDkuOTMwNjQgNi45NzQ3NVoiIGZpbGw9IiM4NTVDNTIiLz4KPHBhdGggZD0iTTYuOTE0NDkgOC41NzcyM0M2LjU0NDUxIDkuMTE5NyA2LjcyOTUgOS45OTQ2NSA2LjA5MzI4IDEwLjAzNTlDNS40NTcwNyAxMC4wNzcyIDUuMDg3MDggOS4wNTU5NSA1LjUzODMxIDguMjAxQzYuMDI1NzkgNy4yNzg1NSA3LjE4MDcyIDYuODY3MzIgNy41MDk0NiA3LjMwNDhDNy44Mzk0NCA3Ljc0MzUyIDcuMjY1NzIgOC4wNjIyNiA2LjkxNDQ5IDguNTc3MjNaIiBmaWxsPSIjRDY3NjU5Ii8+Cjwvc3ZnPgo= \ No newline at end of file diff --git a/pkg/types/querybuildertypes/querybuildertypesv5/migrate_envelope.go b/pkg/types/querybuildertypes/querybuildertypesv5/migrate_envelope.go new file mode 100644 index 00000000000..a06acf9a9d6 --- /dev/null +++ b/pkg/types/querybuildertypes/querybuildertypesv5/migrate_envelope.go @@ -0,0 +1,127 @@ +package querybuildertypesv5 + +// WrapInV5Envelope translates a single v4 builder query/formula map into a +// v5 query envelope ({"type": ..., "spec": ...}). It is a pure shape transform +// over untyped maps: v4 builder field names (groupBy/orderBy/selectColumns/ +// dataSource) are rewritten to their v5 equivalents and a `signal` is derived +// from the data source. queryType selects the envelope type, except a formula +// (detected when name != queryMap["expression"]) is always emitted as +// "builder_formula". +// +// Migration code (pkg/transition) and the v1→v2 dashboard conversion both +// produce v5 envelopes, so this lives here with the v5 query types rather than +// in an infra-level package. +func WrapInV5Envelope(name string, queryMap map[string]any, queryType string) map[string]any { + // Create a properly structured v5 query + v5Query := map[string]any{ + "name": name, + "disabled": queryMap["disabled"], + "legend": queryMap["legend"], + } + + if name != queryMap["expression"] { + // formula + queryType = "builder_formula" + v5Query["expression"] = queryMap["expression"] + if functions, ok := queryMap["functions"]; ok { + v5Query["functions"] = functions + } + return map[string]any{ + "type": queryType, + "spec": v5Query, + } + } + + // Add signal based on data source + if dataSource, ok := queryMap["dataSource"].(string); ok { + switch dataSource { + case "traces": + v5Query["signal"] = "traces" + case "logs": + v5Query["signal"] = "logs" + case "metrics": + v5Query["signal"] = "metrics" + } + } + + if stepInterval, ok := queryMap["stepInterval"]; ok { + v5Query["stepInterval"] = stepInterval + } + + if aggregations, ok := queryMap["aggregations"]; ok { + v5Query["aggregations"] = aggregations + } + + if filter, ok := queryMap["filter"]; ok { + v5Query["filter"] = filter + } + + // Copy groupBy with proper structure + if groupBy, ok := queryMap["groupBy"].([]any); ok { + v5GroupBy := make([]any, len(groupBy)) + for i, gb := range groupBy { + if gbMap, ok := gb.(map[string]any); ok { + v5GroupBy[i] = map[string]any{ + "name": gbMap["key"], + "fieldDataType": gbMap["dataType"], + "fieldContext": gbMap["type"], + } + } + } + v5Query["groupBy"] = v5GroupBy + } + + // Copy orderBy with proper structure + if orderBy, ok := queryMap["orderBy"].([]any); ok { + v5OrderBy := make([]any, len(orderBy)) + for i, ob := range orderBy { + if obMap, ok := ob.(map[string]any); ok { + v5OrderBy[i] = map[string]any{ + "key": map[string]any{ + "name": obMap["columnName"], + "fieldDataType": obMap["dataType"], + "fieldContext": obMap["type"], + }, + "direction": obMap["order"], + } + } + } + v5Query["order"] = v5OrderBy + } + + // Copy selectColumns as selectFields + if selectColumns, ok := queryMap["selectColumns"].([]any); ok { + v5SelectFields := make([]any, len(selectColumns)) + for i, col := range selectColumns { + if colMap, ok := col.(map[string]any); ok { + v5SelectFields[i] = map[string]any{ + "name": colMap["key"], + "fieldDataType": colMap["dataType"], + "fieldContext": colMap["type"], + } + } + } + v5Query["selectFields"] = v5SelectFields + } + + // Copy limit and offset + if limit, ok := queryMap["limit"]; ok { + v5Query["limit"] = limit + } + if offset, ok := queryMap["offset"]; ok { + v5Query["offset"] = offset + } + + if having, ok := queryMap["having"]; ok { + v5Query["having"] = having + } + + if functions, ok := queryMap["functions"]; ok { + v5Query["functions"] = functions + } + + return map[string]any{ + "type": queryType, + "spec": v5Query, + } +}