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, + } +}